Skip to content

feat: add W3CTraceContext with tracestate support#173

Merged
andylokandy merged 3 commits into
fast:mainfrom
TennyZhuang:feat/w3c-tracestate
May 4, 2026
Merged

feat: add W3CTraceContext with tracestate support#173
andylokandy merged 3 commits into
fast:mainfrom
TennyZhuang:feat/w3c-tracestate

Conversation

@TennyZhuang
Copy link
Copy Markdown
Contributor

Summary

Adds W3CTraceContext, a boundary/header wrapper that extends SpanContext with optional W3C tracestate support. This enables vendor-specific key-value pairs to survive propagation across process boundaries via the standard traceparent/tracestate header pair.

Closes #171

Design

  • Separate type from SpanContext to preserve SpanContext: Copy. W3CTraceContext is Clone but not Copy since it holds an Option<String>.
  • Boundary wrapper semantics: W3CTraceContext is explicitly scoped for encoding/decoding W3C headers at RPC injection/extraction points. The tracestate field is not carried through fastrace's internal span machinery.
  • Empty string normalization: Both decode(..., Some("")) and with_tracestate("") normalize to None for API consistency.
  • Case-insensitive header matching (eq_ignore_ascii_case) per W3C spec, avoiding per-header allocation.

API

// Decode from W3C headers
let ctx = W3CTraceContext::decode(traceparent, Some(tracestate))?;

// Build programmatically
let ctx = W3CTraceContext::new(span_context).with_tracestate("rw=frontend");

// Encode for outgoing headers
let headers: Vec<(String, String)> = ctx.encode_headers();

// Decode from arbitrary header iterators (case-insensitive)
let ctx = W3CTraceContext::decode_headers(headers.iter().map(|(k, v)| (k.as_str(), v.as_str())))?;

// Convert to SpanContext (discards tracestate)
let span_ctx: SpanContext = ctx.span_context;

Test plan

  • 14 unit tests covering: construction, builder, round-trip encode/decode, empty/missing tracestate, header encode/decode, case-insensitive keys, From conversion
  • All 45 lib tests + 64 doc tests pass
  • Reviewed by two agents (Kimi: PASS, Codex: PASS after two fixes)

@tisonkun
Copy link
Copy Markdown
Collaborator

tisonkun commented May 4, 2026

IIRC we have some downstream extension libs that supports W3C trace context - https://github.com/fast/fastrace-reqwest/blob/main/src/lib.rs

But there can be some difference from the changeset here.

@TennyZhuang
Copy link
Copy Markdown
Contributor Author

Thanks for the pointer! Yes, fastrace-reqwest currently handles traceparent-only injection via SpanContext::encode_w3c_traceparent().

