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