Skip to main content

fast_telemetry/span/
types.rs

1//! Active and completed span types.
2//!
3//! A [`Span`] accumulates events and attributes during its lifetime. When it
4//! drops (or [`Span::end`] is called), a [`CompletedSpan`] is submitted to the
5//! associated [`SpanCollector`](super::SpanCollector).
6//!
7//! Child spans are created via [`Span::child`], which inherits the trace ID
8//! and collector reference from the parent — no context cloning required.
9
10use std::borrow::Cow;
11use std::ptr::NonNull;
12use std::sync::Arc;
13
14use super::collector::SpanCollector;
15use super::context::{SpanContext, SpanEnterGuard};
16use super::ids::{SpanId, TraceId};
17
18// ---------------------------------------------------------------------------
19// CollectorRef — Arc-free borrowed reference to SpanCollector
20// ---------------------------------------------------------------------------
21
22/// A borrowed reference to a [`SpanCollector`], avoiding `Arc` refcount contention.
23///
24/// # Safety
25///
26/// The `SpanCollector` must outlive all `Span`s that reference it.
27/// This is guaranteed by the usage pattern: the collector is held as
28/// `Arc<SpanCollector>` in long-lived application state and spans are
29/// request-scoped. The public API (`start_span`) takes `&Arc<Self>`,
30/// so the `Arc` must be alive when creating spans.
31#[derive(Clone, Copy)]
32pub(crate) struct CollectorRef(NonNull<SpanCollector>);
33
34// SAFETY: SpanCollector is Send+Sync (contains only Mutex<Vec> outboxes).
35// Sharing a raw pointer to it across threads is safe because the underlying
36// data is thread-safe and the pointee outlives all Spans.
37unsafe impl Send for CollectorRef {}
38unsafe impl Sync for CollectorRef {}
39
40impl CollectorRef {
41    /// Create a `CollectorRef` from an `Arc<SpanCollector>`.
42    pub(crate) fn from_arc(arc: &Arc<SpanCollector>) -> Self {
43        Self(NonNull::from(arc.as_ref()))
44    }
45
46    /// Dereference to the underlying `SpanCollector`.
47    ///
48    /// # Safety (upheld by construction)
49    ///
50    /// Safe because `CollectorRef` is only created via `from_arc`, and the
51    /// `Arc<SpanCollector>` in application state outlives all request-scoped spans.
52    #[inline]
53    pub(crate) fn as_ref(&self) -> &SpanCollector {
54        // SAFETY: The SpanCollector is alive as long as the Arc in app state.
55        unsafe { self.0.as_ref() }
56    }
57}
58
59/// Monotonic clock anchor — one `SystemTime::now()` at init, then `Instant` for all reads.
60///
61/// `Instant` is immune to NTP clock adjustments and slightly faster on most platforms.
62/// The anchor captures a single wall-clock reference at first call; subsequent reads
63/// use monotonic `Instant::elapsed()` offset from that anchor.
64static CLOCK_ANCHOR: std::sync::LazyLock<(std::time::Instant, u64)> =
65    std::sync::LazyLock::new(|| {
66        let wall = std::time::SystemTime::now()
67            .duration_since(std::time::UNIX_EPOCH)
68            .unwrap_or_default()
69            .as_nanos() as u64;
70        (std::time::Instant::now(), wall)
71    });
72
73/// Current wall-clock time as nanoseconds since Unix epoch.
74///
75/// Uses a monotonic `Instant` offset from a single wall-clock anchor, avoiding
76/// repeated `SystemTime::now()` calls and providing NTP-immune timestamps.
77#[inline]
78pub(crate) fn now_nanos() -> u64 {
79    let (ref mono, wall) = *CLOCK_ANCHOR;
80    wall + mono.elapsed().as_nanos() as u64
81}
82
83// ---------------------------------------------------------------------------
84// Enums
85// ---------------------------------------------------------------------------
86
87/// Indicates the role of a span in the trace.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum SpanKind {
90    /// Default. An internal operation within an application.
91    Internal,
92    /// Server-side handling of an RPC or HTTP request.
93    Server,
94    /// Client-side request to a remote service.
95    Client,
96    /// Producer sending a message to a broker.
97    Producer,
98    /// Consumer receiving a message from a broker.
99    Consumer,
100}
101
102/// Final status of a span.
103#[derive(Clone, Debug, Default, PartialEq, Eq)]
104pub enum SpanStatus {
105    /// Status not explicitly set (default).
106    #[default]
107    Unset,
108    /// The operation completed successfully.
109    Ok,
110    /// The operation contained an error.
111    Error { message: Cow<'static, str> },
112}
113
114// ---------------------------------------------------------------------------
115// SpanAttribute / SpanValue / SpanEvent
116// ---------------------------------------------------------------------------
117
118/// A key-value attribute attached to a span.
119pub struct SpanAttribute {
120    pub key: Cow<'static, str>,
121    pub value: SpanValue,
122}
123
124impl SpanAttribute {
125    /// Create a new attribute from a static key and any value that converts to [`SpanValue`].
126    pub fn new(key: impl Into<Cow<'static, str>>, value: impl Into<SpanValue>) -> Self {
127        Self {
128            key: key.into(),
129            value: value.into(),
130        }
131    }
132}
133
134/// Typed attribute value.
135pub enum SpanValue {
136    String(Cow<'static, str>),
137    I64(i64),
138    F64(f64),
139    Bool(bool),
140    /// UUID stored inline (16 bytes), formatted to hyphenated hex only at export time.
141    Uuid(uuid::Uuid),
142}
143
144impl From<&'static str> for SpanValue {
145    fn from(s: &'static str) -> Self {
146        Self::String(Cow::Borrowed(s))
147    }
148}
149
150impl From<String> for SpanValue {
151    fn from(s: String) -> Self {
152        Self::String(Cow::Owned(s))
153    }
154}
155
156impl From<Cow<'static, str>> for SpanValue {
157    fn from(s: Cow<'static, str>) -> Self {
158        Self::String(s)
159    }
160}
161
162impl From<i64> for SpanValue {
163    fn from(v: i64) -> Self {
164        Self::I64(v)
165    }
166}
167
168impl From<f64> for SpanValue {
169    fn from(v: f64) -> Self {
170        Self::F64(v)
171    }
172}
173
174impl From<bool> for SpanValue {
175    fn from(v: bool) -> Self {
176        Self::Bool(v)
177    }
178}
179
180impl From<uuid::Uuid> for SpanValue {
181    fn from(v: uuid::Uuid) -> Self {
182        Self::Uuid(v)
183    }
184}
185
186/// A timestamped event recorded during a span's lifetime.
187pub struct SpanEvent {
188    pub name: Cow<'static, str>,
189    pub time_ns: u64,
190    pub attributes: Vec<SpanAttribute>,
191}
192
193// ---------------------------------------------------------------------------
194// CompletedSpan
195// ---------------------------------------------------------------------------
196
197/// A finalized span with start and end timestamps, ready for export.
198pub struct CompletedSpan {
199    pub trace_id: TraceId,
200    pub span_id: SpanId,
201    pub parent_span_id: SpanId,
202    pub name: Cow<'static, str>,
203    pub kind: SpanKind,
204    pub start_time_ns: u64,
205    pub end_time_ns: u64,
206    pub status: SpanStatus,
207    pub attributes: Vec<SpanAttribute>,
208    pub events: Vec<SpanEvent>,
209}
210
211// ---------------------------------------------------------------------------
212// Span
213// ---------------------------------------------------------------------------
214
215/// An active span that accumulates events and attributes.
216///
217/// Created via [`SpanCollector::start_span`] (root) or [`Span::child`] (nested).
218/// Submits a [`CompletedSpan`] to the collector when dropped or when
219/// [`end()`](Span::end) is called explicitly.
220///
221/// Internally holds `Option<CompletedSpan>` with `end_time_ns = 0` until
222/// the span finishes. On drop, `end_time_ns` is set and the span is submitted
223/// — no field-by-field move between separate structs.
224#[must_use = "span is submitted to collector on drop — bind it to a variable"]
225pub struct Span {
226    data: Option<CompletedSpan>,
227    collector: CollectorRef,
228    _enter_guard: Option<SpanEnterGuard>,
229}
230
231impl Span {
232    /// Create a no-op span that skips all recording and submission.
233    ///
234    /// Used by adaptive sampling to avoid span creation overhead entirely.
235    #[inline]
236    pub(crate) fn noop(collector: CollectorRef) -> Self {
237        Self {
238            data: None,
239            collector,
240            _enter_guard: None,
241        }
242    }
243
244    /// Create a new root span (new trace ID).
245    #[inline]
246    pub(crate) fn new_root(
247        name: impl Into<Cow<'static, str>>,
248        kind: SpanKind,
249        collector: CollectorRef,
250    ) -> Self {
251        let data = CompletedSpan {
252            trace_id: TraceId::random(),
253            span_id: SpanId::random(),
254            parent_span_id: SpanId::INVALID,
255            name: name.into(),
256            kind,
257            start_time_ns: now_nanos(),
258            end_time_ns: 0,
259            status: SpanStatus::Unset,
260            attributes: Vec::new(),
261            events: Vec::new(),
262        };
263        Self {
264            data: Some(data),
265            collector,
266            _enter_guard: None,
267        }
268    }
269
270    /// Create a root span from a remote parent (incoming W3C traceparent).
271    pub(crate) fn new_from_remote(
272        name: impl Into<Cow<'static, str>>,
273        kind: SpanKind,
274        parent_ctx: SpanContext,
275        collector: CollectorRef,
276    ) -> Self {
277        let data = CompletedSpan {
278            trace_id: parent_ctx.trace_id,
279            span_id: SpanId::random(),
280            parent_span_id: parent_ctx.span_id,
281            name: name.into(),
282            kind,
283            start_time_ns: now_nanos(),
284            end_time_ns: 0,
285            status: SpanStatus::Unset,
286            attributes: Vec::new(),
287            events: Vec::new(),
288        };
289        Self {
290            data: Some(data),
291            collector,
292            _enter_guard: None,
293        }
294    }
295
296    /// Create a child span inheriting the trace ID and collector from this span.
297    ///
298    /// The child's `parent_span_id` is set to this span's `span_id`.
299    /// If this span is a no-op (sampled out), the child is also a no-op.
300    #[inline]
301    pub fn child(&self, name: impl Into<Cow<'static, str>>, kind: SpanKind) -> Span {
302        let Some(parent_data) = self.data.as_ref() else {
303            return Span::noop(self.collector);
304        };
305        let data = CompletedSpan {
306            trace_id: parent_data.trace_id,
307            span_id: SpanId::random(),
308            parent_span_id: parent_data.span_id,
309            name: name.into(),
310            kind,
311            start_time_ns: now_nanos(),
312            end_time_ns: 0,
313            status: SpanStatus::Unset,
314            attributes: Vec::new(),
315            events: Vec::new(),
316        };
317        Span {
318            data: Some(data),
319            collector: self.collector,
320            _enter_guard: None,
321        }
322    }
323
324    /// Set this span as the thread-local "current span" for logging integration.
325    ///
326    /// The thread-local context is cleared when this span drops. Nested calls
327    /// properly restore the previous context.
328    pub fn enter(&mut self) -> &mut Self {
329        if self._enter_guard.is_none()
330            && let Some(data) = self.data.as_ref()
331        {
332            self._enter_guard = Some(SpanEnterGuard::enter(SpanContext {
333                trace_id: data.trace_id,
334                span_id: data.span_id,
335                trace_flags: 0x01, // sampled
336            }));
337        }
338        self
339    }
340
341    /// Add a timestamped event to this span.
342    ///
343    /// Takes ownership of the attributes vector to avoid cloning.
344    pub fn add_event(
345        &mut self,
346        name: impl Into<Cow<'static, str>>,
347        attributes: Vec<SpanAttribute>,
348    ) {
349        if let Some(data) = self.data.as_mut() {
350            data.events.push(SpanEvent {
351                name: name.into(),
352                time_ns: now_nanos(),
353                attributes,
354            });
355        }
356    }
357
358    /// Add a timestamped event with no attributes.
359    pub fn add_simple_event(&mut self, name: impl Into<Cow<'static, str>>) {
360        if let Some(data) = self.data.as_mut() {
361            data.events.push(SpanEvent {
362                name: name.into(),
363                time_ns: now_nanos(),
364                attributes: Vec::new(),
365            });
366        }
367    }
368
369    /// Set a span attribute.
370    #[inline]
371    pub fn set_attribute(
372        &mut self,
373        key: impl Into<Cow<'static, str>>,
374        value: impl Into<SpanValue>,
375    ) {
376        if let Some(data) = self.data.as_mut() {
377            data.attributes.push(SpanAttribute {
378                key: key.into(),
379                value: value.into(),
380            });
381        }
382    }
383
384    /// Set the span status.
385    #[inline]
386    pub fn set_status(&mut self, status: SpanStatus) {
387        if let Some(data) = self.data.as_mut() {
388            data.status = status;
389        }
390    }
391
392    /// Get the trace ID of this span.
393    pub fn trace_id(&self) -> TraceId {
394        self.data.as_ref().map_or(TraceId::INVALID, |d| d.trace_id)
395    }
396
397    /// Get the span ID of this span.
398    pub fn span_id(&self) -> SpanId {
399        self.data.as_ref().map_or(SpanId::INVALID, |d| d.span_id)
400    }
401
402    /// Encode this span's context as a W3C `traceparent` header for outgoing requests.
403    ///
404    /// Returns an empty string for no-op (sampled-out) spans.
405    pub fn traceparent(&self) -> String {
406        let Some(data) = self.data.as_ref() else {
407            return String::new();
408        };
409        let ctx = SpanContext {
410            trace_id: data.trace_id,
411            span_id: data.span_id,
412            trace_flags: 0x01, // sampled
413        };
414        ctx.to_traceparent()
415    }
416
417    /// Explicitly end the span and submit it to the collector.
418    ///
419    /// Equivalent to dropping the span. After calling this, the span is consumed.
420    pub fn end(self) {
421        // Drop impl handles submission.
422        drop(self);
423    }
424}
425
426impl Drop for Span {
427    fn drop(&mut self) {
428        if let Some(mut completed) = self.data.take() {
429            completed.end_time_ns = now_nanos();
430            self.collector.as_ref().submit(completed);
431        }
432        // _enter_guard drops automatically, restoring thread-local context.
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    fn test_collector() -> Arc<SpanCollector> {
441        Arc::new(SpanCollector::new(1, 64))
442    }
443
444    /// Flush thread-local buffer + drain for same-thread tests.
445    fn flush_and_drain(collector: &SpanCollector, buf: &mut Vec<CompletedSpan>) {
446        collector.flush_local();
447        collector.drain_into(buf);
448    }
449
450    #[test]
451    fn root_span_lifecycle() {
452        let collector = test_collector();
453        {
454            let mut span = Span::new_root(
455                "test_op",
456                SpanKind::Server,
457                CollectorRef::from_arc(&collector),
458            );
459            span.set_attribute("key", "value");
460            span.add_simple_event("checkpoint");
461            span.set_status(SpanStatus::Ok);
462        } // span drops here
463
464        let mut buf = Vec::new();
465        flush_and_drain(&collector, &mut buf);
466        assert_eq!(buf.len(), 1);
467
468        let completed = &buf[0];
469        assert!(!completed.trace_id.is_invalid());
470        assert!(!completed.span_id.is_invalid());
471        assert!(completed.parent_span_id.is_invalid()); // root span
472        assert_eq!(completed.name, "test_op");
473        assert_eq!(completed.kind, SpanKind::Server);
474        assert_eq!(completed.status, SpanStatus::Ok);
475        assert_eq!(completed.attributes.len(), 1);
476        assert_eq!(completed.events.len(), 1);
477        assert!(completed.end_time_ns >= completed.start_time_ns);
478    }
479
480    #[test]
481    fn child_inherits_trace_id() {
482        let collector = test_collector();
483        let root_trace_id;
484        let root_span_id;
485        {
486            let root = Span::new_root(
487                "parent",
488                SpanKind::Server,
489                CollectorRef::from_arc(&collector),
490            );
491            root_trace_id = root.trace_id();
492            root_span_id = root.span_id();
493            {
494                let _child = root.child("child_op", SpanKind::Client);
495                // child drops first
496            }
497            // root drops second
498        }
499
500        let mut buf = Vec::new();
501        flush_and_drain(&collector, &mut buf);
502        assert_eq!(buf.len(), 2);
503
504        // Child was dropped first, so it's first in the queue.
505        let child = &buf[0];
506        let parent = &buf[1];
507
508        assert_eq!(child.trace_id, root_trace_id);
509        assert_eq!(child.parent_span_id, root_span_id);
510        assert_eq!(parent.trace_id, root_trace_id);
511        assert!(parent.parent_span_id.is_invalid());
512    }
513
514    #[test]
515    fn explicit_end() {
516        let collector = test_collector();
517        let span = Span::new_root(
518            "explicit",
519            SpanKind::Internal,
520            CollectorRef::from_arc(&collector),
521        );
522        span.end();
523
524        let mut buf = Vec::new();
525        flush_and_drain(&collector, &mut buf);
526        assert_eq!(buf.len(), 1);
527    }
528
529    #[test]
530    fn traceparent_encoding() {
531        let collector = test_collector();
532        let span = Span::new_root(
533            "tp_test",
534            SpanKind::Server,
535            CollectorRef::from_arc(&collector),
536        );
537        let tp = span.traceparent();
538
539        // Format: 00-{32 hex}-{16 hex}-01
540        assert!(tp.starts_with("00-"));
541        assert!(tp.ends_with("-01"));
542        assert_eq!(tp.len(), 55);
543    }
544
545    #[test]
546    fn from_remote_parent() {
547        let collector = test_collector();
548        let remote_ctx = SpanContext::from_traceparent(
549            "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
550        )
551        .expect("valid traceparent");
552
553        let span = Span::new_from_remote(
554            "server_handler",
555            SpanKind::Server,
556            remote_ctx,
557            CollectorRef::from_arc(&collector),
558        );
559        assert_eq!(span.trace_id(), remote_ctx.trace_id);
560        drop(span);
561
562        let mut buf = Vec::new();
563        flush_and_drain(&collector, &mut buf);
564        let completed = &buf[0];
565        assert_eq!(completed.trace_id, remote_ctx.trace_id);
566        assert_eq!(completed.parent_span_id, remote_ctx.span_id);
567    }
568}