rig_core/telemetry/
mod.rs1use crate::completion::GetTokenUsage;
7use serde::Serialize;
8
9pub trait ProviderResponseExt {
11 type OutputMessage: Serialize;
13 type Usage: Serialize;
15
16 fn get_response_id(&self) -> Option<String>;
18
19 fn get_response_model_name(&self) -> Option<String>;
21
22 fn get_output_messages(&self) -> Vec<Self::OutputMessage>;
24
25 fn get_text_response(&self) -> Option<String>;
27
28 fn get_usage(&self) -> Option<Self::Usage>;
30}
31
32pub trait SpanCombinator {
35 fn record_token_usage<U>(&self, usage: &U)
37 where
38 U: GetTokenUsage;
39
40 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 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 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}