uptrakit_wire/
trace_context.rs1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct TraceContext {
29 pub trace_id: String,
31 #[serde(skip_serializing_if = "Option::is_none", default)]
34 pub span_id: Option<String>,
35}
36
37impl TraceContext {
38 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
73pub fn current_trace_context() -> TraceContext {
79 TraceContext::generate()
80}
81
82#[cfg(test)]
83mod tests {
84 #![expect(
85 clippy::assertions_on_result_states,
86 reason = "test assertions — is_ok/is_err provides readable failure messages"
87 )]
88 use super::*;
89
90 #[test]
91 fn generate_produces_valid_trace_id() {
92 let ctx = TraceContext::generate();
93 assert_eq!(ctx.trace_id.len(), 32, "trace_id must be 32 hex chars");
94 assert!(
95 ctx.trace_id.chars().all(|c| c.is_ascii_hexdigit()),
96 "trace_id must contain only hex characters"
97 );
98 assert!(ctx.span_id.is_none(), "generated context has no span_id");
99 }
100
101 #[test]
102 fn generate_produces_unique_ids() {
103 let a = TraceContext::generate();
104 let b = TraceContext::generate();
105 assert_ne!(a.trace_id, b.trace_id, "two generated IDs must differ");
106 }
107
108 #[test]
109 fn display_without_span_id() {
110 let ctx = TraceContext {
111 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
112 span_id: None,
113 };
114 assert_eq!(ctx.to_string(), "0123456789abcdef0123456789abcdef");
115 }
116
117 #[test]
118 fn display_with_span_id() {
119 let ctx = TraceContext {
120 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
121 span_id: Some("fedcba9876543210".to_string()),
122 };
123 assert_eq!(
124 ctx.to_string(),
125 "0123456789abcdef0123456789abcdef:fedcba9876543210"
126 );
127 }
128
129 #[test]
130 fn serde_roundtrip_with_span_id() {
131 let ctx = TraceContext {
132 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
133 span_id: Some("fedcba9876543210".to_string()),
134 };
135 let json = serde_json::to_string(&ctx).unwrap();
136 assert!(json.contains("span_id"));
137 let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
138 assert_eq!(deserialized, ctx);
139 }
140
141 #[test]
142 fn serde_roundtrip_without_span_id() {
143 let ctx = TraceContext {
144 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
145 span_id: None,
146 };
147 let json = serde_json::to_string(&ctx).unwrap();
148 assert!(!json.contains("span_id"), "None span_id must be omitted");
149 let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
150 assert_eq!(deserialized, ctx);
151 }
152
153 #[test]
154 fn deserialize_missing_span_id() {
155 let json = r#"{"trace_id":"0123456789abcdef0123456789abcdef"}"#;
156 let ctx: TraceContext = serde_json::from_str(json).unwrap();
157 assert_eq!(ctx.trace_id, "0123456789abcdef0123456789abcdef");
158 assert!(ctx.span_id.is_none());
159 }
160
161 #[test]
162 fn wire_validate_valid() {
163 let ctx = TraceContext {
164 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
165 span_id: Some("fedcba9876543210".to_string()),
166 };
167 assert!(ctx.wire_validate().is_ok());
168 }
169
170 #[test]
171 fn wire_validate_trace_id_too_long() {
172 let ctx = TraceContext {
173 trace_id: "a".repeat(33),
174 span_id: None,
175 };
176 let err = ctx.wire_validate().unwrap_err();
177 assert_eq!(err.field, "trace_context.trace_id");
178 }
179
180 #[test]
181 fn wire_validate_span_id_too_long() {
182 let ctx = TraceContext {
183 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
184 span_id: Some("a".repeat(17)),
185 };
186 let err = ctx.wire_validate().unwrap_err();
187 assert_eq!(err.field, "trace_context.span_id");
188 }
189
190 #[test]
191 fn current_trace_context_generates_valid() {
192 let ctx = current_trace_context();
193 assert!(ctx.wire_validate().is_ok());
194 }
195}