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::Rng;
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 hex chars → 16 bytes. parent-id: 16 hex chars → 8
54        // bytes. flags: 2 hex chars. A wrong length fails the decode.
55        let mut trace_bytes = [0u8; 16];
56        hex::decode_to_slice(parts[1], &mut trace_bytes).ok()?;
57        let mut span_bytes = [0u8; 8];
58        hex::decode_to_slice(parts[2], &mut span_bytes).ok()?;
59        let mut flags = [0u8; 1];
60        hex::decode_to_slice(parts[3], &mut flags).ok()?;
61
62        Some((TraceId(trace_bytes), SpanId(span_bytes), flags[0]))
63    }
64
65    /// Render as a W3C `traceparent` header value.
66    ///
67    /// Format: `00-<32 lowercase hex>-<16 lowercase hex>-<2 hex flags>`
68    pub fn to_traceparent_header(&self, span_id: SpanId, flags: u8) -> String {
69        format!("00-{self}-{span_id}-{flags:02x}")
70    }
71}
72
73impl fmt::Display for TraceId {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.write_str(&hex::encode(self.0))
76    }
77}
78
79impl fmt::Debug for TraceId {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        write!(f, "TraceId({self})")
82    }
83}
84
85/// Parse error for `TraceId::from_str`.
86#[derive(Debug, thiserror::Error)]
87#[error("invalid TraceId: expected 32 lowercase hex chars, got {0:?}")]
88pub struct TraceIdParseError(String);
89
90impl FromStr for TraceId {
91    type Err = TraceIdParseError;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        let mut bytes = [0u8; 16];
95        hex::decode_to_slice(s, &mut bytes).map_err(|_| TraceIdParseError(s.to_owned()))?;
96        Ok(TraceId(bytes))
97    }
98}
99
100// ── SpanId ───────────────────────────────────────────────────────────────────
101
102/// A 64-bit span identifier (W3C traceparent parent-id field).
103#[derive(Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
104pub struct SpanId(pub [u8; 8]);
105
106impl SpanId {
107    /// The all-zero sentinel.
108    pub const ZERO: Self = Self([0u8; 8]);
109
110    /// Generate a random, non-zero span ID.
111    pub fn generate() -> Self {
112        let mut bytes = [0u8; 8];
113        rand::rng().fill_bytes(&mut bytes);
114        if bytes == [0u8; 8] {
115            bytes[7] = 1;
116        }
117        Self(bytes)
118    }
119}
120
121impl fmt::Display for SpanId {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        f.write_str(&hex::encode(self.0))
124    }
125}
126
127impl fmt::Debug for SpanId {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        write!(f, "SpanId({self})")
130    }
131}
132
133/// Parse error for `SpanId::from_str`.
134#[derive(Debug, thiserror::Error)]
135#[error("invalid SpanId: expected 16 lowercase hex chars, got {0:?}")]
136pub struct SpanIdParseError(String);
137
138impl FromStr for SpanId {
139    type Err = SpanIdParseError;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        let mut bytes = [0u8; 8];
143        hex::decode_to_slice(s, &mut bytes).map_err(|_| SpanIdParseError(s.to_owned()))?;
144        Ok(SpanId(bytes))
145    }
146}
147
148// ── Tests ────────────────────────────────────────────────────────────────────
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn generate_produces_nonzero_ids() {
156        let id = TraceId::generate();
157        assert_ne!(id, TraceId::ZERO);
158    }
159
160    #[test]
161    fn two_successive_generate_calls_differ() {
162        let a = TraceId::generate();
163        let b = TraceId::generate();
164        // Astronomically unlikely to collide.
165        assert_ne!(a, b);
166    }
167
168    #[test]
169    fn display_produces_32_lowercase_hex_chars() {
170        let id = TraceId::generate();
171        let s = id.to_string();
172        assert_eq!(s.len(), 32);
173        assert!(
174            s.chars()
175                .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
176        );
177    }
178
179    #[test]
180    fn from_str_roundtrips_through_display() {
181        let id = TraceId::generate();
182        let s = id.to_string();
183        let parsed: TraceId = s.parse().expect("parse must succeed");
184        assert_eq!(id, parsed);
185    }
186
187    #[test]
188    fn from_traceparent_extracts_correct_values() {
189        let s = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
190        let (tid, sid, flags) = TraceId::from_traceparent(s).expect("valid traceparent");
191        // Verify trace-id bytes.
192        let expected_trace = hex::decode("4bf92f3577b34da6a3ce929d0e0e4736").unwrap();
193        assert_eq!(tid.0.as_slice(), expected_trace.as_slice());
194        // Verify span-id bytes.
195        let expected_span = hex::decode("00f067aa0ba902b7").unwrap();
196        assert_eq!(sid.0.as_slice(), expected_span.as_slice());
197        assert_eq!(flags, 0x01);
198    }
199
200    #[test]
201    fn from_traceparent_rejects_wrong_version() {
202        assert!(
203            TraceId::from_traceparent("01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
204                .is_none()
205        );
206    }
207
208    #[test]
209    fn from_traceparent_rejects_wrong_trace_length() {
210        // 30 hex chars instead of 32.
211        assert!(
212            TraceId::from_traceparent("00-4bf92f3577b34da6a3ce929d0e0e47-00f067aa0ba902b7-01")
213                .is_none()
214        );
215    }
216
217    #[test]
218    fn from_traceparent_rejects_wrong_field_count() {
219        assert!(
220            TraceId::from_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7")
221                .is_none()
222        );
223    }
224
225    #[test]
226    fn from_traceparent_rejects_non_hex_chars() {
227        assert!(
228            TraceId::from_traceparent("00-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-00f067aa0ba902b7-01")
229                .is_none()
230        );
231    }
232
233    #[test]
234    fn to_traceparent_header_roundtrips_with_from_traceparent() {
235        let tid = TraceId::generate();
236        let sid = SpanId::generate();
237        let header = tid.to_traceparent_header(sid, 0x01);
238        let (parsed_tid, parsed_sid, flags) = TraceId::from_traceparent(&header).expect("valid");
239        assert_eq!(parsed_tid, tid);
240        assert_eq!(parsed_sid, sid);
241        assert_eq!(flags, 0x01);
242    }
243
244    #[test]
245    fn span_id_generate_nonzero() {
246        let sid = SpanId::generate();
247        assert_ne!(sid, SpanId::ZERO);
248    }
249
250    #[test]
251    fn span_id_display_16_lowercase_hex() {
252        let sid = SpanId::generate();
253        let s = sid.to_string();
254        assert_eq!(s.len(), 16);
255        assert!(
256            s.chars()
257                .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
258        );
259    }
260}