1use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{SystemTime, UNIX_EPOCH};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct TraceContext {
20 pub trace_id: String,
21 pub span_id: String,
22 pub flags: String,
23}
24
25impl TraceContext {
26 pub fn traceparent(&self) -> String {
28 format!("00-{}-{}-{}", self.trace_id, self.span_id, self.flags)
29 }
30}
31
32pub fn outbound_traceparent(trace_id: &str) -> String {
36 format!("00-{}-{}-01", trace_id, new_span_id())
37}
38
39pub fn resolve(run_id: &str, incoming: Option<&str>) -> TraceContext {
43 match incoming.and_then(parse) {
44 Some(up) => TraceContext {
45 trace_id: up.trace_id,
46 span_id: new_span_id(),
47 flags: up.flags,
48 },
49 None => TraceContext {
50 trace_id: trace_id_from(run_id),
51 span_id: new_span_id(),
52 flags: "01".into(),
53 },
54 }
55}
56
57pub fn parse(s: &str) -> Option<TraceContext> {
60 let p: Vec<&str> = s.trim().split('-').collect();
61 if p.len() != 4 {
62 return None;
63 }
64 let (ver, tid, sid, flags) = (p[0], p[1], p[2], p[3]);
65 if ver.len() != 2 || tid.len() != 32 || sid.len() != 16 || flags.len() != 2 {
66 return None;
67 }
68 if ![ver, tid, sid, flags].iter().all(|s| is_hex(s)) {
69 return None;
70 }
71 if tid.bytes().all(|b| b == b'0') || sid.bytes().all(|b| b == b'0') {
72 return None; }
74 Some(TraceContext {
75 trace_id: tid.to_string(),
76 span_id: sid.to_string(),
77 flags: flags.to_string(),
78 })
79}
80
81fn is_hex(s: &str) -> bool {
82 !s.is_empty() && s.bytes().all(|b| b.is_ascii_hexdigit())
83}
84
85fn fnv1a(data: &[u8], seed: u64) -> u64 {
88 let mut h = seed;
89 for &b in data {
90 h ^= b as u64;
91 h = h.wrapping_mul(0x0000_0100_0000_01b3);
92 }
93 h
94}
95
96fn trace_id_from(run_id: &str) -> String {
99 let a = fnv1a(run_id.as_bytes(), 0xcbf2_9ce4_8422_2325) | 1; let b = fnv1a(run_id.as_bytes(), 0x8422_2325_cbf2_9ce4);
101 format!("{a:016x}{b:016x}")
102}
103
104pub fn new_span_id() -> String {
107 static COUNTER: AtomicU64 = AtomicU64::new(0);
108 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
109 let nanos = SystemTime::now()
110 .duration_since(UNIX_EPOCH)
111 .map(|d| d.as_nanos() as u64)
112 .unwrap_or(0);
113 let seed = 0xcbf2_9ce4_8422_2325 ^ (std::process::id() as u64) ^ n;
114 let mixed = fnv1a(&nanos.to_le_bytes(), seed) | 1; format!("{mixed:016x}")
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn parse_valid_traceparent() {
124 let tc = parse("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").unwrap();
125 assert_eq!(tc.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736");
126 assert_eq!(tc.span_id, "00f067aa0ba902b7");
127 assert_eq!(tc.flags, "01");
128 assert_eq!(
129 tc.traceparent(),
130 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
131 );
132 }
133
134 #[test]
135 fn parse_rejects_malformed() {
136 assert!(parse("garbage").is_none());
137 assert!(parse("00-short-00f067aa0ba902b7-01").is_none());
138 assert!(parse("00-zzzz2f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").is_none());
139 assert!(parse("00-00000000000000000000000000000000-00f067aa0ba902b7-01").is_none());
141 }
142
143 #[test]
144 fn resolve_continues_upstream_trace() {
145 let up = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
146 let tc = resolve("run-1", Some(up));
147 assert_eq!(tc.trace_id, "4bf92f3577b34da6a3ce929d0e0e4736"); assert_ne!(tc.span_id, "00f067aa0ba902b7"); assert_eq!(tc.flags, "01");
150 }
151
152 #[test]
153 fn resolve_mints_deterministically_from_run_id() {
154 let a = resolve("run-abc", None);
155 let b = resolve("run-abc", None);
156 assert_eq!(a.trace_id, b.trace_id); assert_ne!(a.span_id, b.span_id); assert_eq!(a.trace_id.len(), 32);
159 let c = resolve("run-xyz", None);
160 assert_ne!(a.trace_id, c.trace_id); }
162
163 #[test]
164 fn resolve_mints_on_invalid_incoming() {
165 let tc = resolve("run-1", Some("not-a-traceparent"));
166 assert_eq!(tc.trace_id.len(), 32);
167 assert_eq!(tc.flags, "01");
168 }
169}