1use core::fmt;
23
24#[derive(Clone, Copy, PartialEq, Eq, Hash)]
26pub struct TraceId(pub u128);
27
28impl TraceId {
29 pub const INVALID: TraceId = TraceId(0);
31
32 pub fn is_valid(&self) -> bool {
34 self.0 != 0
35 }
36}
37
38impl fmt::Debug for TraceId {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 write!(f, "TraceId({:032x})", self.0)
41 }
42}
43
44impl fmt::Display for TraceId {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "{:032x}", self.0)
47 }
48}
49
50impl fmt::LowerHex for TraceId {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 fmt::LowerHex::fmt(&self.0, f)
53 }
54}
55
56#[derive(Clone, Copy, PartialEq, Eq, Hash)]
58pub struct SpanId(pub u64);
59
60impl SpanId {
61 pub const INVALID: SpanId = SpanId(0);
63
64 pub fn is_valid(&self) -> bool {
66 self.0 != 0
67 }
68}
69
70impl fmt::Debug for SpanId {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "SpanId({:016x})", self.0)
73 }
74}
75
76impl fmt::Display for SpanId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 write!(f, "{:016x}", self.0)
79 }
80}
81
82impl fmt::LowerHex for SpanId {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 fmt::LowerHex::fmt(&self.0, f)
85 }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct TraceContext {
94 pub trace_id: TraceId,
96 pub span_id: SpanId,
98 pub parent_span_id: SpanId,
100 pub flags: u8,
102}
103
104const W3C_VERSION: u8 = 0x00;
106
107pub const FLAG_SAMPLED: u8 = 0x01;
109
110pub const TRACE_CONTEXT_BYTES: usize = 32;
112
113impl TraceContext {
114 pub fn new_root(random_bytes: &[u8; 24]) -> Self {
119 let trace_id = u128::from_be_bytes(random_bytes[0..16].try_into().unwrap());
120 let span_id = u64::from_be_bytes(random_bytes[16..24].try_into().unwrap());
121 TraceContext {
122 trace_id: TraceId(trace_id),
123 span_id: SpanId(span_id),
124 parent_span_id: SpanId::INVALID,
125 flags: FLAG_SAMPLED,
126 }
127 }
128
129 pub fn child(&self, random_span_bytes: &[u8; 8]) -> Self {
134 let new_span_id = u64::from_be_bytes(*random_span_bytes);
135 TraceContext {
136 trace_id: self.trace_id,
137 span_id: SpanId(new_span_id),
138 parent_span_id: self.span_id,
139 flags: self.flags,
140 }
141 }
142
143 pub fn is_sampled(&self) -> bool {
145 self.flags & FLAG_SAMPLED != 0
146 }
147
148 pub fn set_sampled(&mut self, sampled: bool) {
150 if sampled {
151 self.flags |= FLAG_SAMPLED;
152 } else {
153 self.flags &= !FLAG_SAMPLED;
154 }
155 }
156
157 pub fn encode(&self) -> [u8; TRACE_CONTEXT_BYTES] {
161 let mut buf = [0u8; TRACE_CONTEXT_BYTES];
162 buf[0] = W3C_VERSION;
163 buf[1..17].copy_from_slice(&self.trace_id.0.to_be_bytes());
164 buf[17..25].copy_from_slice(&self.span_id.0.to_be_bytes());
165 buf[25] = self.flags;
166 buf
168 }
169
170 pub fn decode(buf: &[u8; TRACE_CONTEXT_BYTES]) -> Result<Self, TraceContextError> {
172 if buf[0] != W3C_VERSION {
173 return Err(TraceContextError::UnsupportedVersion(buf[0]));
174 }
175 let trace_id = u128::from_be_bytes(buf[1..17].try_into().unwrap());
176 let span_id = u64::from_be_bytes(buf[17..25].try_into().unwrap());
177 let flags = buf[25];
178
179 Ok(TraceContext {
180 trace_id: TraceId(trace_id),
181 span_id: SpanId(span_id),
182 parent_span_id: SpanId::INVALID, flags,
184 })
185 }
186
187 pub fn to_w3c_string(&self) -> alloc::string::String {
191 alloc::format!(
192 "{:02x}-{:032x}-{:016x}-{:02x}",
193 W3C_VERSION,
194 self.trace_id.0,
195 self.span_id.0,
196 self.flags,
197 )
198 }
199
200 pub fn from_w3c_string(s: &str) -> Result<Self, TraceContextError> {
205 if s.len() != 55 {
206 return Err(TraceContextError::InvalidFormat);
207 }
208 let parts: alloc::vec::Vec<&str> = s.split('-').collect();
209 if parts.len() != 4 {
210 return Err(TraceContextError::InvalidFormat);
211 }
212 if parts[0].len() != 2
213 || parts[1].len() != 32
214 || parts[2].len() != 16
215 || parts[3].len() != 2
216 {
217 return Err(TraceContextError::InvalidFormat);
218 }
219 let version =
220 u8::from_str_radix(parts[0], 16).map_err(|_| TraceContextError::InvalidFormat)?;
221 if version != W3C_VERSION {
222 return Err(TraceContextError::UnsupportedVersion(version));
223 }
224 let trace_id =
225 u128::from_str_radix(parts[1], 16).map_err(|_| TraceContextError::InvalidFormat)?;
226 let span_id =
227 u64::from_str_radix(parts[2], 16).map_err(|_| TraceContextError::InvalidFormat)?;
228 let flags =
229 u8::from_str_radix(parts[3], 16).map_err(|_| TraceContextError::InvalidFormat)?;
230
231 Ok(TraceContext {
232 trace_id: TraceId(trace_id),
233 span_id: SpanId(span_id),
234 parent_span_id: SpanId::INVALID,
235 flags,
236 })
237 }
238
239 pub fn is_empty(&self) -> bool {
241 !self.trace_id.is_valid() && !self.span_id.is_valid()
242 }
243}
244
245impl Default for TraceContext {
246 fn default() -> Self {
247 TraceContext {
248 trace_id: TraceId::INVALID,
249 span_id: SpanId::INVALID,
250 parent_span_id: SpanId::INVALID,
251 flags: 0,
252 }
253 }
254}
255
256impl fmt::Display for TraceContext {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(f, "{}", self.to_w3c_string())
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
264pub enum TraceContextError {
265 UnsupportedVersion(u8),
267 InvalidFormat,
269}
270
271impl fmt::Display for TraceContextError {
272 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 match self {
274 TraceContextError::UnsupportedVersion(v) => {
275 write!(f, "unsupported traceparent version: {v:#04x}")
276 }
277 TraceContextError::InvalidFormat => write!(f, "invalid traceparent format"),
278 }
279 }
280}
281
282impl From<TraceContext> for grafos_core::EdgeTraceContext {
308 fn from(ctx: TraceContext) -> Self {
309 grafos_core::EdgeTraceContext {
310 trace_id: ctx.trace_id.0,
311 span_id: ctx.span_id.0,
312 parent_span_id: ctx.parent_span_id.0,
313 flags: ctx.flags as u32,
315 }
316 }
317}
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
324pub struct EdgeTraceContextToObserveError {
325 pub flags: u32,
328}
329
330impl fmt::Display for EdgeTraceContextToObserveError {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 write!(
333 f,
334 "EdgeTraceContext → TraceContext: flags 0x{:08x} has high bits set; runtime TraceContext is W3C-shaped with u8 flags",
335 self.flags
336 )
337 }
338}
339
340#[cfg(feature = "std")]
341impl std::error::Error for EdgeTraceContextToObserveError {}
342
343impl TryFrom<grafos_core::EdgeTraceContext> for TraceContext {
344 type Error = EdgeTraceContextToObserveError;
345
346 fn try_from(ctx: grafos_core::EdgeTraceContext) -> Result<Self, Self::Error> {
347 if ctx.flags > u8::MAX as u32 {
351 return Err(EdgeTraceContextToObserveError { flags: ctx.flags });
352 }
353 Ok(TraceContext {
354 trace_id: TraceId(ctx.trace_id),
355 span_id: SpanId(ctx.span_id),
356 parent_span_id: SpanId(ctx.parent_span_id),
357 flags: ctx.flags as u8,
358 })
359 }
360}
361
362#[cfg(feature = "std")]
367mod thread_local_ctx {
368 use super::TraceContext;
369 use std::cell::RefCell;
370
371 thread_local! {
372 static CURRENT: RefCell<TraceContext> = RefCell::new(TraceContext::default());
373 }
374
375 impl TraceContext {
376 pub fn current() -> TraceContext {
378 CURRENT.with(|c| *c.borrow())
379 }
380
381 pub fn set_current(ctx: TraceContext) {
383 CURRENT.with(|c| *c.borrow_mut() = ctx);
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 fn test_random_bytes() -> [u8; 24] {
393 let mut bytes = [0u8; 24];
395 for (i, b) in bytes.iter_mut().enumerate() {
396 *b = (i as u8).wrapping_add(0x10);
397 }
398 bytes
399 }
400
401 #[test]
402 fn new_root_creates_valid_context() {
403 let ctx = TraceContext::new_root(&test_random_bytes());
404 assert!(ctx.trace_id.is_valid());
405 assert!(ctx.span_id.is_valid());
406 assert_eq!(ctx.parent_span_id, SpanId::INVALID);
407 assert!(ctx.is_sampled());
408 }
409
410 #[test]
411 fn child_inherits_trace_id() {
412 let root = TraceContext::new_root(&test_random_bytes());
413 let child_bytes: [u8; 8] = [0xAA; 8];
414 let child = root.child(&child_bytes);
415
416 assert_eq!(child.trace_id, root.trace_id);
417 assert_ne!(child.span_id, root.span_id);
418 assert_eq!(child.parent_span_id, root.span_id);
419 assert_eq!(child.flags, root.flags);
420 }
421
422 #[test]
423 fn encode_decode_roundtrip() {
424 let ctx = TraceContext::new_root(&test_random_bytes());
425 let encoded = ctx.encode();
426 let decoded = TraceContext::decode(&encoded).unwrap();
427
428 assert_eq!(decoded.trace_id, ctx.trace_id);
429 assert_eq!(decoded.span_id, ctx.span_id);
430 assert_eq!(decoded.flags, ctx.flags);
431 }
432
433 #[test]
434 fn w3c_string_roundtrip() {
435 let ctx = TraceContext::new_root(&test_random_bytes());
436 let s = ctx.to_w3c_string();
437
438 assert_eq!(s.len(), 55);
439 assert!(s.starts_with("00-"));
440
441 let parsed = TraceContext::from_w3c_string(&s).unwrap();
442 assert_eq!(parsed.trace_id, ctx.trace_id);
443 assert_eq!(parsed.span_id, ctx.span_id);
444 assert_eq!(parsed.flags, ctx.flags);
445 }
446
447 #[test]
448 fn w3c_string_format() {
449 let ctx = TraceContext {
450 trace_id: TraceId(0x0102030405060708090a0b0c0d0e0f10),
451 span_id: SpanId(0x1112131415161718),
452 parent_span_id: SpanId::INVALID,
453 flags: 0x01,
454 };
455 let s = ctx.to_w3c_string();
456 assert_eq!(s, "00-0102030405060708090a0b0c0d0e0f10-1112131415161718-01");
457 }
458
459 #[test]
460 fn from_w3c_string_rejects_bad_version() {
461 let s = "01-0102030405060708090a0b0c0d0e0f10-1112131415161718-01";
462 let err = TraceContext::from_w3c_string(s).unwrap_err();
463 assert_eq!(err, TraceContextError::UnsupportedVersion(0x01));
464 }
465
466 #[test]
467 fn from_w3c_string_rejects_bad_length() {
468 assert_eq!(
469 TraceContext::from_w3c_string("too-short"),
470 Err(TraceContextError::InvalidFormat)
471 );
472 }
473
474 #[test]
475 fn sampled_flag_manipulation() {
476 let mut ctx = TraceContext::default();
477 assert!(!ctx.is_sampled());
478 ctx.set_sampled(true);
479 assert!(ctx.is_sampled());
480 ctx.set_sampled(false);
481 assert!(!ctx.is_sampled());
482 }
483
484 #[test]
485 fn default_is_empty() {
486 let ctx = TraceContext::default();
487 assert!(ctx.is_empty());
488 }
489
490 #[test]
491 fn decode_rejects_bad_version() {
492 let mut buf = [0u8; 32];
493 buf[0] = 0xFF;
494 assert_eq!(
495 TraceContext::decode(&buf),
496 Err(TraceContextError::UnsupportedVersion(0xFF))
497 );
498 }
499
500 #[cfg(feature = "std")]
501 #[test]
502 fn thread_local_current_context() {
503 let saved = TraceContext::current();
505
506 let ctx = TraceContext::new_root(&test_random_bytes());
507 TraceContext::set_current(ctx);
508 let got = TraceContext::current();
509 assert_eq!(got.trace_id, ctx.trace_id);
510 assert_eq!(got.span_id, ctx.span_id);
511
512 TraceContext::set_current(saved);
513 }
514
515 #[test]
516 fn display_uses_w3c_format() {
517 let ctx = TraceContext::new_root(&test_random_bytes());
518 let display = alloc::format!("{ctx}");
519 let w3c = ctx.to_w3c_string();
520 assert_eq!(display, w3c);
521 }
522
523 #[test]
524 fn trace_id_display() {
525 let tid = TraceId(0x0102030405060708090a0b0c0d0e0f10);
526 assert_eq!(alloc::format!("{tid}"), "0102030405060708090a0b0c0d0e0f10");
527 }
528
529 #[test]
530 fn span_id_display() {
531 let sid = SpanId(0x1112131415161718);
532 assert_eq!(alloc::format!("{sid}"), "1112131415161718");
533 }
534
535 #[test]
546 fn from_trace_context_widens_flags_losslessly() {
547 let ctx = TraceContext {
548 trace_id: TraceId(0xdeadbeef_cafe_babe_0123_4567_89ab_cdef),
549 span_id: SpanId(0x1234_5678_9abc_def0),
550 parent_span_id: SpanId(0xfeed_face_dead_beef),
551 flags: FLAG_SAMPLED,
552 };
553 let edge: grafos_core::EdgeTraceContext = ctx.into();
554 assert_eq!(edge.trace_id, ctx.trace_id.0);
555 assert_eq!(edge.span_id, ctx.span_id.0);
556 assert_eq!(edge.parent_span_id, ctx.parent_span_id.0);
557 assert_eq!(edge.flags, FLAG_SAMPLED as u32);
558 assert_eq!(edge.flags & 0xffff_ff00, 0);
560 }
561
562 #[test]
563 fn from_trace_context_preserves_zero_flags() {
564 let ctx = TraceContext::default();
565 let edge: grafos_core::EdgeTraceContext = ctx.into();
566 assert_eq!(edge.flags, 0);
567 assert!(edge.is_zero());
568 }
569
570 #[test]
571 fn from_trace_context_preserves_max_u8_flags() {
572 let ctx = TraceContext {
573 trace_id: TraceId(1),
574 span_id: SpanId(2),
575 parent_span_id: SpanId(3),
576 flags: u8::MAX,
577 };
578 let edge: grafos_core::EdgeTraceContext = ctx.into();
579 assert_eq!(edge.flags, u8::MAX as u32);
580 assert_eq!(edge.flags, 0x0000_00ff);
581 }
582
583 #[test]
584 fn try_from_edge_trace_context_narrows_u8_range_losslessly() {
585 let edge = grafos_core::EdgeTraceContext {
586 trace_id: 0xa5a5_a5a5_a5a5_a5a5_a5a5_a5a5_a5a5_a5a5,
587 span_id: 0x0123_4567_89ab_cdef,
588 parent_span_id: 0xfedc_ba98_7654_3210,
589 flags: grafos_core::EDGE_TRACE_FLAG_SAMPLED,
590 };
591 let ctx = TraceContext::try_from(edge).expect("u8-fitting flags must narrow");
592 assert_eq!(ctx.trace_id.0, edge.trace_id);
593 assert_eq!(ctx.span_id.0, edge.span_id);
594 assert_eq!(ctx.parent_span_id.0, edge.parent_span_id);
595 assert_eq!(ctx.flags, FLAG_SAMPLED);
596 }
597
598 #[test]
599 fn try_from_edge_trace_context_fails_closed_on_high_bits() {
600 let edge = grafos_core::EdgeTraceContext {
603 flags: 0x8000_0000,
604 ..grafos_core::EdgeTraceContext::default()
605 };
606 let err = TraceContext::try_from(edge).expect_err("high bits must fail closed");
607 assert_eq!(err, EdgeTraceContextToObserveError { flags: 0x8000_0000 });
608 }
609
610 #[test]
611 fn try_from_edge_trace_context_fails_closed_on_mid_bits() {
612 let edge = grafos_core::EdgeTraceContext {
615 flags: 0x0000_0100,
616 ..grafos_core::EdgeTraceContext::default()
617 };
618 let err = TraceContext::try_from(edge).expect_err("bit 8 must fail closed");
619 assert_eq!(err.flags, 0x0000_0100);
620 }
621
622 #[test]
623 fn try_from_edge_trace_context_at_u8_max_boundary() {
624 let at_max = grafos_core::EdgeTraceContext {
626 flags: 0xff,
627 ..grafos_core::EdgeTraceContext::default()
628 };
629 let just_over = grafos_core::EdgeTraceContext {
630 flags: 0x100,
631 ..grafos_core::EdgeTraceContext::default()
632 };
633 assert!(TraceContext::try_from(at_max).is_ok());
634 assert!(TraceContext::try_from(just_over).is_err());
635 }
636
637 #[test]
638 fn bridge_round_trip_is_identity_in_u8_range() {
639 let original = TraceContext {
644 trace_id: TraceId(0x1111_2222_3333_4444_5555_6666_7777_8888),
645 span_id: SpanId(0x9999_aaaa_bbbb_cccc),
646 parent_span_id: SpanId(0xdddd_eeee_ffff_0000),
647 flags: 0x42,
648 };
649 let edge: grafos_core::EdgeTraceContext = original.into();
650 let round: TraceContext =
651 TraceContext::try_from(edge).expect("u8-sourced round-trip must succeed");
652 assert_eq!(round.trace_id, original.trace_id);
653 assert_eq!(round.span_id, original.span_id);
654 assert_eq!(round.parent_span_id, original.parent_span_id);
655 assert_eq!(round.flags, original.flags);
656 }
657
658 #[test]
659 fn bridge_error_display_is_human_readable() {
660 let err = EdgeTraceContextToObserveError { flags: 0xdead_beef };
661 let s = alloc::format!("{err}");
662 assert!(
663 s.contains("0xdeadbeef"),
664 "error must surface the rejected flags value: {s}"
665 );
666 assert!(
667 s.contains("u8") || s.contains("W3C"),
668 "error must explain why narrowing failed: {s}"
669 );
670 }
671}