Skip to main content

renacer_core/
lazy_span.rs

1/// Sprint 36: Lazy Span Creation
2///
3/// Defers expensive span building operations until the span is actually exported.
4/// This reduces overhead when:
5/// - OTLP export is disabled
6/// - Sampling drops the span
7/// - Span is never finished
8use std::borrow::Cow;
9
10/// Lazy span builder that defers work until commit
11pub struct LazySpan {
12    name: Option<Cow<'static, str>>,
13    attributes: Vec<(Cow<'static, str>, String)>,
14    timestamp_nanos: u64,
15    duration_nanos: u64,
16    status_code: i32,
17    committed: bool,
18}
19
20impl LazySpan {
21    /// Create a new lazy span (minimal allocation)
22    pub fn new() -> Self {
23        LazySpan {
24            name: None,
25            attributes: Vec::new(),
26            timestamp_nanos: 0,
27            duration_nanos: 0,
28            status_code: 0,
29            committed: false,
30        }
31    }
32
33    /// Set span name (zero-copy for static strings)
34    pub fn with_name_static(mut self, name: &'static str) -> Self {
35        self.name = Some(Cow::Borrowed(name));
36        self
37    }
38
39    /// Set span name (owned string)
40    pub fn with_name_owned(mut self, name: String) -> Self {
41        self.name = Some(Cow::Owned(name));
42        self
43    }
44
45    /// Add attribute (zero-copy key)
46    pub fn with_attribute_static(mut self, key: &'static str, value: String) -> Self {
47        self.attributes.push((Cow::Borrowed(key), value));
48        self
49    }
50
51    /// Add attribute (owned key)
52    pub fn with_attribute_owned(mut self, key: String, value: String) -> Self {
53        self.attributes.push((Cow::Owned(key), value));
54        self
55    }
56
57    /// Set timestamp
58    pub fn with_timestamp(mut self, timestamp_nanos: u64) -> Self {
59        self.timestamp_nanos = timestamp_nanos;
60        self
61    }
62
63    /// Set duration
64    pub fn with_duration(mut self, duration_nanos: u64) -> Self {
65        self.duration_nanos = duration_nanos;
66        self
67    }
68
69    /// Set status code
70    pub fn with_status(mut self, status_code: i32) -> Self {
71        self.status_code = status_code;
72        self
73    }
74
75    /// Check if span has been committed
76    pub fn is_committed(&self) -> bool {
77        self.committed
78    }
79
80    /// Commit the span (mark as ready for export)
81    ///
82    /// This is when the actual work happens. Before this, the span is just
83    /// a lightweight builder collecting parameters.
84    pub fn commit(mut self) -> CommittedSpan {
85        self.committed = true;
86        CommittedSpan {
87            name: self.name.unwrap_or(Cow::Borrowed("")),
88            attributes: self.attributes,
89            timestamp_nanos: self.timestamp_nanos,
90            duration_nanos: self.duration_nanos,
91            status_code: self.status_code,
92        }
93    }
94
95    /// Drop without committing (zero-cost when span not exported)
96    pub fn cancel(self) {
97        // Just drop - no export happens
98    }
99}
100
101impl Default for LazySpan {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107/// Committed span ready for export
108pub struct CommittedSpan {
109    pub name: Cow<'static, str>,
110    pub attributes: Vec<(Cow<'static, str>, String)>,
111    pub timestamp_nanos: u64,
112    pub duration_nanos: u64,
113    pub status_code: i32,
114}
115
116/// Convenience macro for creating lazy spans with zero-copy
117#[macro_export]
118macro_rules! lazy_span {
119    ($name:expr) => {
120        LazySpan::new().with_name_static($name)
121    };
122    ($name:expr, $($key:expr => $value:expr),* $(,)?) => {
123        {
124            let mut span = LazySpan::new().with_name_static($name);
125            $(
126                span = span.with_attribute_static($key, $value);
127            )*
128            span
129        }
130    };
131}
132
133// Compile-time thread-safety verification (Sprint 59)
134static_assertions::assert_impl_all!(LazySpan: Send, Sync);
135static_assertions::assert_impl_all!(CommittedSpan: Send, Sync);
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_lazy_span_minimal() {
143        let span = LazySpan::new();
144        assert!(!span.is_committed());
145        assert_eq!(span.name, None);
146        assert_eq!(span.attributes.len(), 0);
147    }
148
149    #[test]
150    fn test_lazy_span_builder() {
151        let span = LazySpan::new()
152            .with_name_static("syscall:open")
153            .with_attribute_static("syscall.name", "open".to_string())
154            .with_attribute_static("syscall.result", "3".to_string())
155            .with_timestamp(1234567890)
156            .with_duration(1000);
157
158        assert!(!span.is_committed());
159        assert_eq!(span.name, Some(Cow::Borrowed("syscall:open")));
160        assert_eq!(span.attributes.len(), 2);
161    }
162
163    #[test]
164    fn test_lazy_span_commit() {
165        let span = LazySpan::new()
166            .with_name_static("test")
167            .with_timestamp(100)
168            .with_duration(50);
169
170        let committed = span.commit();
171        assert_eq!(committed.name.as_ref(), "test");
172        assert_eq!(committed.timestamp_nanos, 100);
173        assert_eq!(committed.duration_nanos, 50);
174    }
175
176    #[test]
177    fn test_lazy_span_cancel() {
178        let span = LazySpan::new()
179            .with_name_static("cancelled")
180            .with_attribute_static("test", "value".to_string());
181
182        // Just drop it - no work done
183        span.cancel();
184        // Test passes if no panic
185    }
186
187    #[test]
188    fn test_lazy_span_zero_copy() {
189        let span = LazySpan::new()
190            .with_name_static("syscall:open")
191            .with_attribute_static("syscall.name", "open".to_string());
192
193        if let Some(Cow::Borrowed(_)) = span.name {
194            // Zero-copy for static name
195        } else {
196            panic!("Expected borrowed name");
197        }
198
199        assert!(matches!(span.attributes[0].0, Cow::Borrowed(_)));
200    }
201
202    #[test]
203    fn test_lazy_span_owned() {
204        let dynamic_name = format!("dynamic_{}", 42);
205        let span = LazySpan::new()
206            .with_name_owned(dynamic_name.clone())
207            .with_attribute_owned("key".to_string(), "value".to_string());
208
209        if let Some(Cow::Owned(_)) = span.name {
210            // Owned for dynamic name
211        } else {
212            panic!("Expected owned name");
213        }
214
215        assert!(matches!(span.attributes[0].0, Cow::Owned(_)));
216    }
217
218    #[test]
219    fn test_span_not_committed_by_default() {
220        let span = LazySpan::new().with_name_static("test");
221        assert!(!span.is_committed());
222    }
223
224    #[test]
225    fn test_multiple_attributes() {
226        let span = LazySpan::new()
227            .with_name_static("test")
228            .with_attribute_static("key1", "value1".to_string())
229            .with_attribute_static("key2", "value2".to_string())
230            .with_attribute_static("key3", "value3".to_string());
231
232        assert_eq!(span.attributes.len(), 3);
233        assert_eq!(span.attributes[0].1, "value1");
234        assert_eq!(span.attributes[1].1, "value2");
235        assert_eq!(span.attributes[2].1, "value3");
236    }
237}