Otel.Propagator.TextMap.TraceContext (otel v0.4.1)

Copy Markdown View Source

W3C Trace Context Level 2 propagator (W3C 20-http_request_header_format.md §Traceparent Header L51-L244; OTel context/api-propagators.md §TextMap L114-L203).

Injects and extracts the traceparent and tracestate HTTP headers. Wire format per W3C §Header Field Values L75-L96 (ABNF):

value          = version "-" version-format
version        = 2HEXDIGLC    ; "00" for this spec; "ff" invalid
version-format = trace-id "-" parent-id "-" trace-flags
trace-id       = 32HEXDIGLC   ; 16 bytes; all-zeros forbidden
parent-id      = 16HEXDIGLC   ; 8 bytes; all-zeros forbidden
trace-flags    = 2HEXDIGLC    ; 8 bit flags

Example header value:

00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01

Design notes

Two places where we diverge from opentelemetry-erlang's otel_propagator_trace_context.erl to follow spec more strictly.

1. Lowercase-hex enforcement

W3C §trace-id L111 "If the trace-id value is invalid (for example if it contains non-allowed characters or all zeros), vendors MUST ignore the traceparent" and §parent-id L117 "Vendors MUST ignore the traceparent when the parent-id is invalid (for example, if it contains non-lowercase hex characters)".

We call lowercase_hex?/1 on every hex segment (version, trace-id, parent-id, trace-flags) to enforce 2HEXDIGLC. Erlang's binary_to_integer/2 accepts both cases, so its decoder silently lets uppercase hex through — we diverge to meet the spec MUST.

2. Flag byte preservation

W3C §Other Flags L202 "Vendors MUST set those to zero" applies to outgoing traffic only. On incoming, preserving the whole byte keeps us forward-compatible with future flag bits (e.g. the random-trace-id bit already defined in Level 2). Erlang rejects anything other than "00"/"01" (error(badarg)); we accept the full 0..255 range and store the byte verbatim.

3. Participating propagator, not pass-through service

W3C §Versioning L232 advises "Pass-through services should not analyze the version. They should expect that headers may have larger size limits in the future and only disallow prohibitively large headers."

This guidance targets pass-through services — intermediaries that forward the traceparent header without parsing it (proxies, gateways, load balancers).

This module is a participating propagator: we decode the header into a SpanContext, use it as the parent of new spans, and re-emit our own traceparent outbound. The participating-role MUSTs at W3C §Versioning L233-L244 apply to us, and decode_traceparent/1 satisfies each:

  • L233 unparseable version prefix → restart trace. Pattern-match failure bubbles a MatchError to extract/3's catch, which returns the original context (= fresh trace start).
  • L235 higher-version header shorter than 55 chars → restart. The binary pattern requires at least 55 bytes (clause 1) or a 55-byte core plus dash plus suffix (clause 2); anything shorter fails to match.
  • L236-L238 hex / dash shape checks for trace-id (32 hex + dash), parent-id (16 hex + dash), and flags (2 chars at end or followed by dash) — enforced by the binary pattern itself and lowercase_hex?/1.
  • L243 "MUST NOT parse or assume anything about unknown fields" — clause 2 captures the trailing bytes as _rest and discards them.
  • L244 "MUST use these fields to construct the new traceparent field according to the highest version of the specification known to the implementation"encode_traceparent/1 emits "00-...", the highest version this module implements.

The L232 hint about "prohibitively large headers" is a pass-through concern, not a MUST, and does not apply to our role. HTTP-layer size limits (Cowboy, Plug, etc.) govern the upper bound in practice.

Public API

FunctionRole
inject/3SDK (OTel API MUST) — TextMap Inject (L155-L182)
extract/3SDK (OTel API MUST) — TextMap Extract (L185-L203); MUST NOT throw on parse failure (L100-L102)
fields/0SDK (OTel API MUST) — Fields (L133-L152)
encode_traceparent/1Application (W3C header serialization) — §traceparent L75-L96
decode_traceparent/1Application (W3C header parsing) — §traceparent L75-L96 + §Versioning L228-L244
lowercase_hex?/1Application (W3C format predicate) — 2HEXDIGLC check

References

  • W3C Trace Context Level 2 §Traceparent Header: w3c-trace-context/spec/20-http_request_header_format.md L51-L244
  • W3C Trace Context Level 2 §Versioning: same file L227-L244
  • OTel Context §TextMap Propagator: opentelemetry-specification/specification/context/api-propagators.md L114-L203
  • Reference impl: opentelemetry-erlang/apps/opentelemetry_api/src/otel_propagator_trace_context.erl

