Skip to main content

rig_core/telemetry/
mod.rs

1//! This module primarily concerns being able to orchestrate telemetry across a given pipeline or workflow.
2//! This includes tracing, being able to send traces to an OpenTelemetry collector, setting up your
3//! agents with the correct tracing style so you can emit the right traces for platforms like Langfuse,
4//! and more.
5
6use crate::completion::GetTokenUsage;
7use serde::Serialize;
8
9/// Provider request metadata used to populate GenAI telemetry spans.
10pub trait ProviderRequestExt {
11    /// Provider-native message type used for serialized input messages.
12    type InputMessage: Serialize;
13
14    /// Returns serialized input messages sent to the provider.
15    fn get_input_messages(&self) -> Vec<Self::InputMessage>;
16    /// Returns the system prompt, if represented separately by the provider.
17    fn get_system_prompt(&self) -> Option<String>;
18    /// Returns the model name requested from the provider.
19    fn get_model_name(&self) -> String;
20    /// Returns the primary prompt text, when available.
21    fn get_prompt(&self) -> Option<String>;
22}
23
24/// Provider response metadata used to populate GenAI telemetry spans.
25pub trait ProviderResponseExt {
26    /// Provider-native output message type.
27    type OutputMessage: Serialize;
28    /// Provider-native usage type.
29    type Usage: Serialize;
30
31    /// Returns the provider response ID, if supplied.
32    fn get_response_id(&self) -> Option<String>;
33
34    /// Returns the provider response model name, if supplied.
35    fn get_response_model_name(&self) -> Option<String>;
36
37    /// Returns serialized output messages produced by the provider.
38    fn get_output_messages(&self) -> Vec<Self::OutputMessage>;
39
40    /// Returns the primary text response, when available.
41    fn get_text_response(&self) -> Option<String>;
42
43    /// Returns provider-native usage metrics, if supplied.
44    fn get_usage(&self) -> Option<Self::Usage>;
45}
46
47/// A trait designed specifically to be used with Spans for the purpose of recording telemetry.
48/// Implemented for [`tracing::Span`] to record GenAI semantic convention fields.
49pub trait SpanCombinator {
50    /// Record Rig-normalized token usage fields on the span.
51    fn record_token_usage<U>(&self, usage: &U)
52    where
53        U: GetTokenUsage;
54
55    /// Record provider response metadata such as response ID and model name.
56    fn record_response_metadata<R>(&self, response: &R)
57    where
58        R: ProviderResponseExt;
59
60    /// Record serialized model input messages.
61    fn record_model_input<T>(&self, messages: &T)
62    where
63        T: Serialize;
64
65    /// Record serialized model output messages.
66    fn record_model_output<T>(&self, messages: &T)
67    where
68        T: Serialize;
69}
70
71impl SpanCombinator for tracing::Span {
72    fn record_token_usage<U>(&self, usage: &U)
73    where
74        U: GetTokenUsage,
75    {
76        if self.is_disabled() {
77            return;
78        }
79
80        let usage = usage.token_usage();
81        // Zero-valued usage is the documented sentinel for missing provider
82        // usage metrics; leave the span fields unset.
83        if usage.has_values() {
84            self.record("gen_ai.usage.input_tokens", usage.input_tokens);
85            self.record("gen_ai.usage.output_tokens", usage.output_tokens);
86            self.record(
87                "gen_ai.usage.cache_read.input_tokens",
88                usage.cached_input_tokens,
89            );
90            self.record(
91                "gen_ai.usage.cache_creation.input_tokens",
92                usage.cache_creation_input_tokens,
93            );
94            self.record(
95                "gen_ai.usage.tool_use_prompt_tokens",
96                usage.tool_use_prompt_tokens,
97            );
98            self.record("gen_ai.usage.reasoning_tokens", usage.reasoning_tokens);
99        }
100    }
101
102    fn record_response_metadata<R>(&self, response: &R)
103    where
104        R: ProviderResponseExt,
105    {
106        if self.is_disabled() {
107            return;
108        }
109
110        if let Some(id) = response.get_response_id() {
111            self.record("gen_ai.response.id", id);
112        }
113
114        if let Some(model_name) = response.get_response_model_name() {
115            self.record("gen_ai.response.model", model_name);
116        }
117    }
118
119    fn record_model_input<T>(&self, input: &T)
120    where
121        T: Serialize,
122    {
123        if self.is_disabled() {
124            return;
125        }
126
127        if let Ok(input_as_json_string) = serde_json::to_string(input) {
128            self.record("gen_ai.input.messages", input_as_json_string);
129        }
130    }
131
132    fn record_model_output<T>(&self, output: &T)
133    where
134        T: Serialize,
135    {
136        if self.is_disabled() {
137            return;
138        }
139
140        if let Ok(output_as_json_string) = serde_json::to_string(output) {
141            self.record("gen_ai.output.messages", output_as_json_string);
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::completion::{GetTokenUsage, Usage};
150    use std::sync::{Arc, Mutex};
151    use tracing::field::{Field, Visit};
152    use tracing::{Id, Subscriber};
153    use tracing_subscriber::layer::{Context, SubscriberExt};
154    use tracing_subscriber::{Layer, Registry, registry::LookupSpan};
155
156    #[derive(Clone)]
157    struct TestUsage(Usage);
158
159    impl GetTokenUsage for TestUsage {
160        fn token_usage(&self) -> Usage {
161            self.0
162        }
163    }
164
165    #[derive(Clone, Default)]
166    struct CapturedFields(Arc<Mutex<Vec<(String, u64)>>>);
167
168    impl CapturedFields {
169        fn push(&self, name: &str, value: u64) {
170            if let Ok(mut fields) = self.0.lock() {
171                fields.push((name.to_string(), value));
172            }
173        }
174
175        fn contains(&self, name: &str, value: u64) -> bool {
176            self.0.lock().is_ok_and(|fields| {
177                fields
178                    .iter()
179                    .any(|field| field == &(name.to_string(), value))
180            })
181        }
182    }
183
184    struct FieldCaptureLayer {
185        fields: CapturedFields,
186    }
187
188    impl<S> Layer<S> for FieldCaptureLayer
189    where
190        S: Subscriber,
191        S: for<'lookup> LookupSpan<'lookup>,
192    {
193        fn on_record(&self, _span: &Id, values: &tracing::span::Record<'_>, _ctx: Context<'_, S>) {
194            values.record(&mut FieldCaptureVisitor {
195                fields: self.fields.clone(),
196            });
197        }
198    }
199
200    struct FieldCaptureVisitor {
201        fields: CapturedFields,
202    }
203
204    impl Visit for FieldCaptureVisitor {
205        fn record_u64(&mut self, field: &Field, value: u64) {
206            self.fields.push(field.name(), value);
207        }
208
209        fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {}
210    }
211
212    #[test]
213    fn record_token_usage_records_tool_use_prompt_tokens() {
214        let fields = CapturedFields::default();
215        let subscriber = Registry::default().with(FieldCaptureLayer {
216            fields: fields.clone(),
217        });
218        let usage = TestUsage(Usage {
219            input_tokens: 1,
220            output_tokens: 2,
221            total_tokens: 15,
222            cached_input_tokens: 3,
223            cache_creation_input_tokens: 4,
224            tool_use_prompt_tokens: 12,
225            reasoning_tokens: 5,
226        });
227
228        // Scoped-subscriber tests must not run concurrently; see
229        // `test_utils::scoped_tracing_subscriber_guard`.
230        let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
231        tracing::subscriber::with_default(subscriber, || {
232            let span = tracing::info_span!(
233                "usage_recording",
234                gen_ai.usage.input_tokens = tracing::field::Empty,
235                gen_ai.usage.output_tokens = tracing::field::Empty,
236                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
237                gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
238                gen_ai.usage.tool_use_prompt_tokens = tracing::field::Empty,
239                gen_ai.usage.reasoning_tokens = tracing::field::Empty,
240            );
241
242            span.record_token_usage(&usage);
243        });
244
245        assert!(fields.contains("gen_ai.usage.tool_use_prompt_tokens", 12));
246    }
247}