Skip to main content

lc_callbacks/tracing/
tracer.rs

1use std::sync::Arc;
2use std::time::Instant;
3
4use uuid::Uuid;
5
6use crate::tracing::span::{make_span, SpanId, SpanKind, SpanStatus, TraceSpan};
7use crate::tracing::TracingBackend;
8
9/// Type alias for the span stack stored in thread-local and task-local storage.
10type SpanStack = std::cell::RefCell<Vec<SpanId>>;
11
12// Thread-local span stack for synchronous contexts.
13#[allow(clippy::missing_const_for_thread_local)]
14mod span_stack_tls {
15    use super::SpanStack;
16
17    std::thread_local! {
18        pub(super) static SPAN_STACK: SpanStack = const { std::cell::RefCell::new(Vec::new()) };
19    }
20}
21
22// Task-local span stack for async contexts (survives thread migration).
23tokio::task_local! {
24    pub(super) static ASYNC_SPAN_STACK: SpanStack;
25}
26
27/// Initialize the task-local span stack for the current async task.
28///
29/// Call this at the top of your async function to enable task-local span tracking.
30/// If not initialized, the tracer falls back to thread-local storage.
31///
32/// # Example
33/// ```ignore
34/// tokio::spawn(async {
35///     init_task_span_stack().await;
36///     // ... use tracer
37/// });
38/// ```
39pub async fn init_task_span_stack() {
40    let _ = ASYNC_SPAN_STACK
41        .scope(std::cell::RefCell::new(Vec::new()), async {})
42        .await;
43}
44
45/// Try to get the current span ID from task-local, falling back to thread-local.
46fn get_current_span_id() -> Option<SpanId> {
47    // Try task-local first (works across thread migration in async)
48    if let Ok(id) = ASYNC_SPAN_STACK.try_with(|stack: &SpanStack| stack.borrow().last().cloned()) {
49        return id;
50    }
51    // Fall back to thread-local for sync contexts
52    span_stack_tls::SPAN_STACK.with(|s| s.borrow().last().cloned())
53}
54
55/// Push a span ID onto the appropriate stack (task-local if available, else thread-local).
56fn push_span_id(id: SpanId) {
57    let id_clone = id.clone();
58    if ASYNC_SPAN_STACK
59        .try_with(|stack: &SpanStack| stack.borrow_mut().push(id))
60        .is_ok()
61    {
62        return;
63    }
64    span_stack_tls::SPAN_STACK.with(|s| s.borrow_mut().push(id_clone));
65}
66
67/// Pop a span ID from the appropriate stack if it matches.
68fn pop_span_id_if_matches(span_id: &str) {
69    if ASYNC_SPAN_STACK
70        .try_with(|stack: &SpanStack| {
71            let mut s = stack.borrow_mut();
72            if s.last().map(|id: &String| id.as_str()) == Some(span_id) {
73                s.pop();
74            }
75        })
76        .is_ok()
77    {
78        return;
79    }
80    span_stack_tls::SPAN_STACK.with(|s| {
81        let mut stack = s.borrow_mut();
82        if stack.last().map(|id: &String| id.as_str()) == Some(span_id) {
83            stack.pop();
84        }
85    });
86}
87
88/// Clear the thread-local span stack.
89///
90/// This is primarily useful for test isolation when spans from prior tests
91/// leak into the thread-local state.
92pub fn clear_span_stack() {
93    span_stack_tls::SPAN_STACK.with(|s| s.borrow_mut().clear());
94    let _ = ASYNC_SPAN_STACK.try_with(|stack: &SpanStack| stack.borrow_mut().clear());
95}
96
97/// The main tracer that manages span lifecycle.
98pub struct Tracer {
99    backend: Arc<dyn TracingBackend>,
100}
101
102impl Tracer {
103    /// Create a new tracer backed by the given backend.
104    pub fn new(backend: Arc<dyn TracingBackend>) -> Self {
105        Self { backend }
106    }
107
108    /// Start a new root span.
109    pub fn start(&self, name: &str, kind: SpanKind) -> SpanGuard {
110        let id = Uuid::now_v7().to_string();
111        let span = make_span(id.clone(), None, name, kind);
112
113        self.backend.start_span(&span);
114
115        // Push this span onto the appropriate stack
116        push_span_id(id.clone());
117
118        SpanGuard {
119            span,
120            backend: Arc::clone(&self.backend),
121            start_instant: Instant::now(),
122            dropped: false,
123        }
124    }
125
126    /// Start a child span under the current span.
127    ///
128    /// If no span is active, starts a root span instead.
129    pub fn start_child(&self, name: &str, kind: SpanKind) -> SpanGuard {
130        let parent_id = get_current_span_id();
131        let id = Uuid::now_v7().to_string();
132        let span = make_span(id.clone(), parent_id, name, kind);
133
134        self.backend.start_span(&span);
135
136        // Push this span onto the appropriate stack
137        push_span_id(id.clone());
138
139        SpanGuard {
140            span,
141            backend: Arc::clone(&self.backend),
142            start_instant: Instant::now(),
143            dropped: false,
144        }
145    }
146
147    /// Start a child span with an explicit parent ID.
148    pub fn start_child_with_parent(
149        &self,
150        name: &str,
151        kind: SpanKind,
152        parent_id: SpanId,
153    ) -> SpanGuard {
154        let id = Uuid::now_v7().to_string();
155        let span = make_span(id.clone(), Some(parent_id), name, kind);
156
157        self.backend.start_span(&span);
158
159        push_span_id(id.clone());
160
161        SpanGuard {
162            span,
163            backend: Arc::clone(&self.backend),
164            start_instant: Instant::now(),
165            dropped: false,
166        }
167    }
168
169    /// Get the current span ID from the active stack.
170    pub fn current_span_id(&self) -> Option<SpanId> {
171        get_current_span_id()
172    }
173
174    /// Flush all pending spans to the backend.
175    pub fn flush(&self) {
176        self.backend.flush();
177    }
178
179    /// End a span (called from SpanGuard::drop).
180    fn end_span(backend: &Arc<dyn TracingBackend>, span: &TraceSpan) {
181        backend.end_span(span);
182        // Pop from the appropriate stack (only if it matches)
183        pop_span_id_if_matches(&span.id);
184    }
185}
186
187impl Clone for Tracer {
188    fn clone(&self) -> Self {
189        Self {
190            backend: Arc::clone(&self.backend),
191        }
192    }
193}
194
195// ---------------------------------------------------------------------------
196// SpanGuard — RAII guard that ends the span when dropped
197// ---------------------------------------------------------------------------
198
199/// RAII guard that ends the span when dropped.
200///
201/// Use builder-style methods to attach data before the span completes.
202pub struct SpanGuard {
203    span: TraceSpan,
204    backend: Arc<dyn TracingBackend>,
205    start_instant: Instant,
206    /// If true, the span has been manually ended and Drop should not end it again.
207    dropped: bool,
208}
209
210impl SpanGuard {
211    /// Get the span ID.
212    pub fn id(&self) -> &str {
213        &self.span.id
214    }
215
216    /// Get the parent span ID.
217    pub fn parent_id(&self) -> Option<&str> {
218        self.span.parent_id.as_deref()
219    }
220
221    /// Set token usage on this span.
222    pub fn with_tokens(mut self, usage: crate::tracing::SpanTokenUsage) -> Self {
223        self.span.tokens = Some(usage);
224        self
225    }
226
227    /// Set cost on this span.
228    pub fn with_cost(mut self, cost: f64) -> Self {
229        self.span.cost = Some(cost);
230        self
231    }
232
233    /// Add a metadata key-value pair.
234    pub fn with_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
235        if let Some(obj) = self.span.metadata.as_object_mut() {
236            obj.insert(key.to_string(), value);
237        }
238        self
239    }
240
241    /// Mark this span as having errored.
242    pub fn set_error(&mut self, msg: &str) {
243        self.span.status = SpanStatus::Error(msg.to_string());
244    }
245
246    /// Manually end the span now instead of waiting for Drop.
247    pub fn end(mut self) {
248        if !self.dropped {
249            self.span.end_time = Some(chrono::Utc::now().to_rfc3339());
250            self.span.latency_ms = Some(self.start_instant.elapsed().as_millis() as u64);
251            Tracer::end_span(&self.backend, &self.span);
252            self.dropped = true;
253        }
254    }
255}
256
257impl Drop for SpanGuard {
258    fn drop(&mut self) {
259        if !self.dropped {
260            self.span.end_time = Some(chrono::Utc::now().to_rfc3339());
261            self.span.latency_ms = Some(self.start_instant.elapsed().as_millis() as u64);
262            Tracer::end_span(&self.backend, &self.span);
263            self.dropped = true;
264        }
265    }
266}