Skip to main content

uptrakit_wire/
trace_context.rs

1//! Distributed tracing context propagated through wire protocol messages.
2//!
3//! Carries W3C-compatible trace and span identifiers for correlating messages
4//! across service boundaries. When `tracing-opentelemetry` is wired in later,
5//! [`current_trace_context`] will extract IDs from the current span context;
6//! until then it generates random trace IDs.
7
8use std::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::limits::{
13    MAX_SPAN_ID_LEN, MAX_TRACE_ID_LEN, WireValidate, WireValidationError, check_opt_string_len,
14    check_string_len,
15};
16
17/// Distributed tracing context for correlating messages across service boundaries.
18///
19/// ## Wire format
20///
21/// ```json
22/// {"trace_id":"0123456789abcdef0123456789abcdef","span_id":"0123456789abcdef"}
23/// ```
24///
25/// - `trace_id`: 32 lowercase hex characters (128-bit identifier)
26/// - `span_id`: 16 lowercase hex characters (64-bit identifier), omitted when absent
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct TraceContext {
29    /// 128-bit trace identifier encoded as 32 lowercase hex characters.
30    pub trace_id: String,
31    /// 64-bit span identifier encoded as 16 lowercase hex characters.
32    /// `None` when no parent span is active.
33    #[serde(skip_serializing_if = "Option::is_none", default)]
34    pub span_id: Option<String>,
35}
36
37impl TraceContext {
38    /// Generate a new trace context with a random trace ID and no span ID.
39    ///
40    /// The trace ID is a 128-bit random value formatted as 32 lowercase hex
41    /// characters (UUID v4 without hyphens).
42    pub fn generate() -> Self {
43        Self {
44            trace_id: uuid::Uuid::new_v4().simple().to_string(),
45            span_id: None,
46        }
47    }
48}
49
50impl Default for TraceContext {
51    fn default() -> Self {
52        Self::generate()
53    }
54}
55
56impl fmt::Display for TraceContext {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match &self.span_id {
59            Some(span) => write!(f, "{}:{}", self.trace_id, span),
60            None => f.write_str(&self.trace_id),
61        }
62    }
63}
64
65impl WireValidate for TraceContext {
66    fn wire_validate(&self) -> Result<(), WireValidationError> {
67        check_string_len(&self.trace_id, MAX_TRACE_ID_LEN, "trace_context.trace_id")?;
68        check_opt_string_len(&self.span_id, MAX_SPAN_ID_LEN, "trace_context.span_id")?;
69        Ok(())
70    }
71}
72
73/// Returns the trace context for the current execution context.
74///
75/// Currently generates a new random trace context. When `tracing-opentelemetry`
76/// is wired in, this function will extract trace/span IDs from the current span
77/// context, making all existing propagation plumbing light up automatically.
78pub fn current_trace_context() -> TraceContext {
79    TraceContext::generate()
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn generate_produces_valid_trace_id() {
88        let ctx = TraceContext::generate();
89        assert_eq!(ctx.trace_id.len(), 32, "trace_id must be 32 hex chars");
90        assert!(
91            ctx.trace_id.chars().all(|c| c.is_ascii_hexdigit()),
92            "trace_id must contain only hex characters"
93        );
94        assert!(ctx.span_id.is_none(), "generated context has no span_id");
95    }
96
97    #[test]
98    fn generate_produces_unique_ids() {
99        let a = TraceContext::generate();
100        let b = TraceContext::generate();
101        assert_ne!(a.trace_id, b.trace_id, "two generated IDs must differ");
102    }
103
104    #[test]
105    fn display_without_span_id() {
106        let ctx = TraceContext {
107            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
108            span_id: None,
109        };
110        assert_eq!(ctx.to_string(), "0123456789abcdef0123456789abcdef");
111    }
112
113    #[test]
114    fn display_with_span_id() {
115        let ctx = TraceContext {
116            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
117            span_id: Some("fedcba9876543210".to_string()),
118        };
119        assert_eq!(
120            ctx.to_string(),
121            "0123456789abcdef0123456789abcdef:fedcba9876543210"
122        );
123    }
124
125    #[test]
126    fn serde_roundtrip_with_span_id() {
127        let ctx = TraceContext {
128            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
129            span_id: Some("fedcba9876543210".to_string()),
130        };
131        let json = serde_json::to_string(&ctx).unwrap();
132        assert!(json.contains("span_id"));
133        let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
134        assert_eq!(deserialized, ctx);
135    }
136
137    #[test]
138    fn serde_roundtrip_without_span_id() {
139        let ctx = TraceContext {
140            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
141            span_id: None,
142        };
143        let json = serde_json::to_string(&ctx).unwrap();
144        assert!(!json.contains("span_id"), "None span_id must be omitted");
145        let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
146        assert_eq!(deserialized, ctx);
147    }
148
149    #[test]
150    fn deserialize_missing_span_id() {
151        let json = r#"{"trace_id":"0123456789abcdef0123456789abcdef"}"#;
152        let ctx: TraceContext = serde_json::from_str(json).unwrap();
153        assert_eq!(ctx.trace_id, "0123456789abcdef0123456789abcdef");
154        assert!(ctx.span_id.is_none());
155    }
156
157    #[test]
158    fn wire_validate_valid() {
159        let ctx = TraceContext {
160            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
161            span_id: Some("fedcba9876543210".to_string()),
162        };
163        assert!(ctx.wire_validate().is_ok());
164    }
165
166    #[test]
167    fn wire_validate_trace_id_too_long() {
168        let ctx = TraceContext {
169            trace_id: "a".repeat(33),
170            span_id: None,
171        };
172        let err = ctx.wire_validate().unwrap_err();
173        assert_eq!(err.field, "trace_context.trace_id");
174    }
175
176    #[test]
177    fn wire_validate_span_id_too_long() {
178        let ctx = TraceContext {
179            trace_id: "0123456789abcdef0123456789abcdef".to_string(),
180            span_id: Some("a".repeat(17)),
181        };
182        let err = ctx.wire_validate().unwrap_err();
183        assert_eq!(err.field, "trace_context.span_id");
184    }
185
186    #[test]
187    fn current_trace_context_generates_valid() {
188        let ctx = current_trace_context();
189        assert!(ctx.wire_validate().is_ok());
190    }
191}