Skip to main content

nodedb_types/
trace.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! W3C-compatible 128-bit trace identifiers and 64-bit span identifiers.
4//!
5//! `TraceId` is a 16-byte value matching the W3C `traceparent` wire format:
6//! `00-<32 lowercase hex>-<16 lowercase hex>-<2 hex flags>`.
7//!
8//! Both types are copy-cheap value types safe to pass by value anywhere.
9
10use std::fmt;
11use std::str::FromStr;
12
13use rand::RngCore;
14use serde::{Deserialize, Serialize};
15
16// ── TraceId ──────────────────────────────────────────────────────────────────
17
18/// A 128-bit distributed trace identifier (W3C traceparent compatible).
19#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub struct TraceId(pub [u8; 16]);
21
22impl TraceId {
23    /// The all-zero sentinel used when no trace is active.
24    pub const ZERO: Self = Self([0u8; 16]);
25
26    /// Generate a random, non-zero trace ID.
27    pub fn generate() -> Self {
28        let mut bytes = [0u8; 16];
29        rand::rng().fill_bytes(&mut bytes);
30        // Extremely unlikely, but guarantee non-zero.
31        if bytes == [0u8; 16] {
32            bytes[15] = 1;
33        }
34        Self(bytes)
35    }
36
37    /// Parse a W3C `traceparent` header value.
38    ///
39    /// Expected format: `00-<32 hex>-<16 hex>-<2 hex>`
40    ///
41    /// Returns `(trace_id, parent_span_id, flags)` on success, `None` on any
42    /// malformed input (wrong version, wrong lengths, non-hex chars, wrong
43    /// field count).
44    pub fn from_traceparent(s: &str) -> Option<(TraceId, SpanId, u8)> {
45        let parts: Vec<&str> = s.splitn(4, '-').collect();
46        if parts.len() != 4 {
47            return None;
48        }
49        // Version must be "00".
50        if parts[0] != "00" {
51            return None;
52        }
53        // trace-id: 32 lowercase hex chars → 16 bytes.
54        let trace_hex = parts[1];
55        if trace_hex.len() != 32 {
56            return None;
57        }
58        let mut trace_bytes = [0u8; 16];
59        for (i, chunk) in trace_hex.as_bytes().chunks(2).enumerate() {
60            let hi = hex_val(chunk[0])?;
61            let lo = hex_val(chunk[1])?;
62            trace_bytes[i] = (hi << 4) | lo;
63        }
64        // parent-id: 16 lowercase hex chars → 8 bytes.
65        let span_hex = parts[2];
66        if span_hex.len() != 16 {
67            return None;
68        }
69        let mut span_bytes = [0u8; 8];
70        for (i, chunk) in span_hex.as_bytes().chunks(2).enumerate() {
71            let hi = hex_val(chunk[0])?;
72            let lo = hex_val(chunk[1])?;
73            span_bytes[i] = (hi << 4) | lo;
74        }
75        // flags: exactly 2 hex chars.
76        let flags_hex = parts[3];
77        if flags_hex.len() != 2 {
78            return None;
79        }
80        let fhi = hex_val(flags_hex.as_bytes()[0])?;
81        let flo = hex_val(flags_hex.as_bytes()[1])?;
82        let flags = (fhi << 4) | flo;
83
84        Some((TraceId(trace_bytes), SpanId(span_bytes), flags))
85    }
86
87    /// Render as a W3C `traceparent` header value.
88    ///
89    /// Format: `00-<32 lowercase hex>-<16 lowercase hex>-<2 hex flags>`
90    pub fn to_traceparent_header(&self, span_id: SpanId, flags: u8) -> String {
91        format!("00-{self}-{span_id}-{flags:02x}")
92    }
93}
94
95/// Decode a single ASCII hex nibble; returns `None` for non-hex characters.
96#[inline]
97fn hex_val(b: u8) -> Option<u8> {
98    match b {
99        b'0'..=b'9' => Some(b - b'0'),
100        b'a'..=b'f' => Some(b - b'a' + 10),
101        b'A'..=b'F' => Some(b - b'A' + 10),
102        _ => None,
103    }
104}
105
106impl fmt::Display for TraceId {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        for byte in &self.0 {
109            write!(f, "{byte:02x}")?;
110        }
111        Ok(())
112    }
113}
114
115impl fmt::Debug for TraceId {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        write!(f, "TraceId({self})")
118    }
119}
120
121/// Parse error for `TraceId::from_str`.
122#[derive(Debug, thiserror::Error)]
123#[error("invalid TraceId: expected 32 lowercase hex chars, got {0:?}")]
124pub struct TraceIdParseError(String);
125
126impl FromStr for TraceId {
127    type Err = TraceIdParseError;
128
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        if s.len() != 32 {
131            return Err(TraceIdParseError(s.to_owned()));
132        }
133        let mut bytes = [0u8; 16];
134        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
135            let hi = hex_val(chunk[0]).ok_or_else(|| TraceIdParseError(s.to_owned()))?;
136            let lo = hex_val(chunk[1]).ok_or_else(|| TraceIdParseError(s.to_owned()))?;
137            bytes[i] = (hi << 4) | lo;
138        }
139        Ok(TraceId(bytes))
140    }
141}
142
143// ── SpanId ───────────────────────────────────────────────────────────────────
144
145/// A 64-bit span identifier (W3C traceparent parent-id field).
146#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
147pub struct SpanId(pub [u8; 8]);
148
149impl SpanId {
150    /// The all-zero sentinel.
151    pub const ZERO: Self = Self([0u8; 8]);
152
153    /// Generate a random, non-zero span ID.
154    pub fn generate() -> Self {
155        let mut bytes = [0u8; 8];
156        rand::rng().fill_bytes(&mut bytes);
157        if bytes == [0u8; 8] {
158            bytes[7] = 1;
159        }
160        Self(bytes)
161    }
162}
163
164impl fmt::Display for SpanId {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        for byte in &self.0 {
167            write!(f, "{byte:02x}")?;
168        }
169        Ok(())
170    }
171}
172
173impl fmt::Debug for SpanId {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        write!(f, "SpanId({self})")
176    }
177}
178
179/// Parse error for `SpanId::from_str`.
180#[derive(Debug, thiserror::Error)]
181#[error("invalid SpanId: expected 16 lowercase hex chars, got {0:?}")]
182pub struct SpanIdParseError(String);
183
184impl FromStr for SpanId {
185    type Err = SpanIdParseError;
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        if s.len() != 16 {
189            return Err(SpanIdParseError(s.to_owned()));
190        }
191        let mut bytes = [0u8; 8];
192        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
193            let hi = hex_val(chunk[0]).ok_or_else(|| SpanIdParseError(s.to_owned()))?;
194            let lo = hex_val(chunk[1]).ok_or_else(|| SpanIdParseError(s.to_owned()))?;
195            bytes[i] = (hi << 4) | lo;
196        }
197        Ok(SpanId(bytes))
198    }
199}
200
201// ── Tests ────────────────────────────────────────────────────────────────────
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn generate_produces_nonzero_ids() {
209        let id = TraceId::generate();
210        assert_ne!(id, TraceId::ZERO);
211    }
212
213    #[test]
214    fn two_successive_generate_calls_differ() {
215        let a = TraceId::generate();
216        let b = TraceId::generate();
217        // Astronomically unlikely to collide.
218        assert_ne!(a, b);
219    }
220
221    #[test]
222    fn display_produces_32_lowercase_hex_chars() {
223        let id = TraceId::generate();
224        let s = id.to_string();
225        assert_eq!(s.len(), 32);
226        assert!(
227            s.chars()
228                .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
229        );
230    }
231
232    #[test]
233    fn from_str_roundtrips_through_display() {
234        let id = TraceId::generate();
235        let s = id.to_string();
236        let parsed: TraceId = s.parse().expect("parse must succeed");
237        assert_eq!(id, parsed);
238    }
239
240    #[test]
241    fn from_traceparent_extracts_correct_values() {
242        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
243        let (tid, sid, flags) = TraceId::from_traceparent(s).expect("valid traceparent");
244        // Verify trace-id bytes.
245        let expected_trace = hex_decode_16("4bf92f3577b34da6a3ce929d0e0e4736");
246        assert_eq!(tid.0, expected_trace);
247        // Verify span-id bytes.
248        let expected_span = hex_decode_8("00f067aa0ba902b7");
249        assert_eq!(sid.0, expected_span);
250        assert_eq!(flags, 0x01);
251    }
252
253    #[test]
254    fn from_traceparent_rejects_wrong_version() {
255        assert!(
256            TraceId::from_traceparent("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
257                .is_none()
258        );
259    }
260
261    #[test]
262    fn from_traceparent_rejects_wrong_trace_length() {
263        // 30 hex chars instead of 32.
264        assert!(
265            TraceId::from_traceparent("00-4bf92f3577b34da6a3ce929d0e0e47-00f067aa0ba902b7-01")
266                .is_none()
267        );
268    }
269
270    #[test]
271    fn from_traceparent_rejects_wrong_field_count() {
272        assert!(
273            TraceId::from_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7")
274                .is_none()
275        );
276    }
277
278    #[test]
279    fn from_traceparent_rejects_non_hex_chars() {
280        assert!(
281            TraceId::from_traceparent("00-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-00f067aa0ba902b7-01")
282                .is_none()
283        );
284    }
285
286    #[test]
287    fn to_traceparent_header_roundtrips_with_from_traceparent() {
288        let tid = TraceId::generate();
289        let sid = SpanId::generate();
290        let header = tid.to_traceparent_header(sid, 0x01);
291        let (parsed_tid, parsed_sid, flags) = TraceId::from_traceparent(&header).expect("valid");
292        assert_eq!(parsed_tid, tid);
293        assert_eq!(parsed_sid, sid);
294        assert_eq!(flags, 0x01);
295    }
296
297    #[test]
298    fn span_id_generate_nonzero() {
299        let sid = SpanId::generate();
300        assert_ne!(sid, SpanId::ZERO);
301    }
302
303    #[test]
304    fn span_id_display_16_lowercase_hex() {
305        let sid = SpanId::generate();
306        let s = sid.to_string();
307        assert_eq!(s.len(), 16);
308        assert!(
309            s.chars()
310                .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
311        );
312    }
313
314    // ── helpers ──────────────────────────────────────────────────────────────
315
316    fn hex_decode_16(s: &str) -> [u8; 16] {
317        let mut out = [0u8; 16];
318        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
319            out[i] = u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap();
320        }
321        out
322    }
323
324    fn hex_decode_8(s: &str) -> [u8; 8] {
325        let mut out = [0u8; 8];
326        for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
327            out[i] = u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap();
328        }
329        out
330    }
331}