0.11.0¶
Released 2026-08-11.
Vocabulary and correlation — the contract-hygiene release. 0.11.0 fixes a read-surface answer that was quietly wrong, and gives producers the type vocabulary they were previously obliged to retype by hand.
No wire-contract break, no data migration. Every change is additive; nothing about the event log or its replay changes.
telemetry_keys no longer answers with someone else's identity¶
telemetry_keys derives the labels that locate an entity's metrics and logs. To
be useful it walks one hop and inherits keys from a neighbour — a listener has no
host.id of its own, it gains its host's through runs_on.
It walked every relation. For an entity with no key of its own that meant
inheriting from whatever happened to touch it. A db entity observed by a
monitoring agent through monitors came back carrying that agent's
service.name and service.instance.id, sourced honestly as "service.instance
incoming via monitors" — and a consumer that trusted the answer went and queried
the wrong machine's telemetry.
Inheritance is now restricted to ownership hops, and it is directional:
| Relation | Direction inherited | Reading |
|---|---|---|
runs_on |
outgoing | a process or listener gains its host's keys |
bound_to, listens_on |
outgoing | an address or listener gains its interface's |
has_interface, has_route |
incoming | an interface or route gains its host's or device's |
monitors is an observation and depends_on, connected_to, next_hop_via are
peer relations: the neighbour there describes something else, so it contributes
nothing. Graph traversal is unchanged — find_path and get_neighbors still
cross those edges. Only key derivation was at fault.
An empty answer now means something¶
Some entity types legitimately have no telemetry of their own: an address or a route is a graph fact, not a measurement. The tool used to say it had found nothing, which a consumer cannot distinguish from "keep looking". It now says no key exists, so the consumer can stop.
Where the round trip actually holds¶
The guidance no longer implies a uniform guarantee, because there isn't one:
| Path | Round trip |
|---|---|
| OTLP rail into a metrics or logs backend | guaranteed |
| Prometheus-family backend fed by a collector | holds where that collector flattens resource attributes into labels — a deployment choice, not a property of the wire |
| A producer's own scrape endpoint | not guaranteed — no resource there, so resource-borne keys are absent |
db.instance.id joins the key set, and both it and network.device.id now carry
the caveat that a remote target's identity rides each datapoint rather than
the producer's resource: a database or an SNMP device has no resource of its own,
so those keys are absent from any label set derived by flattening one.
The type vocabulary is exported¶
Entity.Type and Relationship.Type were free strings. The registered
vocabulary lived in an internal package a producer cannot import, so every
producer kept private constants — and one product could spell the same type two
ways in two of its own sources with nothing failing.
pkg/emit/wire now exports the whole set: twelve entity types, thirteen relation
types, plus EntityTypes() and RelationTypes() for validating a value you did
not write yourself.
host := emit.Entity{
Type: wire.TypeHost,
ID: map[string]string{"host.id": "srv-001"},
Relationships: []emit.Relationship{{
Type: wire.RelTypeRunsOn,
TargetType: wire.TypeHost,
TargetID: map[string]string{"host.id": "srv-001"},
}},
}
An invented or mistyped type is now a compile error instead of a batch rejected at runtime. The package stays deliberately stdlib-only: importing the vocabulary pulls no protocol stack into a producer's module graph.
Toise's own registry derives from those same constants rather than declaring its own copy, and a test compares the two sets in both directions. A type Toise accepts but does not publish would leave producers unable to name it; a type published but not accepted would be refused after a producer trusted it. Neither can now ship.
Available as pkg/emit/v0.6.0. Naming follows the constants already
published — TypeHost, RelTypeRunsOn — and RelTypeSameAs,
RelTypeDependsOn and TypeNetworkEndpoint keep their names and values, so
existing producer code does not move.
Typed attributes are the normal path, not the exception¶
Entity carries two descriptive maps that always landed in the same place on the
wire: RichAttributes, which keeps each value's type, and Attributes, a
shortcut for values that really are strings.
The string map was declared first and the typed one documented as "the rarer structured case". A producer reading the SDK concluded it could not carry typed values at all, and planned a re-encoding around a limitation that did not exist. A capacity, a frequency or a flag is a number or a boolean — that is the common case, and it stays one on the wire. The presentation is now the right way round.
Nothing on the wire changed.
Who owns what¶
The API stability policy records the split settled with a producer maintainer: each side owns what it can verify. Toise holds the vocabulary and the wire form; a producer holds the transport and the verification of its own emission.
It follows that a producer whose export pipeline already owns batching, backpressure, retries and tenant propagation should not adopt the SDK's client at runtime — routing entity events through a second path would cost it those four properties to gain nothing. Importing the vocabulary, or the encoder inside a differential test that fails the build when its own encoding drifts, gets the guarantee without the coupling.
Upgrading¶
Nothing to do. Producers wanting the vocabulary move to pkg/emit/v0.6.0 and
replace their literals; existing string literals keep working, since the values
are unchanged.