use rainier_support::Error;
const VERSION: &str = "00";
pub const TRACEPARENT: &str = "traceparent";
pub const TRACESTATE: &str = "tracestate";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceContext {
trace_id: String,
parent_id: String,
sampled: bool,
}
impl TraceContext {
pub fn parse(header: &str) -> Result<Self, Error> {
let parts: Vec<&str> = header.trim().split('-').collect();
let [version, trace_id, parent_id, flags] = parts.as_slice() else {
return Err(Error::bad_request("a traceparent has four dash-separated fields."));
};
if version.len() != 2 || !is_hex(version) || *version == "ff" {
return Err(Error::bad_request("that traceparent version is not valid."));
}
if trace_id.len() != 32 || !is_hex(trace_id) || trace_id.bytes().all(|b| b == b'0') {
return Err(Error::bad_request("a trace id is 32 hex characters and not all zero."));
}
if parent_id.len() != 16 || !is_hex(parent_id) || parent_id.bytes().all(|b| b == b'0') {
return Err(Error::bad_request("a span id is 16 hex characters and not all zero."));
}
if flags.len() != 2 || !is_hex(flags) {
return Err(Error::bad_request("trace flags are two hex characters."));
}
let sampled = u8::from_str_radix(flags, 16).unwrap_or(0) & 0x01 == 0x01;
Ok(Self { trace_id: trace_id.to_lowercase(), parent_id: parent_id.to_lowercase(), sampled })
}
pub fn extract(header: Option<&str>, sampled: bool) -> Self {
match header.map(Self::parse) {
Some(Ok(context)) => context,
Some(Err(e)) => {
tracing::debug!(error = %e.message(), "ignoring a malformed traceparent");
Self::start(sampled)
}
None => Self::start(sampled),
}
}
pub fn start(sampled: bool) -> Self {
Self { trace_id: random_hex(32), parent_id: random_hex(16), sampled }
}
pub fn trace_id(&self) -> &str {
&self.trace_id
}
pub fn parent_id(&self) -> &str {
&self.parent_id
}
pub fn is_sampled(&self) -> bool {
self.sampled
}
pub fn to_header(&self) -> String {
format!("{VERSION}-{}-{}-{:02x}", self.trace_id, self.parent_id, u8::from(self.sampled))
}
pub fn child(&self) -> Self {
Self { trace_id: self.trace_id.clone(), parent_id: random_hex(16), sampled: self.sampled }
}
}
fn is_hex(value: &str) -> bool {
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
fn random_hex(len: usize) -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let sequence = COUNTER.fetch_add(1, Ordering::Relaxed);
let local = 0u8;
let address = std::ptr::addr_of!(local) as u64;
let mut out = String::with_capacity(len);
let mut state = nanos ^ sequence.rotate_left(17) ^ address.rotate_left(33);
while out.len() < len {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
out.push_str(&format!("{state:016x}"));
}
out.truncate(len);
out
}
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
#[test]
fn the_specifications_own_example_parses() {
let context = TraceContext::parse(EXAMPLE).expect("valid");
assert_eq!(context.trace_id(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(context.parent_id(), "00f067aa0ba902b7");
assert!(context.is_sampled());
}
#[test]
fn it_round_trips() {
let context = TraceContext::parse(EXAMPLE).expect("valid");
assert_eq!(context.to_header(), EXAMPLE);
}
#[test]
fn an_unsampled_trace_says_so() {
let header = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00";
let context = TraceContext::parse(header).expect("valid");
assert!(!context.is_sampled());
assert_eq!(context.to_header(), header);
}
#[test]
fn a_malformed_header_is_refused_rather_than_half_read() {
for hostile in [
"",
"nonsense",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7",
"00-tooshort-00f067aa0ba902b7-01",
"00-00000000000000000000000000000000-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01",
"ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-zz",
] {
assert!(TraceContext::parse(hostile).is_err(), "should refuse `{hostile}`");
}
}
#[test]
fn a_future_version_with_extra_fields_is_still_read() {
let header = "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra";
assert!(TraceContext::parse(header).is_err(), "five fields is not this version");
let known = "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
assert!(TraceContext::parse(known).is_ok());
}
#[test]
fn a_missing_header_starts_a_trace_rather_than_dropping_one() {
let context = TraceContext::extract(None, true);
assert_eq!(context.trace_id().len(), 32);
assert_eq!(context.parent_id().len(), 16);
assert!(context.is_sampled());
}
#[test]
fn a_broken_header_starts_a_trace_rather_than_failing_the_request() {
let context = TraceContext::extract(Some("garbage"), true);
assert_eq!(context.trace_id().len(), 32);
}
#[test]
fn a_child_stays_in_the_trace_and_gets_its_own_span() {
let parent = TraceContext::parse(EXAMPLE).expect("valid");
let child = parent.child();
assert_eq!(child.trace_id(), parent.trace_id());
assert_ne!(child.parent_id(), parent.parent_id());
assert_eq!(child.is_sampled(), parent.is_sampled());
}
#[test]
fn generated_ids_do_not_repeat() {
let ids: std::collections::HashSet<String> =
(0..1000).map(|_| TraceContext::start(true).trace_id().to_string()).collect();
assert_eq!(ids.len(), 1000, "a thousand traces should have a thousand ids");
}
#[test]
fn a_generated_header_parses_back() {
let started = TraceContext::start(false);
let round_tripped = TraceContext::parse(&started.to_header()).expect("valid");
assert_eq!(round_tripped, started);
}
}