1use crate::{
4 ClientRateLimitInfo, EndpointMetrics, ErrorMetrics, ErrorRecord, ErrorSummary, LatencyMetrics,
5 RateLimitEvent, RateLimitEventType, RateLimitMetrics, RequestMetrics, RequestRecord,
6 ThroughputMetrics,
7};
8use chrono::Utc;
9use dashmap::DashMap;
10use parking_lot::RwLock;
11use std::collections::{HashMap, VecDeque};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::time::Instant;
14
15pub struct MetricsCollector {
17 total_requests: AtomicU64,
19 success_requests: AtomicU64,
20 client_errors: AtomicU64,
21 server_errors: AtomicU64,
22 requests_by_method: DashMap<String, AtomicU64>,
23 requests_by_status: DashMap<u16, AtomicU64>,
24
25 latency_samples: RwLock<VecDeque<f64>>,
28 total_latency_us: AtomicU64,
29 min_latency_us: AtomicU64,
30 max_latency_us: AtomicU64,
31
32 total_errors: AtomicU64,
34 errors_by_type: DashMap<String, AtomicU64>,
35 errors_by_status: DashMap<u16, AtomicU64>,
36 recent_errors: RwLock<VecDeque<ErrorSummary>>,
37
38 rate_limit_checks: AtomicU64,
40 rate_limit_allowed: AtomicU64,
41 rate_limit_limited: AtomicU64,
42 rate_limited_clients: DashMap<String, ClientRateLimitInfo>,
43 rate_limit_utilization_sum: RwLock<f64>,
46
47 endpoint_metrics: DashMap<String, EndpointData>,
49
50 throughput_epoch: Instant,
57 request_buckets: RwLock<VecDeque<(u64, u64)>>,
58 total_response_bytes: AtomicU64,
59 peak_rps: RwLock<f64>,
60
61 max_latency_samples: usize,
63 max_recent_errors: usize,
64 max_endpoints: usize,
65 max_rate_limit_clients: usize,
66
67 enable_endpoint_metrics: bool,
69 enable_rate_limit_tracking: bool,
70 throughput_window_secs: u64,
71}
72
73struct EndpointData {
74 requests: AtomicU64,
75 errors: AtomicU64,
76 total_latency_us: AtomicU64,
77 latency_samples: RwLock<VecDeque<f64>>,
78}
79
80impl Default for EndpointData {
81 fn default() -> Self {
82 Self {
83 requests: AtomicU64::new(0),
84 errors: AtomicU64::new(0),
85 total_latency_us: AtomicU64::new(0),
86 latency_samples: RwLock::new(VecDeque::with_capacity(1000)),
87 }
88 }
89}
90
91impl MetricsCollector {
92 pub fn new() -> Self {
93 Self::with_limits(10_000, 100, 500, 1000)
94 }
95
96 pub fn with_limits(
97 max_latency_samples: usize,
98 max_recent_errors: usize,
99 max_endpoints: usize,
100 max_rate_limit_clients: usize,
101 ) -> Self {
102 Self {
103 total_requests: AtomicU64::new(0),
104 success_requests: AtomicU64::new(0),
105 client_errors: AtomicU64::new(0),
106 server_errors: AtomicU64::new(0),
107 requests_by_method: DashMap::new(),
108 requests_by_status: DashMap::new(),
109
110 latency_samples: RwLock::new(VecDeque::with_capacity(max_latency_samples)),
111 total_latency_us: AtomicU64::new(0),
112 min_latency_us: AtomicU64::new(u64::MAX),
113 max_latency_us: AtomicU64::new(0),
114
115 total_errors: AtomicU64::new(0),
116 errors_by_type: DashMap::new(),
117 errors_by_status: DashMap::new(),
118 recent_errors: RwLock::new(VecDeque::with_capacity(max_recent_errors)),
119
120 rate_limit_checks: AtomicU64::new(0),
121 rate_limit_allowed: AtomicU64::new(0),
122 rate_limit_limited: AtomicU64::new(0),
123 rate_limited_clients: DashMap::new(),
124 rate_limit_utilization_sum: RwLock::new(0.0),
125
126 endpoint_metrics: DashMap::new(),
127
128 throughput_epoch: Instant::now(),
129 request_buckets: RwLock::new(VecDeque::with_capacity(3601)),
130 total_response_bytes: AtomicU64::new(0),
131 peak_rps: RwLock::new(0.0),
132
133 max_latency_samples,
134 max_recent_errors,
135 max_endpoints,
136 max_rate_limit_clients,
137
138 enable_endpoint_metrics: true,
139 enable_rate_limit_tracking: true,
140 throughput_window_secs: 60,
141 }
142 }
143
144 pub fn from_config(config: &crate::AnalyticsConfig) -> Self {
147 let mut collector = Self::with_limits(
148 config.max_latency_samples,
149 config.max_recent_errors,
150 config.max_endpoints,
151 config.max_rate_limit_clients,
152 );
153 collector.enable_endpoint_metrics = config.enable_endpoint_metrics;
154 collector.enable_rate_limit_tracking = config.enable_rate_limit_tracking;
155 collector.throughput_window_secs = config.throughput_window_secs.max(1);
156 collector
157 }
158
159 pub fn record_request(&self, record: RequestRecord) {
161 self.total_requests.fetch_add(1, Ordering::Relaxed);
163
164 if record.is_success() {
165 self.success_requests.fetch_add(1, Ordering::Relaxed);
166 } else if record.is_client_error() {
167 self.client_errors.fetch_add(1, Ordering::Relaxed);
168 } else if record.is_server_error() {
169 self.server_errors.fetch_add(1, Ordering::Relaxed);
170 }
171
172 self.requests_by_method
174 .entry(record.method.clone())
175 .or_insert_with(|| AtomicU64::new(0))
176 .fetch_add(1, Ordering::Relaxed);
177
178 self.requests_by_status
180 .entry(record.status)
181 .or_insert_with(|| AtomicU64::new(0))
182 .fetch_add(1, Ordering::Relaxed);
183
184 let latency_ms = record.duration.as_secs_f64() * 1000.0;
186 self.record_latency(latency_ms);
187
188 if let Some(size) = record.response_size {
190 self.total_response_bytes.fetch_add(size, Ordering::Relaxed);
191 }
192
193 if self.enable_endpoint_metrics {
198 let endpoint_key = format!("{} {}", record.method, record.path);
199 let tracked = self.endpoint_metrics.contains_key(&endpoint_key)
200 || self.endpoint_metrics.len() < self.max_endpoints;
201 if tracked {
202 let endpoint = self.endpoint_metrics.entry(endpoint_key).or_default();
203
204 endpoint.requests.fetch_add(1, Ordering::Relaxed);
205 if !record.is_success() {
206 endpoint.errors.fetch_add(1, Ordering::Relaxed);
207 }
208 endpoint
209 .total_latency_us
210 .fetch_add((latency_ms * 1000.0) as u64, Ordering::Relaxed);
211
212 let mut samples = endpoint.latency_samples.write();
213 if samples.len() >= 1000 {
214 samples.pop_front();
215 }
216 samples.push_back(latency_ms);
217 }
218 }
219
220 let now = Instant::now();
232 let sec = now
233 .saturating_duration_since(self.throughput_epoch)
234 .as_secs();
235 let window = self.throughput_window_secs.max(1);
236
237 let mut buckets = self.request_buckets.write();
238
239 match buckets.back_mut() {
241 Some((s, count)) if *s == sec => *count += 1,
242 _ => buckets.push_back((sec, 1)),
243 }
244
245 let horizon = sec.saturating_sub(3600);
247 while let Some((s, _)) = buckets.front() {
248 if *s < horizon {
249 buckets.pop_front();
250 } else {
251 break;
252 }
253 }
254
255 let window_start = sec.saturating_sub(window - 1);
261 let in_window: u64 = sum_buckets_since(&buckets, window_start);
262 drop(buckets);
263
264 let current_rps = in_window as f64 / window as f64;
265 let mut peak = self.peak_rps.write();
266 if current_rps > *peak {
267 *peak = current_rps;
268 }
269 }
270
271 fn record_latency(&self, latency_ms: f64) {
272 let latency_us = (latency_ms * 1000.0) as u64;
275
276 self.total_latency_us
278 .fetch_add(latency_us, Ordering::Relaxed);
279
280 let mut current_min = self.min_latency_us.load(Ordering::Relaxed);
282 while latency_us < current_min {
283 match self.min_latency_us.compare_exchange_weak(
284 current_min,
285 latency_us,
286 Ordering::Relaxed,
287 Ordering::Relaxed,
288 ) {
289 Ok(_) => break,
290 Err(c) => current_min = c,
291 }
292 }
293
294 let mut current_max = self.max_latency_us.load(Ordering::Relaxed);
296 while latency_us > current_max {
297 match self.max_latency_us.compare_exchange_weak(
298 current_max,
299 latency_us,
300 Ordering::Relaxed,
301 Ordering::Relaxed,
302 ) {
303 Ok(_) => break,
304 Err(c) => current_max = c,
305 }
306 }
307
308 let mut samples = self.latency_samples.write();
310 if samples.len() >= self.max_latency_samples {
311 samples.pop_front();
312 }
313 samples.push_back(latency_ms);
314 }
315
316 pub fn record_rate_limit(&self, event: RateLimitEvent) {
318 if !self.enable_rate_limit_tracking {
320 return;
321 }
322
323 self.rate_limit_checks.fetch_add(1, Ordering::Relaxed);
324
325 *self.rate_limit_utilization_sum.write() += event.utilization();
328
329 match event.event_type {
330 RateLimitEventType::Allowed => {
331 self.rate_limit_allowed.fetch_add(1, Ordering::Relaxed);
332 }
333 RateLimitEventType::Limited => {
334 self.rate_limit_limited.fetch_add(1, Ordering::Relaxed);
335
336 let tracked = self.rate_limited_clients.contains_key(&event.client_id)
339 || self.rate_limited_clients.len() < self.max_rate_limit_clients;
340 if tracked {
341 self.rate_limited_clients
342 .entry(event.client_id.clone())
343 .and_modify(|info| {
344 info.times_limited += 1;
345 info.last_limited = Utc::now();
346 })
347 .or_insert_with(|| ClientRateLimitInfo {
348 client_id: event.client_id,
349 times_limited: 1,
350 last_limited: Utc::now(),
351 });
352 }
353 }
354 RateLimitEventType::Warning => {
355 self.rate_limit_allowed.fetch_add(1, Ordering::Relaxed);
357 }
358 }
359 }
360
361 pub fn record_error(&self, error: ErrorRecord) {
363 self.total_errors.fetch_add(1, Ordering::Relaxed);
364
365 self.errors_by_type
367 .entry(error.error_type.clone())
368 .or_insert_with(|| AtomicU64::new(0))
369 .fetch_add(1, Ordering::Relaxed);
370
371 if let Some(status) = error.status {
373 self.errors_by_status
374 .entry(status)
375 .or_insert_with(|| AtomicU64::new(0))
376 .fetch_add(1, Ordering::Relaxed);
377 }
378
379 let mut recent = self.recent_errors.write();
381 if recent.len() >= self.max_recent_errors {
382 recent.pop_front();
383 }
384 recent.push_back(ErrorSummary {
385 error_type: error.error_type,
386 message: error.message,
387 count: 1,
388 last_seen: error.timestamp,
389 });
390 }
391
392 pub fn request_metrics(&self) -> RequestMetrics {
394 let by_method: HashMap<String, u64> = self
395 .requests_by_method
396 .iter()
397 .map(|entry| (entry.key().clone(), entry.value().load(Ordering::Relaxed)))
398 .collect();
399
400 let by_status: HashMap<u16, u64> = self
401 .requests_by_status
402 .iter()
403 .map(|entry| (*entry.key(), entry.value().load(Ordering::Relaxed)))
404 .collect();
405
406 RequestMetrics {
407 total: self.total_requests.load(Ordering::Relaxed),
408 success: self.success_requests.load(Ordering::Relaxed),
409 client_errors: self.client_errors.load(Ordering::Relaxed),
410 server_errors: self.server_errors.load(Ordering::Relaxed),
411 by_method,
412 by_status,
413 }
414 }
415
416 pub fn latency_metrics(&self) -> LatencyMetrics {
418 let mut values: Vec<f64> = {
419 let samples = self.latency_samples.read();
420 samples.iter().copied().collect()
421 };
422 let total = self.total_requests.load(Ordering::Relaxed);
423
424 if values.is_empty() || total == 0 {
425 return LatencyMetrics::default();
426 }
427
428 let len = values.len();
429 let avg = self.total_latency_us.load(Ordering::Relaxed) as f64 / total as f64 / 1000.0;
430 let min = self.min_latency_us.load(Ordering::Relaxed);
431 let max = self.max_latency_us.load(Ordering::Relaxed);
432
433 LatencyMetrics {
437 avg_ms: avg,
438 min_ms: if min == u64::MAX {
439 0.0
440 } else {
441 min as f64 / 1000.0
442 },
443 max_ms: max as f64 / 1000.0,
444 p50_ms: percentile_select(&mut values, 50.0),
445 p90_ms: percentile_select(&mut values, 90.0),
446 p95_ms: percentile_select(&mut values, 95.0),
447 p99_ms: percentile_select(&mut values, 99.0),
448 samples: len as u64,
449 }
450 }
451
452 pub fn error_metrics(&self) -> ErrorMetrics {
454 let by_type: HashMap<String, u64> = self
455 .errors_by_type
456 .iter()
457 .map(|entry| (entry.key().clone(), entry.value().load(Ordering::Relaxed)))
458 .collect();
459
460 let by_status: HashMap<u16, u64> = self
461 .errors_by_status
462 .iter()
463 .map(|entry| (*entry.key(), entry.value().load(Ordering::Relaxed)))
464 .collect();
465
466 let recent: Vec<ErrorSummary> = self.recent_errors.read().iter().cloned().collect();
467
468 ErrorMetrics {
469 total: self.total_errors.load(Ordering::Relaxed),
470 by_type,
471 by_status,
472 recent,
473 }
474 }
475
476 pub fn rate_limit_metrics(&self) -> RateLimitMetrics {
478 let total_checks = self.rate_limit_checks.load(Ordering::Relaxed);
479 let allowed = self.rate_limit_allowed.load(Ordering::Relaxed);
480 let limited = self.rate_limit_limited.load(Ordering::Relaxed);
481
482 let mut top_limited: Vec<ClientRateLimitInfo> = self
483 .rate_limited_clients
484 .iter()
485 .map(|entry| entry.value().clone())
486 .collect();
487
488 top_limited.sort_by_key(|e| std::cmp::Reverse(e.times_limited));
489 top_limited.truncate(10);
490
491 let avg_utilization = if total_checks > 0 {
494 *self.rate_limit_utilization_sum.read() / total_checks as f64
495 } else {
496 0.0
497 };
498
499 RateLimitMetrics {
500 total_checks,
501 allowed,
502 limited,
503 unique_clients_limited: self.rate_limited_clients.len() as u64,
504 avg_utilization,
505 top_limited_clients: top_limited,
506 }
507 }
508
509 pub fn endpoint_metrics(&self) -> Vec<EndpointMetrics> {
511 self.endpoint_metrics
512 .iter()
513 .map(|entry| {
514 let key = entry.key();
515 let data = entry.value();
516 let requests = data.requests.load(Ordering::Relaxed);
517 let errors = data.errors.load(Ordering::Relaxed);
518 let total_latency_us = data.total_latency_us.load(Ordering::Relaxed);
519
520 let mut values: Vec<f64> = data.latency_samples.read().iter().copied().collect();
524 let p99 = percentile_select(&mut values, 99.0);
525
526 let parts: Vec<&str> = key.splitn(2, ' ').collect();
527 let (method, path) = if parts.len() == 2 {
528 (parts[0].to_string(), parts[1].to_string())
529 } else {
530 ("".to_string(), key.clone())
531 };
532
533 EndpointMetrics {
534 path,
535 method,
536 requests,
537 errors,
538 avg_latency_ms: if requests > 0 {
539 total_latency_us as f64 / requests as f64 / 1000.0
540 } else {
541 0.0
542 },
543 p99_latency_ms: p99,
544 error_rate: if requests > 0 {
545 (errors as f64 / requests as f64) * 100.0
546 } else {
547 0.0
548 },
549 }
550 })
551 .collect()
552 }
553
554 pub fn throughput_metrics(&self) -> ThroughputMetrics {
556 let buckets = self.request_buckets.read();
557 let now = Instant::now();
558 let sec = now
559 .saturating_duration_since(self.throughput_epoch)
560 .as_secs();
561
562 let sum_since = |cutoff: u64| -> u64 { sum_buckets_since(&buckets, cutoff) };
566
567 let requests_last_minute = sum_since(sec.saturating_sub(59));
569 let requests_last_hour = sum_since(sec.saturating_sub(3599));
570
571 let window = self.throughput_window_secs.max(1);
573 let requests_in_window = sum_since(sec.saturating_sub(window - 1));
574 let rps = requests_in_window as f64 / window as f64;
575
576 ThroughputMetrics {
577 requests_per_second: rps,
578 requests_last_minute,
579 requests_last_hour,
580 peak_rps: *self.peak_rps.read(),
581 avg_response_size: {
582 let total = self.total_requests.load(Ordering::Relaxed);
583 self.total_response_bytes
584 .load(Ordering::Relaxed)
585 .checked_div(total)
586 .unwrap_or(0)
587 },
588 total_bytes_transferred: self.total_response_bytes.load(Ordering::Relaxed),
589 }
590 }
591
592 pub fn reset(&self) {
594 self.total_requests.store(0, Ordering::Relaxed);
595 self.success_requests.store(0, Ordering::Relaxed);
596 self.client_errors.store(0, Ordering::Relaxed);
597 self.server_errors.store(0, Ordering::Relaxed);
598 self.requests_by_method.clear();
599 self.requests_by_status.clear();
600
601 self.latency_samples.write().clear();
602 self.total_latency_us.store(0, Ordering::Relaxed);
603 self.min_latency_us.store(u64::MAX, Ordering::Relaxed);
604 self.max_latency_us.store(0, Ordering::Relaxed);
605
606 self.total_errors.store(0, Ordering::Relaxed);
607 self.errors_by_type.clear();
608 self.errors_by_status.clear();
609 self.recent_errors.write().clear();
610
611 self.rate_limit_checks.store(0, Ordering::Relaxed);
612 self.rate_limit_allowed.store(0, Ordering::Relaxed);
613 self.rate_limit_limited.store(0, Ordering::Relaxed);
614 self.rate_limited_clients.clear();
615 *self.rate_limit_utilization_sum.write() = 0.0;
616
617 self.endpoint_metrics.clear();
618
619 self.request_buckets.write().clear();
620 self.total_response_bytes.store(0, Ordering::Relaxed);
621 *self.peak_rps.write() = 0.0;
622 }
623}
624
625impl Default for MetricsCollector {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[cfg(test)]
636fn percentile(sorted: &[f64], pct: f64) -> f64 {
637 if sorted.is_empty() {
638 return 0.0;
639 }
640
641 let idx = ((pct / 100.0) * (sorted.len() - 1) as f64).round() as usize;
642 sorted[idx.min(sorted.len() - 1)]
643}
644
645fn percentile_select(data: &mut [f64], pct: f64) -> f64 {
651 if data.is_empty() {
652 return 0.0;
653 }
654 let idx = (((pct / 100.0) * (data.len() - 1) as f64).round() as usize).min(data.len() - 1);
655 let (_, nth, _) = data.select_nth_unstable_by(idx, |a, b| {
656 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
657 });
658 *nth
659}
660
661fn sum_buckets_since(buckets: &std::collections::VecDeque<(u64, u64)>, cutoff: u64) -> u64 {
666 buckets
667 .iter()
668 .rev()
669 .take_while(|(s, _)| *s >= cutoff)
670 .map(|(_, c)| c)
671 .sum()
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use std::time::Duration;
678
679 #[test]
680 fn test_percentile_calculation() {
681 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
682 assert_eq!(percentile(&data, 50.0), 6.0);
685 assert_eq!(percentile(&data, 90.0), 9.0);
686 assert_eq!(percentile(&data, 100.0), 10.0);
687 }
688
689 #[test]
690 fn test_collector_requests() {
691 let collector = MetricsCollector::new();
692
693 collector.record_request(RequestRecord::new(
694 "GET",
695 "/api/users",
696 200,
697 Duration::from_millis(50),
698 ));
699
700 collector.record_request(RequestRecord::new(
701 "POST",
702 "/api/users",
703 201,
704 Duration::from_millis(100),
705 ));
706
707 collector.record_request(RequestRecord::new(
708 "GET",
709 "/api/users/1",
710 404,
711 Duration::from_millis(10),
712 ));
713
714 let metrics = collector.request_metrics();
715 assert_eq!(metrics.total, 3);
716 assert_eq!(metrics.success, 2);
717 assert_eq!(metrics.client_errors, 1);
718 }
719
720 #[test]
721 fn test_percentile_select_matches_sorted() {
722 let mut data = vec![10.0, 2.0, 7.0, 1.0, 9.0, 3.0, 8.0, 4.0, 6.0, 5.0];
723 let mut sorted = data.clone();
724 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
725 for pct in [50.0, 90.0, 95.0, 99.0, 100.0] {
726 assert_eq!(percentile_select(&mut data, pct), percentile(&sorted, pct));
727 }
728 }
729
730 #[test]
734 fn test_endpoint_cap_does_not_freeze_existing() {
735 let collector = MetricsCollector::from_config(&crate::AnalyticsConfig {
736 max_endpoints: 1,
737 ..crate::AnalyticsConfig::default()
738 });
739
740 collector.record_request(RequestRecord::new(
742 "GET",
743 "/a",
744 200,
745 Duration::from_millis(5),
746 ));
747 collector.record_request(RequestRecord::new(
749 "GET",
750 "/b",
751 200,
752 Duration::from_millis(5),
753 ));
754 collector.record_request(RequestRecord::new(
756 "GET",
757 "/a",
758 200,
759 Duration::from_millis(5),
760 ));
761
762 let endpoints = collector.endpoint_metrics();
763 assert_eq!(endpoints.len(), 1);
764 let a = endpoints.iter().find(|e| e.path == "/a").unwrap();
765 assert_eq!(
766 a.requests, 2,
767 "existing endpoint must keep incrementing at cap"
768 );
769 }
770
771 #[test]
773 fn test_rate_limit_client_cap_does_not_freeze_existing() {
774 let collector = MetricsCollector::from_config(&crate::AnalyticsConfig {
775 max_rate_limit_clients: 1,
776 ..crate::AnalyticsConfig::default()
777 });
778
779 collector.record_rate_limit(RateLimitEvent::limited("c1", 100, 100, 60));
780 collector.record_rate_limit(RateLimitEvent::limited("c2", 100, 100, 60)); collector.record_rate_limit(RateLimitEvent::limited("c1", 100, 100, 60)); let metrics = collector.rate_limit_metrics();
784 assert_eq!(metrics.unique_clients_limited, 1);
785 let c1 = metrics
786 .top_limited_clients
787 .iter()
788 .find(|c| c.client_id == "c1")
789 .unwrap();
790 assert_eq!(
791 c1.times_limited, 2,
792 "existing client must keep counting at cap"
793 );
794 }
795
796 #[test]
799 fn test_submillisecond_latency_not_truncated() {
800 let collector = MetricsCollector::new();
801 collector.record_request(RequestRecord::new(
802 "GET",
803 "/fast",
804 200,
805 Duration::from_micros(400), ));
807
808 let latency = collector.latency_metrics();
809 assert!(latency.avg_ms > 0.0, "avg latency must not truncate to 0");
810 assert!(latency.min_ms > 0.0, "min latency must not truncate to 0");
811 assert!((latency.avg_ms - 0.4).abs() < 0.05);
812 }
813
814 #[test]
817 fn test_avg_utilization_uses_event_utilization() {
818 let collector = MetricsCollector::new();
819 collector.record_rate_limit(RateLimitEvent::limited("c1", 100, 100, 60)); collector.record_rate_limit(RateLimitEvent::allowed("c2", 50, 100, 60)); let metrics = collector.rate_limit_metrics();
824 assert!(
825 (metrics.avg_utilization - 75.0).abs() < 1e-6,
826 "avg_utilization should be mean of per-event utilization, got {}",
827 metrics.avg_utilization
828 );
829 }
830
831 #[test]
834 fn test_requests_last_hour_is_real_count() {
835 let collector = MetricsCollector::new();
836 collector.record_request(RequestRecord::new(
837 "GET",
838 "/x",
839 200,
840 Duration::from_millis(5),
841 ));
842 let throughput = collector.throughput_metrics();
843 assert_eq!(throughput.requests_last_hour, 1);
844 assert_eq!(throughput.requests_last_minute, 1);
845 }
846
847 #[test]
850 fn test_config_toggles_honored() {
851 let collector = MetricsCollector::from_config(&crate::AnalyticsConfig {
852 enable_endpoint_metrics: false,
853 enable_rate_limit_tracking: false,
854 ..crate::AnalyticsConfig::default()
855 });
856
857 collector.record_request(RequestRecord::new(
858 "GET",
859 "/x",
860 200,
861 Duration::from_millis(5),
862 ));
863 collector.record_rate_limit(RateLimitEvent::limited("c1", 100, 100, 60));
864
865 assert!(
866 collector.endpoint_metrics().is_empty(),
867 "endpoint metrics disabled -> no endpoints"
868 );
869 assert_eq!(
870 collector.rate_limit_metrics().total_checks,
871 0,
872 "rate limit tracking disabled -> no checks recorded"
873 );
874 assert_eq!(collector.request_metrics().total, 1);
876 }
877
878 #[test]
884 fn test_peak_rps_from_incremental_buckets() {
885 let collector = MetricsCollector::new(); for _ in 0..120 {
887 collector.record_request(RequestRecord::new(
888 "GET",
889 "/x",
890 200,
891 Duration::from_millis(1),
892 ));
893 }
894 let t = collector.throughput_metrics();
895 assert_eq!(t.requests_last_minute, 120);
896 assert_eq!(t.requests_last_hour, 120);
897 assert!(
899 (t.requests_per_second - 2.0).abs() < 1e-9,
900 "current rps = {}",
901 t.requests_per_second
902 );
903 assert!((t.peak_rps - 2.0).abs() < 1e-9, "peak rps = {}", t.peak_rps);
904 }
905
906 #[test]
911 fn test_request_buckets_are_count_bounded() {
912 let collector = MetricsCollector::new();
913 for _ in 0..10_000 {
914 collector.record_request(RequestRecord::new(
915 "GET",
916 "/x",
917 200,
918 Duration::from_millis(1),
919 ));
920 }
921 let buckets = collector.request_buckets.read();
922 assert!(
923 buckets.len() <= 2,
924 "10k same-second requests must downsample to <=2 buckets, got {}",
925 buckets.len()
926 );
927 let total: u64 = buckets.iter().map(|(_, c)| c).sum();
928 assert_eq!(total, 10_000, "bucket counts must preserve the exact total");
929 }
930}