use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use super::{SpanEvent, TraceMetadata};
pub type SpanId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SpanStatus {
Ok,
Error { message: String },
Cancelled,
InProgress,
}
impl SpanStatus {
pub fn is_ok(&self) -> bool {
matches!(self, SpanStatus::Ok)
}
pub fn is_error(&self) -> bool {
matches!(self, SpanStatus::Error { .. })
}
pub fn error_message(&self) -> Option<&str> {
match self {
SpanStatus::Error { message } => Some(message),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanContext {
pub trace_id: String,
pub span_id: SpanId,
pub parent_span_id: Option<SpanId>,
pub baggage: HashMap<String, String>,
}
impl SpanContext {
pub fn new(trace_id: impl Into<String>, span_id: impl Into<String>) -> Self {
Self {
trace_id: trace_id.into(),
span_id: span_id.into(),
parent_span_id: None,
baggage: HashMap::new(),
}
}
pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
self.parent_span_id = Some(parent_id.into());
self
}
pub fn with_baggage(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.baggage.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Span {
pub span_id: SpanId,
pub parent_span_id: Option<SpanId>,
pub name: String,
pub start_time: DateTime<Utc>,
pub end_time: Option<DateTime<Utc>>,
pub status: SpanStatus,
pub events: Vec<SpanEvent>,
pub metadata: TraceMetadata,
pub kind: SpanKind,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum SpanKind {
#[default]
Internal,
Client,
Server,
Producer,
Consumer,
LlmCall,
ToolExecution,
AgentOperation,
}
impl Span {
pub fn new(name: impl Into<String>) -> Self {
Self {
span_id: Uuid::new_v4().to_string(),
parent_span_id: None,
name: name.into(),
start_time: Utc::now(),
end_time: None,
status: SpanStatus::InProgress,
events: Vec::new(),
metadata: TraceMetadata::default(),
kind: SpanKind::Internal,
}
}
pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
self.parent_span_id = Some(parent_id.into());
self
}
pub fn with_kind(mut self, kind: SpanKind) -> Self {
self.kind = kind;
self
}
pub fn with_metadata(mut self, metadata: TraceMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn add_event(&mut self, event: SpanEvent) {
self.events.push(event);
}
pub fn end(&mut self, status: SpanStatus) {
self.end_time = Some(Utc::now());
self.status = status;
}
pub fn end_ok(&mut self) {
self.end(SpanStatus::Ok);
}
pub fn end_error(&mut self, message: impl Into<String>) {
self.end(SpanStatus::Error {
message: message.into(),
});
}
pub fn duration_ms(&self) -> Option<i64> {
self.end_time
.map(|end| (end - self.start_time).num_milliseconds())
}
pub fn is_active(&self) -> bool {
self.end_time.is_none()
}
pub fn context(&self, trace_id: &str) -> SpanContext {
SpanContext {
trace_id: trace_id.to_string(),
span_id: self.span_id.clone(),
parent_span_id: self.parent_span_id.clone(),
baggage: HashMap::new(),
}
}
pub fn set_metadata(&mut self, metadata: TraceMetadata) {
self.metadata = metadata;
}
pub fn merge_metadata(&mut self, additional: TraceMetadata) {
for (k, v) in additional.attributes {
self.metadata.attributes.insert(k, v);
}
if additional.tokens_used.is_some() {
self.metadata.tokens_used = additional.tokens_used;
}
if additional.cost_usd.is_some() {
self.metadata.cost_usd = additional.cost_usd;
}
if additional.model.is_some() {
self.metadata.model = additional.model;
}
if additional.agent_id.is_some() {
self.metadata.agent_id = additional.agent_id;
}
if additional.task_id.is_some() {
self.metadata.task_id = additional.task_id;
}
}
}
pub struct SpanBuilder {
name: String,
parent_id: Option<String>,
kind: SpanKind,
metadata: TraceMetadata,
}
impl SpanBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
parent_id: None,
kind: SpanKind::Internal,
metadata: TraceMetadata::default(),
}
}
pub fn parent(mut self, parent_id: impl Into<String>) -> Self {
self.parent_id = Some(parent_id.into());
self
}
pub fn kind(mut self, kind: SpanKind) -> Self {
self.kind = kind;
self
}
pub fn llm_call(self) -> Self {
self.kind(SpanKind::LlmCall)
}
pub fn tool_execution(self) -> Self {
self.kind(SpanKind::ToolExecution)
}
pub fn agent_operation(self) -> Self {
self.kind(SpanKind::AgentOperation)
}
pub fn metadata(mut self, metadata: TraceMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn agent(mut self, agent_id: impl Into<String>) -> Self {
self.metadata.agent_id = Some(agent_id.into());
self
}
pub fn task(mut self, task_id: impl Into<String>) -> Self {
self.metadata.task_id = Some(task_id.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.metadata.model = Some(model.into());
self
}
pub fn build(self) -> Span {
let mut span = Span::new(self.name)
.with_kind(self.kind)
.with_metadata(self.metadata);
if let Some(parent) = self.parent_id {
span = span.with_parent(parent);
}
span
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_creation() {
let span = Span::new("test_operation");
assert_eq!(span.name, "test_operation");
assert!(span.is_active());
assert_eq!(span.status, SpanStatus::InProgress);
}
#[test]
fn test_span_end() {
let mut span = Span::new("test");
span.end_ok();
assert!(!span.is_active());
assert!(span.status.is_ok());
assert!(span.end_time.is_some());
}
#[test]
fn test_span_error() {
let mut span = Span::new("test");
span.end_error("Something went wrong");
assert!(span.status.is_error());
assert_eq!(span.status.error_message(), Some("Something went wrong"));
}
#[test]
fn test_span_duration() {
let mut span = Span::new("test");
std::thread::sleep(std::time::Duration::from_millis(10));
span.end_ok();
let duration = span.duration_ms().unwrap();
assert!(duration >= 10);
}
#[test]
fn test_span_builder() {
let span = SpanBuilder::new("llm_call")
.llm_call()
.agent("frontend")
.model("claude-3-opus")
.build();
assert_eq!(span.name, "llm_call");
assert_eq!(span.kind, SpanKind::LlmCall);
assert_eq!(span.metadata.agent_id, Some("frontend".to_string()));
assert_eq!(span.metadata.model, Some("claude-3-opus".to_string()));
}
#[test]
fn test_span_context() {
let span = Span::new("test");
let context = span.context("trace-123");
assert_eq!(context.trace_id, "trace-123");
assert_eq!(context.span_id, span.span_id);
}
#[test]
fn test_nested_spans() {
let parent = Span::new("parent");
let child = SpanBuilder::new("child").parent(&parent.span_id).build();
assert_eq!(child.parent_span_id, Some(parent.span_id.clone()));
}
}