1use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Mutex;
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy)]
12pub struct HistogramBucket {
13 pub le: f64,
15 pub count: u64,
17}
18
19#[derive(Debug)]
21pub struct Histogram {
22 buckets: Vec<f64>,
24 counts: Vec<AtomicU64>,
26 sum: AtomicU64,
28 total: AtomicU64,
30}
31
32impl Histogram {
33 pub fn new() -> Self {
35 let buckets =
37 vec![1.0, 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0];
38 let counts = buckets.iter().map(|_| AtomicU64::new(0)).collect();
39
40 Self { buckets, counts, sum: AtomicU64::new(0), total: AtomicU64::new(0) }
41 }
42
43 pub fn with_buckets(buckets: Vec<f64>) -> Self {
45 let counts = buckets.iter().map(|_| AtomicU64::new(0)).collect();
46
47 Self { buckets, counts, sum: AtomicU64::new(0), total: AtomicU64::new(0) }
48 }
49
50 pub fn observe(&self, duration: Duration) {
52 let ms = duration.as_secs_f64() * 1000.0;
53
54 let us = (ms * 1000.0) as u64;
56 self.sum.fetch_add(us, Ordering::Relaxed);
57 self.total.fetch_add(1, Ordering::Relaxed);
58
59 for (i, &le) in self.buckets.iter().enumerate() {
61 if ms <= le {
62 self.counts[i].fetch_add(1, Ordering::Relaxed);
63 }
64 }
65 }
66
67 pub fn get_buckets(&self) -> Vec<HistogramBucket> {
69 self.buckets
70 .iter()
71 .zip(self.counts.iter())
72 .map(|(&le, count)| HistogramBucket { le, count: count.load(Ordering::Relaxed) })
73 .collect()
74 }
75
76 pub fn count(&self) -> u64 {
78 self.total.load(Ordering::Relaxed)
79 }
80
81 pub fn sum_ms(&self) -> f64 {
83 let us = self.sum.load(Ordering::Relaxed);
84 us as f64 / 1000.0
85 }
86
87 pub fn percentile(&self, p: f64) -> f64 {
89 let total = self.count();
90 if total == 0 {
91 return 0.0;
92 }
93
94 let target = (total as f64 * p / 100.0).ceil() as u64;
95 let buckets = self.get_buckets();
96
97 for bucket in &buckets {
98 if bucket.count >= target {
99 return bucket.le;
100 }
101 }
102
103 self.buckets.last().copied().unwrap_or(0.0)
105 }
106
107 pub fn p50(&self) -> f64 {
109 self.percentile(50.0)
110 }
111
112 pub fn p90(&self) -> f64 {
114 self.percentile(90.0)
115 }
116
117 pub fn p99(&self) -> f64 {
119 self.percentile(99.0)
120 }
121
122 pub fn mean(&self) -> f64 {
124 let count = self.count();
125 if count == 0 {
126 return 0.0;
127 }
128 self.sum_ms() / count as f64
129 }
130
131 pub fn reset(&self) {
133 self.sum.store(0, Ordering::Relaxed);
134 self.total.store(0, Ordering::Relaxed);
135 for count in &self.counts {
136 count.store(0, Ordering::Relaxed);
137 }
138 }
139}
140
141impl Default for Histogram {
142 fn default() -> Self {
143 Self::new()
144 }
145}
146
147#[derive(Debug, Default)]
149pub struct Counter {
150 value: AtomicU64,
151}
152
153impl Counter {
154 pub fn new() -> Self {
156 Self { value: AtomicU64::new(0) }
157 }
158
159 pub fn inc(&self) {
161 self.value.fetch_add(1, Ordering::Relaxed);
162 }
163
164 pub fn inc_by(&self, n: u64) {
166 self.value.fetch_add(n, Ordering::Relaxed);
167 }
168
169 pub fn get(&self) -> u64 {
171 self.value.load(Ordering::Relaxed)
172 }
173
174 pub fn reset(&self) {
176 self.value.store(0, Ordering::Relaxed);
177 }
178}
179
180#[derive(Debug)]
182pub struct RagMetrics {
183 pub query_latency: Histogram,
185 pub index_load_latency: Histogram,
187 pub cache_hits: Counter,
189 pub cache_misses: Counter,
191 pub total_queries: Counter,
193 pub docs_retrieved: Counter,
195 spans: Mutex<HashMap<String, SpanStats>>,
197}
198
199#[derive(Debug, Clone, Default)]
201pub struct SpanStats {
202 pub count: u64,
204 pub total_us: u64,
206 pub min_us: u64,
208 pub max_us: u64,
210}
211
212impl RagMetrics {
213 pub fn new() -> Self {
215 Self {
216 query_latency: Histogram::new(),
217 index_load_latency: Histogram::new(),
218 cache_hits: Counter::new(),
219 cache_misses: Counter::new(),
220 total_queries: Counter::new(),
221 docs_retrieved: Counter::new(),
222 spans: Mutex::new(HashMap::new()),
223 }
224 }
225
226 pub fn record_span(&self, name: &str, duration: Duration) {
228 let us = duration.as_micros() as u64;
229
230 let mut spans = self.spans.lock().unwrap_or_else(|e| e.into_inner());
231 let stats = spans.entry(name.to_string()).or_default();
232
233 stats.count += 1;
234 stats.total_us += us;
235
236 if stats.min_us == 0 || us < stats.min_us {
237 stats.min_us = us;
238 }
239 if us > stats.max_us {
240 stats.max_us = us;
241 }
242 }
243
244 pub fn get_span_stats(&self, name: &str) -> Option<SpanStats> {
246 let spans = self.spans.lock().unwrap_or_else(|e| e.into_inner());
247 spans.get(name).cloned()
248 }
249
250 pub fn all_span_stats(&self) -> HashMap<String, SpanStats> {
252 let spans = self.spans.lock().unwrap_or_else(|e| e.into_inner());
253 spans.clone()
254 }
255
256 pub fn cache_hit_rate(&self) -> f64 {
258 let hits = self.cache_hits.get();
259 let misses = self.cache_misses.get();
260 let total = hits + misses;
261 if total == 0 {
262 return 0.0;
263 }
264 hits as f64 / total as f64
265 }
266
267 pub fn reset(&self) {
269 self.query_latency.reset();
270 self.index_load_latency.reset();
271 self.cache_hits.reset();
272 self.cache_misses.reset();
273 self.total_queries.reset();
274 self.docs_retrieved.reset();
275 self.spans.lock().unwrap_or_else(|e| e.into_inner()).clear();
276 }
277
278 pub fn summary(&self) -> MetricsSummary {
280 MetricsSummary {
281 total_queries: self.total_queries.get(),
282 query_latency_p50_ms: self.query_latency.p50(),
283 query_latency_p90_ms: self.query_latency.p90(),
284 query_latency_p99_ms: self.query_latency.p99(),
285 query_latency_mean_ms: self.query_latency.mean(),
286 cache_hit_rate: self.cache_hit_rate(),
287 cache_hits: self.cache_hits.get(),
288 cache_misses: self.cache_misses.get(),
289 docs_retrieved: self.docs_retrieved.get(),
290 spans: self.all_span_stats(),
291 }
292 }
293}
294
295impl Default for RagMetrics {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301#[derive(Debug, Clone)]
303pub struct MetricsSummary {
304 pub total_queries: u64,
306 pub query_latency_p50_ms: f64,
308 pub query_latency_p90_ms: f64,
310 pub query_latency_p99_ms: f64,
312 pub query_latency_mean_ms: f64,
314 pub cache_hit_rate: f64,
316 pub cache_hits: u64,
318 pub cache_misses: u64,
320 pub docs_retrieved: u64,
322 pub spans: HashMap<String, SpanStats>,
324}
325
326impl std::fmt::Display for MetricsSummary {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 writeln!(f, "RAG Metrics Summary")?;
329 writeln!(f, "===================")?;
330 writeln!(f, "Total Queries: {}", self.total_queries)?;
331 writeln!(f)?;
332 writeln!(f, "Query Latency:")?;
333 writeln!(f, " p50: {:.2}ms", self.query_latency_p50_ms)?;
334 writeln!(f, " p90: {:.2}ms", self.query_latency_p90_ms)?;
335 writeln!(f, " p99: {:.2}ms", self.query_latency_p99_ms)?;
336 writeln!(f, " mean: {:.2}ms", self.query_latency_mean_ms)?;
337 writeln!(f)?;
338 writeln!(f, "Cache:")?;
339 writeln!(f, " Hit Rate: {:.1}%", self.cache_hit_rate * 100.0)?;
340 writeln!(f, " Hits: {}", self.cache_hits)?;
341 writeln!(f, " Misses: {}", self.cache_misses)?;
342 writeln!(f)?;
343 writeln!(f, "Documents Retrieved: {}", self.docs_retrieved)?;
344
345 if !self.spans.is_empty() {
346 writeln!(f)?;
347 writeln!(f, "Spans:")?;
348 for (name, stats) in &self.spans {
349 let avg_us = if stats.count > 0 { stats.total_us / stats.count } else { 0 };
350 writeln!(
351 f,
352 " {}: count={}, avg={:.2}ms, min={:.2}ms, max={:.2}ms",
353 name,
354 stats.count,
355 avg_us as f64 / 1000.0,
356 stats.min_us as f64 / 1000.0,
357 stats.max_us as f64 / 1000.0
358 )?;
359 }
360 }
361
362 Ok(())
363 }
364}
365
366pub struct TimedSpan<'a> {
368 name: String,
369 start: Instant,
370 metrics: &'a RagMetrics,
371}
372
373impl<'a> TimedSpan<'a> {
374 pub fn new(name: &str, metrics: &'a RagMetrics) -> Self {
376 Self { name: name.to_string(), start: crate::timing::start_timer(), metrics }
377 }
378
379 pub fn elapsed(&self) -> Duration {
381 self.start.elapsed()
382 }
383}
384
385impl Drop for TimedSpan<'_> {
386 fn drop(&mut self) {
387 let duration = self.start.elapsed();
388 self.metrics.record_span(&self.name, duration);
389 }
390}
391
392pub static GLOBAL_METRICS: std::sync::LazyLock<RagMetrics> =
394 std::sync::LazyLock::new(RagMetrics::new);
395
396pub fn span(name: &str) -> TimedSpan<'static> {
398 TimedSpan::new(name, &GLOBAL_METRICS)
399}
400
401pub fn record_query_latency(duration: Duration) {
403 GLOBAL_METRICS.query_latency.observe(duration);
404 GLOBAL_METRICS.total_queries.inc();
405}
406
407pub fn record_cache_hit() {
409 GLOBAL_METRICS.cache_hits.inc();
410}
411
412pub fn record_cache_miss() {
414 GLOBAL_METRICS.cache_misses.inc();
415}
416
417pub fn get_summary() -> MetricsSummary {
419 GLOBAL_METRICS.summary()
420}
421
422pub fn reset_metrics() {
424 GLOBAL_METRICS.reset();
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430
431 #[test]
432 fn test_histogram_creation() {
433 let hist = Histogram::new();
434 assert_eq!(hist.count(), 0);
435 assert_eq!(hist.sum_ms(), 0.0);
436 }
437
438 #[test]
439 fn test_histogram_observe() {
440 let hist = Histogram::new();
441 hist.observe(Duration::from_millis(5));
442 hist.observe(Duration::from_millis(10));
443 hist.observe(Duration::from_millis(50));
444
445 assert_eq!(hist.count(), 3);
446 assert!((hist.sum_ms() - 65.0).abs() < 1.0);
448 }
449
450 #[test]
451 fn test_histogram_percentiles() {
452 let hist = Histogram::new();
453
454 for i in 1..=100 {
456 hist.observe(Duration::from_millis(i));
457 }
458
459 let p50 = hist.p50();
461 assert!(p50 >= 50.0, "p50 should be >= 50ms, got {}", p50);
462
463 let p99 = hist.p99();
465 assert!(p99 >= p50, "p99 should be >= p50");
466 }
467
468 #[test]
469 fn test_histogram_mean() {
470 let hist = Histogram::new();
471 hist.observe(Duration::from_millis(10));
472 hist.observe(Duration::from_millis(20));
473 hist.observe(Duration::from_millis(30));
474
475 let mean = hist.mean();
476 assert!((mean - 20.0).abs() < 1.0, "mean should be ~20ms, got {}", mean);
477 }
478
479 #[test]
480 fn test_histogram_reset() {
481 let hist = Histogram::new();
482 hist.observe(Duration::from_millis(10));
483 assert_eq!(hist.count(), 1);
484
485 hist.reset();
486 assert_eq!(hist.count(), 0);
487 assert_eq!(hist.sum_ms(), 0.0);
488 }
489
490 #[test]
491 fn test_counter_basic() {
492 let counter = Counter::new();
493 assert_eq!(counter.get(), 0);
494
495 counter.inc();
496 assert_eq!(counter.get(), 1);
497
498 counter.inc_by(5);
499 assert_eq!(counter.get(), 6);
500 }
501
502 #[test]
503 fn test_counter_reset() {
504 let counter = Counter::new();
505 counter.inc_by(100);
506 assert_eq!(counter.get(), 100);
507
508 counter.reset();
509 assert_eq!(counter.get(), 0);
510 }
511
512 #[test]
513 fn test_rag_metrics_creation() {
514 let metrics = RagMetrics::new();
515 assert_eq!(metrics.total_queries.get(), 0);
516 assert_eq!(metrics.cache_hits.get(), 0);
517 }
518
519 #[test]
520 fn test_rag_metrics_record_span() {
521 let metrics = RagMetrics::new();
522
523 metrics.record_span("test_span", Duration::from_millis(10));
524 metrics.record_span("test_span", Duration::from_millis(20));
525
526 let stats = metrics.get_span_stats("test_span").expect("unexpected failure");
527 assert_eq!(stats.count, 2);
528 assert_eq!(stats.total_us, 30_000);
529 assert_eq!(stats.min_us, 10_000);
530 assert_eq!(stats.max_us, 20_000);
531 }
532
533 #[test]
534 fn test_rag_metrics_cache_hit_rate() {
535 let metrics = RagMetrics::new();
536
537 assert_eq!(metrics.cache_hit_rate(), 0.0);
539
540 metrics.cache_hits.inc_by(3);
542 metrics.cache_misses.inc_by(2);
543 assert!((metrics.cache_hit_rate() - 0.6).abs() < 0.001);
544 }
545
546 #[test]
547 fn test_rag_metrics_summary() {
548 let metrics = RagMetrics::new();
549
550 metrics.total_queries.inc_by(100);
551 metrics.cache_hits.inc_by(80);
552 metrics.cache_misses.inc_by(20);
553 metrics.docs_retrieved.inc_by(500);
554
555 for _ in 0..50 {
557 metrics.query_latency.observe(Duration::from_millis(15));
558 }
559 for _ in 0..50 {
560 metrics.query_latency.observe(Duration::from_millis(25));
561 }
562
563 let summary = metrics.summary();
564 assert_eq!(summary.total_queries, 100);
565 assert_eq!(summary.cache_hits, 80);
566 assert_eq!(summary.cache_misses, 20);
567 assert!((summary.cache_hit_rate - 0.8).abs() < 0.001);
568 assert_eq!(summary.docs_retrieved, 500);
569 }
570
571 #[test]
572 fn test_rag_metrics_reset() {
573 let metrics = RagMetrics::new();
574
575 metrics.total_queries.inc_by(100);
576 metrics.cache_hits.inc_by(50);
577 metrics.record_span("span1", Duration::from_millis(10));
578
579 metrics.reset();
580
581 assert_eq!(metrics.total_queries.get(), 0);
582 assert_eq!(metrics.cache_hits.get(), 0);
583 assert!(metrics.all_span_stats().is_empty());
584 }
585
586 #[test]
587 fn test_timed_span() {
588 let metrics = RagMetrics::new();
589
590 {
591 let _span = TimedSpan::new("test", &metrics);
592 std::thread::sleep(Duration::from_millis(5));
593 }
594
595 let stats = metrics.get_span_stats("test").expect("unexpected failure");
596 assert_eq!(stats.count, 1);
597 assert!(stats.total_us >= 5_000, "should be at least 5ms");
598 }
599
600 #[test]
601 fn test_metrics_summary_display() {
602 let metrics = RagMetrics::new();
603 metrics.total_queries.inc_by(10);
604 metrics.cache_hits.inc_by(8);
605 metrics.cache_misses.inc_by(2);
606
607 let summary = metrics.summary();
608 let display = format!("{}", summary);
609
610 assert!(display.contains("RAG Metrics Summary"));
611 assert!(display.contains("Total Queries: 10"));
612 assert!(display.contains("Hit Rate: 80.0%"));
613 }
614
615 #[test]
616 fn test_histogram_custom_buckets() {
617 let hist = Histogram::with_buckets(vec![1.0, 10.0, 100.0]);
618 hist.observe(Duration::from_millis(5));
619
620 let buckets = hist.get_buckets();
621 assert_eq!(buckets.len(), 3);
622 assert_eq!(buckets[0].le, 1.0);
623 assert_eq!(buckets[1].le, 10.0);
624 assert_eq!(buckets[2].le, 100.0);
625 }
626
627 #[test]
628 fn test_global_metrics() {
629 reset_metrics();
631
632 record_cache_hit();
633 record_cache_hit();
634 record_cache_miss();
635
636 let summary = get_summary();
637 assert_eq!(summary.cache_hits, 2);
638 assert_eq!(summary.cache_misses, 1);
639
640 reset_metrics();
641 let summary = get_summary();
642 assert_eq!(summary.cache_hits, 0);
643 }
644
645 #[test]
646 fn test_span_helper() {
647 reset_metrics();
648
649 {
650 let _s = span("helper_test");
651 std::thread::sleep(Duration::from_millis(1));
652 }
653
654 let stats = GLOBAL_METRICS.get_span_stats("helper_test");
655 assert!(stats.is_some());
656 assert_eq!(stats.expect("unexpected failure").count, 1);
657
658 reset_metrics();
659 }
660
661 #[test]
662 fn test_histogram_percentile() {
663 let hist = Histogram::new();
664
665 for _ in 0..10 {
667 hist.observe(Duration::from_millis(5));
668 }
669 for _ in 0..90 {
670 hist.observe(Duration::from_millis(50));
671 }
672
673 let p10 = hist.percentile(0.10);
675 assert!(p10 <= 10.0, "p10 should be <= 10ms, got {}", p10);
676 }
677
678 #[test]
679 fn test_histogram_p90() {
680 let hist = Histogram::new();
681 for i in 1..=100 {
682 hist.observe(Duration::from_millis(i));
683 }
684
685 let p90 = hist.p90();
686 assert!(p90 >= 90.0, "p90 should be >= 90ms, got {}", p90);
687 }
688
689 #[test]
690 fn test_timed_span_elapsed() {
691 let metrics = RagMetrics::new();
692 let span = TimedSpan::new("elapsed_test", &metrics);
693 let elapsed = span.elapsed();
694 assert!(elapsed >= Duration::ZERO);
696 }
697
698 #[test]
699 #[ignore = "flaky: global metrics state races with parallel tests (reset_metrics/get_summary)"]
700 fn test_record_query_latency() {
701 reset_metrics();
702 record_query_latency(Duration::from_millis(10));
703 record_query_latency(Duration::from_millis(20));
704
705 let summary = get_summary();
706 assert_eq!(summary.total_queries, 2);
707 assert!(summary.query_latency_p50_ms >= 10.0);
708
709 reset_metrics();
710 }
711
712 #[test]
713 fn test_histogram_default() {
714 let hist = Histogram::default();
715 assert_eq!(hist.count(), 0);
716 }
717
718 #[test]
719 fn test_counter_default() {
720 let counter = Counter::default();
721 assert_eq!(counter.get(), 0);
722 }
723
724 #[test]
725 fn test_span_stats_default() {
726 let stats = SpanStats::default();
727 assert_eq!(stats.count, 0);
728 assert_eq!(stats.total_us, 0);
729 assert_eq!(stats.min_us, 0);
731 assert_eq!(stats.max_us, 0);
732 }
733
734 #[test]
735 fn test_metrics_summary_fields() {
736 let metrics = RagMetrics::new();
737 let summary = metrics.summary();
738 assert_eq!(summary.total_queries, 0);
739 assert_eq!(summary.cache_hits, 0);
740 assert_eq!(summary.query_latency_p50_ms, 0.0);
741 assert_eq!(summary.query_latency_p99_ms, 0.0);
742 }
743
744 #[test]
745 fn test_all_span_stats() {
746 let metrics = RagMetrics::new();
747 metrics.record_span("span_a", Duration::from_millis(10));
748 metrics.record_span("span_b", Duration::from_millis(20));
749
750 let all = metrics.all_span_stats();
751 assert!(all.contains_key("span_a"));
752 assert!(all.contains_key("span_b"));
753 assert_eq!(all.len(), 2);
754 }
755
756 #[test]
757 fn test_histogram_empty_percentile() {
758 let hist = Histogram::new();
759 assert_eq!(hist.p50(), 0.0);
761 }
762
763 #[test]
764 fn test_histogram_empty_mean() {
765 let hist = Histogram::new();
766 assert_eq!(hist.mean(), 0.0);
768 }
769
770 #[test]
771 fn test_histogram_bucket_fields() {
772 let bucket = HistogramBucket { le: 100.0, count: 42 };
773 assert_eq!(bucket.le, 100.0);
774 assert_eq!(bucket.count, 42);
775 }
776
777 #[test]
778 fn test_histogram_bucket_copy() {
779 let bucket = HistogramBucket { le: 50.0, count: 10 };
780 let copied = bucket;
781 assert_eq!(copied.le, bucket.le);
782 assert_eq!(copied.count, bucket.count);
783 }
784
785 #[test]
786 fn test_span_stats_clone() {
787 let stats = SpanStats { count: 5, total_us: 5000, min_us: 100, max_us: 2000 };
788 let cloned = stats.clone();
789 assert_eq!(cloned.count, 5);
790 assert_eq!(cloned.total_us, 5000);
791 }
792
793 #[test]
794 fn test_get_span_stats_none() {
795 let metrics = RagMetrics::new();
796 assert!(metrics.get_span_stats("nonexistent").is_none());
797 }
798
799 #[test]
800 fn test_metrics_summary_display_with_spans() {
801 let metrics = RagMetrics::new();
802 metrics.record_span("tokenize", Duration::from_millis(10));
803 metrics.record_span("retrieve", Duration::from_millis(50));
804
805 let summary = metrics.summary();
806 let display = format!("{}", summary);
807
808 assert!(display.contains("Spans:"));
809 assert!(display.contains("tokenize"));
810 assert!(display.contains("retrieve"));
811 }
812
813 #[test]
814 fn test_metrics_summary_clone() {
815 let metrics = RagMetrics::new();
816 metrics.total_queries.inc_by(5);
817 let summary = metrics.summary();
818 let cloned = summary.clone();
819 assert_eq!(cloned.total_queries, 5);
820 }
821
822 #[test]
823 fn test_histogram_percentile_returns_first_matching_bucket() {
824 let hist = Histogram::with_buckets(vec![1.0, 2.0, 3.0]);
825 hist.observe(Duration::from_micros(500)); let p50 = hist.percentile(50.0);
831 assert_eq!(p50, 1.0);
832 }
833
834 #[test]
835 fn test_rag_metrics_default() {
836 let metrics = RagMetrics::default();
837 assert_eq!(metrics.total_queries.get(), 0);
838 }
839
840 #[test]
841 fn test_histogram_observe_large_values() {
842 let hist = Histogram::new();
843 hist.observe(Duration::from_secs(15)); assert_eq!(hist.count(), 1);
846 let p99 = hist.p99();
848 assert_eq!(p99, 10000.0); }
850
851 #[test]
852 fn test_histogram_debug() {
853 let hist = Histogram::new();
854 let debug = format!("{:?}", hist);
855 assert!(debug.contains("Histogram"));
856 }
857
858 #[test]
859 fn test_counter_debug() {
860 let counter = Counter::new();
861 let debug = format!("{:?}", counter);
862 assert!(debug.contains("Counter"));
863 }
864
865 #[test]
866 fn test_rag_metrics_debug() {
867 let metrics = RagMetrics::new();
868 let debug = format!("{:?}", metrics);
869 assert!(debug.contains("RagMetrics"));
870 }
871
872 #[test]
873 fn test_span_stats_debug() {
874 let stats = SpanStats::default();
875 let debug = format!("{:?}", stats);
876 assert!(debug.contains("SpanStats"));
877 }
878
879 #[test]
880 fn test_metrics_summary_debug() {
881 let metrics = RagMetrics::new();
882 let summary = metrics.summary();
883 let debug = format!("{:?}", summary);
884 assert!(debug.contains("MetricsSummary"));
885 }
886}