1use std::cell::RefCell;
11use std::collections::BTreeMap;
12use std::rc::Rc;
13use std::time::Instant;
14
15use crate::value::VmValue;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SpanKind {
20 Pipeline,
21 FnCall,
22 LlmCall,
23 ToolCall,
24 Import,
25 Parallel,
26 Spawn,
27 Step,
29}
30
31impl SpanKind {
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Self::Pipeline => "pipeline",
35 Self::FnCall => "fn_call",
36 Self::LlmCall => "llm_call",
37 Self::ToolCall => "tool_call",
38 Self::Import => "import",
39 Self::Parallel => "parallel",
40 Self::Spawn => "spawn",
41 Self::Step => "step",
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct Span {
49 pub span_id: u64,
50 pub parent_id: Option<u64>,
51 pub kind: SpanKind,
52 pub name: String,
53 pub start_ms: u64,
54 pub duration_ms: u64,
55 pub metadata: BTreeMap<String, serde_json::Value>,
56}
57
58struct OpenSpan {
60 span_id: u64,
61 parent_id: Option<u64>,
62 kind: SpanKind,
63 name: String,
64 started_at: Instant,
65 metadata: BTreeMap<String, serde_json::Value>,
66}
67
68pub struct SpanCollector {
71 next_id: u64,
72 active_stack: Vec<u64>,
74 open: BTreeMap<u64, OpenSpan>,
76 completed: Vec<Span>,
78 epoch: Instant,
80}
81
82impl Default for SpanCollector {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl SpanCollector {
89 pub fn new() -> Self {
90 Self {
91 next_id: 1,
92 active_stack: Vec::new(),
93 open: BTreeMap::new(),
94 completed: Vec::new(),
95 epoch: Instant::now(),
96 }
97 }
98
99 pub fn start(&mut self, kind: SpanKind, name: String) -> u64 {
101 let id = self.next_id;
102 self.next_id += 1;
103 let parent_id = self.active_stack.last().copied();
104 let now = Instant::now();
105
106 crate::events::emit_span_start(id, parent_id, &name, kind.as_str(), BTreeMap::new());
107
108 self.open.insert(
109 id,
110 OpenSpan {
111 span_id: id,
112 parent_id,
113 kind,
114 name,
115 started_at: now,
116 metadata: BTreeMap::new(),
117 },
118 );
119 self.active_stack.push(id);
120 id
121 }
122
123 pub fn set_metadata(&mut self, span_id: u64, key: &str, value: serde_json::Value) {
125 if let Some(span) = self.open.get_mut(&span_id) {
126 span.metadata.insert(key.to_string(), value);
127 }
128 }
129
130 pub fn end(&mut self, span_id: u64) {
132 if let Some(span) = self.open.remove(&span_id) {
133 let duration = span.started_at.elapsed();
134 let start_ms = span.started_at.duration_since(self.epoch).as_millis() as u64;
135 let duration_ms = duration.as_millis() as u64;
136
137 let mut end_meta = span.metadata.clone();
138 end_meta.insert(
139 "duration_ms".to_string(),
140 serde_json::Value::Number(serde_json::Number::from(duration_ms)),
141 );
142 crate::events::emit_span_end(span_id, end_meta);
143
144 self.completed.push(Span {
145 span_id: span.span_id,
146 parent_id: span.parent_id,
147 kind: span.kind,
148 name: span.name,
149 start_ms,
150 duration_ms,
151 metadata: span.metadata,
152 });
153
154 if let Some(pos) = self.active_stack.iter().rposition(|&id| id == span_id) {
155 self.active_stack.remove(pos);
156 }
157 }
158 }
159
160 pub fn current_span_id(&self) -> Option<u64> {
162 self.active_stack.last().copied()
163 }
164
165 pub fn take_spans(&mut self) -> Vec<Span> {
167 std::mem::take(&mut self.completed)
168 }
169
170 pub fn spans(&self) -> &[Span] {
172 &self.completed
173 }
174
175 pub fn reset(&mut self) {
177 self.active_stack.clear();
178 self.open.clear();
179 self.completed.clear();
180 self.next_id = 1;
181 self.epoch = Instant::now();
182 }
183}
184
185thread_local! {
186 static COLLECTOR: RefCell<SpanCollector> = RefCell::new(SpanCollector::new());
187 static TRACING_ENABLED: RefCell<bool> = const { RefCell::new(false) };
188}
189
190pub fn set_tracing_enabled(enabled: bool) {
192 TRACING_ENABLED.with(|e| *e.borrow_mut() = enabled);
193 if enabled {
194 COLLECTOR.with(|c| c.borrow_mut().reset());
195 }
196}
197
198pub fn is_tracing_enabled() -> bool {
200 TRACING_ENABLED.with(|e| *e.borrow())
201}
202
203pub fn span_start(kind: SpanKind, name: String) -> u64 {
205 if !is_tracing_enabled() {
206 return 0;
207 }
208 COLLECTOR.with(|c| c.borrow_mut().start(kind, name))
209}
210
211pub fn span_set_metadata(span_id: u64, key: &str, value: serde_json::Value) {
213 if span_id == 0 {
214 return;
215 }
216 COLLECTOR.with(|c| c.borrow_mut().set_metadata(span_id, key, value));
217}
218
219pub fn span_end(span_id: u64) {
221 if span_id == 0 {
222 return;
223 }
224 COLLECTOR.with(|c| c.borrow_mut().end(span_id));
225}
226
227pub fn current_span_id() -> Option<u64> {
229 if !is_tracing_enabled() {
230 return None;
231 }
232 COLLECTOR.with(|c| c.borrow().current_span_id())
233}
234
235pub fn take_spans() -> Vec<Span> {
237 COLLECTOR.with(|c| c.borrow_mut().take_spans())
238}
239
240pub fn peek_spans() -> Vec<Span> {
242 COLLECTOR.with(|c| c.borrow().spans().to_vec())
243}
244
245pub fn reset_tracing() {
247 COLLECTOR.with(|c| c.borrow_mut().reset());
248}
249
250pub fn span_to_vm_value(span: &Span) -> VmValue {
252 let mut d = BTreeMap::new();
253 d.insert("span_id".into(), VmValue::Int(span.span_id as i64));
254 d.insert(
255 "parent_id".into(),
256 span.parent_id
257 .map(|id| VmValue::Int(id as i64))
258 .unwrap_or(VmValue::Nil),
259 );
260 d.insert("kind".into(), VmValue::String(Rc::from(span.kind.as_str())));
261 d.insert("name".into(), VmValue::String(Rc::from(span.name.as_str())));
262 d.insert("start_ms".into(), VmValue::Int(span.start_ms as i64));
263 d.insert("duration_ms".into(), VmValue::Int(span.duration_ms as i64));
264
265 if !span.metadata.is_empty() {
266 let meta: BTreeMap<String, VmValue> = span
267 .metadata
268 .iter()
269 .map(|(k, v)| (k.clone(), crate::stdlib::json_to_vm_value(v)))
270 .collect();
271 d.insert("metadata".into(), VmValue::Dict(Rc::new(meta)));
272 }
273
274 VmValue::Dict(Rc::new(d))
275}
276
277pub fn format_summary() -> String {
279 let spans = peek_spans();
280 if spans.is_empty() {
281 return "No spans recorded.".into();
282 }
283
284 let mut lines = Vec::new();
285 let total_ms: u64 = spans
286 .iter()
287 .filter(|s| s.parent_id.is_none())
288 .map(|s| s.duration_ms)
289 .sum();
290
291 lines.push(format!("Trace: {} spans, {total_ms}ms total", spans.len()));
292 lines.push(String::new());
293
294 fn print_tree(spans: &[Span], parent_id: Option<u64>, depth: usize, lines: &mut Vec<String>) {
295 let children: Vec<&Span> = spans.iter().filter(|s| s.parent_id == parent_id).collect();
296 for span in children {
297 let indent = " ".repeat(depth);
298 let meta_str = if span.metadata.is_empty() {
299 String::new()
300 } else {
301 let parts: Vec<String> = span
302 .metadata
303 .iter()
304 .map(|(k, v)| format!("{k}={v}"))
305 .collect();
306 format!(" ({})", parts.join(", "))
307 };
308 lines.push(format!(
309 "{indent}{} {} {}ms{meta_str}",
310 span.kind.as_str(),
311 span.name,
312 span.duration_ms,
313 ));
314 print_tree(spans, Some(span.span_id), depth + 1, lines);
315 }
316 }
317
318 print_tree(&spans, None, 0, &mut lines);
319 lines.join("\n")
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_span_collector_basic() {
328 let mut c = SpanCollector::new();
329 let id = c.start(SpanKind::Pipeline, "main".into());
330 assert_eq!(id, 1);
331 assert_eq!(c.current_span_id(), Some(1));
332 c.end(id);
333 assert_eq!(c.current_span_id(), None);
334 assert_eq!(c.spans().len(), 1);
335 assert_eq!(c.spans()[0].name, "main");
336 assert_eq!(c.spans()[0].parent_id, None);
337 }
338
339 #[test]
340 fn test_span_parent_child() {
341 let mut c = SpanCollector::new();
342 let parent = c.start(SpanKind::Pipeline, "main".into());
343 let child = c.start(SpanKind::FnCall, "helper".into());
344 c.end(child);
345 c.end(parent);
346 assert_eq!(c.spans().len(), 2);
347 assert_eq!(c.spans()[0].parent_id, Some(parent));
348 assert_eq!(c.spans()[1].parent_id, None);
349 }
350
351 #[test]
352 fn test_span_metadata() {
353 let mut c = SpanCollector::new();
354 let id = c.start(SpanKind::LlmCall, "gpt-4".into());
355 c.set_metadata(id, "tokens", serde_json::json!(100));
356 c.end(id);
357 assert_eq!(c.spans()[0].metadata["tokens"], serde_json::json!(100));
358 }
359
360 #[test]
361 fn test_noop_when_disabled() {
362 set_tracing_enabled(false);
363 let id = span_start(SpanKind::Pipeline, "test".into());
364 assert_eq!(id, 0);
365 span_end(id);
366 }
367}