use chrono::Utc;
use serde::{Deserialize, Serialize};
pub type SpanId = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SpanKind {
Llm,
Chain,
Tool,
Retriever,
Agent,
Custom(String),
}
impl std::fmt::Display for SpanKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpanKind::Llm => write!(f, "llm"),
SpanKind::Chain => write!(f, "chain"),
SpanKind::Tool => write!(f, "tool"),
SpanKind::Retriever => write!(f, "retriever"),
SpanKind::Agent => write!(f, "agent"),
SpanKind::Custom(name) => write!(f, "custom:{}", name),
}
}
}
impl From<crate::RunType> for SpanKind {
fn from(run_type: crate::RunType) -> Self {
match run_type {
crate::RunType::Llm => SpanKind::Llm,
crate::RunType::Chain => SpanKind::Chain,
crate::RunType::Tool => SpanKind::Tool,
crate::RunType::Retriever => SpanKind::Retriever,
crate::RunType::Embedding => SpanKind::Custom("embedding".to_string()),
crate::RunType::Prompt => SpanKind::Custom("prompt".to_string()),
crate::RunType::Parser => SpanKind::Custom("parser".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SpanTokenUsage {
pub prompt_tokens: usize,
pub completion_tokens: usize,
pub total_tokens: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum SpanStatus {
Ok,
Error(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceSpan {
pub id: SpanId,
pub parent_id: Option<SpanId>,
pub name: String,
pub kind: SpanKind,
pub start_time: Option<String>,
pub end_time: Option<String>,
pub tokens: Option<SpanTokenUsage>,
pub cost: Option<f64>,
pub latency_ms: Option<u64>,
pub metadata: serde_json::Value,
pub status: SpanStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_system: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_request_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_response_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_finish_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_request_max_tokens: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_request_temperature: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_operation_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gen_ai_tool_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TraceNode {
pub span: TraceSpan,
pub children: Vec<TraceNode>,
}
fn estimate_span_cost(span: &TraceSpan) -> Option<f64> {
let tokens = span.tokens.as_ref()?;
let model = span
.gen_ai_request_model
.as_deref()
.or(span.gen_ai_response_model.as_deref())?;
crate::pricing::estimate_cost_usd(tokens.prompt_tokens, tokens.completion_tokens, model)
}
pub fn aggregate_cost(spans: &[TraceSpan]) -> f64 {
spans
.iter()
.map(|s| s.cost.or_else(|| estimate_span_cost(s)).unwrap_or(0.0))
.sum()
}
pub(crate) fn build_tree(root: &TraceSpan, all_spans: &[TraceSpan]) -> TraceNode {
let children: Vec<TraceNode> = all_spans
.iter()
.filter(|s| s.parent_id.as_deref() == Some(root.id.as_str()))
.map(|child| build_tree(child, all_spans))
.collect();
TraceNode {
span: root.clone(),
children,
}
}
pub(crate) fn make_span(
id: String,
parent_id: Option<SpanId>,
name: &str,
kind: SpanKind,
) -> TraceSpan {
TraceSpan {
id,
parent_id,
name: name.to_string(),
kind,
start_time: Some(Utc::now().to_rfc3339()),
end_time: None,
tokens: None,
cost: None,
latency_ms: None,
metadata: serde_json::Value::Object(serde_json::Map::new()),
status: SpanStatus::Ok,
gen_ai_system: None,
gen_ai_request_model: None,
gen_ai_response_model: None,
gen_ai_finish_reason: None,
gen_ai_request_max_tokens: None,
gen_ai_request_temperature: None,
gen_ai_operation_name: None,
gen_ai_tool_name: None,
}
}