Skip to main content

ferrin_core/telemetry/
mod.rs

1//! Telemetry: lifecycle callbacks, options and the built-in `tracing` spans.
2//!
3//! Callbacks are synchronous (they run on the pipeline hot path); the two
4//! `execute_*` wrappers are asynchronous because they must wrap the actual
5//! call. Integrations are injected per call through
6//! [`TelemetryOptions::integrations`]; there is no global registry.
7
8use std::collections::BTreeMap;
9use std::fmt;
10use std::sync::Arc;
11
12use ferrin_spec::BoxFuture;
13use ferrin_spec::GenerateResult;
14use ferrin_spec::JsonValue;
15use ferrin_spec::StreamResult;
16use ferrin_spec::ToolCallId;
17use ferrin_spec::ToolName;
18use ferrin_tool::ToolError;
19
20use crate::error::Error;
21
22pub(crate) mod dispatcher;
23mod events;
24pub(crate) mod spans;
25
26pub(crate) use dispatcher::TelemetryDispatcher;
27pub use events::AbortEvent;
28pub use events::EmbedEndEvent;
29pub use events::EmbedStartEvent;
30pub use events::EndEvent;
31pub use events::ErrorEvent;
32pub use events::ErrorPhase;
33pub use events::ModelCallEndEvent;
34pub use events::ModelCallStartEvent;
35pub use events::ModelIdentity;
36pub use events::RecordedInputs;
37pub use events::RerankEndEvent;
38pub use events::RerankStartEvent;
39pub use events::StartEvent;
40pub use events::StepEndEvent;
41pub use events::StepStartEvent;
42pub use events::ToolExecutionEndEvent;
43pub use events::ToolExecutionStartEvent;
44pub use events::ToolOutcome;
45pub use spans::WARNINGS_TARGET;
46
47/// Context of a model call handed to [`Telemetry::execute_language_model_call`].
48#[derive(Debug, Clone)]
49pub struct ModelCallContext {
50    /// Call id.
51    pub call_id: String,
52    /// Zero-based step index.
53    pub step_number: u32,
54    /// The model.
55    pub model: ModelIdentity,
56    /// Function id from the telemetry options.
57    pub function_id: Option<String>,
58}
59
60/// Context of a tool execution handed to [`Telemetry::execute_tool`].
61#[derive(Debug, Clone)]
62pub struct ToolExecutionContext {
63    /// Call id.
64    pub call_id: String,
65    /// Tool call id.
66    pub tool_call_id: ToolCallId,
67    /// Tool name.
68    pub tool_name: ToolName,
69    /// Input (only when `record_inputs`).
70    pub input: Option<JsonValue>,
71}
72
73/// Result of a model call as seen by [`Telemetry::execute_language_model_call`].
74#[derive(Debug)]
75#[non_exhaustive]
76pub enum ModelCallOutcome {
77    /// A non-streaming result.
78    Generate(Box<GenerateResult>),
79    /// A streaming result (the stream has not been consumed yet).
80    Stream(Box<StreamResult>),
81}
82
83/// A telemetry integration.
84///
85/// Every method has a no-op default; implement the ones you need. Callbacks
86/// must not block: hand events to your own channel when processing is slow.
87pub trait Telemetry: Send + Sync + 'static {
88    /// A call started.
89    fn on_start(&self, _event: &StartEvent) {}
90    /// A step started.
91    fn on_step_start(&self, _event: &StepStartEvent) {}
92    /// A model call is about to be made.
93    fn on_language_model_call_start(&self, _event: &ModelCallStartEvent) {}
94    /// A model call finished.
95    fn on_language_model_call_end(&self, _event: &ModelCallEndEvent) {}
96    /// A tool execution started.
97    fn on_tool_execution_start(&self, _event: &ToolExecutionStartEvent) {}
98    /// A tool execution finished.
99    fn on_tool_execution_end(&self, _event: &ToolExecutionEndEvent) {}
100    /// A step finished.
101    fn on_step_end(&self, _event: &StepEndEvent) {}
102    /// An embedding call started.
103    fn on_embed_start(&self, _event: &EmbedStartEvent) {}
104    /// An embedding call finished.
105    fn on_embed_end(&self, _event: &EmbedEndEvent) {}
106    /// A rerank call started.
107    fn on_rerank_start(&self, _event: &RerankStartEvent) {}
108    /// A rerank call finished.
109    fn on_rerank_end(&self, _event: &RerankEndEvent) {}
110    /// A call finished.
111    fn on_end(&self, _event: &EndEvent) {}
112    /// A streaming call was aborted.
113    fn on_abort(&self, _event: &AbortEvent) {}
114    /// An error occurred.
115    fn on_error(&self, _event: &ErrorEvent<'_>) {}
116
117    /// Runs a model call inside integration-specific context (for example an
118    /// OpenTelemetry span). The default runs `call` unchanged.
119    fn execute_language_model_call<'a>(
120        &'a self,
121        _ctx: &'a ModelCallContext,
122        call: BoxFuture<'a, Result<ModelCallOutcome, Error>>,
123    ) -> BoxFuture<'a, Result<ModelCallOutcome, Error>> {
124        call
125    }
126
127    /// Runs a tool execution inside integration-specific context.
128    fn execute_tool<'a>(
129        &'a self,
130        _ctx: &'a ToolExecutionContext,
131        call: BoxFuture<'a, Result<ToolOutcome, ToolError>>,
132    ) -> BoxFuture<'a, Result<ToolOutcome, ToolError>> {
133        call
134    }
135}
136
137/// Telemetry configuration of a call.
138#[derive(Clone, Default)]
139pub struct TelemetryOptions {
140    /// Whether telemetry callbacks fire at all.
141    pub enabled: bool,
142    /// Whether prompts, call options and tool inputs are included in events.
143    pub record_inputs: bool,
144    /// Whether generated content and tool outputs are included in events.
145    pub record_outputs: bool,
146    /// Identifier of the calling function for grouping.
147    pub function_id: Option<String>,
148    /// Free-form metadata attached to the start event.
149    pub metadata: BTreeMap<String, JsonValue>,
150    /// Whether the tools context is attached to tool events.
151    pub include_tools_context: bool,
152    /// Integrations that receive the events.
153    pub integrations: Vec<Arc<dyn Telemetry>>,
154}
155
156impl TelemetryOptions {
157    /// Enabled options recording neither inputs nor outputs.
158    #[must_use]
159    pub fn enabled() -> Self {
160        Self {
161            enabled: true,
162            ..Self::default()
163        }
164    }
165
166    /// Adds an integration.
167    #[must_use]
168    pub fn with_integration(mut self, integration: Arc<dyn Telemetry>) -> Self {
169        self.integrations.push(integration);
170        self
171    }
172}
173
174impl fmt::Debug for TelemetryOptions {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.debug_struct("TelemetryOptions")
177            .field("enabled", &self.enabled)
178            .field("record_inputs", &self.record_inputs)
179            .field("record_outputs", &self.record_outputs)
180            .field("function_id", &self.function_id)
181            .field("metadata", &self.metadata)
182            .field("include_tools_context", &self.include_tools_context)
183            .field("integrations", &self.integrations.len())
184            .finish()
185    }
186}