ADR-0064. The Telemetry Attribute Catalogue Is Not a Wishlist¶
observability/attributes.py declares the span-attribute keys this library
puts on spans. A group of those constants had no write site anywhere in the
tree: nothing set them, no adapter emitted them, no test observed them. They
are deleted. The lock family is the exception — the PostgreSQL lock manager was
already emitting those attributes under hand-written string literals, so the
literals are replaced by the declared constants and the family stays.
Status¶
Accepted.
Date: 2026-08-09
ADR Impact
| ADR | Status |
|---|---|
| 0016 | Amended. 0016 §9 decided the naming rule — OTel semantic conventions for db.*/messaging.*, eventsource. prefix for everything else — and stated that "the constant list in attributes.py doubles as the catalogue of what this library will put on a span." The naming rule stands unchanged. The catalogue claim is the part this ADR repairs: it was aspirational, not descriptive, and 0016's own text cites keys that were never emitted. Nothing about the telemetry extra, OTEL_AVAILABLE, NullTracer, or the Tracer protocol is disturbed. |
| 0040 | Stands. observability/ remains a settled out-of-ring package; removing constants from it does not move it. |
Context¶
The 2026-08-09 backpressure audit surfaced a class of defect this project keeps
repeating: a capability the library declares, documents, and is supposed to
dispatch to, and never does. The criterion that separates it from ordinary
public API is who is contractually obligated to invoke this — the user, or
us? EventStore.append has no internal caller because users call it. A span
attribute is the opposite: a user never sets one. If the library does not emit
the key, nobody does.
That makes the attribute catalogue an unusually clean case. Every constant here is either emitted by our own code or emitted by nobody, and the second kind is worse than absent. It reads to a user as a contract — a key they can build a dashboard query or an alert on — and the query returns nothing, forever, with no error to explain why. 0016 §9 made this explicit in the direction that turned out to be false: the catalogue would tell you what the library puts on a span. It did not.
Verification inverted one of the audit's assumptions. The lock family was
assumed inert on the same evidence as the rest — no reference outside the
declaration and the package re-export. In fact the PostgreSQL lock manager
opened acquire and release spans and set the key, id, and timeout on them, with
literals ("lock.key") that differed from the declared constants
("eventsource.lock.key"). Two declaration sites for one fact, disagreeing,
with nothing failing — and the grep for the constant name could not see it,
because the emitting site never used the name. So the lock finding was not
"declared but never dispatched" at all; it was the naming rule of 0016 §9 being
violated at the only site that mattered, in a way that also broke the prefix
guarantee the rule exists to provide.
Decision¶
A telemetry attribute constant exists only if the library emits it. The
catalogue in attributes.py is descriptive of current behavior, not a reserved
namespace for behavior someone might add.
Constants with no write site are deleted rather than implemented. Deleting is the default because the alternative — inventing a plausible emission site to justify a key nobody asked for — adds unowned surface and untested code to satisfy a declaration that was itself the mistake. A future need for a key is cheap: add the constant in the same change that sets it.
Where a write site already existed under a string literal, the literal is
replaced by the constant and the constant survives. This is the second half of
0016 §9's rule ("constants rather than string literals") applied where it had
lapsed, and it restores the eventsource. prefix on those keys.
Attribute keys are a public telemetry schema. Their removal is a breaking change for anyone importing the names, and the emitted key strings change where literals were corrected. Pre-1.0 the project takes such removals without a deprecation shim, so both land directly and are recorded in the changelog.
Consequences¶
Positive:
- The catalogue means what 0016 said it means. Importing a constant is now evidence that something emits it.
- The lock spans carry prefixed, collision-safe keys consistent with every
other
eventsource.*attribute, and a test drives the realacquire()andrelease()paths and asserts the attributes reached the exported spans — the gate in.claude/rules/definition-of-done.mdfor a mechanism the library is obligated to invoke. - The failure mode that produced this ADR is now visible: a constant with no emitter is a one-line diff away from being noticed, because the emitters no longer hide behind literals.
Negative, accepted:
- Users importing a deleted constant get an
ImportErrorwith no deprecation window. That is the standing pre-1.0 policy and the cost is bounded: a name that was never emitted cannot appear in a working dashboard. - Dashboards keyed on the old literal lock attributes stop matching. The decision accepts this rather than emitting both spellings, which would reinstate the two-declaration-sites defect it exists to remove.
- Only the PostgreSQL lock manager emits lock spans; the in-memory lock manager emits none. The decision accepts that asymmetry — tracing is per-adapter under 0016, not a port obligation, and the in-memory manager is a test and single-process double where a distributed-lock span has no consumer.
Alternatives Rejected¶
Implement every declared attribute. This is the shape that produced the problem. Each key would need an emission site chosen to fit the name rather than a need, plus a test asserting a value nobody consumes. The catalogue would stay honest by construction and be uniformly useless.
Keep them, documented as reserved. A reserved namespace is a promise with no delivery date, and the audit trail shows this project does not come back for them. It also does not fix the user-facing failure: a documented-but-unemitted key still returns an empty query.
Emit both the old literal and the prefixed constant on lock spans. Doubles every lock span's attribute count permanently to protect dashboards that this library never advertised, and re-creates exactly the divergence — one fact, two spellings — that the change removes.