use serde_json::Value;
const TRACEPARENT_LEN: usize = 55;
const TRACEPARENT_KEY: &str = "traceparent";
const TRACESTATE_KEY: &str = "tracestate";
const MAX_TRACESTATE_LEN: usize = 512;
const MAX_TRACESTATE_MEMBERS: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceContext {
version: u8,
trace_id: String,
parent_id: String,
trace_flags: u8,
tracestate: Option<String>,
}
impl TraceContext {
pub fn parse(traceparent: &str, tracestate: Option<&str>) -> Option<Self> {
let (version, trace_id, parent_id, trace_flags) = parse_traceparent(traceparent)?;
Some(Self {
version,
trace_id: trace_id.to_string(),
parent_id: parent_id.to_string(),
trace_flags,
tracestate: tracestate.and_then(valid_tracestate).map(str::to_string),
})
}
pub fn from_meta(meta: &Value) -> Option<Self> {
let meta = meta.as_object()?;
let traceparent = meta.get(TRACEPARENT_KEY)?;
let Some(traceparent) = traceparent.as_str() else {
tracing::debug!("ignoring trace context: traceparent is not a string");
return None;
};
let tracestate = meta.get(TRACESTATE_KEY).and_then(Value::as_str);
let parsed = Self::parse(traceparent, tracestate);
if parsed.is_none() {
tracing::debug!(
traceparent_len = traceparent.len(),
"ignoring trace context: malformed traceparent"
);
}
parsed
}
pub fn version(&self) -> u8 {
self.version
}
pub fn trace_id(&self) -> &str {
&self.trace_id
}
pub fn parent_id(&self) -> &str {
&self.parent_id
}
pub fn trace_flags(&self) -> u8 {
self.trace_flags
}
pub fn sampled(&self) -> bool {
self.trace_flags & 0x01 != 0
}
pub fn tracestate(&self) -> Option<&str> {
self.tracestate.as_deref()
}
pub fn traceparent(&self) -> String {
format!(
"{:02x}-{}-{}-{:02x}",
self.version, self.trace_id, self.parent_id, self.trace_flags
)
}
pub fn entries(&self) -> impl Iterator<Item = (&'static str, String)> + '_ {
std::iter::once((TRACEPARENT_KEY, self.traceparent())).chain(
self.tracestate()
.map(|state| (TRACESTATE_KEY, state.to_string())),
)
}
}
fn parse_traceparent(value: &str) -> Option<(u8, &str, &str, u8)> {
if !value.is_ascii() || value.len() < TRACEPARENT_LEN {
return None;
}
let bytes = value.as_bytes();
if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
return None;
}
let version = value.get(0..2)?;
let trace_id = value.get(3..35)?;
let parent_id = value.get(36..52)?;
let flags = value.get(53..55)?;
if !is_lower_hex(version)
|| !is_lower_hex(trace_id)
|| !is_lower_hex(parent_id)
|| !is_lower_hex(flags)
{
return None;
}
if is_all_zero(trace_id) || is_all_zero(parent_id) {
return None;
}
let version = u8::from_str_radix(version, 16).ok()?;
match version {
0xff => return None,
0x00 if value.len() != TRACEPARENT_LEN => return None,
_ if value.len() > TRACEPARENT_LEN && bytes[TRACEPARENT_LEN] != b'-' => return None,
_ => {}
}
let flags = u8::from_str_radix(flags, 16).ok()?;
Some((version, trace_id, parent_id, flags))
}
fn valid_tracestate(value: &str) -> Option<&str> {
if value.chars().count() > MAX_TRACESTATE_LEN {
return None;
}
let members = value
.split(',')
.filter(|member| !member.trim().is_empty())
.count();
if members == 0 || members > MAX_TRACESTATE_MEMBERS {
return None;
}
Some(value)
}
fn is_lower_hex(value: &str) -> bool {
value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn is_all_zero(value: &str) -> bool {
value.bytes().all(|byte| byte == b'0')
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const TRACE_ID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
const PARENT_ID: &str = "00f067aa0ba902b7";
const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
#[test]
fn parses_a_valid_traceparent_without_tracestate() {
let ctx = TraceContext::parse(VALID, None).unwrap();
assert_eq!(ctx.version(), 0);
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.parent_id(), PARENT_ID);
assert_eq!(ctx.trace_flags(), 1);
assert!(ctx.sampled());
assert_eq!(ctx.tracestate(), None);
}
#[test]
fn parses_a_valid_traceparent_with_tracestate() {
let ctx =
TraceContext::parse(VALID, Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")).unwrap();
assert_eq!(
ctx.tracestate(),
Some("congo=t61rcWkgMzE,rojo=00f067aa0ba902b7")
);
}
#[test]
fn unsampled_flag_is_reported() {
let ctx = TraceContext::parse(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00",
None,
)
.unwrap();
assert!(!ctx.sampled());
assert_eq!(ctx.trace_flags(), 0);
}
#[test]
fn sampled_is_bit_zero_only() {
let ctx = TraceContext::parse(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02",
None,
)
.unwrap();
assert!(!ctx.sampled());
assert_eq!(ctx.trace_flags(), 2);
}
#[test]
fn rejects_wrong_length() {
for value in [
"",
"00",
&VALID[..54],
&format!("{VALID}0"),
&format!("{VALID}-extra"),
"00-4bf92f3577b34da6a3ce929d0e0e473-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e47366-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
] {
assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
}
}
#[test]
fn rejects_uppercase_hex() {
for value in [
"00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00F067AA0BA902B7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0A",
"0A-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
] {
assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
}
}
#[test]
fn rejects_non_hex_and_bad_separators() {
for value in [
"00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902bz-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0x",
"zz-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"00_4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736_00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7_01",
"00-4bf92f3577b34da6a3ce929d0e0e473\u{e9}-00f067aa0ba902b7-01",
] {
assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
}
}
#[test]
fn rejects_all_zero_trace_id() {
let value = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
assert!(TraceContext::parse(value, None).is_none());
}
#[test]
fn rejects_all_zero_parent_id() {
let value = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
assert!(TraceContext::parse(value, None).is_none());
}
#[test]
fn rejects_version_ff() {
let value = "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
assert!(TraceContext::parse(value, None).is_none());
}
#[test]
fn future_version_reads_the_first_four_fields() {
let ctx = TraceContext::parse(
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
None,
)
.unwrap();
assert_eq!(ctx.version(), 1);
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.parent_id(), PARENT_ID);
assert!(ctx.sampled());
let ctx = TraceContext::parse(
"cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-will-be-like",
None,
)
.unwrap();
assert_eq!(ctx.version(), 0xcc);
assert_eq!(ctx.trace_id(), TRACE_ID);
}
#[test]
fn future_version_still_enforces_the_field_rules() {
for value in [
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01x",
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-010",
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
"01-00000000000000000000000000000000-00f067aa0ba902b7-01-x",
"01-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01-x",
"01-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01-x",
] {
assert!(TraceContext::parse(value, None).is_none(), "{value:?}");
}
}
#[test]
fn oversized_tracestate_is_dropped_but_traceparent_kept() {
let too_long = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN));
let ctx = TraceContext::parse(VALID, Some(&too_long)).unwrap();
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.tracestate(), None);
let at_limit = format!("k={}", "v".repeat(MAX_TRACESTATE_LEN - 2));
assert_eq!(at_limit.len(), MAX_TRACESTATE_LEN);
let ctx = TraceContext::parse(VALID, Some(&at_limit)).unwrap();
assert_eq!(ctx.tracestate(), Some(at_limit.as_str()));
}
#[test]
fn tracestate_over_the_member_limit_is_dropped() {
let members = |n: usize| {
(0..n)
.map(|i| format!("k{i}=v"))
.collect::<Vec<_>>()
.join(",")
};
let ok = members(MAX_TRACESTATE_MEMBERS);
let ctx = TraceContext::parse(VALID, Some(&ok)).unwrap();
assert_eq!(ctx.tracestate(), Some(ok.as_str()));
let too_many = members(MAX_TRACESTATE_MEMBERS + 1);
let ctx = TraceContext::parse(VALID, Some(&too_many)).unwrap();
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.tracestate(), None);
}
#[test]
fn empty_tracestate_reads_as_absent() {
for value in ["", " ", ",", " , "] {
let ctx = TraceContext::parse(VALID, Some(value)).unwrap();
assert_eq!(ctx.tracestate(), None, "{value:?}");
}
}
#[test]
fn traceparent_round_trips() {
let ctx = TraceContext::parse(VALID, None).unwrap();
assert_eq!(ctx.traceparent(), VALID);
let unsampled = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
let ctx = TraceContext::parse(unsampled, None).unwrap();
assert_eq!(ctx.traceparent(), unsampled);
let ctx = TraceContext::parse(
"cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a-future",
None,
)
.unwrap();
assert_eq!(
ctx.traceparent(),
"cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-0a"
);
assert!(TraceContext::parse(&ctx.traceparent(), None).is_some());
}
#[test]
fn entries_are_the_meta_keys() {
let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
let entries: Vec<_> = ctx.entries().collect();
assert_eq!(
entries,
[
("traceparent", VALID.to_string()),
("tracestate", "vendor=value".to_string()),
]
);
let ctx = TraceContext::parse(VALID, None).unwrap();
let entries: Vec<_> = ctx.entries().collect();
assert_eq!(entries, [("traceparent", VALID.to_string())]);
}
#[test]
fn entries_read_back_through_from_meta() {
let ctx = TraceContext::parse(VALID, Some("vendor=value")).unwrap();
let meta: serde_json::Map<String, Value> = ctx
.entries()
.map(|(key, value)| (key.to_string(), Value::String(value)))
.collect();
assert_eq!(TraceContext::from_meta(&Value::Object(meta)), Some(ctx));
}
#[test]
fn from_meta_reads_both_keys() {
let meta = json!({
"traceparent": VALID,
"tracestate": "vendor=value",
"progressToken": "p",
});
let ctx = TraceContext::from_meta(&meta).unwrap();
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.tracestate(), Some("vendor=value"));
}
#[test]
fn from_meta_ignores_absent_and_malformed_values() {
for meta in [
json!(null),
json!("traceparent"),
json!({}),
json!({"tracestate": "vendor=value"}),
json!({"traceparent": 7}),
json!({"traceparent": null}),
json!({"traceparent": "not-a-traceparent", "tracestate": "vendor=value"}),
] {
assert!(TraceContext::from_meta(&meta).is_none(), "{meta}");
}
}
#[test]
fn from_meta_treats_a_non_string_tracestate_as_absent() {
let meta = json!({"traceparent": VALID, "tracestate": 5});
let ctx = TraceContext::from_meta(&meta).unwrap();
assert_eq!(ctx.trace_id(), TRACE_ID);
assert_eq!(ctx.tracestate(), None);
}
}