Skip to main content

fast_telemetry/span/
ids.rs

1//! Trace and span identifiers with fast thread-local RNG.
2//!
3//! Uses xorshift128+ for ID generation — no external RNG dependency, no syscalls
4//! per ID. Seeded from wall-clock time + thread ID for cross-thread uniqueness.
5
6use std::cell::RefCell;
7use std::fmt;
8
9use crate::thread_id;
10
11// ---------------------------------------------------------------------------
12// TraceId
13// ---------------------------------------------------------------------------
14
15/// 128-bit trace identifier. All spans in a distributed trace share the same `TraceId`.
16#[derive(Clone, Copy, PartialEq, Eq, Hash)]
17pub struct TraceId(pub(crate) [u8; 16]);
18
19impl TraceId {
20    /// All-zeros sentinel indicating an invalid / absent trace.
21    pub const INVALID: Self = Self([0; 16]);
22
23    /// Generate a random trace ID using the thread-local PRNG.
24    pub fn random() -> Self {
25        let (a, b) = rng_next_u128();
26        let mut bytes = [0u8; 16];
27        bytes[..8].copy_from_slice(&a.to_le_bytes());
28        bytes[8..].copy_from_slice(&b.to_le_bytes());
29        Self(bytes)
30    }
31
32    /// Parse from a 32-character lowercase hex string.
33    pub fn from_hex(hex: &str) -> Option<Self> {
34        if hex.len() != 32 {
35            return None;
36        }
37        let mut bytes = [0u8; 16];
38        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
39            bytes[i] = hex_byte(chunk[0], chunk[1])?;
40        }
41        Some(Self(bytes))
42    }
43
44    /// Returns `true` if this is the all-zeros invalid trace ID.
45    pub fn is_invalid(self) -> bool {
46        self == Self::INVALID
47    }
48
49    /// Returns the raw 16-byte representation.
50    pub fn as_bytes(&self) -> &[u8; 16] {
51        &self.0
52    }
53}
54
55impl fmt::Display for TraceId {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        for b in &self.0 {
58            write!(f, "{:02x}", b)?;
59        }
60        Ok(())
61    }
62}
63
64impl fmt::Debug for TraceId {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "TraceId({})", self)
67    }
68}
69
70// ---------------------------------------------------------------------------
71// SpanId
72// ---------------------------------------------------------------------------
73
74/// 64-bit span identifier. Unique within a trace.
75#[derive(Clone, Copy, PartialEq, Eq, Hash)]
76pub struct SpanId(pub(crate) [u8; 8]);
77
78impl SpanId {
79    /// All-zeros sentinel indicating an invalid / absent span (root span has no parent).
80    pub const INVALID: Self = Self([0; 8]);
81
82    /// Generate a random span ID using the thread-local PRNG.
83    pub fn random() -> Self {
84        Self(rng_next_u64().to_le_bytes())
85    }
86
87    /// Parse from a 16-character lowercase hex string.
88    pub fn from_hex(hex: &str) -> Option<Self> {
89        if hex.len() != 16 {
90            return None;
91        }
92        let mut bytes = [0u8; 8];
93        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
94            bytes[i] = hex_byte(chunk[0], chunk[1])?;
95        }
96        Some(Self(bytes))
97    }
98
99    /// Returns `true` if this is the all-zeros invalid span ID.
100    pub fn is_invalid(self) -> bool {
101        self == Self::INVALID
102    }
103
104    /// Returns the raw 8-byte representation.
105    pub fn as_bytes(&self) -> &[u8; 8] {
106        &self.0
107    }
108}
109
110impl fmt::Display for SpanId {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        for b in &self.0 {
113            write!(f, "{:02x}", b)?;
114        }
115        Ok(())
116    }
117}
118
119impl fmt::Debug for SpanId {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(f, "SpanId({})", self)
122    }
123}
124
125// ---------------------------------------------------------------------------
126// Thread-local xorshift128+ PRNG
127// ---------------------------------------------------------------------------
128
129struct Xorshift128Plus {
130    s0: u64,
131    s1: u64,
132}
133
134impl Xorshift128Plus {
135    fn next(&mut self) -> u64 {
136        let mut s1 = self.s0;
137        let s0 = self.s1;
138        self.s0 = s0;
139        s1 ^= s1 << 23;
140        s1 ^= s1 >> 17;
141        s1 ^= s0;
142        s1 ^= s0 >> 26;
143        self.s1 = s1;
144        s0.wrapping_add(s1)
145    }
146}
147
148thread_local! {
149    static RNG: RefCell<Xorshift128Plus> = RefCell::new({
150        // Seed from wall-clock time and thread ID for uniqueness across threads.
151        let nanos = std::time::SystemTime::now()
152            .duration_since(std::time::UNIX_EPOCH)
153            .unwrap_or_default()
154            .as_nanos() as u64;
155        let tid = thread_id::thread_id() as u64;
156
157        // Mix bits so nearby seeds don't produce correlated sequences.
158        let s0 = nanos ^ (tid.wrapping_mul(0x9E37_79B9_7F4A_7C15));
159        let s1 = nanos.wrapping_mul(0x6C62_272E_07BB_0142) ^ tid;
160
161        // Ensure non-zero state (xorshift requires at least one non-zero).
162        let s0 = if s0 == 0 { 0xDEAD_BEEF_CAFE_BABE } else { s0 };
163        let s1 = if s1 == 0 { 0x0123_4567_89AB_CDEF } else { s1 };
164
165        Xorshift128Plus { s0, s1 }
166    });
167}
168
169/// Generate 128 bits of pseudo-random data as two u64s.
170fn rng_next_u128() -> (u64, u64) {
171    RNG.with(|rng| {
172        let mut rng = rng.borrow_mut();
173        let a = rng.next();
174        let b = rng.next();
175        (a, b)
176    })
177}
178
179/// Generate 64 bits of pseudo-random data.
180#[inline]
181fn rng_next_u64() -> u64 {
182    RNG.with(|rng| rng.borrow_mut().next())
183}
184
185// ---------------------------------------------------------------------------
186// Hex helpers
187// ---------------------------------------------------------------------------
188
189fn hex_digit(c: u8) -> Option<u8> {
190    match c {
191        b'0'..=b'9' => Some(c - b'0'),
192        b'a'..=b'f' => Some(c - b'a' + 10),
193        b'A'..=b'F' => Some(c - b'A' + 10),
194        _ => None,
195    }
196}
197
198fn hex_byte(hi: u8, lo: u8) -> Option<u8> {
199    Some(hex_digit(hi)? << 4 | hex_digit(lo)?)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::collections::HashSet;
206
207    #[test]
208    fn trace_id_random_is_non_zero() {
209        let id = TraceId::random();
210        assert_ne!(id, TraceId::INVALID);
211    }
212
213    #[test]
214    fn span_id_random_is_non_zero() {
215        // Extremely unlikely to be all zeros, but generate several to be safe.
216        for _ in 0..100 {
217            let id = SpanId::random();
218            assert_ne!(id, SpanId::INVALID);
219        }
220    }
221
222    #[test]
223    fn trace_id_uniqueness() {
224        let mut set = HashSet::new();
225        for _ in 0..10_000 {
226            assert!(set.insert(TraceId::random()));
227        }
228    }
229
230    #[test]
231    fn span_id_uniqueness() {
232        let mut set = HashSet::new();
233        for _ in 0..10_000 {
234            assert!(set.insert(SpanId::random()));
235        }
236    }
237
238    #[test]
239    fn trace_id_hex_roundtrip() {
240        let id = TraceId::random();
241        let hex = id.to_string();
242        assert_eq!(hex.len(), 32);
243        let parsed = TraceId::from_hex(&hex).expect("valid hex");
244        assert_eq!(parsed, id);
245    }
246
247    #[test]
248    fn span_id_hex_roundtrip() {
249        let id = SpanId::random();
250        let hex = id.to_string();
251        assert_eq!(hex.len(), 16);
252        let parsed = SpanId::from_hex(&hex).expect("valid hex");
253        assert_eq!(parsed, id);
254    }
255
256    #[test]
257    fn trace_id_from_hex_known() {
258        let id = TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").expect("valid");
259        assert_eq!(id.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
260    }
261
262    #[test]
263    fn span_id_from_hex_known() {
264        let id = SpanId::from_hex("00f067aa0ba902b7").expect("valid");
265        assert_eq!(id.to_string(), "00f067aa0ba902b7");
266    }
267
268    #[test]
269    fn trace_id_from_hex_rejects_bad_input() {
270        assert!(TraceId::from_hex("too_short").is_none());
271        assert!(TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e473x").is_none()); // bad char
272        assert!(TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e47").is_none()); // 30 chars
273    }
274
275    #[test]
276    fn span_id_from_hex_rejects_bad_input() {
277        assert!(SpanId::from_hex("short").is_none());
278        assert!(SpanId::from_hex("00f067aa0ba902bx").is_none());
279    }
280
281    #[test]
282    fn invalid_sentinels() {
283        assert!(TraceId::INVALID.is_invalid());
284        assert!(SpanId::INVALID.is_invalid());
285        assert!(!TraceId::random().is_invalid());
286    }
287
288    #[test]
289    fn cross_thread_uniqueness() {
290        use std::sync::Arc;
291        use std::sync::Mutex;
292
293        let ids: Arc<Mutex<Vec<TraceId>>> = Arc::new(Mutex::new(Vec::new()));
294        let mut handles = Vec::new();
295
296        for _ in 0..4 {
297            let ids = Arc::clone(&ids);
298            handles.push(std::thread::spawn(move || {
299                let local: Vec<TraceId> = (0..1000).map(|_| TraceId::random()).collect();
300                ids.lock().expect("lock").extend(local);
301            }));
302        }
303        for h in handles {
304            h.join().expect("thread join");
305        }
306
307        let all = ids.lock().expect("lock");
308        let set: HashSet<_> = all.iter().collect();
309        assert_eq!(set.len(), all.len(), "duplicate trace IDs across threads");
310    }
311}