Skip to main content

lc_callbacks/tracing/
span.rs

1use chrono::Utc;
2use serde::{Deserialize, Serialize};
3
4/// Unique span identifier.
5pub type SpanId = String;
6
7/// Kind of span for categorization.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "snake_case")]
10pub enum SpanKind {
11    /// LLM inference call
12    Llm,
13    /// Chain execution
14    Chain,
15    /// Tool invocation
16    Tool,
17    /// Retriever query
18    Retriever,
19    /// Agent execution
20    Agent,
21    /// Custom span kind
22    Custom(String),
23}
24
25impl std::fmt::Display for SpanKind {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            SpanKind::Llm => write!(f, "llm"),
29            SpanKind::Chain => write!(f, "chain"),
30            SpanKind::Tool => write!(f, "tool"),
31            SpanKind::Retriever => write!(f, "retriever"),
32            SpanKind::Agent => write!(f, "agent"),
33            SpanKind::Custom(name) => write!(f, "custom:{}", name),
34        }
35    }
36}
37
38impl From<crate::RunType> for SpanKind {
39    fn from(run_type: crate::RunType) -> Self {
40        match run_type {
41            crate::RunType::Llm => SpanKind::Llm,
42            crate::RunType::Chain => SpanKind::Chain,
43            crate::RunType::Tool => SpanKind::Tool,
44            crate::RunType::Retriever => SpanKind::Retriever,
45            // RunType variants without a dedicated SpanKind collapse to Custom
46            crate::RunType::Embedding => SpanKind::Custom("embedding".to_string()),
47            crate::RunType::Prompt => SpanKind::Custom("prompt".to_string()),
48            crate::RunType::Parser => SpanKind::Custom("parser".to_string()),
49        }
50    }
51}
52
53/// Token usage recorded in a span.
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct SpanTokenUsage {
56    pub prompt_tokens: usize,
57    pub completion_tokens: usize,
58    pub total_tokens: usize,
59}
60
61/// Status of a span.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63#[serde(rename_all = "snake_case")]
64pub enum SpanStatus {
65    /// Span completed successfully
66    Ok,
67    /// Span ended with an error
68    Error(String),
69}
70
71/// A single trace span with parent-child relationships.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TraceSpan {
74    /// Unique span identifier
75    pub id: SpanId,
76    /// Parent span ID (None for root spans)
77    pub parent_id: Option<SpanId>,
78    /// Human-readable span name
79    pub name: String,
80    /// Span category
81    pub kind: SpanKind,
82    /// ISO 8601 start time
83    pub start_time: Option<String>,
84    /// ISO 8601 end time
85    pub end_time: Option<String>,
86    /// Token usage (for LLM spans)
87    pub tokens: Option<SpanTokenUsage>,
88    /// Estimated cost in USD
89    pub cost: Option<f64>,
90    /// Measured latency in milliseconds
91    pub latency_ms: Option<u64>,
92    /// Arbitrary key-value metadata
93    pub metadata: serde_json::Value,
94    /// Span completion status
95    pub status: SpanStatus,
96
97    // --- OTel GenAI SemConv fields ---
98    /// gen_ai.system: The LLM provider name (e.g., "openai", "anthropic")
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub gen_ai_system: Option<String>,
101    /// gen_ai.request.model: The model requested
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub gen_ai_request_model: Option<String>,
104    /// gen_ai.response.model: The actual model used
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub gen_ai_response_model: Option<String>,
107    /// gen_ai.response.finish_reason: Why the model stopped generating
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub gen_ai_finish_reason: Option<String>,
110    /// gen_ai.request.max_tokens: Maximum tokens requested
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub gen_ai_request_max_tokens: Option<u64>,
113    /// gen_ai.request.temperature: Temperature parameter
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub gen_ai_request_temperature: Option<f64>,
116    /// gen_ai.operation.name: The operation name (chat, completion)
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub gen_ai_operation_name: Option<String>,
119    /// gen_ai.tool.name: The tool name (for tool spans)
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub gen_ai_tool_name: Option<String>,
122}
123
124/// A node in the trace tree (span + children).
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct TraceNode {
127    pub span: TraceSpan,
128    pub children: Vec<TraceNode>,
129}
130
131pub(crate) fn build_tree(root: &TraceSpan, all_spans: &[TraceSpan]) -> TraceNode {
132    let children: Vec<TraceNode> = all_spans
133        .iter()
134        .filter(|s| s.parent_id.as_deref() == Some(root.id.as_str()))
135        .map(|child| build_tree(child, all_spans))
136        .collect();
137
138    TraceNode {
139        span: root.clone(),
140        children,
141    }
142}
143
144/// Helper to create a new span with common defaults.
145pub(crate) fn make_span(
146    id: String,
147    parent_id: Option<SpanId>,
148    name: &str,
149    kind: SpanKind,
150) -> TraceSpan {
151    TraceSpan {
152        id,
153        parent_id,
154        name: name.to_string(),
155        kind,
156        start_time: Some(Utc::now().to_rfc3339()),
157        end_time: None,
158        tokens: None,
159        cost: None,
160        latency_ms: None,
161        metadata: serde_json::Value::Object(serde_json::Map::new()),
162        status: SpanStatus::Ok,
163        gen_ai_system: None,
164        gen_ai_request_model: None,
165        gen_ai_response_model: None,
166        gen_ai_finish_reason: None,
167        gen_ai_request_max_tokens: None,
168        gen_ai_request_temperature: None,
169        gen_ai_operation_name: None,
170        gen_ai_tool_name: None,
171    }
172}