Skip to content

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 unchangedfind_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.