Skip to main content

mesofact_core/proxy/
trace.rs

1//! W3C Trace Context (`traceparent`) generation + parsing for the proxy.
2//!
3//! The proxy generates a `traceparent` per request, or continues an inbound one
4//! (reusing its trace-id, minting a fresh span-id for the proxy span). The
5//! resulting header is echoed on the response and passed to the Bun worker as
6//! `req.ctx.trace` so a render log line can correlate with the proxy's. Adapter
7//! child spans + OTLP export are post-MVP (see architecture ยง"Observability").
8//!
9//! Format (version 00): `00-<32 hex trace-id>-<16 hex parent-id>-<2 hex flags>`.
10//!
11//! IDs are minted from a SplitMix64 stream seeded by wall-clock nanos X'd with a
12//! process-global counter โ€” unique enough for trace correlation without pulling
13//! in a CSPRNG (trace-ids are identifiers, not secrets).
14
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::time::{SystemTime, UNIX_EPOCH};
17
18static COUNTER: AtomicU64 = AtomicU64::new(0);
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct TraceParent {
22    /// 32 lowercase hex chars (128-bit), never all-zero.
23    pub trace_id: String,
24    /// 16 lowercase hex chars (64-bit) โ€” the proxy's span id.
25    pub span_id: String,
26}
27
28impl TraceParent {
29    /// Continue an inbound `traceparent` (reuse trace-id, new proxy span-id) or
30    /// mint a fresh one when the header is absent or malformed.
31    pub fn incoming_or_new(header: Option<&str>) -> Self {
32        if let Some(trace_id) = header.and_then(parse_trace_id) {
33            return Self {
34                trace_id,
35                span_id: hex16(next_u64()),
36            };
37        }
38        Self {
39            trace_id: format!("{}{}", hex16(next_u64()), hex16(next_u64())),
40            span_id: hex16(next_u64()),
41        }
42    }
43
44    /// The `traceparent` header value. `01` = sampled.
45    pub fn header_value(&self) -> String {
46        format!("00-{}-{}-01", self.trace_id, self.span_id)
47    }
48}
49
50/// Extract the trace-id from a `traceparent` header if it is well-formed:
51/// version `00`, 32-hex trace-id (not all-zero), 16-hex parent-id (not
52/// all-zero), 2-hex flags. Returns `None` on any deviation so a bad inbound
53/// header falls back to a freshly-minted trace.
54fn parse_trace_id(header: &str) -> Option<String> {
55    let mut parts = header.trim().split('-');
56    let version = parts.next()?;
57    let trace_id = parts.next()?;
58    let parent_id = parts.next()?;
59    let flags = parts.next()?;
60    if parts.next().is_some() {
61        return None;
62    }
63    if version != "00"
64        || trace_id.len() != 32
65        || parent_id.len() != 16
66        || flags.len() != 2
67        || !is_hex(trace_id)
68        || !is_hex(parent_id)
69        || !is_hex(flags)
70        || trace_id.bytes().all(|b| b == b'0')
71        || parent_id.bytes().all(|b| b == b'0')
72    {
73        return None;
74    }
75    Some(trace_id.to_ascii_lowercase())
76}
77
78fn is_hex(s: &str) -> bool {
79    s.bytes().all(|b| b.is_ascii_hexdigit())
80}
81
82fn hex16(v: u64) -> String {
83    format!("{v:016x}")
84}
85
86/// Next pseudo-random u64 from a SplitMix64 step over (nanos ^ counter).
87fn next_u64() -> u64 {
88    let nanos = SystemTime::now()
89        .duration_since(UNIX_EPOCH)
90        .map(|d| d.as_nanos() as u64)
91        .unwrap_or(0);
92    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
93    splitmix64(nanos ^ n.wrapping_mul(0x9E37_79B9_7F4A_7C15))
94}
95
96fn splitmix64(seed: u64) -> u64 {
97    let mut z = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
98    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
99    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
100    z ^ (z >> 31)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn fresh_traceparent_is_well_formed() {
109        let tp = TraceParent::incoming_or_new(None);
110        assert_eq!(tp.trace_id.len(), 32);
111        assert_eq!(tp.span_id.len(), 16);
112        assert!(is_hex(&tp.trace_id) && is_hex(&tp.span_id));
113        let h = tp.header_value();
114        assert!(h.starts_with("00-") && h.ends_with("-01"));
115        // round-trips: parsing our own output yields the same trace-id.
116        assert_eq!(parse_trace_id(&h), Some(tp.trace_id));
117    }
118
119    #[test]
120    fn continues_inbound_trace_with_new_span() {
121        let inbound = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
122        let tp = TraceParent::incoming_or_new(Some(inbound));
123        assert_eq!(tp.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736");
124        // Proxy mints its own span; it must not echo the inbound parent-id.
125        assert_ne!(tp.span_id, "00f067aa0ba902b7");
126    }
127
128    #[test]
129    fn malformed_inbound_falls_back_to_fresh() {
130        for bad in [
131            "garbage",
132            "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", // version
133            "00-tooshort-00f067aa0ba902b7-01",
134            "00-00000000000000000000000000000000-00f067aa0ba902b7-01", // all-zero trace
135            "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01", // all-zero parent
136            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7", // missing flags
137        ] {
138            let tp = TraceParent::incoming_or_new(Some(bad));
139            assert_ne!(tp.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736", "bad={bad}");
140            assert_eq!(tp.trace_id.len(), 32, "bad={bad}");
141        }
142    }
143
144    #[test]
145    fn ids_are_unique_across_calls() {
146        let a = TraceParent::incoming_or_new(None);
147        let b = TraceParent::incoming_or_new(None);
148        assert_ne!(a.trace_id, b.trace_id);
149        assert_ne!(a.span_id, b.span_id);
150    }
151}