rig_core/telemetry/
mod.rs1use crate::completion::GetTokenUsage;
7use serde::Serialize;
8
9pub trait ProviderRequestExt {
11 type InputMessage: Serialize;
13
14 fn get_input_messages(&self) -> Vec<Self::InputMessage>;
16 fn get_system_prompt(&self) -> Option<String>;
18 fn get_model_name(&self) -> String;
20 fn get_prompt(&self) -> Option<String>;
22}
23
24pub trait ProviderResponseExt {
26 type OutputMessage: Serialize;
28 type Usage: Serialize;
30
31 fn get_response_id(&self) -> Option<String>;
33
34 fn get_response_model_name(&self) -> Option<String>;
36
37 fn get_output_messages(&self) -> Vec<Self::OutputMessage>;
39
40 fn get_text_response(&self) -> Option<String>;
42
43 fn get_usage(&self) -> Option<Self::Usage>;
45}
46
47pub trait SpanCombinator {
50 fn record_token_usage<U>(&self, usage: &U)
52 where
53 U: GetTokenUsage;
54
55 fn record_response_metadata<R>(&self, response: &R)
57 where
58 R: ProviderResponseExt;
59
60 fn record_model_input<T>(&self, messages: &T)
62 where
63 T: Serialize;
64
65 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 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 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}