#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OwnedTraceContext {
pub trace_id: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub operation: String,
pub correlation_id: Option<String>,
}
impl OwnedTraceContext {
#[must_use]
pub fn root(
trace_id: impl Into<String>,
span_id: impl Into<String>,
operation: impl Into<String>,
) -> Self {
Self {
trace_id: trace_id.into(),
span_id: span_id.into(),
parent_span_id: None,
operation: operation.into(),
correlation_id: None,
}
}
#[must_use]
pub fn child(&self, span_id: impl Into<String>, operation: impl Into<String>) -> Self {
Self {
trace_id: self.trace_id.clone(),
span_id: span_id.into(),
parent_span_id: Some(self.span_id.clone()),
operation: operation.into(),
correlation_id: self.correlation_id.clone(),
}
}
#[must_use]
pub fn as_child_span_spec(&self) -> ChildSpanSpec<'_> {
ChildSpanSpec {
trace_id: &self.trace_id,
parent_span_id: &self.span_id,
operation: &self.operation,
correlation_id: self.correlation_id.as_deref(),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ChildSpanSpec<'a> {
pub trace_id: &'a str,
pub parent_span_id: &'a str,
pub operation: &'a str,
pub correlation_id: Option<&'a str>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TraceparentParseError {
InvalidSegmentCount,
InvalidVersion,
InvalidTraceId,
InvalidParentId,
InvalidFlags,
}
impl TraceparentParseError {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::InvalidSegmentCount => {
"traceparent must have exactly four dash-separated segments"
}
Self::InvalidVersion => "traceparent version must be '00'",
Self::InvalidTraceId => "trace-id must be exactly 32 lowercase hex characters",
Self::InvalidParentId => "parent-id must be exactly 16 lowercase hex characters",
Self::InvalidFlags => "trace-flags must be exactly two lowercase hex characters",
}
}
}
fn is_lowercase_hex(s: &str) -> bool {
s.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Traceparent {
pub version: u8,
pub trace_id: String,
pub parent_id: String,
pub trace_flags: u8,
}
impl Traceparent {
pub fn parse(header: &str) -> Result<Self, TraceparentParseError> {
let segments: Vec<&str> = header.split('-').collect();
if segments.len() != 4 {
return Err(TraceparentParseError::InvalidSegmentCount);
}
let (version_str, trace_id_str, parent_id_str, flags_str) =
(segments[0], segments[1], segments[2], segments[3]);
if version_str != "00" {
return Err(TraceparentParseError::InvalidVersion);
}
if trace_id_str.len() != 32 || !is_lowercase_hex(trace_id_str) {
return Err(TraceparentParseError::InvalidTraceId);
}
if parent_id_str.len() != 16 || !is_lowercase_hex(parent_id_str) {
return Err(TraceparentParseError::InvalidParentId);
}
if flags_str.len() != 2 || !is_lowercase_hex(flags_str) {
return Err(TraceparentParseError::InvalidFlags);
}
let trace_flags =
u8::from_str_radix(flags_str, 16).map_err(|_| TraceparentParseError::InvalidFlags)?;
Ok(Self {
version: 0,
trace_id: trace_id_str.to_owned(),
parent_id: parent_id_str.to_owned(),
trace_flags,
})
}
#[must_use]
pub fn to_header(&self) -> String {
format!(
"00-{}-{}-{:02x}",
self.trace_id, self.parent_id, self.trace_flags
)
}
#[must_use]
pub fn into_owned_trace_context(self, operation: impl Into<String>) -> OwnedTraceContext {
OwnedTraceContext {
trace_id: self.trace_id,
span_id: self.parent_id,
parent_span_id: None,
operation: operation.into(),
correlation_id: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Traced<M> {
pub context: OwnedTraceContext,
pub message: M,
}
impl<M> Traced<M> {
#[must_use]
pub fn new(context: OwnedTraceContext, message: M) -> Self {
Self { context, message }
}
#[must_use]
pub fn into_parts(self) -> (OwnedTraceContext, M) {
(self.context, self.message)
}
#[must_use]
pub fn as_context(&self) -> &OwnedTraceContext {
&self.context
}
#[must_use]
pub fn map<N>(self, f: impl FnOnce(M) -> N) -> Traced<N> {
Traced {
context: self.context,
message: f(self.message),
}
}
}
#[cfg(test)]
mod tests {
use super::{
ChildSpanSpec, OwnedTraceContext, Traced, Traceparent, TraceparentParseError,
};
const TRACE_ID: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
const PARENT_ID: &str = "00f067aa0ba902b7";
#[test]
fn owned_context_root_has_no_parent() {
let ctx = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "session.handle");
assert_eq!(ctx.trace_id, TRACE_ID);
assert_eq!(ctx.span_id, PARENT_ID);
assert_eq!(ctx.operation, "session.handle");
assert!(ctx.parent_span_id.is_none());
assert!(ctx.correlation_id.is_none());
}
#[test]
fn owned_context_child_inherits_trace_id() {
let parent = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "session.handle");
let child = parent.child("abcdef0123456789", "policy.enforce");
assert_eq!(child.trace_id, TRACE_ID);
assert_eq!(child.span_id, "abcdef0123456789");
assert_eq!(child.parent_span_id.as_deref(), Some(PARENT_ID));
assert_eq!(child.operation, "policy.enforce");
}
#[test]
fn child_span_spec_fields_match_context() {
let ctx = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "audit.write");
let spec: ChildSpanSpec<'_> = ctx.as_child_span_spec();
assert_eq!(spec.trace_id, TRACE_ID);
assert_eq!(spec.parent_span_id, PARENT_ID);
assert_eq!(spec.operation, "audit.write");
assert!(spec.correlation_id.is_none());
}
#[test]
fn traceparent_parses_valid_header() {
let header = format!("00-{TRACE_ID}-{PARENT_ID}-01");
let tp = Traceparent::parse(&header).expect("valid header must parse");
assert_eq!(tp.version, 0);
assert_eq!(tp.trace_id, TRACE_ID);
assert_eq!(tp.parent_id, PARENT_ID);
assert_eq!(tp.trace_flags, 1);
}
#[test]
fn traceparent_to_header_round_trips() {
let original = format!("00-{TRACE_ID}-{PARENT_ID}-01");
let tp = Traceparent::parse(&original).expect("valid header must parse");
assert_eq!(tp.to_header(), original);
}
#[test]
fn traceparent_into_owned_context_preserves_ids() {
let header = format!("00-{TRACE_ID}-{PARENT_ID}-01");
let tp = Traceparent::parse(&header).expect("valid header must parse");
let ctx = tp.into_owned_trace_context("session.forward");
assert_eq!(ctx.trace_id, TRACE_ID);
assert_eq!(ctx.span_id, PARENT_ID);
assert_eq!(ctx.operation, "session.forward");
assert!(ctx.parent_span_id.is_none());
}
#[test]
fn traceparent_rejects_too_few_segments() {
let err = Traceparent::parse("00-abc").unwrap_err();
assert_eq!(err, TraceparentParseError::InvalidSegmentCount);
}
#[test]
fn traceparent_rejects_non_zero_version() {
let header = format!("ff-{TRACE_ID}-{PARENT_ID}-00");
let err = Traceparent::parse(&header).unwrap_err();
assert_eq!(err, TraceparentParseError::InvalidVersion);
}
#[test]
fn traceparent_rejects_short_trace_id() {
let short = &TRACE_ID[..31];
let header = format!("00-{short}-{PARENT_ID}-00");
let err = Traceparent::parse(&header).unwrap_err();
assert_eq!(err, TraceparentParseError::InvalidTraceId);
}
#[test]
fn traceparent_rejects_short_parent_id() {
let short = &PARENT_ID[..15];
let header = format!("00-{TRACE_ID}-{short}-00");
let err = Traceparent::parse(&header).unwrap_err();
assert_eq!(err, TraceparentParseError::InvalidParentId);
}
#[test]
fn traceparent_rejects_invalid_flags() {
let header = format!("00-{TRACE_ID}-{PARENT_ID}-1");
let err = Traceparent::parse(&header).unwrap_err();
assert_eq!(err, TraceparentParseError::InvalidFlags);
}
#[test]
fn traced_new_and_into_parts() {
let ctx = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "actor.send");
let traced = Traced::new(ctx.clone(), 42u32);
let (extracted_ctx, extracted_msg) = traced.into_parts();
assert_eq!(extracted_ctx, ctx);
assert_eq!(extracted_msg, 42u32);
}
#[test]
fn traced_as_context_borrows() {
let ctx = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "actor.recv");
let traced = Traced::new(ctx, "payload");
assert_eq!(traced.as_context().trace_id, TRACE_ID);
let _ = traced.message;
}
#[test]
fn traced_map_preserves_context() {
let ctx = OwnedTraceContext::root(TRACE_ID, PARENT_ID, "actor.map");
let traced: Traced<u32> = Traced::new(ctx.clone(), 10u32);
let mapped: Traced<String> = traced.map(|n| format!("value:{n}"));
assert_eq!(mapped.context, ctx);
assert_eq!(mapped.message, "value:10");
}
#[test]
fn parse_error_as_str_non_empty() {
let variants = [
TraceparentParseError::InvalidSegmentCount,
TraceparentParseError::InvalidVersion,
TraceparentParseError::InvalidTraceId,
TraceparentParseError::InvalidParentId,
TraceparentParseError::InvalidFlags,
];
for variant in &variants {
assert!(!variant.as_str().is_empty(), "{variant:?} has empty as_str");
}
}
}