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 response metadata used to populate GenAI telemetry spans.
10pub trait ProviderResponseExt {
11    /// Provider-native output message type.
12    type OutputMessage: Serialize;
13    /// Provider-native usage type.
14    type Usage: Serialize;
15
16    /// Returns the provider response ID, if supplied.
17    fn get_response_id(&self) -> Option<String>;
18
19    /// Returns the provider response model name, if supplied.
20    fn get_response_model_name(&self) -> Option<String>;
21
22    /// Returns serialized output messages produced by the provider.
23    fn get_output_messages(&self) -> Vec<Self::OutputMessage>;
24
25    /// Returns the primary text response, when available.
26    fn get_text_response(&self) -> Option<String>;
27
28    /// Returns provider-native usage metrics, if supplied.
29    fn get_usage(&self) -> Option<Self::Usage>;
30}
31
32/// A trait designed specifically to be used with Spans for the purpose of recording telemetry.
33/// Implemented for [`tracing::Span`] to record GenAI semantic convention fields.
34pub trait SpanCombinator {
35    /// Record Rig-normalized token usage fields on the span.
36    fn record_token_usage<U>(&self, usage: &U)
37    where
38        U: GetTokenUsage;
39
40    /// Record provider response metadata such as response ID and model name.
41    fn record_response_metadata<R>(&self, response: &R)
42    where
43        R: ProviderResponseExt;
44}
45
46impl SpanCombinator for tracing::Span {
47    fn record_token_usage<U>(&self, usage: &U)
48    where
49        U: GetTokenUsage,
50    {
51        if self.is_disabled() {
52            return;
53        }
54
55        let usage = usage.token_usage();
56        // Zero-valued usage is the documented sentinel for missing provider
57        // usage metrics; leave the span fields unset.
58        if usage.has_values() {
59            self.record("gen_ai.usage.input_tokens", usage.input_tokens);
60            self.record("gen_ai.usage.output_tokens", usage.output_tokens);
61            self.record(
62                "gen_ai.usage.cache_read.input_tokens",
63                usage.cached_input_tokens,
64            );
65            self.record(
66                "gen_ai.usage.cache_creation.input_tokens",
67                usage.cache_creation_input_tokens,
68            );
69            self.record(
70                "gen_ai.usage.tool_use_prompt_tokens",
71                usage.tool_use_prompt_tokens,
72            );
73            self.record("gen_ai.usage.reasoning_tokens", usage.reasoning_tokens);
74        }
75    }
76
77    fn record_response_metadata<R>(&self, response: &R)
78    where
79        R: ProviderResponseExt,
80    {
81        if self.is_disabled() {
82            return;
83        }
84
85        if let Some(id) = response.get_response_id() {
86            self.record("gen_ai.response.id", id);
87        }
88
89        if let Some(model_name) = response.get_response_model_name() {
90            self.record("gen_ai.response.model", model_name);
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::completion::{GetTokenUsage, Usage};
99    use std::sync::{Arc, Mutex};
100    use tracing::field::{Field, Visit};
101    use tracing::{Id, Subscriber};
102    use tracing_subscriber::layer::{Context, SubscriberExt};
103    use tracing_subscriber::{Layer, Registry, registry::LookupSpan};
104
105    #[derive(Clone)]
106    struct TestUsage(Usage);
107
108    impl GetTokenUsage for TestUsage {
109        fn token_usage(&self) -> Usage {
110            self.0
111        }
112    }
113
114    #[derive(Clone, Default)]
115    struct CapturedFields(Arc<Mutex<Vec<(String, u64)>>>);
116
117    impl CapturedFields {
118        fn push(&self, name: &str, value: u64) {
119            if let Ok(mut fields) = self.0.lock() {
120                fields.push((name.to_string(), value));
121            }
122        }
123
124        fn contains(&self, name: &str, value: u64) -> bool {
125            self.0.lock().is_ok_and(|fields| {
126                fields
127                    .iter()
128                    .any(|field| field == &(name.to_string(), value))
129            })
130        }
131    }
132
133    struct FieldCaptureLayer {
134        fields: CapturedFields,
135    }
136
137    impl<S> Layer<S> for FieldCaptureLayer
138    where
139        S: Subscriber,
140        S: for<'lookup> LookupSpan<'lookup>,
141    {
142        fn on_record(&self, _span: &Id, values: &tracing::span::Record<'_>, _ctx: Context<'_, S>) {
143            values.record(&mut FieldCaptureVisitor {
144                fields: self.fields.clone(),
145            });
146        }
147    }
148
149    struct FieldCaptureVisitor {
150        fields: CapturedFields,
151    }
152
153    impl Visit for FieldCaptureVisitor {
154        fn record_u64(&mut self, field: &Field, value: u64) {
155            self.fields.push(field.name(), value);
156        }
157
158        fn record_debug(&mut self, _field: &Field, _value: &dyn std::fmt::Debug) {}
159    }
160
161    #[test]
162    fn record_token_usage_records_tool_use_prompt_tokens() {
163        let fields = CapturedFields::default();
164        let subscriber = Registry::default().with(FieldCaptureLayer {
165            fields: fields.clone(),
166        });
167        let usage = TestUsage(Usage {
168            input_tokens: 1,
169            output_tokens: 2,
170            total_tokens: 15,
171            cached_input_tokens: 3,
172            cache_creation_input_tokens: 4,
173            tool_use_prompt_tokens: 12,
174            reasoning_tokens: 5,
175        });
176
177        // Scoped-subscriber tests must not run concurrently; see
178        // `test_utils::scoped_tracing_subscriber_guard`.
179        let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
180        tracing::subscriber::with_default(subscriber, || {
181            let span = tracing::info_span!(
182                "usage_recording",
183                gen_ai.usage.input_tokens = tracing::field::Empty,
184                gen_ai.usage.output_tokens = tracing::field::Empty,
185                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
186                gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
187                gen_ai.usage.tool_use_prompt_tokens = tracing::field::Empty,
188                gen_ai.usage.reasoning_tokens = tracing::field::Empty,
189            );
190
191            span.record_token_usage(&usage);
192        });
193
194        assert!(fields.contains("gen_ai.usage.tool_use_prompt_tokens", 12));
195    }
196}