Skip to main content

enact_core/context/
trace.rs

1//! Trace Context - W3C Trace Context for distributed tracing
2//!
3//! Implements W3C Trace Context propagation for OpenTelemetry integration.
4//!
5//! @see https://www.w3.org/TR/trace-context/
6
7use serde::{Deserialize, Serialize};
8
9/// TraceContext - W3C Trace Context for distributed tracing
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct TraceContext {
12    /// Trace ID (32 hex chars)
13    pub trace_id: String,
14    /// Span ID (16 hex chars)
15    pub span_id: String,
16    /// Sampling flags
17    pub trace_flags: Option<u8>,
18    /// Vendor-specific state
19    pub trace_state: Option<String>,
20}
21
22impl TraceContext {
23    /// Create a new TraceContext with generated IDs
24    pub fn new() -> Self {
25        Self {
26            trace_id: Self::generate_trace_id(),
27            span_id: Self::generate_span_id(),
28            trace_flags: Some(1), // sampled
29            trace_state: None,
30        }
31    }
32
33    /// Create a child span context (same trace, new span)
34    pub fn child_span(&self) -> Self {
35        Self {
36            trace_id: self.trace_id.clone(),
37            span_id: Self::generate_span_id(),
38            trace_flags: self.trace_flags,
39            trace_state: self.trace_state.clone(),
40        }
41    }
42
43    /// Parse from W3C traceparent header
44    pub fn from_traceparent(header: &str) -> Option<Self> {
45        let parts: Vec<&str> = header.split('-').collect();
46        if parts.len() >= 3 {
47            Some(Self {
48                trace_id: parts[1].to_string(),
49                span_id: parts[2].to_string(),
50                trace_flags: parts.get(3).and_then(|f| u8::from_str_radix(f, 16).ok()),
51                trace_state: None,
52            })
53        } else {
54            None
55        }
56    }
57
58    /// Format as W3C traceparent header
59    pub fn to_traceparent(&self) -> String {
60        format!(
61            "00-{}-{}-{:02x}",
62            self.trace_id,
63            self.span_id,
64            self.trace_flags.unwrap_or(0)
65        )
66    }
67
68    fn generate_trace_id() -> String {
69        use std::time::{SystemTime, UNIX_EPOCH};
70        let now = SystemTime::now()
71            .duration_since(UNIX_EPOCH)
72            .unwrap()
73            .as_nanos();
74        format!("{:032x}", now)
75    }
76
77    fn generate_span_id() -> String {
78        use std::sync::atomic::{AtomicU64, Ordering};
79        use std::time::{SystemTime, UNIX_EPOCH};
80        static COUNTER: AtomicU64 = AtomicU64::new(0);
81
82        let now = SystemTime::now()
83            .duration_since(UNIX_EPOCH)
84            .unwrap()
85            .as_nanos() as u64;
86        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
87        // Mix timestamp with counter to ensure uniqueness
88        format!("{:016x}", now ^ (counter << 48))
89    }
90}
91
92impl Default for TraceContext {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_trace_context_new() {
104        let ctx = TraceContext::new();
105        assert_eq!(ctx.trace_id.len(), 32);
106        assert_eq!(ctx.span_id.len(), 16);
107    }
108
109    #[test]
110    fn test_child_span() {
111        let parent = TraceContext::new();
112        let child = parent.child_span();
113
114        assert_eq!(child.trace_id, parent.trace_id);
115        assert_ne!(child.span_id, parent.span_id);
116    }
117
118    #[test]
119    fn test_traceparent_roundtrip() {
120        let ctx = TraceContext::new();
121        let header = ctx.to_traceparent();
122        let parsed = TraceContext::from_traceparent(&header).unwrap();
123
124        assert_eq!(parsed.trace_id, ctx.trace_id);
125        assert_eq!(parsed.span_id, ctx.span_id);
126    }
127}