enact-core 0.0.2

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Trace Context - W3C Trace Context for distributed tracing
//!
//! Implements W3C Trace Context propagation for OpenTelemetry integration.
//!
//! @see https://www.w3.org/TR/trace-context/

use serde::{Deserialize, Serialize};

/// TraceContext - W3C Trace Context for distributed tracing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceContext {
    /// Trace ID (32 hex chars)
    pub trace_id: String,
    /// Span ID (16 hex chars)
    pub span_id: String,
    /// Sampling flags
    pub trace_flags: Option<u8>,
    /// Vendor-specific state
    pub trace_state: Option<String>,
}

impl TraceContext {
    /// Create a new TraceContext with generated IDs
    pub fn new() -> Self {
        Self {
            trace_id: Self::generate_trace_id(),
            span_id: Self::generate_span_id(),
            trace_flags: Some(1), // sampled
            trace_state: None,
        }
    }

    /// Create a child span context (same trace, new span)
    pub fn child_span(&self) -> Self {
        Self {
            trace_id: self.trace_id.clone(),
            span_id: Self::generate_span_id(),
            trace_flags: self.trace_flags,
            trace_state: self.trace_state.clone(),
        }
    }

    /// Parse from W3C traceparent header
    pub fn from_traceparent(header: &str) -> Option<Self> {
        let parts: Vec<&str> = header.split('-').collect();
        if parts.len() >= 3 {
            Some(Self {
                trace_id: parts[1].to_string(),
                span_id: parts[2].to_string(),
                trace_flags: parts.get(3).and_then(|f| u8::from_str_radix(f, 16).ok()),
                trace_state: None,
            })
        } else {
            None
        }
    }

    /// Format as W3C traceparent header
    pub fn to_traceparent(&self) -> String {
        format!(
            "00-{}-{}-{:02x}",
            self.trace_id,
            self.span_id,
            self.trace_flags.unwrap_or(0)
        )
    }

    fn generate_trace_id() -> String {
        use std::time::{SystemTime, UNIX_EPOCH};
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        format!("{:032x}", now)
    }

    fn generate_span_id() -> String {
        use std::sync::atomic::{AtomicU64, Ordering};
        use std::time::{SystemTime, UNIX_EPOCH};
        static COUNTER: AtomicU64 = AtomicU64::new(0);

        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos() as u64;
        let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
        // Mix timestamp with counter to ensure uniqueness
        format!("{:016x}", now ^ (counter << 48))
    }
}

impl Default for TraceContext {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_trace_context_new() {
        let ctx = TraceContext::new();
        assert_eq!(ctx.trace_id.len(), 32);
        assert_eq!(ctx.span_id.len(), 16);
    }

    #[test]
    fn test_child_span() {
        let parent = TraceContext::new();
        let child = parent.child_span();

        assert_eq!(child.trace_id, parent.trace_id);
        assert_ne!(child.span_id, parent.span_id);
    }

    #[test]
    fn test_traceparent_roundtrip() {
        let ctx = TraceContext::new();
        let header = ctx.to_traceparent();
        let parsed = TraceContext::from_traceparent(&header).unwrap();

        assert_eq!(parsed.trace_id, ctx.trace_id);
        assert_eq!(parsed.span_id, ctx.span_id);
    }
}