grafos_observe/
export.rs

1//! Span exporters for grafOS distributed tracing.
2//!
3//! - [`NullExporter`]: Collects spans in memory for test assertions.
4//! - [`SpanExporter`]: Trait for pluggable export backends.
5
6extern crate alloc;
7
8use alloc::vec::Vec;
9
10use crate::span::ResourceSpan;
11
12/// Trait for exporting completed spans.
13pub trait SpanExporter {
14    /// Export a batch of spans. Implementations should not block the caller
15    /// for extended periods; buffering and async flush are preferred.
16    fn export(&self, spans: &[ResourceSpan]);
17
18    /// Flush any buffered spans. Called on shutdown.
19    fn flush(&self);
20}
21
22/// A test exporter that collects spans in memory.
23///
24/// Thread-safe via `std::sync::Mutex` (available when `std` feature is on)
25/// or via single-threaded usage in `no_std` tests.
26///
27/// # Examples
28///
29/// ```
30/// use grafos_observe::export::NullExporter;
31/// use grafos_observe::export::SpanExporter;
32/// use grafos_observe::span::ResourceSpan;
33/// use grafos_observe::trace::TraceContext;
34///
35/// let exporter = NullExporter::new();
36/// let span = ResourceSpan::new("test", TraceContext::default());
37/// exporter.export(&[span]);
38/// assert_eq!(exporter.len(), 1);
39///
40/// let spans = exporter.drain();
41/// assert_eq!(spans.len(), 1);
42/// assert_eq!(spans[0].name, "test");
43/// ```
44pub struct NullExporter {
45    #[cfg(feature = "std")]
46    spans: std::sync::Mutex<Vec<ResourceSpan>>,
47    #[cfg(not(feature = "std"))]
48    spans: core::cell::RefCell<Vec<ResourceSpan>>,
49}
50
51impl NullExporter {
52    /// Create a new null exporter.
53    pub fn new() -> Self {
54        NullExporter {
55            #[cfg(feature = "std")]
56            spans: std::sync::Mutex::new(Vec::new()),
57            #[cfg(not(feature = "std"))]
58            spans: core::cell::RefCell::new(Vec::new()),
59        }
60    }
61
62    /// Number of spans collected so far.
63    pub fn len(&self) -> usize {
64        #[cfg(feature = "std")]
65        {
66            self.spans.lock().unwrap().len()
67        }
68        #[cfg(not(feature = "std"))]
69        {
70            self.spans.borrow().len()
71        }
72    }
73
74    /// Whether any spans have been collected.
75    pub fn is_empty(&self) -> bool {
76        self.len() == 0
77    }
78
79    /// Drain all collected spans, returning them and clearing the buffer.
80    pub fn drain(&self) -> Vec<ResourceSpan> {
81        #[cfg(feature = "std")]
82        {
83            let mut guard = self.spans.lock().unwrap();
84            core::mem::take(&mut *guard)
85        }
86        #[cfg(not(feature = "std"))]
87        {
88            let mut guard = self.spans.borrow_mut();
89            core::mem::take(&mut *guard)
90        }
91    }
92
93    /// Get a snapshot (clone) of all collected spans.
94    pub fn snapshot(&self) -> Vec<ResourceSpan> {
95        #[cfg(feature = "std")]
96        {
97            self.spans.lock().unwrap().clone()
98        }
99        #[cfg(not(feature = "std"))]
100        {
101            self.spans.borrow().clone()
102        }
103    }
104}
105
106impl Default for NullExporter {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl SpanExporter for NullExporter {
113    fn export(&self, spans: &[ResourceSpan]) {
114        #[cfg(feature = "std")]
115        {
116            let mut guard = self.spans.lock().unwrap();
117            guard.extend(spans.iter().cloned());
118        }
119        #[cfg(not(feature = "std"))]
120        {
121            let mut guard = self.spans.borrow_mut();
122            guard.extend(spans.iter().cloned());
123        }
124    }
125
126    fn flush(&self) {
127        // Nothing to flush — spans are already in memory.
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::trace::TraceContext;
135
136    #[test]
137    fn null_exporter_collects_and_drains() {
138        let exporter = NullExporter::new();
139        assert!(exporter.is_empty());
140
141        let span1 = ResourceSpan::new("op1", TraceContext::default());
142        let span2 = ResourceSpan::new("op2", TraceContext::default());
143        exporter.export(&[span1, span2]);
144
145        assert_eq!(exporter.len(), 2);
146
147        let drained = exporter.drain();
148        assert_eq!(drained.len(), 2);
149        assert_eq!(drained[0].name, "op1");
150        assert_eq!(drained[1].name, "op2");
151        assert!(exporter.is_empty());
152    }
153
154    #[test]
155    fn null_exporter_snapshot_does_not_drain() {
156        let exporter = NullExporter::new();
157        let span = ResourceSpan::new("snap", TraceContext::default());
158        exporter.export(&[span]);
159
160        let snap = exporter.snapshot();
161        assert_eq!(snap.len(), 1);
162        assert_eq!(exporter.len(), 1); // still there
163    }
164
165    #[test]
166    fn null_exporter_multiple_exports() {
167        let exporter = NullExporter::new();
168        for i in 0..10 {
169            let name = alloc::format!("span_{i}");
170            let span = ResourceSpan::new(&name, TraceContext::default());
171            exporter.export(&[span]);
172        }
173        assert_eq!(exporter.len(), 10);
174    }
175}