1use async_trait::async_trait;
12#[async_trait]
13pub trait Metrics: Send + Sync + 'static {
14 async fn incr_counter(&self, name: &str, value: u64);
16
17 async fn record_gauge(&self, name: &str, value: f64);
19
20 async fn start_timer(&self, name: &str) -> Option<Box<dyn Timer + Send>>;
23
24 async fn record_histogram(&self, name: &str, value: f64);
26
27 async fn record_histogram_with_tags(&self, name: &str, value: f64, tags: &[(&str, &str)]);
29
30 async fn incr_counter_with_tags(&self, name: &str, value: u64, tags: &[(&str, &str)]);
32
33 async fn record_gauge_with_tags(&self, name: &str, value: f64, tags: &[(&str, &str)]);
35
36 async fn record_error(&self, name: &str, error_type: &str);
38
39 async fn record_success(&self, name: &str, success: bool);
41}
42
43pub trait Timer: Send {
45 fn stop(self: Box<Self>);
47}
48
49pub struct NoopMetrics;
51
52#[async_trait]
53impl Metrics for NoopMetrics {
54 async fn incr_counter(&self, _name: &str, _value: u64) {}
55 async fn record_gauge(&self, _name: &str, _value: f64) {}
56 async fn start_timer(&self, _name: &str) -> Option<Box<dyn Timer + Send>> {
57 None
58 }
59 async fn record_histogram(&self, _name: &str, _value: f64) {}
60 async fn record_histogram_with_tags(&self, _name: &str, _value: f64, _tags: &[(&str, &str)]) {}
61 async fn incr_counter_with_tags(&self, _name: &str, _value: u64, _tags: &[(&str, &str)]) {}
62 async fn record_gauge_with_tags(&self, _name: &str, _value: f64, _tags: &[(&str, &str)]) {}
63 async fn record_error(&self, _name: &str, _error_type: &str) {}
64 async fn record_success(&self, _name: &str, _success: bool) {}
65}
66
67pub struct NoopTimer;
69impl Timer for NoopTimer {
70 fn stop(self: Box<Self>) {}
71}
72
73impl NoopMetrics {
74 pub fn new() -> Self {
75 NoopMetrics
76 }
77}
78
79impl Default for NoopMetrics {
80 fn default() -> Self {
81 Self::new()
82 }
83}
84
85#[allow(async_fn_in_trait)]
87pub trait MetricsExt: Metrics {
88 async fn record_request(
90 &self,
91 name: &str,
92 timer: Option<Box<dyn Timer + Send>>,
93 success: bool,
94 ) {
95 if let Some(t) = timer {
96 t.stop();
97 }
98 self.record_success(name, success).await;
99 }
100
101 async fn record_request_with_tags(
103 &self,
104 name: &str,
105 timer: Option<Box<dyn Timer + Send>>,
106 success: bool,
107 tags: &[(&str, &str)],
108 ) {
109 if let Some(t) = timer {
110 t.stop();
111 }
112 self.record_success(name, success).await;
113 self.incr_counter_with_tags(&format!("{}.total", name), 1, tags)
115 .await;
116 if success {
117 self.incr_counter_with_tags(&format!("{}.success", name), 1, tags)
118 .await;
119 } else {
120 self.incr_counter_with_tags(&format!("{}.failure", name), 1, tags)
121 .await;
122 }
123 }
124
125 async fn record_error_with_context(&self, name: &str, error_type: &str, context: &str) {
127 self.record_error(name, error_type).await;
128 self.incr_counter_with_tags(name, 1, &[("error_type", error_type), ("context", context)])
129 .await;
130 }
131
132 async fn record_complete_request(
134 &self,
135 name: &str,
136 timer: Option<Box<dyn Timer + Send>>,
137 status_code: u16,
138 success: bool,
139 tags: &[(&str, &str)],
140 ) {
141 if let Some(t) = timer {
142 t.stop();
143 }
144
145 self.record_success(name, success).await;
147 self.record_gauge_with_tags(&format!("{}.status_code", name), status_code as f64, tags)
148 .await;
149
150 self.incr_counter_with_tags(&format!("{}.total", name), 1, tags)
152 .await;
153 if success {
154 self.incr_counter_with_tags(&format!("{}.success", name), 1, tags)
155 .await;
156 } else {
157 self.incr_counter_with_tags(&format!("{}.failure", name), 1, tags)
158 .await;
159 }
160 }
161
162 async fn record_batch_latency_percentiles(
164 &self,
165 name: &str,
166 measurements: &[f64],
167 tags: &[(&str, &str)],
168 ) {
169 if measurements.is_empty() {
170 return;
171 }
172
173 let mut sorted = measurements.to_vec();
174 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
175
176 let len = sorted.len();
177 let p50 = sorted[(len * 50 / 100).min(len - 1)];
178 let p95 = sorted[(len * 95 / 100).min(len - 1)];
179 let p99 = sorted[(len * 99 / 100).min(len - 1)];
180
181 self.record_gauge_with_tags(&format!("{}.latency.p50", name), p50, tags)
182 .await;
183 self.record_gauge_with_tags(&format!("{}.latency.p95", name), p95, tags)
184 .await;
185 self.record_gauge_with_tags(&format!("{}.latency.p99", name), p99, tags)
186 .await;
187 }
188}
189
190impl<T: Metrics> MetricsExt for T {}
191
192pub mod keys {
194 pub fn requests(provider: &str) -> String {
196 format!("{}.requests", provider)
197 }
198 pub fn request_duration_ms(provider: &str) -> String {
200 format!("{}.request_duration_ms", provider)
201 }
202 pub fn success(provider: &str) -> String {
204 format!("{}.success", provider)
205 }
206 pub fn failure(provider: &str) -> String {
207 format!("{}.failure", provider)
208 }
209
210 pub fn latency_p50(provider: &str) -> String {
212 format!("{}.latency_p50", provider)
213 }
214 pub fn latency_p95(provider: &str) -> String {
215 format!("{}.latency_p95", provider)
216 }
217 pub fn latency_p99(provider: &str) -> String {
218 format!("{}.latency_p99", provider)
219 }
220
221 pub fn status_codes(provider: &str) -> String {
223 format!("{}.status_codes", provider)
224 }
225
226 pub fn error_rate(provider: &str) -> String {
228 format!("{}.error_rate", provider)
229 }
230
231 pub fn throughput(provider: &str) -> String {
233 format!("{}.throughput", provider)
234 }
235
236 pub fn cost_usd(provider: &str) -> String {
238 format!("{}.cost_usd", provider)
239 }
240 pub fn cost_per_request(provider: &str) -> String {
241 format!("{}.cost_per_request", provider)
242 }
243 pub fn tokens_input(provider: &str) -> String {
244 format!("{}.tokens_input", provider)
245 }
246 pub fn tokens_output(provider: &str) -> String {
247 format!("{}.tokens_output", provider)
248 }
249
250 pub fn routing_requests(route: &str) -> String {
252 format!("routing.{}.requests", route)
253 }
254 pub fn routing_selected(route: &str) -> String {
255 format!("routing.{}.selected", route)
256 }
257 pub fn routing_health_fail(route: &str) -> String {
258 format!("routing.{}.health_fail", route)
259 }
260}
261
262#[cfg(feature = "cost_metrics")]
264pub mod cost {
265 use crate::metrics::Metrics;
266
267 pub fn estimate_usd(input_tokens: u32, output_tokens: u32) -> f64 {
269 let in_rate = std::env::var("COST_INPUT_PER_1K")
270 .ok()
271 .and_then(|s| s.parse::<f64>().ok())
272 .unwrap_or(0.0);
273 let out_rate = std::env::var("COST_OUTPUT_PER_1K")
274 .ok()
275 .and_then(|s| s.parse::<f64>().ok())
276 .unwrap_or(0.0);
277 (input_tokens as f64 / 1000.0) * in_rate + (output_tokens as f64 / 1000.0) * out_rate
278 }
279
280 pub async fn record_cost<M: Metrics + ?Sized>(m: &M, provider: &str, model: &str, usd: f64) {
282 m.record_histogram_with_tags("cost.usd", usd, &[("provider", provider), ("model", model)])
283 .await;
284 }
285}
286
287