Skip to main content

arcly_http/observability/
lean_telemetry.rs

1//! Zero-allocation trace propagation, span-ID generation, and atomic RED counters.
2//!
3//! ## W3C Trace Context
4//!
5//! `parse_traceparent` extracts an incoming `traceparent` header into a
6//! `TraceCtx` (trace ID + parent span ID). `new_span_id` / `new_trace_id`
7//! generate fresh identifiers for root spans and per-hop server spans.
8//!
9//! ## Counters
10//!
11//! Two relaxed atomics track total and in-flight requests. The `RequestGuard`
12//! RAII type decrements in-flight on drop so the count stays accurate even when
13//! a handler panics.
14
15use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
16use std::sync::OnceLock;
17use std::time::Instant;
18
19#[derive(Clone, Copy)]
20pub struct TraceCtx {
21    pub trace_id: [u8; 16],
22    pub span_id: [u8; 8],
23}
24
25static REQS: AtomicU64 = AtomicU64::new(0);
26static INFLT: AtomicU64 = AtomicU64::new(0);
27
28// ─── W3C traceparent parser ─────────────────────────────────────────────────
29
30/// Parse a W3C `traceparent` header without heap allocation.
31/// Format: `00-<32 hex trace>-<16 hex span>-<2 hex flags>` (55 bytes minimum).
32#[inline]
33pub fn parse_traceparent(h: &[u8]) -> Option<TraceCtx> {
34    if h.len() < 55 || h[2] != b'-' || h[35] != b'-' || h[52] != b'-' {
35        return None;
36    }
37    let mut tid = [0u8; 16];
38    let mut sid = [0u8; 8];
39    hex_into(&h[3..35], &mut tid)?;
40    hex_into(&h[36..52], &mut sid)?;
41    Some(TraceCtx {
42        trace_id: tid,
43        span_id: sid,
44    })
45}
46
47#[inline]
48fn hex_into(src: &[u8], dst: &mut [u8]) -> Option<()> {
49    for (i, byte) in dst.iter_mut().enumerate() {
50        *byte = (nyb(src[2 * i])? << 4) | nyb(src[2 * i + 1])?;
51    }
52    Some(())
53}
54
55#[inline]
56fn nyb(c: u8) -> Option<u8> {
57    match c {
58        b'0'..=b'9' => Some(c - b'0'),
59        b'a'..=b'f' => Some(c - b'a' + 10),
60        b'A'..=b'F' => Some(c - b'A' + 10),
61        _ => None,
62    }
63}
64
65// ─── Span / trace ID generation ─────────────────────────────────────────────
66
67/// Monotonic process-start epoch, initialised once.
68fn monotonic_nanos() -> u64 {
69    static EPOCH: OnceLock<Instant> = OnceLock::new();
70    EPOCH.get_or_init(Instant::now).elapsed().as_nanos() as u64
71}
72
73/// Generate a fresh 64-bit span ID using monotonic time + sequential counter.
74///
75/// Uses xorshift64 mixing so consecutive IDs are not trivially sequential.
76/// Not cryptographically secure -- sufficient for trace correlation.
77pub fn new_span_id() -> [u8; 8] {
78    static COUNTER: AtomicU64 = AtomicU64::new(1);
79    let count = COUNTER.fetch_add(1, Relaxed);
80    let mut v = monotonic_nanos() ^ count.wrapping_mul(0x9E3779B97F4A7C15);
81    v ^= v << 13;
82    v ^= v >> 7;
83    v ^= v << 17;
84    v.to_ne_bytes()
85}
86
87/// Generate a fresh 128-bit trace ID (two independent 64-bit halves).
88pub fn new_trace_id() -> [u8; 16] {
89    let lo = new_span_id();
90    let hi = new_span_id();
91    let mut out = [0u8; 16];
92    out[..8].copy_from_slice(&hi);
93    out[8..].copy_from_slice(&lo);
94    out
95}
96
97// ─── Hex encoding (no external dep) ─────────────────────────────────────────
98
99/// Encode arbitrary bytes as a lowercase hex string.
100pub fn hex_encode(bytes: &[u8]) -> String {
101    bytes.iter().map(|b| format!("{b:02x}")).collect()
102}
103
104// ─── Request counters ────────────────────────────────────────────────────────
105
106/// RAII guard: increments in-flight on construction, decrements on drop.
107pub struct RequestGuard;
108
109#[inline]
110pub fn on_request_start() -> RequestGuard {
111    REQS.fetch_add(1, Relaxed);
112    INFLT.fetch_add(1, Relaxed);
113    RequestGuard
114}
115
116impl Drop for RequestGuard {
117    #[inline]
118    fn drop(&mut self) {
119        INFLT.fetch_sub(1, Relaxed);
120    }
121}
122
123/// `(total_requests, in_flight)` -- relaxed read, suitable for metrics endpoints.
124#[inline]
125pub fn snapshot() -> (u64, u64) {
126    (REQS.load(Relaxed), INFLT.load(Relaxed))
127}