Skip to content

Data model

Toise tracks entities (things that exist), the attributes that describe them, and the relations that connect them — plus the dimension OTel signals don't carry on their own: how that topology changes over time. Its model aligns with the OpenTelemetry entity data model so Toise entities slot into an existing OTel deployment rather than introducing a parallel vocabulary.

The authoritative contract is the Protobuf definition proto/toise/v1/events.proto; this page is the conceptual overview.

Entities

An entity has four parts:

  • a type — a string id such as host or process;
  • an identity — a set of identifying key/value attributes whose values together uniquely identify the entity;
  • a set of descriptive attributes — informational, non-identifying metadata;
  • a schema_url — versions the entity definition.

Attribute values are a typed Value: one of string, int64, double, or bool — a deliberate subset of OTel's AnyValue that keeps the internal model independent of OTLP wire types. Translation happens only at the ingest boundary.

Two identities, not one

Toise carries two distinct identity concepts that must not be confused:

  • Logical entity ID — the stable identifier of an entity across its whole life, even as its identifying attributes evolve. It is a surrogate ULID assigned by Toise on first sight, and it survives identity changes. This is what consumers reference and what relation endpoints (from/to) point at.
  • Identity hash — a deterministic fingerprint of the current identifying attributes (SHA-256 truncated to 128 bits, type-prefixed, e.g. host:1a2b...). It powers O(1) idempotent ingest and changes when an identifying attribute changes; the logical ID does not.

Put volatile facts in descriptive attributes

Identity should be stable for the entity's lifetime. If a host's identity includes its current leased IP, a DHCP renewal forks it into a brand-new entity. Keep changing facts (current address, usage, last-seen state) as descriptive attributes so a re-address is an update on the same entity, not a silent split.

Relations

A relation is a typed, directed edge between two entities, identified by their logical entity IDs (from, to). It carries a structural flag marking whether its appearance or disappearance is significant (alertable) rather than merely descriptive.

Relations are attribute-free on the wire: anything that would describe how two things relate becomes an entity instead (a port is a network.interface, a route is a network.route). See Ingesting data.

Bi-temporality

Every event carries two timestamps, and they are not interchangeable:

  • event_time — when the fact became true in the real world, supplied by the producer.
  • recorded_at — when Toise recorded the event, stamped at ingestion. Never taken from the producer.

For a late or retroactively corrected event, event_time is significantly earlier than recorded_at; that gap is the signal, not noise. Queries default to event_time space (the reality view); the audit view ("what did we know at instant T?") is an opt-in via asKnownAt — see GraphQL bi-temporality.

The change taxonomy

As events are classified, each change is tagged with one of:

ENTITY_CREATED · ENTITY_DELETED · ENTITY_IDENTITY_CHANGED · ENTITY_ATTRIBUTE_UPDATED · ENTITY_STATE_CHANGED · ENTITY_UNCHANGED · RELATION_ADDED · RELATION_REMOVED · RELATION_ATTRIBUTE_CHANGED.

This taxonomy — plus bi-temporality — is Toise's contribution on top of the OTel facts: the engine stores only what producers assert and classifies how it changed, never deriving or guessing state (ADR 0022).

For the full OTel mapping, see docs/data-model/otel-mapping.md.