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