arcly_http/observability/propagation.rs
1//! W3C Trace Context propagation — the single place incoming `traceparent`
2//! headers are parsed and per-hop span IDs are minted.
3//!
4//! Every request boundary (HTTP macro routes, plugin routes) calls
5//! [`extract_trace_context`]; outgoing propagation is handled by
6//! `RequestContext::traceparent()`.
7
8use axum::http::HeaderMap;
9
10use crate::observability::lean_telemetry::{new_span_id, new_trace_id, parse_traceparent};
11
12/// Distributed-tracing identity for one server hop.
13pub struct TraceContext {
14 /// Preserved across the entire distributed call chain.
15 pub trace_id: [u8; 16],
16 /// This hop's freshly-minted span ID.
17 pub span_id: [u8; 8],
18 /// The upstream caller's span ID (all-zeros = this hop is the root).
19 pub parent_span_id: [u8; 8],
20}
21
22/// Continue the caller's trace if a valid `traceparent` header is present;
23/// otherwise start a new root trace.
24pub fn extract_trace_context(headers: &HeaderMap) -> TraceContext {
25 let (trace_id, parent_span_id) = headers
26 .get("traceparent")
27 .and_then(|h| parse_traceparent(h.as_bytes()))
28 .map(|t| (t.trace_id, t.span_id))
29 .unwrap_or_else(|| (new_trace_id(), [0u8; 8]));
30
31 TraceContext {
32 trace_id,
33 span_id: new_span_id(),
34 parent_span_id,
35 }
36}
37
38impl TraceContext {
39 /// W3C `traceparent` string for embedding in non-HTTP envelopes
40 /// (outbox rows, queue messages) — `00-{trace}-{span}-01` where the
41 /// span field is THIS hop's span (the consumer's parent).
42 pub fn to_traceparent(&self) -> String {
43 format!(
44 "00-{}-{}-01",
45 crate::observability::lean_telemetry::hex_encode(&self.trace_id),
46 crate::observability::lean_telemetry::hex_encode(&self.span_id),
47 )
48 }
49
50 /// Continue a trace carried in a message envelope: same trace ID, the
51 /// producer's span becomes this hop's parent, and a fresh span is minted
52 /// for the consumer side — so async hops chain in the trace UI instead
53 /// of starting orphan roots.
54 pub fn from_traceparent(s: &str) -> Option<TraceContext> {
55 let parsed = crate::observability::lean_telemetry::parse_traceparent(s.as_bytes())?;
56 Some(TraceContext {
57 trace_id: parsed.trace_id,
58 span_id: crate::observability::lean_telemetry::new_span_id(),
59 parent_span_id: parsed.span_id,
60 })
61 }
62
63 /// A brand-new root trace (for messages with no carried context).
64 pub fn new_root() -> TraceContext {
65 TraceContext {
66 trace_id: crate::observability::lean_telemetry::new_trace_id(),
67 span_id: crate::observability::lean_telemetry::new_span_id(),
68 parent_span_id: [0u8; 8],
69 }
70 }
71}