lc_callbacks/tracing/
tracer.rs1use 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
9type SpanStack = std::cell::RefCell<Vec<SpanId>>;
11
12#[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
22tokio::task_local! {
24 pub(super) static ASYNC_SPAN_STACK: SpanStack;
25}
26
27pub async fn init_task_span_stack() {
40 let _ = ASYNC_SPAN_STACK
41 .scope(std::cell::RefCell::new(Vec::new()), async {})
42 .await;
43}
44
45fn get_current_span_id() -> Option<SpanId> {
47 if let Ok(id) = ASYNC_SPAN_STACK.try_with(|stack: &SpanStack| stack.borrow().last().cloned()) {
49 return id;
50 }
51 span_stack_tls::SPAN_STACK.with(|s| s.borrow().last().cloned())
53}
54
55fn 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
67fn 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
88pub 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
97pub struct Tracer {
99 backend: Arc<dyn TracingBackend>,
100}
101
102impl Tracer {
103 pub fn new(backend: Arc<dyn TracingBackend>) -> Self {
105 Self { backend }
106 }
107
108 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_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 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_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 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 pub fn current_span_id(&self) -> Option<SpanId> {
171 get_current_span_id()
172 }
173
174 pub fn flush(&self) {
176 self.backend.flush();
177 }
178
179 fn end_span(backend: &Arc<dyn TracingBackend>, span: &TraceSpan) {
181 backend.end_span(span);
182 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
195pub struct SpanGuard {
203 span: TraceSpan,
204 backend: Arc<dyn TracingBackend>,
205 start_instant: Instant,
206 dropped: bool,
208}
209
210impl SpanGuard {
211 pub fn id(&self) -> &str {
213 &self.span.id
214 }
215
216 pub fn parent_id(&self) -> Option<&str> {
218 self.span.parent_id.as_deref()
219 }
220
221 pub fn with_tokens(mut self, usage: crate::tracing::SpanTokenUsage) -> Self {
223 self.span.tokens = Some(usage);
224 self
225 }
226
227 pub fn with_cost(mut self, cost: f64) -> Self {
232 self.span.cost = Some(cost);
233 self
234 }
235
236 pub fn with_gen_ai_request_model(mut self, model: impl Into<String>) -> Self {
238 self.span.gen_ai_request_model = Some(model.into());
239 self
240 }
241
242 pub fn with_gen_ai_response_model(mut self, model: impl Into<String>) -> Self {
244 self.span.gen_ai_response_model = Some(model.into());
245 self
246 }
247
248 pub fn with_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
250 if let Some(obj) = self.span.metadata.as_object_mut() {
251 obj.insert(key.to_string(), value);
252 }
253 self
254 }
255
256 pub fn set_error(&mut self, msg: &str) {
258 self.span.status = SpanStatus::Error(msg.to_string());
259 }
260
261 fn finalize_cost(&mut self) {
263 if self.span.cost.is_some() {
264 return;
265 }
266 let tokens = match &self.span.tokens {
267 Some(t) => t.clone(),
268 None => return,
269 };
270 let model = match self
271 .span
272 .gen_ai_request_model
273 .as_deref()
274 .or(self.span.gen_ai_response_model.as_deref())
275 {
276 Some(m) => m.to_string(),
277 None => return,
278 };
279 self.span.cost = crate::pricing::estimate_cost_usd(
280 tokens.prompt_tokens,
281 tokens.completion_tokens,
282 &model,
283 );
284 }
285
286 pub fn end(mut self) {
288 if !self.dropped {
289 self.finalize_cost();
290 self.span.end_time = Some(chrono::Utc::now().to_rfc3339());
291 self.span.latency_ms = Some(self.start_instant.elapsed().as_millis() as u64);
292 Tracer::end_span(&self.backend, &self.span);
293 self.dropped = true;
294 }
295 }
296}
297
298impl Drop for SpanGuard {
299 fn drop(&mut self) {
300 if !self.dropped {
301 self.finalize_cost();
302 self.span.end_time = Some(chrono::Utc::now().to_rfc3339());
303 self.span.latency_ms = Some(self.start_instant.elapsed().as_millis() as u64);
304 Tracer::end_span(&self.backend, &self.span);
305 self.dropped = true;
306 }
307 }
308}