https://github.com/imp/trace-context-level3-rs
https://github.com/imp/trace-context-level3-rs
Last synced: about 16 hours ago
JSON representation
- Host: GitHub
- URL: https://github.com/imp/trace-context-level3-rs
- Owner: imp
- License: other
- Created: 2026-06-29T06:23:17.000Z (9 days ago)
- Default Branch: main
- Last Pushed: 2026-06-30T12:31:56.000Z (7 days ago)
- Last Synced: 2026-06-30T14:22:13.360Z (7 days ago)
- Language: Rust
- Size: 72.3 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE-APACHE
Awesome Lists containing this project
README
# trace-context-level3-rs
W3C Trace Context Level 3 implementation in Rust.
Parses and serializes the `traceparent` and `tracestate` HTTP headers, propagates
context through Tower middleware, and exposes an axum extractor — all following the
[W3C Trace Context specification](https://w3c.github.io/trace-context/).
## Workspace
| Crate | Description |
|---|---|
| [`trace-context-level3`] | Core types: `TraceParent`, `TraceState`, `TraceId`, `ParentId`, `TraceFlags` |
| [`trace-context-level3-http`] | HTTP extraction and injection via `TraceContext` |
| [`trace-context-level3-tower`] | Tower middleware (`TraceContextLayer`) and optional task-local storage |
| [`trace-context-level3-axum`] | axum `FromRequestParts` extractor (`TraceContext`) |
---
## Core types — `trace-context-level3`
```toml
[dependencies]
trace-context-level3 = { git = "https://github.com/imp/trace-context-level3-rs" }
```
```rust
use trace_context_level3::{TraceParent, TraceState};
// Parse a traceparent header value
let tp: TraceParent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
.parse()
.unwrap();
assert_eq!(tp.trace_id.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(tp.parent_id.to_string(), "00f067aa0ba902b7");
assert!(tp.is_sampled());
// Parse and mutate tracestate
let mut state: TraceState = "vendorname=opaquevalue".parse().unwrap();
state.insert("myvendor", "data").unwrap();
assert_eq!(state.to_string(), "myvendor=data,vendorname=opaquevalue");
```
Random ID generation requires the `rand` feature:
```toml
trace-context-level3 = { ..., features = ["rand"] }
```
`serde` support (`Serialize`/`Deserialize` for all public types) is available as an optional feature:
```toml
trace-context-level3 = { ..., features = ["serde"] }
```
Types serialize as their canonical wire-format strings (`TraceParent`, `TraceState`, `TraceId`, `ParentId`) or as a plain `u8` (`TraceFlags`). `TraceContext` in `trace-context-level3-http` (enabled with `trace-context-level3-http/serde`) serializes as a struct with `"traceparent"` and `"tracestate"` string fields:
```json
{
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"tracestate": "vendor=value"
}
```
---
## HTTP extraction — `trace-context-level3-http`
```toml
[dependencies]
trace-context-level3-http = { git = "https://github.com/imp/trace-context-level3-rs" }
```
```rust
use http::HeaderMap;
use trace_context_level3_http::TraceContext;
// Extract
let mut headers = HeaderMap::new();
headers.insert("traceparent",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".parse().unwrap());
match TraceContext::extract(&headers) {
Ok(Some(ctx)) => println!("trace-id: {}", ctx.traceparent.trace_id),
Ok(None) => println!("no traceparent header"),
Err(e) => println!("invalid traceparent: {e}"),
}
// Inject
let mut outgoing = HeaderMap::new();
ctx.inject(&mut outgoing);
```
`extract` returns `Result, TraceParentError>`:
- `Ok(None)` — no `traceparent` header present
- `Err(_)` — header present but invalid (e.g. multiple values, bad format)
- `Ok(Some(ctx))` — valid context; malformed or missing `tracestate` is treated
leniently and silently dropped, per spec guidance
---
## Tower middleware — `trace-context-level3-tower`
```toml
[dependencies]
trace-context-level3-tower = { git = "https://github.com/imp/trace-context-level3-rs" }
```
`TraceContextLayer` intercepts every request:
1. If a valid `traceparent` header arrives, it advances the span by generating a
fresh `parent-id` (preserving the `trace-id`) and stores the result in request
extensions.
2. If the header is absent or malformed, it starts a new root span with random IDs.
```rust
use tower::ServiceBuilder;
use trace_context_level3_tower::{TraceContextLayer, TraceResponseLayer};
let service = ServiceBuilder::new()
.layer(TraceContextLayer::new()) // outer: extracts/creates context
.layer(TraceResponseLayer::new()) // inner: adds Server-Timing to response
.service(inner);
```
### Response propagation (`Server-Timing`)
`TraceResponseLayer` appends `Server-Timing: trace;desc=` to every
HTTP response, implementing the Level 3 response header. It must be placed after
`TraceContextLayer` in the stack (inside it, closer to the handler).
On the receiving side, `extract_server_timing` reads the header back:
```rust
use trace_context_level3_http::extract_server_timing;
if let Some(tp) = extract_server_timing(response.headers()) {
println!("server span: {tp}");
}
```
### Task-local storage
Enable the `task-local` feature to additionally store the context in a
`tokio::task_local!` for the duration of each request future, so any code in the
call stack can read it without threading it through function arguments:
```toml
trace-context-level3-tower = { ..., features = ["task-local"] }
```
```rust
use trace_context_level3_tower::{TraceContextLayer, TRACE_CONTEXT};
let layer = TraceContextLayer::new().enable_task_local();
// Inside a handler called from within that layer:
TRACE_CONTEXT.with(|ctx| println!("{}", ctx.traceparent));
```
---
## axum extractors — `trace-context-level3-axum`
```toml
[dependencies]
trace-context-level3-axum = { git = "https://github.com/imp/trace-context-level3-rs" }
```
Two extractors are provided, both implementing `FromRequestParts`:
### `TraceContext` — spec-compliant (recommended)
When `traceparent` is absent or invalid a fresh root span is generated, matching
the W3C spec's guidance ("receivers SHOULD start a new trace"). Handlers always
receive a valid context and the extractor never rejects.
```rust
use axum::{Router, routing::get};
use trace_context_level3_axum::TraceContext;
use trace_context_level3_tower::TraceContextLayer;
async fn handler(ctx: TraceContext) -> String {
ctx.traceparent.to_string()
}
let app = Router::new()
.route("/", get(handler))
.layer(TraceContextLayer::new());
```
### `StrictTraceContext` — policy override
Rejects with `400 Bad Request` when `traceparent` is absent or invalid. Use this
when your service must refuse requests that don't carry an upstream trace.
```rust
use trace_context_level3_axum::StrictTraceContext;
async fn strict_handler(ctx: StrictTraceContext) -> String {
ctx.traceparent.to_string()
}
```
Use `Option` to distinguish "context was propagated" from "was
not" without rejecting the request — axum's blanket impl converts any rejection to
`None`.
Both extractors check request extensions first: when `TraceContextLayer` is active
it stores the already-advanced child span there, so neither extractor re-parses
the raw headers.
---
## Running the examples
### In-process propagation demo
Shows both scenarios — incoming traceparent (child span) and no header (root span) —
using `tower::ServiceExt::oneshot` without a real network:
```text
cargo run --example propagation -p trace-context-level3-axum
```
### Live axum server
Starts a server on `127.0.0.1:3000` and prints ready-to-use curl commands:
```text
cargo run --example server -p trace-context-level3-axum
```
```text
# Fresh root span (no incoming header)
curl http://127.0.0.1:3000/
# Child span — middleware advances parent-id, preserves trace-id
curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
http://127.0.0.1:3000/
# With tracestate
curl -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
-H 'tracestate: vendor=value' \
http://127.0.0.1:3000/
```
### Running the full test suite
```text
mise run ci
```
Runs `fmt-check`, `clippy`, `test`, and feature-variant tests in parallel.
---
## Spec compliance
- `traceparent` version `00` parsing and serialisation
- Unknown higher versions normalised to `v00` on parse (forward-compatibility rule)
- `tracestate` entry validation, deduplication (first occurrence wins), and
truncation to 32 entries using the two-step spec algorithm
- Lenient `tracestate` parsing across multiple header values (comma-separated lists
are merged; malformed entries are silently dropped)
- `RANDOM_TRACE_ID` flag (`0x02`) set on freshly generated root spans
- Multiple `traceparent` headers → `TraceParentError::MultipleValues`
- Response propagation via `Server-Timing: trace;desc=` (Level 3)
- Optional `serde` feature: `Serialize`/`Deserialize` for all public types
---
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or
)
at your option.