Trace propagation, in the standard library.
The shipped trace context lived inside :mod:symfonic.observability.otel,
which is gated behind the [otel] extra. That made "is this run correlated
with that log line?" a question only answerable when an optional dependency
was installed — so adopters without the extra had run ids and nothing to join
them to.
Here the trace scope is a W3C traceparent (a hex trace id, a hex span id,
a sampled flag) carried in a :mod:contextvars variable. It works with zero
OTEL installed, and the OTEL adapter adopts this scope rather than minting a
second, disagreeing one.
TraceScope
dataclass
TraceScope(trace_id: str, span_id: str, sampled: bool = True)
A W3C trace context, without a W3C library.
traceparent
Render the traceparent header value.
Source code in src/symfonic/services/observability/trace.py
| def traceparent(self) -> str:
"""Render the ``traceparent`` header value."""
return f"{_VERSION}-{self.trace_id}-{self.span_id}-{'01' if self.sampled else '00'}"
|
bind_run
bind_run(scope: RunScope, trace: TraceScope) -> tuple[object, object]
Bind both carriers; returns the tokens :func:unbind_run needs.
Source code in src/symfonic/services/observability/trace.py
| def bind_run(scope: RunScope, trace: TraceScope) -> tuple[object, object]:
"""Bind both carriers; returns the tokens :func:`unbind_run` needs."""
return _run_scope.set(scope), _trace_scope.set(trace)
|
current_run_scope
current_run_scope() -> RunScope | None
The run being observed in this context, or None outside a run.
Source code in src/symfonic/services/observability/trace.py
| def current_run_scope() -> RunScope | None:
"""The run being observed in this context, or ``None`` outside a run."""
return _run_scope.get()
|
current_trace
current_trace() -> TraceScope | None
The trace scope of the run being observed, or None.
Source code in src/symfonic/services/observability/trace.py
| def current_trace() -> TraceScope | None:
"""The trace scope of the run being observed, or ``None``."""
return _trace_scope.get()
|
parse_traceparent
parse_traceparent(header: str) -> TraceScope | None
Parse a traceparent header, or return None.
Refusing is deliberate. A malformed header that is "best-effort repaired"
produces a trace id that correlates with nothing, which is worse than no
correlation at all because it looks like one.
Source code in src/symfonic/services/observability/trace.py
| def parse_traceparent(header: str) -> TraceScope | None:
"""Parse a ``traceparent`` header, or return ``None``.
Refusing is deliberate. A malformed header that is "best-effort repaired"
produces a trace id that correlates with nothing, which is worse than no
correlation at all because it looks like one.
"""
if not header:
return None
parts = header.split("-")
if len(parts) != 4:
return None
version, trace_id, span_id, flags = parts
if version != _VERSION:
return None
if len(trace_id) != 32 or len(span_id) != 16 or len(flags) != 2:
return None
if trace_id == _ZERO_TRACE or span_id == _ZERO_SPAN:
return None
try:
int(trace_id, 16), int(span_id, 16), int(flags, 16)
except ValueError:
return None
return TraceScope(
trace_id=trace_id, span_id=span_id, sampled=bool(int(flags, 16) & 0x01)
)
|
trace_for_run
trace_for_run(run_id: str) -> TraceScope
Derive a trace scope for run_id.
The trace id is derived from the run id rather than drawn at random so the
same run always lands in the same trace: a log line that only recorded a
run id can still be joined to its trace after the fact, which is exactly
the case where an operator needs the join and no longer has the process.
The span id is random, because two observers of the same run are two spans,
not one.
Source code in src/symfonic/services/observability/trace.py
| def trace_for_run(run_id: str) -> TraceScope:
"""Derive a trace scope for ``run_id``.
The trace id is derived from the run id rather than drawn at random so the
same run always lands in the same trace: a log line that only recorded a
run id can still be joined to its trace after the fact, which is exactly
the case where an operator needs the join and no longer has the process.
The span id is random, because two observers of the same run are two spans,
not one.
"""
digest = hashlib.sha256(run_id.encode("utf-8", errors="ignore")).hexdigest()[:32]
if digest == _ZERO_TRACE: # pragma: no cover - unreachable for sha256
digest = f"{_ZERO_TRACE[:-1]}1"
return TraceScope(trace_id=digest, span_id=secrets.token_hex(8))
|
unbind_run
unbind_run(tokens: tuple[object, object]) -> None
Release the carriers bound by :func:bind_run.
A token minted in another context cannot be reset, and that happens for
real: a terminal event delivered from a drain worker runs in a different
task than the one that opened the run. Falling back to an explicit None
keeps the leak bounded to that context instead of raising inside teardown.
Source code in src/symfonic/services/observability/trace.py
| def unbind_run(tokens: tuple[object, object]) -> None:
"""Release the carriers bound by :func:`bind_run`.
A token minted in another context cannot be reset, and that happens for
real: a terminal event delivered from a drain worker runs in a different
task than the one that opened the run. Falling back to an explicit ``None``
keeps the leak bounded to that context instead of raising inside teardown.
"""
for var, token in zip((_run_scope, _trace_scope), tokens, strict=True):
try:
var.reset(token) # type: ignore[arg-type]
except ValueError:
var.set(None) # type: ignore[arg-type]
|