use crate::time::{Instant, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceSpan {
pub name: String,
pub start_time_ms: u64,
pub end_time_ms: u64,
pub duration_us: u64,
pub status: TraceStatus,
pub attributes: Vec<(String, String)>,
pub children: Vec<TraceSpan>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TraceStatus {
Ok,
Error,
}
pub(crate) struct SpanBuilder {
name: String,
start_wall: u64,
start_instant: Instant,
attributes: Vec<(String, String)>,
children: Vec<TraceSpan>,
}
impl SpanBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
start_wall: now_ms(),
start_instant: Instant::now(),
attributes: Vec::new(),
children: Vec::new(),
}
}
pub fn attr(mut self, key: &str, value: impl ToString) -> Self {
self.attributes.push((key.to_string(), value.to_string()));
self
}
pub fn set_attr(&mut self, key: &str, value: impl ToString) {
self.attributes.push((key.to_string(), value.to_string()));
}
pub fn add_child(&mut self, child: TraceSpan) {
self.children.push(child);
}
pub fn finish(self, status: TraceStatus) -> TraceSpan {
let elapsed = self.start_instant.elapsed();
TraceSpan {
name: self.name,
start_time_ms: self.start_wall,
end_time_ms: self.start_wall + elapsed.as_millis() as u64,
duration_us: elapsed.as_micros() as u64,
status,
attributes: self.attributes,
children: self.children,
}
}
pub fn finish_ok(self) -> TraceSpan {
self.finish(TraceStatus::Ok)
}
pub fn finish_error(self, error: &str) -> TraceSpan {
self.attr("zapcode.error", error).finish(TraceStatus::Error)
}
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionTrace {
pub root: TraceSpan,
}
impl ExecutionTrace {
pub fn print(&self) {
print_span(&self.root, 0, true);
}
pub fn to_string_pretty(&self) -> String {
let mut buf = String::new();
format_span(&self.root, 0, true, &mut buf);
buf
}
}
fn format_duration(us: u64) -> String {
if us < 1000 {
format!("{}µs", us)
} else if us < 1_000_000 {
format!("{:.1}ms", us as f64 / 1000.0)
} else {
format!("{:.2}s", us as f64 / 1_000_000.0)
}
}
fn format_span(span: &TraceSpan, depth: usize, is_last: bool, buf: &mut String) {
let icon = match span.status {
TraceStatus::Ok => "✓",
TraceStatus::Error => "✗",
};
let duration = format_duration(span.duration_us);
let prefix = if depth == 0 {
String::new()
} else {
let connector = if is_last { "└─ " } else { "├─ " };
let indent = "│ ".repeat(depth - 1);
format!("{}{}", indent, connector)
};
buf.push_str(&format!("{}{} {} ({})", prefix, icon, span.name, duration));
for (k, v) in &span.attributes {
if k == "zapcode.error" {
buf.push_str(&format!(" error=\"{}\"", v));
} else if k == "zapcode.tool.name" {
buf.push_str(&format!(" {}", v));
} else if k == "zapcode.tool.args" {
buf.push_str(&format!("({})", v));
} else if k == "zapcode.tool.result" {
let display = if v.len() > 60 { &v[..57] } else { v };
buf.push_str(&format!(" → {}", display));
if v.len() > 60 {
buf.push_str("...");
}
}
}
buf.push('\n');
for (i, child) in span.children.iter().enumerate() {
format_span(child, depth + 1, i == span.children.len() - 1, buf);
}
}
fn print_span(span: &TraceSpan, depth: usize, is_last: bool) {
let mut buf = String::new();
format_span(span, depth, is_last, &mut buf);
print!("{}", buf);
}