You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem:SpanProcessor.on_ending runs after the span is already frozen, so any attribute/event/link a processor adds in the hook is silently dropped (spec MUST violated).
Fix: Introduce an intermediate 'ending' state so on_ending runs while the span is still mutable by the ending thread, then finalize. Rework ConcurrentMultiSpanProcessor.on_ending (thread-pool fan-out is incompatible with single-thread mutability).
📣 Public-facing draft — to be opened upstream in open-telemetry/opentelemetry-python. No internal references; copy verbatim.
Title:SpanProcessor.on_ending runs after the span is frozen — mutations are silently dropped (spec MUST)
Summary
SpanProcessor.on_ending exists, but the SDK freezes the span before invoking it, so any attribute/event/link a processor adds inside on_ending is silently discarded. The specification requires the span to still be mutable while OnEnding runs.
Evidence
In opentelemetry-sdk/.../trace/__init__.py, Span.end() sets self._end_time and marks the attributes immutable (self._attributes._immutable = True) before calling self._span_processor._on_ending(self). All mutators are gated by the @_check_span_ended decorator (and inline _end_time is not None checks in set_attribute/set_attributes), so any write attempted during on_ending is rejected and dropped.
Spec
specification/trace/sdk.md — the OnEnding hook is called synchronously on the thread that ends the span, and the span MUST still be mutable during the callback so a processor can enrich it (e.g. add attributes) before it becomes read-only.
Reference implementation
Java models the span lifecycle with a three-state EndState in SdkSpan.java: the span enters an intermediate "ending" state during which the ending thread may still mutate it, then it is finalized to read-only.
Proposed fix
Introduce an intermediate "ending" state in Span.end(): record end_time, mark the span as ending and remember the ending thread, run on_ending while the span is still writable by that thread only, then finalize (freeze attributes) in a finally block so the span always ends up read-only even if a hook raises. Mutation guards must permit writes during the ending window only from the ending thread and reject them afterward or from other threads. Note: ConcurrentMultiSpanProcessor currently fans on_ending out to a thread pool, which is incompatible with "mutable only from the ending thread"; run its on_ending hooks synchronously on the ending thread (this also matches the documented contract that on_ending is synchronous).
Activity
ocelotl commentedon Jul 22, 2026
🔒 Internal (dash0) — not for upstream.
Tracking for the SpanProcessor.on_ending mutability fix.
oss_12_onending_frozen26417e1e1opentelemetry-sdk+ changelog; span-processor suite 28 passed; full SDK suite 832 passed.ocelotl commentedon Jul 22, 2026
📣 Public-facing draft — to be opened upstream in
open-telemetry/opentelemetry-python. No internal references; copy verbatim.Title:
SpanProcessor.on_endingruns after the span is frozen — mutations are silently dropped (spec MUST)Summary
SpanProcessor.on_endingexists, but the SDK freezes the span before invoking it, so any attribute/event/link a processor adds insideon_endingis silently discarded. The specification requires the span to still be mutable whileOnEndingruns.Evidence
In
opentelemetry-sdk/.../trace/__init__.py,Span.end()setsself._end_timeand marks the attributes immutable (self._attributes._immutable = True) before callingself._span_processor._on_ending(self). All mutators are gated by the@_check_span_endeddecorator (and inline_end_time is not Nonechecks inset_attribute/set_attributes), so any write attempted duringon_endingis rejected and dropped.Spec
specification/trace/sdk.md— theOnEndinghook is called synchronously on the thread that ends the span, and the span MUST still be mutable during the callback so a processor can enrich it (e.g. add attributes) before it becomes read-only.Reference implementation
Java models the span lifecycle with a three-state
EndStateinSdkSpan.java: the span enters an intermediate "ending" state during which the ending thread may still mutate it, then it is finalized to read-only.Proposed fix
Introduce an intermediate "ending" state in
Span.end(): recordend_time, mark the span as ending and remember the ending thread, runon_endingwhile the span is still writable by that thread only, then finalize (freeze attributes) in afinallyblock so the span always ends up read-only even if a hook raises. Mutation guards must permit writes during the ending window only from the ending thread and reject them afterward or from other threads. Note:ConcurrentMultiSpanProcessorcurrently fanson_endingout to a thread pool, which is incompatible with "mutable only from the ending thread"; run itson_endinghooks synchronously on the ending thread (this also matches the documented contract thaton_endingis synchronous).