Skip to main content

hyperi_rustlib/transport/
propagation.rs

1// Project:   hyperi-rustlib
2// File:      src/transport/propagation.rs
3// Purpose:   W3C Trace Context propagation helpers for transport layer
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! # Trace Context Propagation
10//!
11//! W3C Trace Context (traceparent) helpers for automatic context propagation
12//! across transport boundaries. When the `otel` feature is enabled, transports
13//! inject/extract `traceparent` headers transparently.
14//!
15//! Format: `00-{trace_id}-{span_id}-{flags}`
16//! Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`
17
18/// W3C traceparent header name.
19pub const TRACEPARENT_HEADER: &str = "traceparent";
20
21/// Format a W3C traceparent header value from the current OTel span context.
22///
23/// Returns `Some("00-{trace_id}-{span_id}-{flags}")` if there is a valid
24/// span context active, `None` otherwise.
25#[cfg(feature = "transport-trace")]
26#[must_use]
27pub fn current_traceparent() -> Option<String> {
28    use opentelemetry::trace::TraceContextExt;
29
30    let cx = opentelemetry::Context::current();
31    let span = cx.span();
32    let sc = span.span_context();
33
34    if sc.is_valid() {
35        Some(format_traceparent(sc))
36    } else {
37        None
38    }
39}
40
41/// Format a `SpanContext` into a W3C traceparent string.
42///
43/// `TraceId` and `SpanId` implement `Display` as lowercase hex.
44/// `TraceFlags::to_u8()` returns the raw flags byte.
45#[cfg(feature = "transport-trace")]
46fn format_traceparent(sc: &opentelemetry::trace::SpanContext) -> String {
47    format!(
48        "00-{}-{}-{:02x}",
49        sc.trace_id(),
50        sc.span_id(),
51        sc.trace_flags().to_u8()
52    )
53}
54
55/// Format a traceparent string from raw components (for testing without OTel).
56#[must_use]
57pub fn format_traceparent_raw(trace_id: u128, span_id: u64, flags: u8) -> String {
58    format!("00-{trace_id:032x}-{span_id:016x}-{flags:02x}")
59}
60
61/// Validate that a string looks like a well-formed traceparent header.
62///
63/// Does basic structural validation (length, separators, hex chars).
64/// Does NOT validate that trace_id/span_id are non-zero.
65#[must_use]
66pub fn is_valid_traceparent(value: &str) -> bool {
67    // Expected: "00-<32hex>-<16hex>-<2hex>" = 55 chars
68    if value.len() != 55 {
69        return false;
70    }
71
72    let bytes = value.as_bytes();
73
74    // Version: "00"
75    if bytes[0] != b'0' || bytes[1] != b'0' {
76        return false;
77    }
78
79    // Separators at positions 2, 35, 52
80    if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
81        return false;
82    }
83
84    // All other positions must be hex digits
85    let hex_ranges = [3..35, 36..52, 53..55];
86    for range in &hex_ranges {
87        for &b in &bytes[range.clone()] {
88            if !b.is_ascii_hexdigit() {
89                return false;
90            }
91        }
92    }
93
94    true
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn traceparent_format_raw() {
103        let tp = format_traceparent_raw(
104            0x4bf9_2f35_77b3_4da6_a3ce_929d_0e0e_4736,
105            0x00f0_67aa_0ba9_02b7,
106            0x01,
107        );
108        assert_eq!(
109            tp,
110            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
111        );
112        assert_eq!(tp.len(), 55);
113    }
114
115    #[test]
116    fn traceparent_format_zero_padded() {
117        // Low values should be zero-padded to full width
118        let tp = format_traceparent_raw(123, 456, 1);
119        assert!(tp.starts_with("00-"));
120        assert_eq!(tp.len(), 55);
121        assert_eq!(
122            tp,
123            "00-0000000000000000000000000000007b-00000000000001c8-01"
124        );
125    }
126
127    #[test]
128    fn traceparent_format_flags_zero() {
129        let tp = format_traceparent_raw(1, 1, 0);
130        assert!(tp.ends_with("-00"));
131    }
132
133    #[test]
134    fn valid_traceparent() {
135        assert!(is_valid_traceparent(
136            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
137        ));
138    }
139
140    #[test]
141    fn invalid_traceparent_too_short() {
142        assert!(!is_valid_traceparent("00-abc-def-01"));
143    }
144
145    #[test]
146    fn invalid_traceparent_bad_version() {
147        assert!(!is_valid_traceparent(
148            "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
149        ));
150    }
151
152    #[test]
153    fn invalid_traceparent_non_hex() {
154        assert!(!is_valid_traceparent(
155            "00-4bf92f3577b34da6a3ce929d0e0eXXXX-00f067aa0ba902b7-01"
156        ));
157    }
158
159    #[test]
160    fn invalid_traceparent_wrong_separators() {
161        assert!(!is_valid_traceparent(
162            "00_4bf92f3577b34da6a3ce929d0e0e4736_00f067aa0ba902b7_01"
163        ));
164    }
165}