use std::sync::LazyLock;
use crate::core::sampling::{priority, SamplingPriority};
use crate::dd_warn;
use crate::propagation::{
carrier::{Extractor, Injector},
context::{InjectSpanContext, Sampling, SpanContext},
};
pub const B3_TRACE_ID_KEY: &str = "x-b3-traceid";
pub const B3_SPAN_ID_KEY: &str = "x-b3-spanid";
pub const B3_SAMPLED_KEY: &str = "x-b3-sampled";
pub const B3_FLAGS_KEY: &str = "x-b3-flags";
static B3_HEADER_KEYS: LazyLock<[String; 4]> = LazyLock::new(|| {
[
B3_TRACE_ID_KEY.to_owned(),
B3_SPAN_ID_KEY.to_owned(),
B3_SAMPLED_KEY.to_owned(),
B3_FLAGS_KEY.to_owned(),
]
});
pub fn extract(carrier: &dyn Extractor) -> Option<SpanContext> {
let trace_id_hex = carrier.get(B3_TRACE_ID_KEY)?;
let trace_id = parse_trace_id(trace_id_hex)?;
let span_id = match carrier.get(B3_SPAN_ID_KEY) {
Some(hex) => parse_span_id(hex)?,
None => 0,
};
let priority = extract_priority(carrier);
Some(SpanContext {
trace_id,
span_id,
sampling: Sampling {
priority,
mechanism: None,
},
origin: None,
tags: std::collections::HashMap::new(),
links: Vec::new(),
is_remote: true,
tracestate: None,
})
}
pub fn inject(context: &InjectSpanContext, carrier: &mut dyn Injector) {
carrier.set(B3_TRACE_ID_KEY, format_b3_trace_id(context.trace_id));
carrier.set(B3_SPAN_ID_KEY, format!("{:016x}", context.span_id));
let Some(priority) = context.sampling.priority else {
return;
};
let p = priority.into_i8();
if p <= 0 {
carrier.set(B3_SAMPLED_KEY, "0".to_string());
} else if p == 1 {
carrier.set(B3_SAMPLED_KEY, "1".to_string());
} else {
carrier.set(B3_FLAGS_KEY, "1".to_string());
}
}
pub fn keys() -> &'static [String] {
B3_HEADER_KEYS.as_slice()
}
fn parse_trace_id(hex: &str) -> Option<u128> {
if hex.len() > 32 {
dd_warn!("Propagator (b3multi): trace_id {hex:?} exceeds 32 hex chars");
return None;
}
let id = match u128::from_str_radix(hex, 16) {
Ok(id) => id,
Err(e) => {
dd_warn!("Propagator (b3multi): malformed trace_id {hex:?}: {e}");
return None;
}
};
if id == 0 {
return None;
}
Some(id)
}
fn parse_span_id(hex: &str) -> Option<u64> {
if hex.len() > 16 {
dd_warn!("Propagator (b3multi): span_id {hex:?} exceeds 16 hex chars");
return None;
}
match u64::from_str_radix(hex, 16) {
Ok(id) => Some(id),
Err(e) => {
dd_warn!("Propagator (b3multi): malformed span_id {hex:?}: {e}");
None
}
}
}
fn extract_priority(carrier: &dyn Extractor) -> Option<SamplingPriority> {
if carrier.get(B3_FLAGS_KEY) == Some("1") {
return Some(priority::USER_KEEP);
}
match carrier.get(B3_SAMPLED_KEY) {
Some("0") => Some(priority::AUTO_REJECT),
Some("1") => Some(priority::AUTO_KEEP),
_ => None,
}
}
fn format_b3_trace_id(trace_id: u128) -> String {
if trace_id > u64::MAX as u128 {
format!("{trace_id:032x}")
} else {
format!("{trace_id:016x}")
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use std::collections::HashMap;
use crate::core::configuration::{Config, TracePropagationStyle};
use crate::core::sampling::priority;
use crate::propagation::{context::span_context_to_inject, Propagator};
use super::*;
fn headers(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
#[test]
fn extract_missing_trace_id_returns_none() {
let carrier = headers(&[("x-b3-sampled", "1")]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_64_bit_trace_id() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", "00f067aa0ba902b7"),
("x-b3-sampled", "1"),
]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.trace_id, 0x80f1_98ee_5634_3ba8);
assert_eq!(ctx.span_id, 0x00f0_67aa_0ba9_02b7);
assert_eq!(ctx.sampling.priority, Some(priority::AUTO_KEEP));
assert!(ctx.is_remote);
}
#[test]
fn extract_128_bit_trace_id() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba864fe8b2a57d3eff7"),
("x-b3-spanid", "e457b5a2e4d86bd1"),
("x-b3-sampled", "0"),
]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.trace_id, 0x80f1_98ee_5634_3ba8_64fe_8b2a_57d3_eff7u128);
assert_eq!(ctx.span_id, 0xe457_b5a2_e4d8_6bd1);
assert_eq!(ctx.sampling.priority, Some(priority::AUTO_REJECT));
}
#[test]
fn extract_zero_trace_id_returns_none() {
let carrier = headers(&[("x-b3-traceid", "0000000000000000"), ("x-b3-spanid", "1")]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_malformed_trace_id_returns_none() {
let carrier = headers(&[("x-b3-traceid", "nothex"), ("x-b3-spanid", "1")]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_oversized_trace_id_returns_none() {
let oversized = format!("{:033x}", 1u128);
let carrier = headers(&[("x-b3-traceid", oversized.as_str()), ("x-b3-spanid", "1")]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_oversized_span_id_returns_none() {
let oversized = format!("{:017x}", 1u64);
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", oversized.as_str()),
]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_missing_span_id_yields_zero() {
let carrier = headers(&[("x-b3-traceid", "80f198ee56343ba8")]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.span_id, 0);
}
#[test]
fn extract_zero_span_id_yields_zero() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", "0000000000000000"),
]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.span_id, 0);
}
#[test]
fn extract_malformed_span_id_rejects_context() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", "nothex"),
]);
assert_eq!(extract(&carrier), None);
}
#[test]
fn extract_flags_debug_promotes_to_user_keep() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", "1"),
("x-b3-sampled", "0"),
("x-b3-flags", "1"),
]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.sampling.priority, Some(priority::USER_KEEP));
}
#[test]
fn extract_unknown_sampled_value_defers_priority() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-sampled", "maybe"),
]);
let ctx = extract(&carrier).unwrap();
assert_eq!(ctx.sampling.priority, None);
}
#[test]
fn inject_64_bit_trace_id_emits_16_hex() {
let mut ctx = SpanContext {
trace_id: 0x80f1_98ee_5634_3ba8,
span_id: 0x00f0_67aa_0ba9_02b7,
sampling: Sampling {
priority: Some(priority::AUTO_KEEP),
mechanism: None,
},
..Default::default()
};
let mut carrier = HashMap::new();
inject(&span_context_to_inject(&mut ctx), &mut carrier);
assert_eq!(carrier["x-b3-traceid"], "80f198ee56343ba8");
assert_eq!(carrier["x-b3-spanid"], "00f067aa0ba902b7");
assert_eq!(carrier["x-b3-sampled"], "1");
assert!(!carrier.contains_key("x-b3-flags"));
}
#[test]
fn inject_128_bit_trace_id_emits_32_hex() {
let mut ctx = SpanContext {
trace_id: 0x80f1_98ee_5634_3ba8_64fe_8b2a_57d3_eff7u128,
span_id: 0xe457_b5a2_e4d8_6bd1,
sampling: Sampling {
priority: Some(priority::AUTO_REJECT),
mechanism: None,
},
..Default::default()
};
let mut carrier = HashMap::new();
inject(&span_context_to_inject(&mut ctx), &mut carrier);
assert_eq!(carrier["x-b3-traceid"], "80f198ee56343ba864fe8b2a57d3eff7");
assert_eq!(carrier["x-b3-spanid"], "e457b5a2e4d86bd1");
assert_eq!(carrier["x-b3-sampled"], "0");
}
#[test]
fn inject_user_keep_emits_flags_not_sampled() {
let mut ctx = SpanContext {
trace_id: 1,
span_id: 2,
sampling: Sampling {
priority: Some(priority::USER_KEEP),
mechanism: None,
},
..Default::default()
};
let mut carrier = HashMap::new();
inject(&span_context_to_inject(&mut ctx), &mut carrier);
assert_eq!(carrier.get("x-b3-flags").map(String::as_str), Some("1"));
assert!(!carrier.contains_key("x-b3-sampled"));
}
#[test]
fn inject_without_priority_omits_sampled_and_flags() {
let mut ctx = SpanContext {
trace_id: 1,
span_id: 2,
sampling: Sampling {
priority: None,
mechanism: None,
},
..Default::default()
};
let mut carrier = HashMap::new();
inject(&span_context_to_inject(&mut ctx), &mut carrier);
assert!(!carrier.contains_key("x-b3-sampled"));
assert!(!carrier.contains_key("x-b3-flags"));
}
#[test]
fn propagator_dispatch_routes_to_b3multi() {
let carrier = headers(&[
("x-b3-traceid", "80f198ee56343ba8"),
("x-b3-spanid", "1"),
("x-b3-sampled", "1"),
]);
let propagator = TracePropagationStyle::B3Multi;
let ctx = propagator
.try_extract(&carrier, &Config::builder().build())
.map(Result::unwrap)
.expect("b3multi dispatch should produce context");
assert_eq!(ctx.trace_id, 0x80f1_98ee_5634_3ba8);
}
#[test]
fn propagator_dispatch_exposes_keys() {
let propagator = TracePropagationStyle::B3Multi;
let k: &[String] = <TracePropagationStyle as Propagator<Config>>::keys(&propagator);
assert_eq!(k.len(), 4);
assert!(k.iter().any(|s| s == "x-b3-traceid"));
assert!(k.iter().any(|s| s == "x-b3-spanid"));
assert!(k.iter().any(|s| s == "x-b3-sampled"));
assert!(k.iter().any(|s| s == "x-b3-flags"));
}
}