0121: Give openarmature's Mandated Diagnostics a Stable Identity¶
- Status: Accepted
- Author: Chris Colinsky
- Created: 2026-08-19
- Accepted: 2026-08-27
- Targets:
- spec/observability/spec.md §6 Driving span lifecycle: the two
WARNING-level log records §6 mandates (the accepted-shared-provider arm and the cannot-establish-binding arm) MUST carry a stable event name identifying which diagnostic fired. Today §6 mandates the record's severity and nothing else, so nothing about it is portably assertable and any WARNING from anywhere satisfies it. - spec/observability/spec.md §7 Log correlation: define the event-name contract for openarmature's own
diagnostics. Names live in the
openarmature.namespace and are carried on the OTelLogRecordEventNamefield, which the Logs Data Model defines as identifying "the class / type of the Event". Enumerate the names for the four diagnostics openarmature currently specifies. - spec/conformance-adapter/spec.md §5.5: add an
event_namekey toexpected.log_recordsentries, so a fixture can assert which diagnostic fired rather than only that something logged at a severity. - spec/conformance-adapter/spec.md §5.5: add an
otel_observer: {disable_provider_payload: <bool>}directive. The OTel observer's payload flag has no defined case-level control, and thelangfuse_clientparagraph states the OTel side sits at its §5.5.4 default for exactly the fixture family that needs to vary it. Reconcile that paragraph to the new directive. - spec/observability/conformance/158-langfuse-payload-leak-fail-closed.yaml: assert
event_namein all sixlog_recordsblocks, and rewrite case 2 (singleton_preexists_raises_otel_not_suppressing) onto theotel_observerdirective. - spec/observability/conformance/014-otel-llm-payload-truncation.yaml: migrate its bare top-level
disable_provider_payloadto the new directive, so one mechanism configures the OTel observer rather than two. - docs/compatibility.md: add a matrix row for the OpenTelemetry Logs data model, which this proposal makes openarmature's normative text depend on for the first time. The matrix has rows for the semantic conventions and for the trace/span core spec but none for Logs. Status Stable, verified 2026-08-19 against the published data-model specification.
- Related: 0114 (introduced the mode-(b) isolation rule), 0116 (fail-closed arms and fixture 158), 0117 and 0118 (extended fixture 158), 0120 (definition-homes rule, which this proposal's new directives satisfy)
Summary¶
openarmature mandates two WARNING log records in observability §6 but specifies only their severity, so a
conformance fixture can assert that a warning happened and never that the right one did. Downstream
mutation testing confirmed the gap: silencing the mandated emitter left fixture 158 green because an
unrelated warning satisfied the assertion. This gives openarmature's diagnostics stable event names and lets
fixtures assert them. It also adds the missing otel_observer directive, without which one of fixture 158's
cases cannot express what it claims to test.
Motivation¶
A mandated diagnostic with no identity cannot be asserted¶
§6 requires a WARNING-level log record at two points: when the caller has accepted a shared provider, and
when openarmature cannot establish the client's bound provider and suppresses its payload instead. Both are
MUSTs. Neither says anything about the record beyond its severity.
§7's required log-record fields are all cross-cutting: openarmature.correlation_id,
openarmature.session_id, openarmature.user.<key>, trace_id, span_id. Every record emitted during an
invocation carries them, so none of them distinguishes one diagnostic from another.
The consequence shows up in fixture 158, whose six log_records blocks each declare exactly one key:
An adapter satisfies that by emitting any WARNING, from anywhere, during the case. Downstream mutation testing demonstrated this concretely: downgrading only the mandated emitter to a lower severity left the fixture green, because a second, unrelated warning was in flight during the same window. The assertion was live and checking the wrong record.
The log_records directive is not the limitation. It already accepts body and attributes alongside
level, matched as a subset. The limitation is that openarmature specifies nothing stable for a fixture to
match on. A message substring is the obvious candidate and the wrong one: messages are prose, they get
reworded, and pinning them makes every copy-edit a conformance break.
Precedent, and the shape openarmature already reaches for¶
The token-budget diagnostic (§5.5.15, §7) is identified by the openarmature.prompt.* attributes it carries.
Identity through structured fields rather than message text is already the house pattern; it has simply never
been applied to the §6 diagnostics, which are the ones a fixture is required to assert.
Fixture 158 case 2 cannot express what it claims¶
singleton_preexists_raises_otel_not_suppressing differs from case 1 by a single case-level key:
That key has no definition in the conformance-adapter spec. Worse, the langfuse_client paragraph that
governs this fixture family states the observers' payload settings use "langfuse_observer:
{disable_provider_payload: <bool>} for the Langfuse side, and the OTel observer's §5.5.4 default (True)
for the OTel side". So the contract covering this fixture says the OTel side sits at its default here, while
the case sets it through an undefined key.
The case's intent is sound and worth preserving. It is a regression guard against the alternative 0116 considered and rejected, in which the raise fired only when a composed OTel observer was suppressing payload. An implementation still on that rule passes case 1 and fails case 2, and nothing else in the corpus catches it. But as written the case is contradicted where it is used and ambiguous where it is not, so an adapter is entitled to ignore the key, and the case then differs from case 1 in nothing an adapter must honor.
Detailed design¶
§7: the event-name contract¶
The OTel Logs Data Model, which is Stable, defines a top-level LogRecord field EventName: "Name that
identifies the class / type of the Event. This name SHOULD uniquely identify the event structure (both
attributes and body)." That is precisely the affordance missing here, and openarmature's stable-only adoption
policy permits it because the data model document carries Stable status. The field is not separately
stability-marked within it, so adoption rests on the document's status rather than a per-field marker.
Diagnostic event names. A log record openarmature emits to signal a condition it specifies MUST carry an event name on the OTel
LogRecordEventNamefield. Event names are in theopenarmature.namespace, are stable identifiers, and MUST NOT be reworded once shipped; the record's human-readable message is free to change independently.The names openarmature currently defines:
Event name Emitted when Obligation openarmature.langfuse.shared_provider_accepted§6, the caller has accepted a shared provider and openarmature proceeds MUST openarmature.langfuse.payload_suppressed§6, openarmature cannot establish the client's bound provider and suppresses every harvested-payload channel MUST openarmature.token_budget.exceeded§5.5.15, an active prompt's token_budgetis exceededSHOULD openarmature.langfuse.supplied_client_shared_provider§6 mode (a), a caller-supplied client is determined to be bound to a shared provider MAY The obligation column restates the obligation on the record, which this rule does not change. Where a record is emitted, it carries its event name.
Adopting the field is distinct from adopting an upstream name: the Event semantic conventions that prescribe particular event names remain in Development, and openarmature names its own diagnostics in its own namespace rather than taking any name from them.
Naming note. event_name also appears in pipeline-utilities §6.3 as a caller-supplied field on the
failure-isolation middleware. These are unrelated: that one is chosen by the caller to label a catch site and
is a caller-attached dimension (observability §8.4), while these are openarmature-emitted identifiers for
conditions the spec mandates. §7 states the distinction so the two are not conflated.
§6: the two mandated records carry their names¶
Both MUST sites gain a clause naming the event they emit, cross-referencing §7's table. No new obligation to emit is created and no existing one is changed; the records were already mandated, and this says which diagnostic each one is.
conformance-adapter §5.5: event_name on log_records¶
An
expected.log_recordsentry MAY carryevent_name: <string>alongsidelevel,bodyandattributes, asserting the record's OTelEventNamefield equals the given value. Matching remains a subset over the fields an entry specifies, and the list remains non-exhaustive.
This is what lets a fixture pin the mandated diagnostic rather than any record at a severity.
conformance-adapter §5.5: the otel_observer directive¶
otel_observer: {disable_provider_payload: <bool>}configures the composed OTel observer's payload flag for the case, the OTel-side counterpart of the existinglangfuse_observerdirective. Omitted, the OTel observer keeps its §5.5.4 default.An adapter MUST honor the directive when present. A case that sets it and an adapter that ignores it produce the same observable result as a case that does not set it, so silently dropping it makes the case vacuous rather than failing.
The langfuse_client paragraph's sentence about the OTel side sitting at its §5.5.4 default is amended to
point at this directive, since that sentence is what currently contradicts fixture 158 case 2.
Adding both directives to §5.5 satisfies proposal 0120's definition-homes rule: they are used by more than one capability's fixtures in prospect and their contracts are general, so §5 is the correct home.
Fixtures¶
158. All six log_records blocks gain event_name. Five assert
openarmature.langfuse.payload_suppressed, being the cannot-establish arms
(singleton_preexists_suppresses, state_channel_preexists_suppresses, hook_preexists_suppresses, and the
two error-message cases 0118 repointed onto that arm). One asserts
openarmature.langfuse.shared_provider_accepted, the opt-out case
(singleton_preexists_optout_proceeds). Case 2 is rewritten to set
otel_observer: {disable_provider_payload: false} in place of the bare key, so its differentiator is a
directive an adapter is obliged to honor.
014. Its bare top-level disable_provider_payload becomes otel_observer: {disable_provider_payload:
false}. No semantic change; the fixture is OTel-only, so the bare key was unambiguous there. Migrating it
means one mechanism configures the OTel observer rather than two, and leaves no undocumented form behind for
a future fixture to copy.
Conformance test impact¶
The event-name requirement is a new obligation on implementations. An implementation emitting the two §6 records without an event name becomes non-conforming. That is the point: the records were mandated but unidentifiable, so the obligation was unassertable.
Fixture 158's six blocks tighten. An implementation that emits the right record already passes; one that satisfies the old assertion with an unrelated warning starts failing, which is the defect being closed.
Fixture 158 case 2 becomes non-vacuous. Under the current text an adapter may ignore its differentiator, making it a duplicate of case 1. After this it exercises the axis its name claims.
Fixture 014 is a mechanical migration, no assertion change.
No new fixture. The six existing blocks and the two rewritten cases carry the change, and adding a seventh case would assert the same rule the tightened blocks already gate.
Alternatives considered¶
- Do nothing, and record the fixture as deliberate. Rejected: the assertion reads as working while checking the wrong record, which is worse than an absent assertion because it reports coverage that does not exist.
- Assert a message substring. Rejected: messages are prose and get reworded, so every copy-edit becomes a conformance break, and a substring is not a stable identifier in any sense the spec can hold an implementation to.
- Carry the identity on an
openarmature.*log-record attribute rather thanEventName. Rejected: the Logs Data Model defines a field whose stated purpose is identifying the class of the event, in a document carrying Stable status, so introducing a parallel attribute would duplicate a standard mechanism. This was the fallback if the data model had turned out to be Development. - Leave fixture 158 case 2 as a documented duplicate of case 1. Rejected: it guards against a rejected alternative that nothing else in the corpus catches, and the guard costs one directive.
- Define the
otel_observerdirective without migrating fixture 014. Rejected: it would leave two ways to set the same flag, one of them undocumented, which is the pattern proposal 0120 exists to stop.
Open questions¶
- Whether the token-budget and mode-(a) names belong in this proposal. They are included so §7's table is the complete set rather than the two fixture 158 happens to assert, but neither is fixture-gated here and both keep their existing obligation levels. Naming them now costs nothing and avoids a second pass; the alternative is scoping this to the two MUSTs.
- Whether
EventNameshould also carry openarmature's non-diagnostic records. This proposal covers records signalling a condition openarmature specifies. Whether ordinary framework logging should be event-named is a broader question about openarmature's logging surface, left out of scope.