1use serde_json::Value;
65
66const TRACEPARENT_LEN: usize = 55;
68const TRACEPARENT_KEY: &str = "traceparent";
70const TRACESTATE_KEY: &str = "tracestate";
72const MAX_TRACESTATE_LEN: usize = 512;
74const MAX_TRACESTATE_MEMBERS: usize = 32;
76
77#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct TraceContext {
85 version: u8,
86 trace_id: String,
87 parent_id: String,
88 trace_flags: u8,
89 tracestate: Option<String>,
90}
91
92impl TraceContext {
93 pub fn parse(traceparent: &str, tracestate: Option<&str>) -> Option<Self> {
98 let (version, trace_id, parent_id, trace_flags) = parse_traceparent(traceparent)?;
99 Some(Self {
100 version,
101 trace_id: trace_id.to_string(),
102 parent_id: parent_id.to_string(),
103 trace_flags,
104 tracestate: tracestate.and_then(valid_tracestate).map(str::to_string),
105 })
106 }
107
108 pub fn from_meta(meta: &Value) -> Option<Self> {
115 let meta = meta.as_object()?;
116 let traceparent = meta.get(TRACEPARENT_KEY)?;
117 let Some(traceparent) = traceparent.as_str() else {
118 tracing::debug!("ignoring trace context: traceparent is not a string");
119 return None;
120 };
121 let tracestate = meta.get(TRACESTATE_KEY).and_then(Value::as_str);
122 let parsed = Self::parse(traceparent, tracestate);
123 if parsed.is_none() {
124 tracing::debug!(
125 traceparent_len = traceparent.len(),
126 "ignoring trace context: malformed traceparent"
127 );
128 }
129 parsed
130 }
131
132 pub fn version(&self) -> u8 {
134 self.version
135 }
136
137 pub fn trace_id(&self) -> &str {
139 &self.trace_id
140 }
141
142 pub fn parent_id(&self) -> &str {
145 &self.parent_id
146 }
147
148 pub fn trace_flags(&self) -> u8 {
150 self.trace_flags
151 }
152
153 pub fn sampled(&self) -> bool {
155 self.trace_flags & 0x01 != 0
156 }
157
158 pub fn tracestate(&self) -> Option<&str> {
161 self.tracestate.as_deref()
162 }
163
164 pub fn traceparent(&self) -> String {
170 format!(
171 "{:02x}-{}-{}-{:02x}",
172 self.version, self.trace_id, self.parent_id, self.trace_flags
173 )
174 }
175
176 pub fn entries(&self) -> impl Iterator<Item = (&'static str, String)> + '_ {
197 std::iter::once((TRACEPARENT_KEY, self.traceparent())).chain(
198 self.tracestate()
199 .map(|state| (TRACESTATE_KEY, state.to_string())),
200 )
201 }
202}
203
204fn parse_traceparent(value: &str) -> Option<(u8, &str, &str, u8)> {
207 if !value.is_ascii() || value.len() < TRACEPARENT_LEN {
210 return None;
211 }
212 let bytes = value.as_bytes();
213 if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
214 return None;
215 }
216
217 let version = value.get(0..2)?;
218 let trace_id = value.get(3..35)?;
219 let parent_id = value.get(36..52)?;
220 let flags = value.get(53..55)?;
221 if !is_lower_hex(version)
222 || !is_lower_hex(trace_id)
223 || !is_lower_hex(parent_id)
224 || !is_lower_hex(flags)
225 {
226 return None;
227 }
228 if is_all_zero(trace_id) || is_all_zero(parent_id) {
229 return None;
230 }
231
232 let version = u8::from_str_radix(version, 16).ok()?;
233 match version {
234 0xff => return None,
235 0x00 if value.len() != TRACEPARENT_LEN => return None,
237 _ if value.len() > TRACEPARENT_LEN && bytes[TRACEPARENT_LEN] != b'-' => return None,
239 _ => {}
240 }
241
242 let flags = u8::from_str_radix(flags, 16).ok()?;
243 Some((version, trace_id, parent_id, flags))
244}
245
246fn valid_tracestate(value: &str) -> Option<&str> {
248 if value.chars().count() > MAX_TRACESTATE_LEN {
249 return None;
250 }
251 let members = value
252 .split(',')
253 .filter(|member| !member.trim().is_empty())
254 .count();
255 if members == 0 || members > MAX_TRACESTATE_MEMBERS {
256 return None;
257 }
258 Some(value)
259}
260
261fn is_lower_hex(value: &str) -> bool {
262 value
263 .bytes()
264 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
265}
266
267fn is_all_zero(value: &str) -> bool {
268 value.bytes().all(|byte| byte == b'0')
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use serde_json::json;
275
276 const TRACE_ID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
277 const PARENT_ID: &str = "00f067aa0ba902b7";
278 const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
279
280 #[test]
281 fn parses_a_valid_traceparent_without_tracestate() {
282 let ctx = TraceContext::parse(VALID, None).unwrap();
283 assert_eq!(ctx.version(), 0);
284 assert_eq!(ctx.trace_id(), TRACE_ID);
285 assert_eq!(ctx.parent_id(), PARENT_ID);
286 assert_eq!(ctx.trace_flags(), 1);
287 assert!(ctx.sampled());
288 assert_eq!(ctx.tracestate(), None);
289 }
290
291 #[test]
292 fn parses_a_valid_traceparent_with_tracestate() {
293 let ctx =
294 TraceContext::parse(VALID, Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")).unwrap();
295 assert_eq!(
296 ctx.tracestate(),
297 Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")
298 );
299 }
300
301 #[test]
302 fn unsampled_flag_is_reported() {
303 let ctx = TraceContext::parse(
304 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
305 None,
306 )
307 .unwrap();
308 assert!(!ctx.sampled());
309 assert_eq!(ctx.trace_flags(), 0);
310 }
311
312 #[test]
313 fn sampled_is_bit_zero_only() {
314 let ctx = TraceContext::parse(
315 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02",
316 None,
317 )
318 .unwrap();
319 assert!(!ctx.sampled());
320 assert_eq!(ctx.trace_flags(), 2);
321 }
322
323 #[test]
324 fn rejects_wrong_length() {
325 for value in [
326 "",
327 "00",
328 &VALID[..54],
329 &format!("{VALID}0"),
330 &format!("{VALID}-extra"),
332 "00-4bf92f3577b34da6a3ce929d0e0e473-00f067aa0ba902b7-01",
333 "00-4bf92f3577b34da6a3ce929d0e0e47366-00f067aa0ba902b7-01",
334 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b-01",
335 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
336 ] {
337 assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
338 }
339 }
340
341 #[test]
342 fn rejects_uppercase_hex() {
343 for value in [
344 "00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
345 "00-4bf92f3577b34da6a3ce929d0e0e4736-00F067AA0BA902B7-01",
346 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0A",
347 "0A-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
348 ] {
349 assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
350 }
351 }
352
353 #[test]
354 fn rejects_non_hex_and_bad_separators() {
355 for value in [
356 "00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
357 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902bz-01",
358 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0x",
359 "zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
360 "00_4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
361 "00-4bf92f3577b34da6a3ce929d0e0e4736_00f067aa0ba902b7-01",
362 "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7_01",
363 "00-4bf92f3577b34da6a3ce929d0e0e473\u{e9}-00f067aa0ba902b7-01",
365 ] {
366 assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
367 }
368 }
369
370 #[test]
371 fn rejects_all_zero_trace_id() {
372 let value = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
373 assert!(TraceContext::parse(value, None).is_none());
374 }
375
376 #[test]
377 fn rejects_all_zero_parent_id() {
378 let value = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
379 assert!(TraceContext::parse(value, None).is_none());
380 }
381
382 #[test]
383 fn rejects_version_ff() {
384 let value = "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
385 assert!(TraceContext::parse(value, None).is_none());
386 }
387
388 #[test]
389 fn future_version_reads_the_first_four_fields() {
390 let ctx = TraceContext::parse(
392 "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
393 None,
394 )
395 .unwrap();
396 assert_eq!(ctx.version(), 1);
397 assert_eq!(ctx.trace_id(), TRACE_ID);
398 assert_eq!(ctx.parent_id(), PARENT_ID);
399 assert!(ctx.sampled());
400
401 let ctx = TraceContext::parse(
403 "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-will-be-like",
404 None,
405 )
406 .unwrap();
407 assert_eq!(ctx.version(), 0xcc);
408 assert_eq!(ctx.trace_id(), TRACE_ID);
409 }
410
411 #[test]
412 fn future_version_still_enforces_the_field_rules() {
413 for value in [
414 "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01x",
416 "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-010",
417 "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
419 "01-00000000000000000000000000000000-00f067aa0ba902b7-01-x",
420 "01-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01-x",
421 "01-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01-x",
422 ] {
423 assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
424 }
425 }
426
427 #[test]
428 fn oversized_tracestate_is_dropped_but_traceparent_kept() {
429 let too_long = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN));
430 let ctx = TraceContext::parse(VALID, Some(&too_long)).unwrap();
431 assert_eq!(ctx.trace_id(), TRACE_ID);
432 assert_eq!(ctx.tracestate(), None);
433
434 let at_limit = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN - 2));
435 assert_eq!(at_limit.len(), MAX_TRACESTATE_LEN);
436 let ctx = TraceContext::parse(VALID, Some(&at_limit)).unwrap();
437 assert_eq!(ctx.tracestate(), Some(at_limit.as_str()));
438 }
439
440 #[test]
441 fn tracestate_over_the_member_limit_is_dropped() {
442 let members = |n: usize| {
443 (0..n)
444 .map(|i| format!("k{i}=v"))
445 .collect::<Vec<_>>()
446 .join(",")
447 };
448 let ok = members(MAX_TRACESTATE_MEMBERS);
449 let ctx = TraceContext::parse(VALID, Some(&ok)).unwrap();
450 assert_eq!(ctx.tracestate(), Some(ok.as_str()));
451
452 let too_many = members(MAX_TRACESTATE_MEMBERS + 1);
453 let ctx = TraceContext::parse(VALID, Some(&too_many)).unwrap();
454 assert_eq!(ctx.trace_id(), TRACE_ID);
455 assert_eq!(ctx.tracestate(), None);
456 }
457
458 #[test]
459 fn empty_tracestate_reads_as_absent() {
460 for value in ["", " ", ",", " , "] {
461 let ctx = TraceContext::parse(VALID, Some(value)).unwrap();
462 assert_eq!(ctx.tracestate(), None, "{value:?}");
463 }
464 }
465
466 #[test]
467 fn traceparent_round_trips() {
468 let ctx = TraceContext::parse(VALID, None).unwrap();
469 assert_eq!(ctx.traceparent(), VALID);
470
471 let unsampled = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
472 let ctx = TraceContext::parse(unsampled, None).unwrap();
473 assert_eq!(ctx.traceparent(), unsampled);
474
475 let ctx = TraceContext::parse(
477 "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a-future",
478 None,
479 )
480 .unwrap();
481 assert_eq!(
482 ctx.traceparent(),
483 "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a"
484 );
485 assert!(TraceContext::parse(&ctx.traceparent(), None).is_some());
486 }
487
488 #[test]
489 fn entries_are_the_meta_keys() {
490 let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
491 let entries: Vec<_> = ctx.entries().collect();
492 assert_eq!(
493 entries,
494 [
495 ("traceparent", VALID.to_string()),
496 ("tracestate", "vendor=value".to_string()),
497 ]
498 );
499
500 let ctx = TraceContext::parse(VALID, None).unwrap();
501 let entries: Vec<_> = ctx.entries().collect();
502 assert_eq!(entries, [("traceparent", VALID.to_string())]);
503 }
504
505 #[test]
506 fn entries_read_back_through_from_meta() {
507 let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
508 let meta: serde_json::Map<String, Value> = ctx
509 .entries()
510 .map(|(key, value)| (key.to_string(), Value::String(value)))
511 .collect();
512 assert_eq!(TraceContext::from_meta(&Value::Object(meta)), Some(ctx));
513 }
514
515 #[test]
516 fn from_meta_reads_both_keys() {
517 let meta = json!({
518 "traceparent": VALID,
519 "tracestate": "vendor=value",
520 "progressToken": "p",
521 });
522 let ctx = TraceContext::from_meta(&meta).unwrap();
523 assert_eq!(ctx.trace_id(), TRACE_ID);
524 assert_eq!(ctx.tracestate(), Some("vendor=value"));
525 }
526
527 #[test]
528 fn from_meta_ignores_absent_and_malformed_values() {
529 for meta in [
530 json!(null),
531 json!("traceparent"),
532 json!({}),
533 json!({"tracestate": "vendor=value"}),
534 json!({"traceparent": 7}),
535 json!({"traceparent": null}),
536 json!({"traceparent": "not-a-traceparent", "tracestate": "vendor=value"}),
537 ] {
538 assert!(TraceContext::from_meta(&meta).is_none(), "{meta}");
539 }
540 }
541
542 #[test]
543 fn from_meta_treats_a_non_string_tracestate_as_absent() {
544 let meta = json!({"traceparent": VALID, "tracestate": 5});
545 let ctx = TraceContext::from_meta(&meta).unwrap();
546 assert_eq!(ctx.trace_id(), TRACE_ID);
547 assert_eq!(ctx.tracestate(), None);
548 }
549}