use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TraceId([u8; 16]);
impl TraceId {
pub fn from_bytes(bytes: [u8; 16]) -> Option<Self> {
(bytes != [0; 16]).then_some(Self(bytes))
}
pub fn to_bytes(self) -> [u8; 16] {
self.0
}
}
impl fmt::Display for TraceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(f, &self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SpanId([u8; 8]);
impl SpanId {
pub fn from_bytes(bytes: [u8; 8]) -> Option<Self> {
(bytes != [0; 8]).then_some(Self(bytes))
}
pub fn to_bytes(self) -> [u8; 8] {
self.0
}
}
impl fmt::Display for SpanId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_hex(f, &self.0)
}
}
fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut buf = [0u8; 32];
for (i, b) in bytes.iter().enumerate() {
buf[i * 2] = HEX[(b >> 4) as usize];
buf[i * 2 + 1] = HEX[(b & 0x0f) as usize];
}
f.write_str(std::str::from_utf8(&buf[..bytes.len() * 2]).map_err(|_| fmt::Error)?)
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TraceContext {
pub trace_id: TraceId,
pub parent_id: Option<SpanId>,
pub flags: u8,
pub tracestate: Option<http::HeaderValue>,
}
impl TraceContext {
pub fn root(trace_id: TraceId) -> Self {
Self {
trace_id,
parent_id: None,
flags: 0,
tracestate: None,
}
}
pub fn from_headers(headers: &http::HeaderMap) -> Option<Self> {
let mut values = headers.get_all("traceparent").iter();
let traceparent = values.next()?;
if values.next().is_some() {
return None;
}
let (trace_id, parent_id, flags) = parse_traceparent(traceparent.as_bytes())?;
let mut states = headers.get_all("tracestate").iter();
let tracestate = match (states.next(), states.next()) {
(None, _) => None,
(Some(only), None) => Some(only.clone()),
(Some(first), Some(second)) => {
let mut joined = first.as_bytes().to_vec();
for value in std::iter::once(second).chain(states) {
joined.push(b',');
joined.extend_from_slice(value.as_bytes());
}
http::HeaderValue::from_bytes(&joined).ok()
}
};
Some(Self {
trace_id,
parent_id: Some(parent_id),
flags,
tracestate,
})
}
pub fn sampled(&self) -> bool {
self.flags & 0x01 != 0
}
}
pub fn parse_traceparent(value: &[u8]) -> Option<(TraceId, SpanId, u8)> {
if value.len() < 55 {
return None;
}
let version = hex_byte(&value[0..2])?;
if version == 0xff {
return None;
}
if version == 0x00 && value.len() != 55 {
return None;
}
if value.len() > 55 && value[55] != b'-' {
return None;
}
if value[2] != b'-' || value[35] != b'-' || value[52] != b'-' {
return None;
}
let mut trace = [0u8; 16];
for (i, byte) in trace.iter_mut().enumerate() {
*byte = hex_byte(&value[3 + i * 2..5 + i * 2])?;
}
let mut parent = [0u8; 8];
for (i, byte) in parent.iter_mut().enumerate() {
*byte = hex_byte(&value[36 + i * 2..38 + i * 2])?;
}
let flags = hex_byte(&value[53..55])?;
Some((
TraceId::from_bytes(trace)?,
SpanId::from_bytes(parent)?,
flags,
))
}
fn hex_byte(pair: &[u8]) -> Option<u8> {
fn digit(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
_ => None,
}
}
Some((digit(pair[0])? << 4) | digit(pair[1])?)
}
pub const MAX_REQUEST_ID_LEN: usize = 128;
pub fn valid_request_id(id: &[u8]) -> bool {
(1..=MAX_REQUEST_ID_LEN).contains(&id.len()) && id.iter().all(|b| (0x21..=0x7e).contains(b))
}
#[cfg(test)]
mod tests {
use super::*;
const VALID: &str = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
#[test]
fn a_valid_traceparent_parses() {
let (trace, parent, flags) = parse_traceparent(VALID.as_bytes()).unwrap();
assert_eq!(trace.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert_eq!(parent.to_string(), "00f067aa0ba902b7");
assert_eq!(flags, 0x01);
}
#[test]
fn version_ff_is_refused() {
let value = VALID.replacen("00", "ff", 1);
assert_eq!(parse_traceparent(value.as_bytes()), None);
}
#[test]
fn all_zero_ids_are_refused() {
let zero_trace = "00-00000000000000000000000000000000-00f067aa0ba902b7-01";
let zero_parent = "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01";
assert_eq!(parse_traceparent(zero_trace.as_bytes()), None);
assert_eq!(parse_traceparent(zero_parent.as_bytes()), None);
}
#[test]
fn malformed_values_are_refused() {
for value in [
"",
"00",
"00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736x00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e473g-00f067aa0ba902b7-01",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-1",
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-extra",
"cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01x",
] {
assert_eq!(parse_traceparent(value.as_bytes()), None, "{value:?}");
}
}
#[test]
fn a_later_version_is_read_as_00() {
let value = "cc-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-what-the-future-holds";
let (trace, _, _) = parse_traceparent(value.as_bytes()).unwrap();
assert_eq!(trace.to_string(), "4bf92f3577b34da6a3ce929d0e0e4736");
assert!(parse_traceparent(VALID.replacen("00", "01", 1).as_bytes()).is_some());
}
#[test]
fn trace_context_comes_from_headers() {
let mut headers = http::HeaderMap::new();
headers.insert("traceparent", VALID.parse().unwrap());
headers.append("tracestate", "rojo=00f067aa0ba902b7".parse().unwrap());
headers.append("tracestate", "congo=t61rcWkgMzE".parse().unwrap());
let context = TraceContext::from_headers(&headers).unwrap();
assert_eq!(
context.trace_id.to_string(),
"4bf92f3577b34da6a3ce929d0e0e4736"
);
assert!(context.sampled());
assert_eq!(
context.tracestate.unwrap(),
"rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"
);
}
#[test]
fn two_traceparents_are_no_traceparent() {
let mut headers = http::HeaderMap::new();
headers.append("traceparent", VALID.parse().unwrap());
headers.append("traceparent", VALID.parse().unwrap());
assert_eq!(TraceContext::from_headers(&headers), None);
}
#[test]
fn tracestate_alone_means_nothing() {
let mut headers = http::HeaderMap::new();
headers.insert("tracestate", "rojo=00f067aa0ba902b7".parse().unwrap());
assert_eq!(TraceContext::from_headers(&headers), None);
}
#[test]
fn request_ids_are_bounded_visible_ascii() {
assert!(valid_request_id(b"0f8fad5b-d9cb-469f-a165-70867728950e"));
assert!(valid_request_id(b"a"));
assert!(valid_request_id(&[b'x'; MAX_REQUEST_ID_LEN]));
assert!(!valid_request_id(b""));
assert!(!valid_request_id(&[b'x'; MAX_REQUEST_ID_LEN + 1]));
assert!(!valid_request_id(b"has space"));
assert!(!valid_request_id(b"tab\there"));
assert!(!valid_request_id("caf\u{e9}".as_bytes()));
}
}