Skip to main content

ferrin_core/telemetry/
mod.rs

1//! Telemetry: lifecycle callbacks, options and the built-in `tracing` spans.
2//!
3//! Lifecycle callbacks are awaited concurrently and isolate unwinding panics.
4//! The two `execute_*` wrappers preserve the actual call result.
5//! Integrations are injected per call through
6//! [`TelemetryOptions::integrations`]; there is no global registry.
7//! Callback settlement and wrapper ordering follow the Vercel AI SDK
8//! (`packages/ai/src/telemetry/create-telemetry-dispatcher.ts`); see `NOTICE`.
9
10use std::collections::BTreeMap;
11use std::fmt;
12use std::sync::Arc;
13
14use ferrin_spec::BoxFuture;
15use ferrin_spec::GenerateResult;
16use ferrin_spec::JsonValue;
17use ferrin_spec::StreamResult;
18use ferrin_spec::ToolCallId;
19use ferrin_spec::ToolName;
20use ferrin_tool::ToolError;
21
22use crate::error::Error;
23
24pub(crate) mod dispatcher;
25mod events;
26mod redact;
27mod redact_provider;
28pub(crate) mod spans;
29
30pub(crate) use dispatcher::TelemetryDispatcher;
31pub use events::AbortEvent;
32pub use events::EmbedEndEvent;
33pub use events::EmbedStartEvent;
34pub use events::EndEvent;
35pub use events::ErrorEvent;
36pub use events::ErrorPhase;
37pub use events::ModelCallEndEvent;
38pub use events::ModelCallStartEvent;
39pub use events::ModelIdentity;
40pub use events::RecordedInputs;
41pub use events::RerankEndEvent;
42pub use events::RerankStartEvent;
43pub use events::StartEvent;
44pub use events::StepEndEvent;
45pub use events::StepStartEvent;
46pub use events::ToolExecutionEndEvent;
47pub use events::ToolExecutionStartEvent;
48pub use events::ToolOutcome;
49pub use spans::WARNINGS_TARGET;
50
51/// Context of a model call handed to [`Telemetry::execute_language_model_call`].
52#[derive(Debug, Clone)]
53pub struct ModelCallContext {
54    /// Call id.
55    pub call_id: String,
56    /// Zero-based step index.
57    pub step_number: u32,
58    /// The model.
59    pub model: ModelIdentity,
60    /// Function id from the telemetry options.
61    pub function_id: Option<String>,
62}
63
64/// Context of a tool execution handed to [`Telemetry::execute_tool`].
65#[derive(Debug, Clone)]
66pub struct ToolExecutionContext {
67    /// Call id.
68    pub call_id: String,
69    /// Tool call id.
70    pub tool_call_id: ToolCallId,
71    /// Tool name.
72    pub tool_name: ToolName,
73    /// Input (only when `record_inputs`).
74    pub input: Option<JsonValue>,
75    /// Whether an integration may record the tool's output.
76    pub record_outputs: bool,
77}
78
79/// Result of a model call as seen by [`Telemetry::execute_language_model_call`].
80#[derive(Debug)]
81#[non_exhaustive]
82pub enum ModelCallOutcome {
83    /// A non-streaming result.
84    Generate(Box<GenerateResult>),
85    /// A streaming result (the stream has not been consumed yet).
86    Stream(Box<StreamResult>),
87}
88
89macro_rules! callback {
90    ($name:ident, $event:ty, $doc:literal) => {
91        #[doc = $doc]
92        fn $name<'a>(&'a self, _event: &'a $event) -> BoxFuture<'a, ()> {
93            Box::pin(async {})
94        }
95    };
96}
97
98/// A telemetry integration with awaited lifecycle callbacks.
99///
100/// Every method has a no-op default. Integrations for the same event run
101/// concurrently; unwinding callback panics cannot interrupt the operation.
102pub trait Telemetry: Send + Sync + 'static {
103    callback!(on_start, StartEvent, "A call started.");
104    callback!(on_step_start, StepStartEvent, "A step started.");
105    callback!(
106        on_language_model_call_start,
107        ModelCallStartEvent,
108        "A model call is about to be made."
109    );
110    callback!(
111        on_language_model_call_end,
112        ModelCallEndEvent,
113        "A model call finished."
114    );
115    callback!(
116        on_tool_execution_start,
117        ToolExecutionStartEvent,
118        "A tool execution started."
119    );
120    callback!(
121        on_tool_execution_end,
122        ToolExecutionEndEvent,
123        "A tool execution finished."
124    );
125    callback!(on_step_end, StepEndEvent, "A step finished.");
126    callback!(
127        on_embed_start,
128        EmbedStartEvent,
129        "An embedding call started."
130    );
131    callback!(on_embed_end, EmbedEndEvent, "An embedding call finished.");
132    callback!(on_rerank_start, RerankStartEvent, "A rerank call started.");
133    callback!(on_rerank_end, RerankEndEvent, "A rerank call finished.");
134    callback!(
135        on_embed_operation_start,
136        crate::embed::EmbedCallStartEvent,
137        "An embedding operation started before its attempts."
138    );
139    callback!(
140        on_embed_operation_end,
141        crate::embed::EmbedCallEndEvent,
142        "An embedding operation completed all its attempts."
143    );
144    callback!(
145        on_rerank_operation_start,
146        crate::rerank::RerankCallStartEvent,
147        "A reranking operation started before its attempts."
148    );
149    callback!(
150        on_rerank_operation_end,
151        crate::rerank::RerankCallEndEvent,
152        "A reranking operation completed all its attempts."
153    );
154    callback!(on_end, EndEvent, "A call finished.");
155    callback!(on_abort, AbortEvent, "A streaming call was aborted.");
156    callback!(on_error, ErrorEvent<'_>, "An error occurred.");
157
158    /// Runs a model call inside integration-specific context (for example an
159    /// OpenTelemetry span). The default runs `call` unchanged.
160    fn execute_language_model_call<'a>(
161        &'a self,
162        _ctx: &'a ModelCallContext,
163        call: BoxFuture<'a, Result<ModelCallOutcome, Error>>,
164    ) -> BoxFuture<'a, Result<ModelCallOutcome, Error>> {
165        call
166    }
167
168    /// Runs a tool execution inside integration-specific context.
169    fn execute_tool<'a>(
170        &'a self,
171        _ctx: &'a ToolExecutionContext,
172        call: BoxFuture<'a, Result<ToolOutcome, ToolError>>,
173    ) -> BoxFuture<'a, Result<ToolOutcome, ToolError>> {
174        call
175    }
176}
177
178/// Telemetry configuration of a call.
179#[derive(Clone, Default)]
180pub struct TelemetryOptions {
181    /// Whether telemetry callbacks fire at all.
182    pub enabled: bool,
183    /// Whether prompts, call options and tool inputs are included in events.
184    pub record_inputs: bool,
185    /// Whether generated content and tool outputs are included in events.
186    pub record_outputs: bool,
187    /// Identifier of the calling function for grouping.
188    pub function_id: Option<String>,
189    /// Free-form metadata attached to the start event.
190    pub metadata: BTreeMap<String, JsonValue>,
191    /// Whether application runtime context is included in telemetry events and steps.
192    pub include_runtime_context: bool,
193    /// Whether the tools context is attached to tool events.
194    pub include_tools_context: bool,
195    /// Integrations that receive the events.
196    pub integrations: Vec<Arc<dyn Telemetry>>,
197}
198
199impl TelemetryOptions {
200    /// Enabled options recording neither inputs nor outputs.
201    #[must_use]
202    pub fn enabled() -> Self {
203        Self {
204            enabled: true,
205            ..Self::default()
206        }
207    }
208
209    /// Adds an integration.
210    #[must_use]
211    pub fn with_integration(mut self, integration: Arc<dyn Telemetry>) -> Self {
212        self.integrations.push(integration);
213        self
214    }
215}
216
217impl fmt::Debug for TelemetryOptions {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.debug_struct("TelemetryOptions")
220            .field("enabled", &self.enabled)
221            .field("record_inputs", &self.record_inputs)
222            .field("record_outputs", &self.record_outputs)
223            .field("function_id", &self.function_id)
224            .field("metadata", &self.metadata)
225            .field("include_runtime_context", &self.include_runtime_context)
226            .field("include_tools_context", &self.include_tools_context)
227            .field("integrations", &self.integrations.len())
228            .finish()
229    }
230}