Summary

Functions

Application (W3C header parsing) — parses a v00 or higher-version traceparent header value into a SpanContext.

Application (W3C header serialization) — encodes a SpanContext as a v00 traceparent header value.

SDK (OTel API MUST) — TextMap "Extract" (api-propagators.md L185-L203) for the W3C traceparent and tracestate headers.

SDK (OTel API MUST) — Fields (api-propagators.md L133-L152).

SDK (OTel API MUST) — TextMap "Inject" (api-propagators.md L155-L182) for the W3C traceparent and tracestate headers.

Application (W3C format predicate) — returns true iff hex is a non-empty string of ASCII lowercase hex digits (0-9a-f).

Functions

decode_traceparent(arg)

@spec decode_traceparent(value :: String.t()) :: Otel.Trace.SpanContext.t()

Application (W3C header parsing) — parses a v00 or higher-version traceparent header value into a SpanContext.

Accepts two forms per W3C §Versioning L228-L244:

  • Exactly 55 bytes with a non-ff version (matches v00 and higher versions carrying no extra fields yet).
  • 55+ bytes with a trailing -<future-fields> suffix for higher versions (v01+) that added fields.

Version "ff" is rejected per W3C §version L86. All hex segments are checked for lowercase per L83 2HEXDIGLC. A pair containing an all-zero trace-id or parent-id is rejected per L94-L95.

Raises MatchError / ArgumentError on malformed input — callers needing the spec-mandated graceful recovery (api-propagators.md L100-L102) should use extract/3, which wraps this call in a catch clause.

encode_traceparent(span_ctx)

@spec encode_traceparent(span_ctx :: Otel.Trace.SpanContext.t()) :: String.t()

Application (W3C header serialization) — encodes a SpanContext as a v00 traceparent header value.

Produces "00-<trace-id>-<parent-id>-<trace-flags>" per W3C §version-format L93 ABNF. All hex segments are lowercase. The trace_flags byte is rendered as two lowercase hex digits regardless of which bits are set (this propagator does not mask reserved bits on output per W3C §Other Flags L202; callers controlling the span context are responsible for zeroing unknown bits).

Callers are expected to have verified the span context is valid via SpanContext.valid?/1 beforehand; this function does not validate.

extract(ctx, carrier, getter)

@spec extract(
  ctx :: Otel.Ctx.t(),
  carrier :: Otel.Propagator.TextMap.carrier(),
  getter :: Otel.Propagator.TextMap.getter()
) :: Otel.Ctx.t()

SDK (OTel API MUST) — TextMap "Extract" (api-propagators.md L185-L203) for the W3C traceparent and tracestate headers.

Parses traceparent via decode_traceparent/1 and folds tracestate in via extract_tracestate/2. The resulting SpanContext is marked is_remote: true — it came from a remote carrier.

Per spec L100-L102 MUST NOT throw on parse failure — malformed headers (bad hex, zero IDs, uppercase, version "ff", etc.) cause the original context to be returned unchanged via a catch _, _ clause covering all three Elixir exit kinds (:error, :throw, :exit). "throw an exception" in the spec uses the general-programming sense; catch gives literal coverage.

fields()

@spec fields() :: [String.t()]

SDK (OTel API MUST) — Fields (api-propagators.md L133-L152).

Returns ["traceparent", "tracestate"] — the two header names this propagator reads and writes.

inject(ctx, carrier, setter)

SDK (OTel API MUST) — TextMap "Inject" (api-propagators.md L155-L182) for the W3C traceparent and tracestate headers.

Reads the current SpanContext from ctx. If it's valid (non-zero trace_id and span_id per SpanContext.valid?/1), emits a traceparent header and, when the tracestate is non-empty, a tracestate header. Invalid span contexts are not propagated — the carrier is returned unchanged.

lowercase_hex?(hex)

@spec lowercase_hex?(hex :: String.t()) :: boolean()

Application (W3C format predicate) — returns true iff hex is a non-empty string of ASCII lowercase hex digits (0-9a-f).

Used internally to enforce W3C §Header Field Values L83 / L94-L95 (2HEXDIGLC / 16HEXDIGLC / 32HEXDIGLC). Exposed so callers doing ad-hoc traceparent manipulation can apply the same check without duplicating the regex.