use std::collections::BTreeMap;
use serde_json::Value;
use crate::context::{bind_context, ContextGuard};
use crate::tracer::set_trace_context;
const MAX_HEADER_LENGTH: usize = 512;
const MAX_TRACESTATE_PAIRS: usize = 32;
const MAX_BAGGAGE_LENGTH: usize = 8192;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PropagationContext {
pub traceparent: Option<String>,
pub tracestate: Option<String>,
pub baggage: Option<String>,
pub trace_id: Option<String>,
pub span_id: Option<String>,
}
pub struct PropagationGuard {
trace_guard: Option<ContextGuard>,
context_guard: Option<ContextGuard>,
}
impl Drop for PropagationGuard {
#[cfg_attr(test, mutants::skip)] fn drop(&mut self) {
drop(self.trace_guard.take());
drop(self.context_guard.take());
}
}
fn is_baggage_token(key: &str) -> bool {
!key.is_empty()
&& key
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b))
}
fn strip_control_chars(value: &str) -> String {
value
.chars()
.filter(|c| !matches!(*c, '\x00'..='\x08' | '\x0a'..='\x1f' | '\x7f'))
.collect()
}
pub fn parse_baggage(raw: &str) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for member in raw.split(',') {
let kv = member.split(';').next().unwrap_or("");
if let Some(eq_idx) = kv.find('=') {
let key = kv[..eq_idx].trim();
if is_baggage_token(key) {
let value = kv[eq_idx + 1..].trim();
result.insert(key.to_string(), strip_control_chars(value));
}
}
}
result
}
fn parse_traceparent(value: Option<&str>) -> (Option<String>, Option<String>, Option<String>) {
let Some(raw) = value else {
return (None, None, None);
};
let parts = raw.split('-').collect::<Vec<_>>();
if parts.len() != 4 {
return (None, None, None);
}
let version = parts[0];
let trace_id = parts[1];
let span_id = parts[2];
let flags = parts[3];
let valid = version.len() == 2
&& trace_id.len() == 32
&& span_id.len() == 16
&& flags.len() == 2
&& !version.eq_ignore_ascii_case("ff")
&& trace_id != "00000000000000000000000000000000"
&& span_id != "0000000000000000"
&& [version, trace_id, span_id, flags]
.iter()
.all(|part| part.chars().all(|ch| ch.is_ascii_hexdigit()));
if !valid {
return (None, None, None);
}
(
Some(raw.to_string()),
Some(trace_id.to_ascii_lowercase()),
Some(span_id.to_ascii_lowercase()),
)
}
fn is_forwardable_tracestate(value: &str) -> bool {
value.split(',').all(is_tracestate_member)
}
fn is_tracestate_member(member: &str) -> bool {
let trimmed = member.trim_matches([' ', '\t']);
let Some((key, val)) = trimmed.split_once('=') else {
return false;
};
if key.is_empty() || key.len() > 256 {
return false;
}
let mut chars = key.chars();
let first = chars.next().expect("key checked non-empty");
if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
return false;
}
if !chars.all(|c| {
c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '-' | '*' | '/' | '@')
}) {
return false;
}
val.chars()
.all(|c| matches!(c, '\x20'..='\x2b' | '\x2d'..='\x3c' | '\x3e'..='\x7e'))
}
pub fn extract_w3c_context(
traceparent: Option<&str>,
tracestate: Option<&str>,
baggage: Option<&str>,
) -> PropagationContext {
let traceparent = traceparent.filter(|value| value.len() <= MAX_HEADER_LENGTH);
let tracestate = tracestate.and_then(|value| {
if value.len() > MAX_HEADER_LENGTH
|| value.split(',').count() > MAX_TRACESTATE_PAIRS
|| !is_forwardable_tracestate(value)
{
None
} else {
Some(value.to_string())
}
});
let baggage = baggage.and_then(|value| {
if value.len() > MAX_BAGGAGE_LENGTH {
None
} else {
Some(value.to_string())
}
});
let (traceparent, trace_id, span_id) = parse_traceparent(traceparent);
PropagationContext {
traceparent,
tracestate,
baggage,
trace_id,
span_id,
}
}
pub fn bind_propagation_context(context: PropagationContext) -> PropagationGuard {
let mut fields = Vec::new();
if let Some(traceparent) = context.traceparent.clone() {
fields.push(("traceparent".to_string(), Value::String(traceparent)));
}
if let Some(tracestate) = context.tracestate.clone() {
fields.push(("tracestate".to_string(), Value::String(tracestate)));
}
if let Some(ref baggage) = context.baggage {
fields.push(("baggage".to_string(), Value::String(baggage.clone())));
for (k, v) in parse_baggage(baggage) {
fields.push((format!("baggage.{k}"), Value::String(v)));
}
}
let context_guard = if fields.is_empty() {
None
} else {
Some(bind_context(fields))
};
let trace_guard = if context.trace_id.is_some() || context.span_id.is_some() {
Some(set_trace_context(context.trace_id, context.span_id))
} else {
None
};
PropagationGuard {
trace_guard,
context_guard,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use crate::context::get_context;
use crate::testing::acquire_test_state_lock;
use crate::tracer::get_trace_context;
#[test]
fn propagation_test_a_parse_baggage_keeps_pairs_and_strips_parameters() {
let baggage = parse_baggage("user=alice;prop=x,env=prod;ttl=100,invalid,=skip");
assert_eq!(baggage.get("user").map(String::as_str), Some("alice"));
assert_eq!(baggage.get("env").map(String::as_str), Some("prod"));
assert_eq!(baggage.len(), 2);
}
#[test]
fn propagation_test_a_bind_propagation_context_roundtrip_restores_state() {
let _guard = acquire_test_state_lock();
let context = PropagationContext {
traceparent: Some(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01".to_string(),
),
tracestate: Some("k=v".to_string()),
baggage: Some("user=alice,env=prod".to_string()),
trace_id: Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string()),
span_id: Some("00f067aa0ba902b7".to_string()),
};
{
let _propagation = bind_propagation_context(context);
let trace = get_trace_context();
let fields = get_context();
assert_eq!(
trace.get("trace_id").and_then(std::clone::Clone::clone),
Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
);
assert_eq!(
trace.get("span_id").and_then(std::clone::Clone::clone),
Some("00f067aa0ba902b7".to_string())
);
assert_eq!(
fields.get("traceparent"),
Some(&json!(
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
))
);
assert_eq!(fields.get("tracestate"), Some(&json!("k=v")));
assert_eq!(fields.get("baggage"), Some(&json!("user=alice,env=prod")));
assert_eq!(fields.get("baggage.user"), Some(&json!("alice")));
assert_eq!(fields.get("baggage.env"), Some(&json!("prod")));
}
assert!(get_context().is_empty());
let trace = get_trace_context();
assert_eq!(trace.get("trace_id"), Some(&None));
assert_eq!(trace.get("span_id"), Some(&None));
}
}