This PR is complementary — W3CTraceContext is a boundary wrapper that adds tracestate support alongside traceparent, with decode/encode_headers for both inject and extract directions. The two main differences:

  1. tracestate supportW3CTraceContext carries and round-trips the tracestate header, which SpanContext alone cannot hold (it's Copy).
  2. Bidirectionaldecode_headers extracts from incoming headers (both traceparent + tracestate), while encode_headers produces both for outgoing requests.

fastrace-reqwest could eventually use W3CTraceContext::encode_headers() instead of calling encode_w3c_traceparent() directly, gaining tracestate propagation for free. But that would be a follow-up change in fastrace-reqwest, not in scope here.

@andylokandy
Copy link
Copy Markdown
Collaborator

Thanks for putting this together. I think this PR works as a boundary helper, but we should be clear about the limitation of that design.

Since tracestate is not carried by SpanContext or the span and local-parent chain, propagation only works if the application keeps the returned W3CTraceContext or the tracestate value separately and uses it again during outbound injection. If code extracts span_context, creates Span::root, and later calls SpanContext::from_span or SpanContext::current_local_parent() to inject outgoing headers, the original tracestate is gone. So this PR does not make fastrace automatically propagate the full W3C context; it only gives users the pieces to do that at the boundary.

The cleaner alternative would be a breaking change: make tracestate part of the propagation context that enters through Span::root and is carried through child spans, local parents, SpanContext::from_span, and SpanContext::current_local_parent(). That likely means SpanContext can no longer be Copy, for example if it stores Option<Arc<str>>, and any code relying on struct literals or implicit copies would need to adjust.

I think we should decide which direction we want before merging. If this PR stays on the boundary-helper path, the docs should state that limitation explicitly, and decode_headers should also preserve repeated tracestate headers by joining them in order.

TennyZhuang pushed a commit to TennyZhuang/fastrace that referenced this pull request May 4, 2026
Per andylokandy's review comment on fast#173, this commit strengthens the
documentation and implementation of W3CTraceContext to clarify its
boundary-helper role and handle repeated tracestate headers correctly.

Changes:
1. Expanded W3CTraceContext top-level docs to explicitly state the
   round-trip limitation: extracting span_context and later using
   SpanContext::from_span or current_local_parent() loses tracestate.
   Added concrete example of the loss scenario.

2. Updated decode_headers to join multiple tracestate headers with
   commas per W3C Trace Context §3.3.1.1. Empty or whitespace-only
   tracestate values are filtered before joining to ensure proper
   normalization (e.g., ["", ""] → None, not ",").

3. Added three new tests covering repeated tracestate headers:
   - repeated non-empty headers joined with commas
   - mixed empty/non-empty headers with filtering
   - all-empty headers normalized to None

All 17 W3C trace context tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@TennyZhuang
Copy link
Copy Markdown
Contributor Author

Thank you for the detailed feedback, @andylokandy. After discussing with the team, we're staying on the boundary-helper path for this PR, and I've pushed a commit addressing your specific asks.

Design decision

The boundary-helper approach aligns well with how RisingWave (and likely other users) already propagate trace context: the inbound W3CTraceContext is decoded at the RPC boundary, the span_context is used to create spans, and the tracestate is stored separately for re-injection on outbound calls. RisingWave's TracingContext already carries SpanContext + Option<String> tracestate as independent fields, so the boundary-helper design fits without friction.

The breaking-change path (carrying tracestate through the span chain) would be the "cleaner" abstraction, but the cost is significant: SpanContext loses Copy, which ripples through the entire API surface. We think that can be explored as a separate future initiative if there's demand.

Changes in this commit (205a8a0)

  1. Strengthened W3CTraceContext top-level docs — explicitly states the round-trip limitation: extracting span_context, creating spans, and later using SpanContext::from_span or current_local_parent() loses the original tracestate. Advises storing the W3CTraceContext or tracestate value separately for outbound propagation.

  2. decode_headers now joins repeated tracestate headers — per W3C Trace Context §3.3.1.1, multiple tracestate headers are joined with commas in encounter order. Empty or whitespace-only values are filtered before joining (so ["", ""]None, not ",").

  3. Three new tests covering the repeated-header edge cases:

    • repeated_tracestate: ["rw=frontend", "congo=t61rcWkgMzE"]Some("rw=frontend,congo=t61rcWkgMzE")
    • repeated_empty_mixed: ["", "rw=frontend", " "]Some("rw=frontend")
    • all_empty_tracestate: ["", " "]None

All 17 W3C trace context tests pass locally. Let me know if you'd like any further changes.

TennyZhuang and others added 3 commits May 4, 2026 15:20
Introduce `W3CTraceContext`, a boundary/header wrapper that pairs a `SpanContext`
with an optional W3C `tracestate` string. This enables vendor-specific key-value
pairs to survive propagation across process boundaries via the standard
`traceparent`/`tracestate` header pair.

Key design decisions:
- Separate type from `SpanContext` to preserve `SpanContext: Copy`
- Empty tracestate strings normalized to `None` for API consistency
- Case-insensitive header matching per W3C spec (no allocation)
- Explicit documentation that tracestate is NOT retained through
  fastrace's internal span machinery

Closes fast#171
Join two-line doc comment to single line to satisfy nightly cargo fmt --all --check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per andylokandy's review comment on fast#173, this commit strengthens the
documentation and implementation of W3CTraceContext to clarify its
boundary-helper role and handle repeated tracestate headers correctly.

Changes:
1. Expanded W3CTraceContext top-level docs to explicitly state the
   round-trip limitation: extracting span_context and later using
   SpanContext::from_span or current_local_parent() loses tracestate.
   Added concrete example of the loss scenario.

2. Updated decode_headers to join multiple tracestate headers with
   commas per W3C Trace Context §3.3.1.1. Empty or whitespace-only
   tracestate values are filtered before joining to ensure proper
   normalization (e.g., ["", ""] → None, not ",").

3. Added three new tests covering repeated tracestate headers:
   - repeated non-empty headers joined with commas
   - mixed empty/non-empty headers with filtering
   - all-empty headers normalized to None

All 17 W3C trace context tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@TennyZhuang TennyZhuang force-pushed the feat/w3c-tracestate branch from 205a8a0 to bcece82 Compare May 4, 2026 07:21
@andylokandy andylokandy merged commit c70ae4d into fast:main May 4, 2026
11 checks passed
@andylokandy
Copy link
Copy Markdown
Collaborator

@TennyZhuang Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tracestate support to W3C trace context API

3 participants