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)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29pub struct TraceContext {
30 pub trace_id: String,
32 #[serde(skip_serializing_if = "Option::is_none", default)]
35 pub span_id: Option<String>,
36}
37
38impl TraceContext {
39 pub fn generate() -> Self {
44 Self {
45 trace_id: uuid::Uuid::new_v4().simple().to_string(),
46 span_id: None,
47 }
48 }
49}
50
51impl Default for TraceContext {
52 fn default() -> Self {
53 Self::generate()
54 }
55}
56
57impl fmt::Display for TraceContext {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match &self.span_id {
60 Some(span) => write!(f, "{}:{}", self.trace_id, span),
61 None => f.write_str(&self.trace_id),
62 }
63 }
64}
65
66impl WireValidate for TraceContext {
67 fn wire_validate(&self) -> Result<(), WireValidationError> {
68 check_string_len(&self.trace_id, MAX_TRACE_ID_LEN, "trace_context.trace_id")?;
69 check_opt_string_len(&self.span_id, MAX_SPAN_ID_LEN, "trace_context.span_id")?;
70 Ok(())
71 }
72}
73
74pub fn current_trace_context() -> TraceContext {
80 TraceContext::generate()
81}
82
83#[cfg(test)]
84mod tests {
85 #![expect(
86 clippy::assertions_on_result_states,
87 reason = "test assertions — is_ok/is_err provides readable failure messages"
88 )]
89 use super::*;
90
91 #[test]
92 fn generate_produces_valid_trace_id() {
93 let ctx = TraceContext::generate();
94 assert_eq!(ctx.trace_id.len(), 32, "trace_id must be 32 hex chars");
95 assert!(
96 ctx.trace_id.chars().all(|c| c.is_ascii_hexdigit()),
97 "trace_id must contain only hex characters"
98 );
99 assert!(ctx.span_id.is_none(), "generated context has no span_id");
100 }
101
102 #[test]
103 fn generate_produces_unique_ids() {
104 let a = TraceContext::generate();
105 let b = TraceContext::generate();
106 assert_ne!(a.trace_id, b.trace_id, "two generated IDs must differ");
107 }
108
109 #[test]
110 fn display_without_span_id() {
111 let ctx = TraceContext {
112 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
113 span_id: None,
114 };
115 assert_eq!(ctx.to_string(), "0123456789abcdef0123456789abcdef");
116 }
117
118 #[test]
119 fn display_with_span_id() {
120 let ctx = TraceContext {
121 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
122 span_id: Some("fedcba9876543210".to_string()),
123 };
124 assert_eq!(
125 ctx.to_string(),
126 "0123456789abcdef0123456789abcdef:fedcba9876543210"
127 );
128 }
129
130 #[test]
131 fn serde_roundtrip_with_span_id() {
132 let ctx = TraceContext {
133 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
134 span_id: Some("fedcba9876543210".to_string()),
135 };
136 let json = serde_json::to_string(&ctx).unwrap();
137 assert!(json.contains("span_id"));
138 let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
139 assert_eq!(deserialized, ctx);
140 }
141
142 #[test]
143 fn serde_roundtrip_without_span_id() {
144 let ctx = TraceContext {
145 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
146 span_id: None,
147 };
148 let json = serde_json::to_string(&ctx).unwrap();
149 assert!(!json.contains("span_id"), "None span_id must be omitted");
150 let deserialized: TraceContext = serde_json::from_str(&json).unwrap();
151 assert_eq!(deserialized, ctx);
152 }
153
154 #[test]
155 fn deserialize_missing_span_id() {
156 let json = r#"{"trace_id":"0123456789abcdef0123456789abcdef"}"#;
157 let ctx: TraceContext = serde_json::from_str(json).unwrap();
158 assert_eq!(ctx.trace_id, "0123456789abcdef0123456789abcdef");
159 assert!(ctx.span_id.is_none());
160 }
161
162 #[test]
163 fn wire_validate_valid() {
164 let ctx = TraceContext {
165 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
166 span_id: Some("fedcba9876543210".to_string()),
167 };
168 assert!(ctx.wire_validate().is_ok());
169 }
170
171 #[test]
172 fn wire_validate_trace_id_too_long() {
173 let ctx = TraceContext {
174 trace_id: "a".repeat(33),
175 span_id: None,
176 };
177 let err = ctx.wire_validate().unwrap_err();
178 assert_eq!(err.field, "trace_context.trace_id");
179 }
180
181 #[test]
182 fn wire_validate_span_id_too_long() {
183 let ctx = TraceContext {
184 trace_id: "0123456789abcdef0123456789abcdef".to_string(),
185 span_id: Some("a".repeat(17)),
186 };
187 let err = ctx.wire_validate().unwrap_err();
188 assert_eq!(err.field, "trace_context.span_id");
189 }
190
191 #[test]
192 fn current_trace_context_generates_valid() {
193 let ctx = current_trace_context();
194 assert!(ctx.wire_validate().is_ok());
195 }
196}