Skip to main content

enact_core/telemetry/
spans.rs

1//! Telemetry Spans - OpenTelemetry span helpers for execution tracing
2//!
3//! This module provides utilities for creating and managing OpenTelemetry spans
4//! that correlate with ExecutionId and StepId for distributed tracing.
5//!
6//! ## Span Hierarchy
7//! ```text
8//! Execution Span (trace_id = TraceContext.trace_id)
9//!   ├── Step Span (span_id = unique per step)
10//!   │     └── Tool Span
11//!   ├── Step Span
12//!   │     └── LLM Span
13//!   └── Child Execution Span (sub-agent)
14//!         └── Step Span...
15//! ```
16//!
17//! @see docs/TECHNICAL/01-EXECUTION-TELEMETRY.md
18
19use crate::kernel::{ExecutionId, StepId, StepType};
20use crate::runner::TraceContext;
21
22/// Span attributes for execution-level spans
23pub struct ExecutionSpanAttributes {
24    /// Execution ID
25    pub execution_id: String,
26    /// Parent ID (if nested execution)
27    pub parent_id: Option<String>,
28    /// Parent type
29    pub parent_type: Option<String>,
30    /// Tenant ID
31    pub tenant_id: Option<String>,
32    /// User ID
33    pub user_id: Option<String>,
34}
35
36impl ExecutionSpanAttributes {
37    /// Create from an ExecutionId
38    pub fn new(execution_id: &ExecutionId) -> Self {
39        Self {
40            execution_id: execution_id.as_str().to_string(),
41            parent_id: None,
42            parent_type: None,
43            tenant_id: None,
44            user_id: None,
45        }
46    }
47
48    /// Add parent context
49    pub fn with_parent(
50        mut self,
51        parent_id: impl Into<String>,
52        parent_type: impl Into<String>,
53    ) -> Self {
54        self.parent_id = Some(parent_id.into());
55        self.parent_type = Some(parent_type.into());
56        self
57    }
58
59    /// Add tenant context
60    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
61        self.tenant_id = Some(tenant_id.into());
62        self
63    }
64
65    /// Add user context
66    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
67        self.user_id = Some(user_id.into());
68        self
69    }
70
71    /// Convert to a map of attributes for OpenTelemetry
72    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
73        let mut attrs = vec![("enact.execution_id", self.execution_id.clone())];
74
75        if let Some(ref parent_id) = self.parent_id {
76            attrs.push(("enact.parent_id", parent_id.clone()));
77        }
78        if let Some(ref parent_type) = self.parent_type {
79            attrs.push(("enact.parent_type", parent_type.clone()));
80        }
81        if let Some(ref tenant_id) = self.tenant_id {
82            attrs.push(("enact.tenant_id", tenant_id.clone()));
83        }
84        if let Some(ref user_id) = self.user_id {
85            attrs.push(("enact.user_id", user_id.clone()));
86        }
87
88        attrs
89    }
90}
91
92/// Span attributes for step-level spans
93pub struct StepSpanAttributes {
94    /// Execution ID
95    pub execution_id: String,
96    /// Step ID
97    pub step_id: String,
98    /// Step type
99    pub step_type: String,
100    /// Step name
101    pub name: String,
102}
103
104impl StepSpanAttributes {
105    /// Create from ExecutionId and StepId
106    pub fn new(
107        execution_id: &ExecutionId,
108        step_id: &StepId,
109        step_type: StepType,
110        name: impl Into<String>,
111    ) -> Self {
112        Self {
113            execution_id: execution_id.as_str().to_string(),
114            step_id: step_id.as_str().to_string(),
115            step_type: step_type.to_string(),
116            name: name.into(),
117        }
118    }
119
120    /// Convert to a map of attributes for OpenTelemetry
121    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
122        vec![
123            ("enact.execution_id", self.execution_id.clone()),
124            ("enact.step_id", self.step_id.clone()),
125            ("enact.step_type", self.step_type.clone()),
126            ("enact.step_name", self.name.clone()),
127        ]
128    }
129}
130
131/// Span attributes for tool calls
132pub struct ToolSpanAttributes {
133    /// Execution ID
134    pub execution_id: String,
135    /// Step ID
136    pub step_id: String,
137    /// Tool name
138    pub tool_name: String,
139}
140
141impl ToolSpanAttributes {
142    /// Create from ExecutionId, StepId, and tool name
143    pub fn new(execution_id: &ExecutionId, step_id: &StepId, tool_name: impl Into<String>) -> Self {
144        Self {
145            execution_id: execution_id.as_str().to_string(),
146            step_id: step_id.as_str().to_string(),
147            tool_name: tool_name.into(),
148        }
149    }
150
151    /// Convert to a map of attributes for OpenTelemetry
152    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
153        vec![
154            ("enact.execution_id", self.execution_id.clone()),
155            ("enact.step_id", self.step_id.clone()),
156            ("enact.tool_name", self.tool_name.clone()),
157        ]
158    }
159}
160
161/// Span attributes for LLM calls
162pub struct LlmSpanAttributes {
163    /// Execution ID
164    pub execution_id: String,
165    /// Step ID
166    pub step_id: String,
167    /// Model provider
168    pub provider: String,
169    /// Model name
170    pub model: String,
171    /// Input tokens (optional)
172    pub input_tokens: Option<u32>,
173    /// Output tokens (optional)
174    pub output_tokens: Option<u32>,
175}
176
177impl LlmSpanAttributes {
178    /// Create from ExecutionId, StepId, provider, and model
179    pub fn new(
180        execution_id: &ExecutionId,
181        step_id: &StepId,
182        provider: impl Into<String>,
183        model: impl Into<String>,
184    ) -> Self {
185        Self {
186            execution_id: execution_id.as_str().to_string(),
187            step_id: step_id.as_str().to_string(),
188            provider: provider.into(),
189            model: model.into(),
190            input_tokens: None,
191            output_tokens: None,
192        }
193    }
194
195    /// Add token counts
196    pub fn with_tokens(mut self, input: u32, output: u32) -> Self {
197        self.input_tokens = Some(input);
198        self.output_tokens = Some(output);
199        self
200    }
201
202    /// Convert to a map of attributes for OpenTelemetry
203    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
204        let mut attrs = vec![
205            ("enact.execution_id", self.execution_id.clone()),
206            ("enact.step_id", self.step_id.clone()),
207            ("gen_ai.system", self.provider.clone()),
208            ("gen_ai.request.model", self.model.clone()),
209        ];
210
211        if let Some(tokens) = self.input_tokens {
212            attrs.push(("gen_ai.usage.input_tokens", tokens.to_string()));
213        }
214        if let Some(tokens) = self.output_tokens {
215            attrs.push(("gen_ai.usage.output_tokens", tokens.to_string()));
216        }
217
218        attrs
219    }
220}
221
222/// Create span name for an execution
223pub fn execution_span_name(execution_id: &ExecutionId) -> String {
224    format!("execution:{}", execution_id.as_str())
225}
226
227/// Create span name for a step
228pub fn step_span_name(step_type: &StepType, name: &str) -> String {
229    format!("step:{}:{}", step_type, name)
230}
231
232/// Create span name for a tool call
233pub fn tool_span_name(tool_name: &str) -> String {
234    format!("tool:{}", tool_name)
235}
236
237/// Create span name for an LLM call
238pub fn llm_span_name(provider: &str, model: &str) -> String {
239    format!("llm:{}:{}", provider, model)
240}
241
242/// Extract trace context from a RuntimeContext trace
243pub fn extract_trace_context(trace: &TraceContext) -> (String, String) {
244    (trace.trace_id.clone(), trace.span_id.clone())
245}