use std::fmt;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::error::DomainError;
const TRACE_ID_HEX_LEN: usize = 32;
const SPAN_ID_HEX_LEN: usize = 16;
const FLAGS_HEX_LEN: usize = 2;
const VERSION_HEX_LEN: usize = 2;
const VERSION: &str = "00";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TraceContext {
trace_id: String,
span_id: String,
flags: String,
}
impl TraceContext {
pub fn parse(header: &str) -> Result<Self, DomainError> {
let parts: Vec<&str> = header.splitn(5, '-').collect();
if parts.len() < 4 {
return Err(DomainError::InvalidCharacters {
field: "traceparent",
});
}
validate_hex(parts[0], VERSION_HEX_LEN, "traceparent.version")?;
validate_hex_nonzero(parts[1], TRACE_ID_HEX_LEN, "traceparent.trace_id")?;
validate_hex_nonzero(parts[2], SPAN_ID_HEX_LEN, "traceparent.span_id")?;
validate_hex(parts[3], FLAGS_HEX_LEN, "traceparent.flags")?;
Ok(Self {
trace_id: parts[1].to_ascii_lowercase(),
span_id: parts[2].to_ascii_lowercase(),
flags: parts[3].to_ascii_lowercase(),
})
}
pub fn new(
trace_id: impl Into<String>,
span_id: impl Into<String>,
flags: impl Into<String>,
) -> Result<Self, DomainError> {
let trace_id = trace_id.into().to_ascii_lowercase();
let span_id = span_id.into().to_ascii_lowercase();
let flags = flags.into().to_ascii_lowercase();
validate_hex_nonzero(&trace_id, TRACE_ID_HEX_LEN, "traceparent.trace_id")?;
validate_hex_nonzero(&span_id, SPAN_ID_HEX_LEN, "traceparent.span_id")?;
validate_hex(&flags, FLAGS_HEX_LEN, "traceparent.flags")?;
Ok(Self {
trace_id,
span_id,
flags,
})
}
#[must_use]
pub fn generate() -> Self {
let trace_uuid = Uuid::new_v4();
let span_bytes = Uuid::new_v4().into_bytes();
let trace_id = hex_encode(trace_uuid.as_bytes());
let mut span_id = hex_encode(&span_bytes[..8]);
if span_id.bytes().all(|c| c == b'0') {
span_id.replace_range(0..1, "1");
}
Self {
trace_id,
span_id,
flags: "01".to_owned(),
}
}
#[must_use]
pub fn trace_id(&self) -> &str {
&self.trace_id
}
#[must_use]
pub fn span_id(&self) -> &str {
&self.span_id
}
#[must_use]
pub fn flags(&self) -> &str {
&self.flags
}
#[must_use]
pub fn to_header(&self) -> String {
format!(
"{VERSION}-{}-{}-{}",
self.trace_id, self.span_id, self.flags
)
}
}
impl fmt::Display for TraceContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_header())
}
}
fn validate_hex(s: &str, expected_len: usize, field: &'static str) -> Result<(), DomainError> {
if s.len() != expected_len {
return Err(DomainError::FieldTooLong {
field,
actual: s.len(),
max: expected_len,
});
}
if !s.bytes().all(|c| c.is_ascii_hexdigit()) {
return Err(DomainError::InvalidCharacters { field });
}
Ok(())
}
fn validate_hex_nonzero(
s: &str,
expected_len: usize,
field: &'static str,
) -> Result<(), DomainError> {
validate_hex(s, expected_len, field)?;
if s.bytes().all(|c| c == b'0') {
return Err(DomainError::InvariantViolated {
reason: "traceparent: trace_id and span_id must not be all zeros",
});
}
Ok(())
}
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
write!(out, "{byte:02x}").unwrap();
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01";
#[test]
fn parse_accepts_the_w3c_example() {
let ctx = TraceContext::parse(SAMPLE).unwrap();
assert_eq!(ctx.trace_id(), "0af7651916cd43dd8448eb211c80319c");
assert_eq!(ctx.span_id(), "b7ad6b7169203331");
assert_eq!(ctx.flags(), "01");
}
#[test]
fn to_header_roundtrips() {
let ctx = TraceContext::parse(SAMPLE).unwrap();
assert_eq!(ctx.to_header(), SAMPLE);
}
#[test]
fn parse_is_case_insensitive_but_stores_lowercase() {
let upper = "00-0AF7651916CD43DD8448EB211C80319C-B7AD6B7169203331-01";
let ctx = TraceContext::parse(upper).unwrap();
assert_eq!(ctx.trace_id(), "0af7651916cd43dd8448eb211c80319c");
assert_eq!(ctx.span_id(), "b7ad6b7169203331");
}
#[test]
fn parse_tolerates_trailing_fields_from_future_versions() {
let extended = format!("{SAMPLE}-extra-future-stuff");
let ctx = TraceContext::parse(&extended).unwrap();
assert_eq!(ctx.trace_id(), "0af7651916cd43dd8448eb211c80319c");
}
#[test]
fn parse_rejects_wrong_trace_id_length() {
let bad = "00-0af7651916cd43dd8448eb211c80319-b7ad6b7169203331-01"; let err = TraceContext::parse(bad).unwrap_err();
assert!(matches!(err, DomainError::FieldTooLong { .. }));
}
#[test]
fn parse_rejects_all_zero_trace_id() {
let bad = "00-00000000000000000000000000000000-b7ad6b7169203331-01";
let err = TraceContext::parse(bad).unwrap_err();
assert!(matches!(err, DomainError::InvariantViolated { .. }));
}
#[test]
fn parse_rejects_all_zero_span_id() {
let bad = "00-0af7651916cd43dd8448eb211c80319c-0000000000000000-01";
let err = TraceContext::parse(bad).unwrap_err();
assert!(matches!(err, DomainError::InvariantViolated { .. }));
}
#[test]
fn parse_rejects_non_hex_characters() {
let bad = "00-gggggggggggggggggggggggggggggggg-b7ad6b7169203331-01";
let err = TraceContext::parse(bad).unwrap_err();
assert!(matches!(err, DomainError::InvalidCharacters { .. }));
}
#[test]
fn parse_rejects_truncated_header() {
let err = TraceContext::parse("00-0af76").unwrap_err();
assert!(matches!(err, DomainError::InvalidCharacters { .. }));
}
#[test]
fn generate_produces_valid_parseable_context() {
let ctx = TraceContext::generate();
let parsed = TraceContext::parse(&ctx.to_header()).unwrap();
assert_eq!(parsed, ctx);
assert_eq!(ctx.flags(), "01");
assert_eq!(ctx.trace_id().len(), TRACE_ID_HEX_LEN);
assert_eq!(ctx.span_id().len(), SPAN_ID_HEX_LEN);
}
#[test]
fn generate_produces_distinct_ids_on_successive_calls() {
let a = TraceContext::generate();
let b = TraceContext::generate();
assert_ne!(a.trace_id(), b.trace_id());
}
#[test]
fn display_matches_to_header() {
let ctx = TraceContext::parse(SAMPLE).unwrap();
assert_eq!(format!("{ctx}"), SAMPLE);
}
#[test]
fn serde_roundtrip_preserves_fields() {
let ctx = TraceContext::parse(SAMPLE).unwrap();
let json = serde_json::to_string(&ctx).unwrap();
let back: TraceContext = serde_json::from_str(&json).unwrap();
assert_eq!(ctx, back);
}
}