1use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, RwLock};
8use std::time::{Duration, Instant};
9
10use crate::security::sanitize_text;
11
12fn sanitize_metric_label(value: &str) -> String {
13 sanitize_text(value)
14}
15
16fn prometheus_label_value(value: &str) -> String {
17 sanitize_metric_label(value)
18 .replace('\\', r"\\")
19 .replace('\n', r"\n")
20 .replace('"', r#"\""#)
21}
22
23#[derive(Debug, Clone)]
25pub struct MetricsConfig {
26 pub enabled: bool,
28 pub histogram_buckets: Vec<f64>,
30 pub endpoint_path: String,
32}
33
34impl Default for MetricsConfig {
35 fn default() -> Self {
36 Self {
37 enabled: true,
38 histogram_buckets: vec![
39 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
40 ],
41 endpoint_path: "/metrics".to_string(),
42 }
43 }
44}
45
46#[derive(Debug, Default)]
48pub struct Counter {
49 value: AtomicU64,
50}
51
52impl Counter {
53 pub fn new() -> Self {
55 Self::default()
56 }
57
58 pub fn inc(&self) {
60 self.value.fetch_add(1, Ordering::Relaxed);
61 }
62
63 pub fn add(&self, v: u64) {
65 self.value.fetch_add(v, Ordering::Relaxed);
66 }
67
68 pub fn get(&self) -> u64 {
70 self.value.load(Ordering::Relaxed)
71 }
72}
73
74#[derive(Debug)]
76pub struct Histogram {
77 buckets: Vec<f64>,
79 bucket_counts: Vec<AtomicU64>,
81 sum: AtomicU64,
83 count: AtomicU64,
85}
86
87impl Histogram {
88 pub fn new(buckets: Vec<f64>) -> Self {
90 let bucket_counts = buckets.iter().map(|_| AtomicU64::new(0)).collect();
91 Self {
92 buckets,
93 bucket_counts,
94 sum: AtomicU64::new(0),
95 count: AtomicU64::new(0),
96 }
97 }
98
99 pub fn with_default_buckets() -> Self {
101 Self::new(vec![
102 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
103 ])
104 }
105
106 pub fn observe(&self, value: f64) {
108 let micros = (value * 1_000_000.0) as u64;
110 self.sum.fetch_add(micros, Ordering::Relaxed);
111 self.count.fetch_add(1, Ordering::Relaxed);
112
113 for (i, &bound) in self.buckets.iter().enumerate() {
115 if value <= bound {
116 self.bucket_counts[i].fetch_add(1, Ordering::Relaxed);
117 }
118 }
119 }
120
121 pub fn observe_duration(&self, duration: Duration) {
123 self.observe(duration.as_secs_f64());
124 }
125
126 pub fn bucket_counts(&self) -> Vec<u64> {
128 self.bucket_counts
129 .iter()
130 .map(|c| c.load(Ordering::Relaxed))
131 .collect()
132 }
133
134 pub fn sum(&self) -> f64 {
136 self.sum.load(Ordering::Relaxed) as f64 / 1_000_000.0
137 }
138
139 pub fn count(&self) -> u64 {
141 self.count.load(Ordering::Relaxed)
142 }
143
144 pub fn buckets(&self) -> &[f64] {
146 &self.buckets
147 }
148}
149
150#[derive(Debug, Default)]
152pub struct LabeledCounter {
153 counters: RwLock<HashMap<String, Arc<Counter>>>,
154}
155
156impl LabeledCounter {
157 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn with_label(&self, label: &str) -> Arc<Counter> {
164 {
165 let counters = self.counters.read().unwrap();
166 if let Some(counter) = counters.get(label) {
167 return Arc::clone(counter);
168 }
169 }
170
171 let mut counters = self.counters.write().unwrap();
172 counters
173 .entry(label.to_string())
174 .or_insert_with(|| Arc::new(Counter::new()))
175 .clone()
176 }
177
178 pub fn inc(&self, label: &str) {
180 self.with_label(label).inc();
181 }
182
183 pub fn all(&self) -> HashMap<String, u64> {
185 let counters = self.counters.read().unwrap();
186 counters.iter().map(|(k, v)| (k.clone(), v.get())).collect()
187 }
188}
189
190#[derive(Debug)]
192pub struct LabeledHistogram {
193 histograms: RwLock<HashMap<String, Arc<Histogram>>>,
194 buckets: Vec<f64>,
195}
196
197impl LabeledHistogram {
198 pub fn new(buckets: Vec<f64>) -> Self {
200 Self {
201 histograms: RwLock::new(HashMap::new()),
202 buckets,
203 }
204 }
205
206 pub fn with_default_buckets() -> Self {
208 Self::new(vec![
209 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
210 ])
211 }
212
213 pub fn with_label(&self, label: &str) -> Arc<Histogram> {
215 {
216 let histograms = self.histograms.read().unwrap();
217 if let Some(histogram) = histograms.get(label) {
218 return Arc::clone(histogram);
219 }
220 }
221
222 let mut histograms = self.histograms.write().unwrap();
223 histograms
224 .entry(label.to_string())
225 .or_insert_with(|| Arc::new(Histogram::new(self.buckets.clone())))
226 .clone()
227 }
228
229 pub fn observe(&self, label: &str, value: f64) {
231 self.with_label(label).observe(value);
232 }
233
234 pub fn labels(&self) -> Vec<String> {
236 let histograms = self.histograms.read().unwrap();
237 histograms.keys().cloned().collect()
238 }
239}
240
241pub struct PrometheusMetrics {
243 pub requests_total: LabeledCounter,
245 pub request_duration_seconds: LabeledHistogram,
247 pub active_connections: Counter,
249 pub bytes_total: LabeledCounter,
251 pub errors_total: LabeledCounter,
253 config: MetricsConfig,
255}
256
257impl PrometheusMetrics {
258 pub fn new(config: MetricsConfig) -> Self {
260 Self {
261 requests_total: LabeledCounter::new(),
262 request_duration_seconds: LabeledHistogram::new(config.histogram_buckets.clone()),
263 active_connections: Counter::new(),
264 bytes_total: LabeledCounter::new(),
265 errors_total: LabeledCounter::new(),
266 config,
267 }
268 }
269
270 pub fn with_defaults() -> Self {
272 Self::new(MetricsConfig::default())
273 }
274
275 pub fn record_request(&self, method: &str, duration: Duration) {
277 let method = sanitize_metric_label(method);
278 self.requests_total.inc(&method);
279 self.request_duration_seconds
280 .observe(&method, duration.as_secs_f64());
281 }
282
283 pub fn record_error(&self, error_type: &str) {
285 self.errors_total.inc(&sanitize_metric_label(error_type));
286 }
287
288 pub fn record_bytes(&self, direction: &str, bytes: u64) {
290 self.bytes_total
291 .with_label(&sanitize_metric_label(direction))
292 .add(bytes);
293 }
294
295 pub fn connection_opened(&self) {
297 self.active_connections.inc();
298 }
299
300 pub fn connection_closed(&self) {
302 }
305
306 pub fn config(&self) -> &MetricsConfig {
308 &self.config
309 }
310
311 pub fn format_prometheus(&self) -> String {
313 let mut output = String::new();
314
315 output.push_str("# HELP dcp_requests_total Total number of requests by method\n");
317 output.push_str("# TYPE dcp_requests_total counter\n");
318 for (method, count) in self.requests_total.all() {
319 let method = prometheus_label_value(&method);
320 output.push_str(&format!(
321 "dcp_requests_total{{method=\"{}\"}} {}\n",
322 method, count
323 ));
324 }
325
326 output.push_str("\n# HELP dcp_request_duration_seconds Request latency in seconds\n");
328 output.push_str("# TYPE dcp_request_duration_seconds histogram\n");
329 for label in self.request_duration_seconds.labels() {
330 let histogram = self.request_duration_seconds.with_label(&label);
331 let buckets = histogram.buckets();
332 let counts = histogram.bucket_counts();
333 let method = prometheus_label_value(&label);
334
335 for (i, &bound) in buckets.iter().enumerate() {
336 output.push_str(&format!(
337 "dcp_request_duration_seconds_bucket{{method=\"{}\",le=\"{}\"}} {}\n",
338 method, bound, counts[i]
339 ));
340 }
341 output.push_str(&format!(
342 "dcp_request_duration_seconds_bucket{{method=\"{}\",le=\"+Inf\"}} {}\n",
343 method,
344 histogram.count()
345 ));
346 output.push_str(&format!(
347 "dcp_request_duration_seconds_sum{{method=\"{}\"}} {}\n",
348 method,
349 histogram.sum()
350 ));
351 output.push_str(&format!(
352 "dcp_request_duration_seconds_count{{method=\"{}\"}} {}\n",
353 method,
354 histogram.count()
355 ));
356 }
357
358 output.push_str("\n# HELP dcp_active_connections Number of active connections\n");
360 output.push_str("# TYPE dcp_active_connections gauge\n");
361 output.push_str(&format!(
362 "dcp_active_connections {}\n",
363 self.active_connections.get()
364 ));
365
366 output.push_str("\n# HELP dcp_bytes_total Total bytes transferred\n");
368 output.push_str("# TYPE dcp_bytes_total counter\n");
369 for (direction, count) in self.bytes_total.all() {
370 let direction = prometheus_label_value(&direction);
371 output.push_str(&format!(
372 "dcp_bytes_total{{direction=\"{}\"}} {}\n",
373 direction, count
374 ));
375 }
376
377 output.push_str("\n# HELP dcp_errors_total Total number of errors by type\n");
379 output.push_str("# TYPE dcp_errors_total counter\n");
380 for (error_type, count) in self.errors_total.all() {
381 let error_type = prometheus_label_value(&error_type);
382 output.push_str(&format!(
383 "dcp_errors_total{{type=\"{}\"}} {}\n",
384 error_type, count
385 ));
386 }
387
388 output
389 }
390}
391
392impl Default for PrometheusMetrics {
393 fn default() -> Self {
394 Self::with_defaults()
395 }
396}
397
398pub struct RequestMetrics {
400 method: String,
401 start: Instant,
402 metrics: Arc<PrometheusMetrics>,
403}
404
405impl RequestMetrics {
406 pub fn start(metrics: Arc<PrometheusMetrics>, method: impl Into<String>) -> Self {
408 Self {
409 method: method.into(),
410 start: Instant::now(),
411 metrics,
412 }
413 }
414
415 pub fn finish(self) {
417 let duration = self.start.elapsed();
418 self.metrics.record_request(&self.method, duration);
419 }
420
421 pub fn finish_with_error(self, error_type: &str) {
423 let duration = self.start.elapsed();
424 self.metrics.record_request(&self.method, duration);
425 self.metrics.record_error(error_type);
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_counter() {
435 let counter = Counter::new();
436 assert_eq!(counter.get(), 0);
437
438 counter.inc();
439 assert_eq!(counter.get(), 1);
440
441 counter.add(5);
442 assert_eq!(counter.get(), 6);
443 }
444
445 #[test]
446 fn test_histogram() {
447 let histogram = Histogram::new(vec![0.1, 0.5, 1.0]);
448
449 histogram.observe(0.05);
450 histogram.observe(0.3);
451 histogram.observe(0.8);
452 histogram.observe(2.0);
453
454 assert_eq!(histogram.count(), 4);
455
456 let counts = histogram.bucket_counts();
457 assert_eq!(counts[0], 1); assert_eq!(counts[1], 2); assert_eq!(counts[2], 3); }
461
462 #[test]
463 fn test_labeled_counter() {
464 let counter = LabeledCounter::new();
465
466 counter.inc("method_a");
467 counter.inc("method_a");
468 counter.inc("method_b");
469
470 let all = counter.all();
471 assert_eq!(all.get("method_a"), Some(&2));
472 assert_eq!(all.get("method_b"), Some(&1));
473 }
474
475 #[test]
476 fn test_prometheus_metrics() {
477 let metrics = PrometheusMetrics::with_defaults();
478
479 metrics.record_request("tools/list", Duration::from_millis(10));
480 metrics.record_request("tools/call", Duration::from_millis(50));
481 metrics.record_error("timeout");
482 metrics.record_bytes("in", 1000);
483 metrics.record_bytes("out", 500);
484
485 let output = metrics.format_prometheus();
486 assert!(output.contains("dcp_requests_total"));
487 assert!(output.contains("dcp_errors_total"));
488 assert!(output.contains("dcp_bytes_total"));
489 }
490
491 #[test]
492 fn test_request_metrics() {
493 let metrics = Arc::new(PrometheusMetrics::with_defaults());
494
495 let request = RequestMetrics::start(Arc::clone(&metrics), "test_method");
496 std::thread::sleep(Duration::from_millis(1));
497 request.finish();
498
499 let all = metrics.requests_total.all();
500 assert_eq!(all.get("test_method"), Some(&1));
501 }
502}