# `Otel.Propagator.TextMap.TraceContext`
[🔗](https://github.com/yangbancode/otel/blob/main/lib/otel/propagator/text_map/trace_context.ex#L1)

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

| Function | Role |
|---|---|
| `inject/3` | **SDK** (OTel API MUST) — TextMap Inject (L155-L182) |
| `extract/3` | **SDK** (OTel API MUST) — TextMap Extract (L185-L203); MUST NOT throw on parse failure (L100-L102) |
| `fields/0` | **SDK** (OTel API MUST) — Fields (L133-L152) |
| `encode_traceparent/1` | **Application** (W3C header serialization) — §traceparent L75-L96 |
| `decode_traceparent/1` | **Application** (W3C header parsing) — §traceparent L75-L96 + §Versioning L228-L244 |
| `lowercase_hex?/1` | **Application** (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`

# `decode_traceparent`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@spec inject(
  ctx :: Otel.Ctx.t(),
  carrier :: Otel.Propagator.TextMap.carrier(),
  setter :: Otel.Propagator.TextMap.setter()
) :: Otel.Propagator.TextMap.carrier()
```

**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?`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
