Skip to main content

fast_telemetry/span/
context.rs

1//! Span context for W3C trace propagation and thread-local current span.
2//!
3//! `SpanContext` is an internal type used at two boundaries:
4//! 1. **Cross-service propagation** — parsing/encoding W3C `traceparent` headers
5//! 2. **Logging integration** — thread-local "current span" surfaced via
6//!    [`current_trace_id()`] and [`current_span_id()`]
7//!
8//! It is NOT passed between functions for parent-child creation — use
9//! [`Span::child()`](super::Span::child) instead.
10
11use std::cell::Cell;
12
13use super::ids::{SpanId, TraceId};
14
15/// Lightweight span context for W3C propagation and logging integration.
16#[derive(Clone, Copy, Debug)]
17pub(crate) struct SpanContext {
18    pub trace_id: TraceId,
19    pub span_id: SpanId,
20    pub trace_flags: u8,
21}
22
23impl SpanContext {
24    pub const INVALID: Self = Self {
25        trace_id: TraceId::INVALID,
26        span_id: SpanId::INVALID,
27        trace_flags: 0,
28    };
29
30    /// Parse a W3C `traceparent` header value.
31    ///
32    /// Format: `{version:2}-{trace_id:32}-{span_id:16}-{flags:2}`
33    /// Example: `00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01`
34    pub fn from_traceparent(header: &str) -> Option<Self> {
35        let bytes = header.as_bytes();
36
37        // Minimum length: 2 + 1 + 32 + 1 + 16 + 1 + 2 = 55
38        if bytes.len() < 55 {
39            return None;
40        }
41
42        // Version check (only support version 00)
43        if bytes[0] != b'0' || bytes[1] != b'0' {
44            return None;
45        }
46        if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
47            return None;
48        }
49
50        let trace_id = TraceId::from_hex(&header[3..35])?;
51        let span_id = SpanId::from_hex(&header[36..52])?;
52
53        let flags_hi = hex_digit(bytes[53])?;
54        let flags_lo = hex_digit(bytes[54])?;
55        let trace_flags = (flags_hi << 4) | flags_lo;
56
57        // Reject all-zero IDs per W3C spec.
58        if trace_id.is_invalid() || span_id.is_invalid() {
59            return None;
60        }
61
62        Some(Self {
63            trace_id,
64            span_id,
65            trace_flags,
66        })
67    }
68
69    /// Encode as a W3C `traceparent` header value.
70    pub fn to_traceparent(self) -> String {
71        format!(
72            "00-{}-{}-{:02x}",
73            self.trace_id, self.span_id, self.trace_flags
74        )
75    }
76}
77
78fn hex_digit(c: u8) -> Option<u8> {
79    match c {
80        b'0'..=b'9' => Some(c - b'0'),
81        b'a'..=b'f' => Some(c - b'a' + 10),
82        b'A'..=b'F' => Some(c - b'A' + 10),
83        _ => None,
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Thread-local current span context (for logging integration)
89// ---------------------------------------------------------------------------
90
91thread_local! {
92    static CURRENT: Cell<SpanContext> = const { Cell::new(SpanContext::INVALID) };
93}
94
95/// Get the trace ID of the currently entered span on this thread, if any.
96///
97/// Returns `None` if no span is currently entered or the trace ID is invalid.
98/// Used by logging integration to correlate log entries with traces.
99pub fn current_trace_id() -> Option<TraceId> {
100    CURRENT.with(|cell| {
101        let ctx = cell.get();
102        if ctx.trace_id.is_invalid() {
103            None
104        } else {
105            Some(ctx.trace_id)
106        }
107    })
108}
109
110/// Get the span ID of the currently entered span on this thread, if any.
111///
112/// Returns `None` if no span is currently entered or the span ID is invalid.
113pub fn current_span_id() -> Option<SpanId> {
114    CURRENT.with(|cell| {
115        let ctx = cell.get();
116        if ctx.span_id.is_invalid() {
117            None
118        } else {
119            Some(ctx.span_id)
120        }
121    })
122}
123
124/// RAII guard that sets the thread-local current span context on creation
125/// and restores the previous value on drop.
126pub(crate) struct SpanEnterGuard {
127    prev: SpanContext,
128}
129
130impl SpanEnterGuard {
131    /// Enter a span context: set it as current and return a guard that
132    /// restores the previous context on drop.
133    pub fn enter(ctx: SpanContext) -> Self {
134        let prev = CURRENT.with(|cell| cell.replace(ctx));
135        Self { prev }
136    }
137}
138
139impl Drop for SpanEnterGuard {
140    fn drop(&mut self) {
141        CURRENT.with(|cell| cell.set(self.prev));
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn traceparent_roundtrip() {
151        let ctx = SpanContext {
152            trace_id: TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").expect("valid"),
153            span_id: SpanId::from_hex("00f067aa0ba902b7").expect("valid"),
154            trace_flags: 0x01,
155        };
156        let header = ctx.to_traceparent();
157        assert_eq!(
158            header,
159            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
160        );
161
162        let parsed = SpanContext::from_traceparent(&header).expect("should parse");
163        assert_eq!(parsed.trace_id, ctx.trace_id);
164        assert_eq!(parsed.span_id, ctx.span_id);
165        assert_eq!(parsed.trace_flags, ctx.trace_flags);
166    }
167
168    #[test]
169    fn traceparent_flags_zero() {
170        let ctx = SpanContext {
171            trace_id: TraceId::from_hex("aaaabbbbccccdddd1111222233334444").expect("valid"),
172            span_id: SpanId::from_hex("1234567890abcdef").expect("valid"),
173            trace_flags: 0x00,
174        };
175        let header = ctx.to_traceparent();
176        assert!(header.ends_with("-00"));
177
178        let parsed = SpanContext::from_traceparent(&header).expect("should parse");
179        assert_eq!(parsed.trace_flags, 0x00);
180    }
181
182    #[test]
183    fn traceparent_rejects_invalid() {
184        // Too short.
185        assert!(SpanContext::from_traceparent("00-abc-def-01").is_none());
186        // Wrong version.
187        assert!(
188            SpanContext::from_traceparent(
189                "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
190            )
191            .is_none()
192        );
193        // Missing dashes.
194        assert!(
195            SpanContext::from_traceparent(
196                "00x4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
197            )
198            .is_none()
199        );
200        // All-zero trace ID.
201        assert!(
202            SpanContext::from_traceparent(
203                "00-00000000000000000000000000000000-00f067aa0ba902b7-01"
204            )
205            .is_none()
206        );
207        // All-zero span ID.
208        assert!(
209            SpanContext::from_traceparent(
210                "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01"
211            )
212            .is_none()
213        );
214    }
215
216    #[test]
217    fn thread_local_enter_exit() {
218        assert!(current_trace_id().is_none());
219        assert!(current_span_id().is_none());
220
221        let ctx = SpanContext {
222            trace_id: TraceId::random(),
223            span_id: SpanId::random(),
224            trace_flags: 1,
225        };
226
227        {
228            let _guard = SpanEnterGuard::enter(ctx);
229            assert_eq!(current_trace_id(), Some(ctx.trace_id));
230            assert_eq!(current_span_id(), Some(ctx.span_id));
231
232            // Nested enter.
233            let inner_ctx = SpanContext {
234                trace_id: ctx.trace_id,
235                span_id: SpanId::random(),
236                trace_flags: 1,
237            };
238            {
239                let _inner_guard = SpanEnterGuard::enter(inner_ctx);
240                assert_eq!(current_span_id(), Some(inner_ctx.span_id));
241            }
242            // Restored after inner guard drops.
243            assert_eq!(current_span_id(), Some(ctx.span_id));
244        }
245
246        // Restored to invalid after outer guard drops.
247        assert!(current_trace_id().is_none());
248        assert!(current_span_id().is_none());
249    }
250}