1use std::fmt;
9
10pub const MAX_TRACEPARENT_BYTES: usize = 128;
12
13pub const MAX_TRACESTATE_BYTES: usize = 512;
15
16const TRACEPARENT_LEN: usize = 55;
18
19pub const FLAG_SAMPLED: u8 = 0x01;
21
22#[derive(Clone, Copy, PartialEq, Eq, Hash)]
24pub struct TraceContext {
25 pub trace_id: [u8; 16],
27 pub span_id: [u8; 8],
29 pub flags: u8,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum TraceError {
36 Length,
38 Layout,
40 NotHex,
42 ForbiddenVersion,
44 ZeroId,
46}
47
48impl fmt::Display for TraceError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 TraceError::Length => f.write_str("traceparent must be exactly 55 bytes"),
52 TraceError::Layout => f.write_str("traceparent field layout is malformed"),
53 TraceError::NotHex => f.write_str("traceparent contains a non-hex-digit byte"),
54 TraceError::ForbiddenVersion => f.write_str("traceparent version ff is forbidden"),
55 TraceError::ZeroId => f.write_str("traceparent trace-id/span-id must not be all zero"),
56 }
57 }
58}
59
60impl std::error::Error for TraceError {}
61
62const fn hex_val(b: u8) -> Option<u8> {
63 match b {
64 b'0'..=b'9' => Some(b - b'0'),
65 b'a'..=b'f' => Some(b - b'a' + 10),
68 _ => None,
69 }
70}
71
72fn decode_hex(src: &[u8], dst: &mut [u8]) -> Result<(), TraceError> {
73 debug_assert_eq!(src.len(), dst.len() * 2);
74 let (pairs, _remainder) = src.as_chunks::<2>();
77 for (out, &[hi, lo]) in dst.iter_mut().zip(pairs) {
78 let hi = hex_val(hi).ok_or(TraceError::NotHex)?;
79 let lo = hex_val(lo).ok_or(TraceError::NotHex)?;
80 *out = (hi << 4) | lo;
81 }
82 Ok(())
83}
84
85fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
86 const HEX: &[u8; 16] = b"0123456789abcdef";
87 for b in bytes {
88 f.write_str(
89 core::str::from_utf8(&[HEX[(*b >> 4) as usize], HEX[(*b & 0x0f) as usize]])
90 .expect("hex digits are ascii"),
91 )?;
92 }
93 Ok(())
94}
95
96impl TraceContext {
97 pub const fn new(trace_id: [u8; 16], span_id: [u8; 8], flags: u8) -> Option<TraceContext> {
102 if is_zero_16(&trace_id) || is_zero_8(&span_id) {
103 return None;
104 }
105 Some(TraceContext {
106 trace_id,
107 span_id,
108 flags,
109 })
110 }
111
112 pub fn parse_traceparent(value: &str) -> Result<TraceContext, TraceError> {
118 let b = value.as_bytes();
119 if b.len() != TRACEPARENT_LEN {
120 return Err(TraceError::Length);
121 }
122 if b[2] != b'-' || b[35] != b'-' || b[52] != b'-' {
123 return Err(TraceError::Layout);
124 }
125
126 let mut version = [0u8; 1];
127 decode_hex(&b[0..2], &mut version)?;
128 if version[0] == 0xff {
129 return Err(TraceError::ForbiddenVersion);
130 }
131
132 let mut trace_id = [0u8; 16];
133 decode_hex(&b[3..35], &mut trace_id)?;
134 let mut span_id = [0u8; 8];
135 decode_hex(&b[36..52], &mut span_id)?;
136 let mut flags = [0u8; 1];
137 decode_hex(&b[53..55], &mut flags)?;
138
139 TraceContext::new(trace_id, span_id, flags[0]).ok_or(TraceError::ZeroId)
140 }
141
142 pub fn to_traceparent(&self) -> String {
144 self.to_string()
145 }
146
147 pub fn trace_id_hex(&self) -> String {
149 let mut s = String::with_capacity(32);
150 push_hex(&mut s, &self.trace_id);
151 s
152 }
153
154 pub fn span_id_hex(&self) -> String {
156 let mut s = String::with_capacity(16);
157 push_hex(&mut s, &self.span_id);
158 s
159 }
160
161 pub const fn is_sampled(&self) -> bool {
163 self.flags & FLAG_SAMPLED != 0
164 }
165
166 pub const fn with_span_id(&self, span_id: [u8; 8]) -> Option<TraceContext> {
168 TraceContext::new(self.trace_id, span_id, self.flags)
169 }
170}
171
172const fn is_zero_16(v: &[u8; 16]) -> bool {
173 let mut i = 0;
174 while i < 16 {
175 if v[i] != 0 {
176 return false;
177 }
178 i += 1;
179 }
180 true
181}
182
183const fn is_zero_8(v: &[u8; 8]) -> bool {
184 let mut i = 0;
185 while i < 8 {
186 if v[i] != 0 {
187 return false;
188 }
189 i += 1;
190 }
191 true
192}
193
194fn push_hex(s: &mut String, bytes: &[u8]) {
195 const HEX: &[u8; 16] = b"0123456789abcdef";
196 for b in bytes {
197 s.push(HEX[(*b >> 4) as usize] as char);
198 s.push(HEX[(*b & 0x0f) as usize] as char);
199 }
200}
201
202impl fmt::Display for TraceContext {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 f.write_str("00-")?;
205 write_hex(f, &self.trace_id)?;
206 f.write_str("-")?;
207 write_hex(f, &self.span_id)?;
208 f.write_str("-")?;
209 write_hex(f, &[self.flags])
210 }
211}
212
213impl fmt::Debug for TraceContext {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 write!(f, "TraceContext({self})")
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
224
225 #[test]
226 fn parses_the_w3c_example() {
227 let t = TraceContext::parse_traceparent(VALID).unwrap();
228 assert_eq!(t.trace_id_hex(), "4bf92f3577b34da6a3ce929d0e0e4736");
229 assert_eq!(t.span_id_hex(), "00f067aa0ba902b7");
230 assert_eq!(t.flags, 1);
231 assert!(t.is_sampled());
232 }
233
234 #[test]
235 fn format_parse_roundtrip() {
236 let t = TraceContext::parse_traceparent(VALID).unwrap();
237 assert_eq!(t.to_traceparent(), VALID);
238 assert_eq!(
239 TraceContext::parse_traceparent(&t.to_traceparent()).unwrap(),
240 t
241 );
242 }
243
244 #[test]
245 fn formats_a_cleared_flags_byte() {
246 let t = TraceContext::new([0x11; 16], [0x22; 8], 0).unwrap();
247 assert_eq!(
248 t.to_traceparent(),
249 "00-11111111111111111111111111111111-2222222222222222-00"
250 );
251 assert!(!t.is_sampled());
252 }
253
254 #[test]
255 fn rejects_invalid_values() {
256 let cases = [
257 (
258 "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
259 TraceError::ForbiddenVersion,
260 ),
261 (
262 "00-00000000000000000000000000000000-00f067aa0ba902b7-01",
263 TraceError::ZeroId,
264 ),
265 (
266 "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
267 TraceError::ZeroId,
268 ),
269 (
270 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0",
271 TraceError::Length,
272 ),
273 (
274 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-011",
275 TraceError::Length,
276 ),
277 ("", TraceError::Length),
278 (
279 "00_4bf92f3577b34da6a3ce929d0e0e4736_00f067aa0ba902b7_01",
280 TraceError::Layout,
281 ),
282 (
283 "00-4bf92f3577b34da6a3ce929d0e0e4736+00f067aa0ba902b7-01",
284 TraceError::Layout,
285 ),
286 (
287 "00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
288 TraceError::NotHex,
289 ),
290 (
291 "00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
292 TraceError::NotHex,
293 ),
294 ];
295 for (input, want) in cases {
296 assert_eq!(
297 TraceContext::parse_traceparent(input),
298 Err(want),
299 "input {input:?}"
300 );
301 }
302 }
303
304 #[test]
305 fn future_versions_parse_with_the_v00_layout() {
306 let t = TraceContext::parse_traceparent(
309 "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
310 )
311 .unwrap();
312 assert_eq!(t.span_id_hex(), "00f067aa0ba902b7");
313 }
314
315 #[test]
316 fn formatted_value_fits_the_wire_cap() {
317 let t = TraceContext::parse_traceparent(VALID).unwrap();
318 assert!(t.to_traceparent().len() <= MAX_TRACEPARENT_BYTES);
319 assert_eq!(t.to_traceparent().len(), TRACEPARENT_LEN);
320 }
321
322 #[test]
323 fn zero_ids_are_unconstructable() {
324 assert!(TraceContext::new([0; 16], [1; 8], 0).is_none());
325 assert!(TraceContext::new([1; 16], [0; 8], 0).is_none());
326 let t = TraceContext::new([1; 16], [1; 8], 0).unwrap();
327 assert!(t.with_span_id([0; 8]).is_none());
328 assert_eq!(t.with_span_id([9; 8]).unwrap().trace_id, [1; 16]);
329 }
330}