1use std::collections::{BTreeMap, HashMap};
33use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
34use std::sync::{Arc, Mutex};
35use std::time::Duration;
36
37use crate::conventions;
38
39#[derive(Debug, Clone)]
41pub struct MetricsConfig {
42 pub enabled: bool,
44}
45
46impl Default for MetricsConfig {
47 fn default() -> Self {
48 Self { enabled: true }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub struct RedSnapshot {
55 pub requests: u64,
57 pub errors: u64,
59 pub duration_total_ms: f64,
61 pub error_rate: f64,
63 pub avg_duration_ms: f64,
65 pub p50_ms: f64,
67 pub p95_ms: f64,
69 pub p99_ms: f64,
71 pub latency_buckets: Vec<(f64, u64)>,
75}
76
77impl RedSnapshot {
78 fn from_counts(requests: u64, errors: u64, duration_micros_total: u64) -> Self {
79 let duration_total_ms = duration_micros_total as f64 / 1_000.0;
80 let error_rate = if requests == 0 { 0.0 } else { errors as f64 / requests as f64 };
81 let avg_duration_ms = if requests == 0 { 0.0 } else { duration_total_ms / requests as f64 };
82
83 Self {
84 requests,
85 errors,
86 duration_total_ms,
87 error_rate,
88 avg_duration_ms,
89 p50_ms: 0.0,
90 p95_ms: 0.0,
91 p99_ms: 0.0,
92 latency_buckets: Vec::new(),
93 }
94 }
95
96 #[must_use]
98 pub fn evaluate_slo(&self, target: SloTarget) -> SloEvaluation {
99 let success_rate = 1.0 - self.error_rate;
100
101 let enough_requests = self.requests >= target.min_requests;
102 let meets_success = success_rate >= target.min_success_rate;
103 let meets_latency = self.avg_duration_ms <= target.max_avg_latency_ms;
104
105 let passed = enough_requests && meets_success && meets_latency;
106 let reason = if passed {
107 None
108 } else if !enough_requests {
109 Some(format!("insufficient requests: {} < {}", self.requests, target.min_requests))
110 } else if !meets_success {
111 Some(format!(
112 "success rate {:.4} below target {:.4}",
113 success_rate, target.min_success_rate
114 ))
115 } else {
116 Some(format!(
117 "avg latency {:.3}ms above target {:.3}ms",
118 self.avg_duration_ms, target.max_avg_latency_ms
119 ))
120 };
121
122 SloEvaluation {
123 passed,
124 requests: self.requests,
125 success_rate,
126 error_rate: self.error_rate,
127 avg_duration_ms: self.avg_duration_ms,
128 reason,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct SloTarget {
136 pub min_success_rate: f64,
138 pub max_avg_latency_ms: f64,
140 pub min_requests: u64,
142}
143
144impl Default for SloTarget {
145 fn default() -> Self {
146 Self { min_success_rate: 0.99, max_avg_latency_ms: 250.0, min_requests: 100 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq)]
152pub struct SloEvaluation {
153 pub passed: bool,
155 pub requests: u64,
157 pub success_rate: f64,
159 pub error_rate: f64,
161 pub avg_duration_ms: f64,
163 pub reason: Option<String>,
165}
166
167#[derive(Debug, Clone)]
169pub struct MetricsSnapshot {
170 pub enabled: bool,
172 pub orders_created: u64,
174 pub customers_created: u64,
176 pub products_created: u64,
178 pub returns_requested: u64,
180 pub carts_created: u64,
182 pub cart_checkouts_completed: u64,
184 pub shipments_created: u64,
186 pub shipments_delivered: u64,
188 pub subscriptions_created: u64,
190 pub payments_completed: u64,
192 pub inventory_adjustments: u64,
194 pub a2a_quotes_created: u64,
196 pub a2a_purchases_created: u64,
198 pub x402_intents_created: u64,
200 pub x402_intents_settled: u64,
202 pub policy_evaluations: u64,
204 pub policy_denials: u64,
206 pub agent_registrations: u64,
208 pub webhook_deliveries: u64,
210 pub webhook_failures: u64,
212 pub pqc_legacy_signatures: u64,
214 pub pqc_hybrid_signatures: u64,
216 pub pqc_strict_signatures: u64,
218 pub pqc_legacy_encryptions: u64,
220 pub pqc_hybrid_encryptions: u64,
222 pub pqc_strict_encryptions: u64,
224 pub order_amount_total: f64,
226 pub payment_amount_total: f64,
228 pub inventory_delta_total: f64,
230 pub red_global: RedSnapshot,
232 pub red_by_operation: BTreeMap<String, RedSnapshot>,
234 pub http_by_route: BTreeMap<(String, String), HttpRouteSnapshot>,
239}
240
241impl MetricsSnapshot {
242 #[must_use]
244 pub fn evaluate_global_slo(&self, target: SloTarget) -> SloEvaluation {
245 self.red_global.evaluate_slo(target)
246 }
247
248 #[must_use]
252 pub fn evaluate_operation_slo(
253 &self,
254 operation: &str,
255 target: SloTarget,
256 ) -> Option<SloEvaluation> {
257 let key = conventions::operation_metric_label(operation);
258 self.red_by_operation.get(&key).map(|snapshot| snapshot.evaluate_slo(target))
259 }
260}
261
262const LATENCY_BUCKET_BOUNDS_MICROS: [u64; 13] = [
267 1_000, 2_500, 5_000, 10_000, 25_000, 50_000, 100_000, 250_000, 500_000, 1_000_000, 2_500_000, 5_000_000, 10_000_000, ];
281
282const LATENCY_BUCKET_COUNT: usize = LATENCY_BUCKET_BOUNDS_MICROS.len() + 1;
284
285#[derive(Debug, Clone)]
306pub struct LatencyHistogram {
307 bucket_counts: [u64; LATENCY_BUCKET_COUNT],
309 count: u64,
311 sum_micros: u64,
313}
314
315impl Default for LatencyHistogram {
316 fn default() -> Self {
317 Self::new()
318 }
319}
320
321impl LatencyHistogram {
322 #[must_use]
324 pub const fn new() -> Self {
325 Self { bucket_counts: [0; LATENCY_BUCKET_COUNT], count: 0, sum_micros: 0 }
326 }
327
328 pub fn record(&mut self, duration_micros: u64) {
330 let idx = LATENCY_BUCKET_BOUNDS_MICROS
331 .iter()
332 .position(|&bound| duration_micros <= bound)
333 .unwrap_or(LATENCY_BUCKET_BOUNDS_MICROS.len());
334 self.bucket_counts[idx] = self.bucket_counts[idx].saturating_add(1);
335 self.count = self.count.saturating_add(1);
336 self.sum_micros = self.sum_micros.saturating_add(duration_micros);
337 }
338
339 #[must_use]
344 pub fn percentile(&self, p: f64) -> f64 {
345 if self.count == 0 {
346 return 0.0;
347 }
348 let p = p.clamp(0.0, 1.0);
349 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
350 let rank = ((p * self.count as f64).ceil() as u64).max(1);
351
352 let mut cumulative = 0u64;
353 for (idx, &bucket_count) in self.bucket_counts.iter().enumerate() {
354 let next = cumulative.saturating_add(bucket_count);
355 if bucket_count > 0 && rank <= next {
356 if idx >= LATENCY_BUCKET_BOUNDS_MICROS.len() {
357 return LATENCY_BUCKET_BOUNDS_MICROS[LATENCY_BUCKET_BOUNDS_MICROS.len() - 1]
359 as f64
360 / 1_000.0;
361 }
362 let lower_ms = if idx == 0 {
363 0.0
364 } else {
365 LATENCY_BUCKET_BOUNDS_MICROS[idx - 1] as f64 / 1_000.0
366 };
367 let upper_ms = LATENCY_BUCKET_BOUNDS_MICROS[idx] as f64 / 1_000.0;
368 let fraction = (rank - cumulative) as f64 / bucket_count as f64;
369 return lower_ms + (upper_ms - lower_ms) * fraction;
370 }
371 cumulative = next;
372 }
373 LATENCY_BUCKET_BOUNDS_MICROS[LATENCY_BUCKET_BOUNDS_MICROS.len() - 1] as f64 / 1_000.0
374 }
375
376 #[must_use]
378 #[allow(clippy::cast_possible_truncation)]
379 pub const fn count(&self) -> usize {
380 self.count as usize
381 }
382
383 #[must_use]
385 pub const fn sum_micros(&self) -> u64 {
386 self.sum_micros
387 }
388
389 #[must_use]
393 pub fn cumulative_buckets(&self) -> Vec<(f64, u64)> {
394 let mut cumulative = 0u64;
395 let mut out = Vec::with_capacity(LATENCY_BUCKET_COUNT);
396 for (idx, &bucket_count) in self.bucket_counts.iter().enumerate() {
397 cumulative = cumulative.saturating_add(bucket_count);
398 let bound = LATENCY_BUCKET_BOUNDS_MICROS
399 .get(idx)
400 .map_or(f64::INFINITY, |µs| micros as f64 / 1_000_000.0);
401 out.push((bound, cumulative));
402 }
403 out
404 }
405}
406
407#[derive(Debug, Clone, Default)]
408struct RedAccumulator {
409 requests: u64,
410 errors: u64,
411 duration_micros_total: u64,
412 histogram: LatencyHistogram,
413}
414
415impl RedAccumulator {
416 fn record(&mut self, duration_micros: u64, is_error: bool) {
417 self.requests = self.requests.saturating_add(1);
418 if is_error {
419 self.errors = self.errors.saturating_add(1);
420 }
421 self.duration_micros_total = self.duration_micros_total.saturating_add(duration_micros);
422 self.histogram.record(duration_micros);
423 }
424
425 fn snapshot(&self) -> RedSnapshot {
426 let mut snap =
427 RedSnapshot::from_counts(self.requests, self.errors, self.duration_micros_total);
428 snap.p50_ms = self.histogram.percentile(0.50);
429 snap.p95_ms = self.histogram.percentile(0.95);
430 snap.p99_ms = self.histogram.percentile(0.99);
431 snap.latency_buckets = self.histogram.cumulative_buckets();
432 snap
433 }
434}
435
436#[derive(Debug, Clone, Default)]
438struct HttpRouteAccumulator {
439 requests: u64,
440 errors_4xx: u64,
441 errors_5xx: u64,
442 duration_micros_total: u64,
443 histogram: LatencyHistogram,
444}
445
446#[derive(Debug, Clone, PartialEq)]
448pub struct HttpRouteSnapshot {
449 pub requests: u64,
451 pub errors_4xx: u64,
453 pub errors_5xx: u64,
455 pub duration_total_ms: f64,
457 pub latency_buckets: Vec<(f64, u64)>,
460}
461
462#[derive(Debug, Default)]
463struct MetricTotals {
464 red_global: RedAccumulator,
467 red_by_operation: HashMap<String, RedAccumulator>,
468 http_by_route: HashMap<(String, String), HttpRouteAccumulator>,
469}
470
471#[derive(Debug)]
472struct MetricsInner {
473 enabled: AtomicBool,
474 orders_created: AtomicU64,
475 customers_created: AtomicU64,
476 products_created: AtomicU64,
477 returns_requested: AtomicU64,
478 carts_created: AtomicU64,
479 cart_checkouts_completed: AtomicU64,
480 shipments_created: AtomicU64,
481 shipments_delivered: AtomicU64,
482 subscriptions_created: AtomicU64,
483 payments_completed: AtomicU64,
484 inventory_adjustments: AtomicU64,
485 a2a_quotes_created: AtomicU64,
486 a2a_purchases_created: AtomicU64,
487 x402_intents_created: AtomicU64,
488 x402_intents_settled: AtomicU64,
489 policy_evaluations: AtomicU64,
490 policy_denials: AtomicU64,
491 agent_registrations: AtomicU64,
492 webhook_deliveries: AtomicU64,
493 webhook_failures: AtomicU64,
494 pqc_legacy_signatures: AtomicU64,
495 pqc_hybrid_signatures: AtomicU64,
496 pqc_strict_signatures: AtomicU64,
497 pqc_legacy_encryptions: AtomicU64,
498 pqc_hybrid_encryptions: AtomicU64,
499 pqc_strict_encryptions: AtomicU64,
500 requests_total: AtomicU64,
501 request_errors_total: AtomicU64,
502 request_duration_micros_total: AtomicU64,
503 order_amount_total_bits: AtomicU64,
505 payment_amount_total_bits: AtomicU64,
506 inventory_delta_total_bits: AtomicU64,
507 totals: Mutex<MetricTotals>,
508}
509
510#[derive(Debug, Clone)]
512pub struct Metrics {
513 inner: Arc<MetricsInner>,
514}
515
516impl Default for Metrics {
517 fn default() -> Self {
518 init_metrics(MetricsConfig::default())
519 }
520}
521
522impl Metrics {
523 const fn is_finite_metric_value(value: f64) -> bool {
524 value.is_finite()
525 }
526
527 fn atomic_f64_add(target: &AtomicU64, value: f64) {
529 let mut current = target.load(Ordering::Relaxed);
530 loop {
531 let current_f64 = f64::from_bits(current);
532 let new_f64 = current_f64 + value;
533 match target.compare_exchange_weak(
534 current,
535 new_f64.to_bits(),
536 Ordering::Relaxed,
537 Ordering::Relaxed,
538 ) {
539 Ok(_) => break,
540 Err(actual) => current = actual,
541 }
542 }
543 }
544
545 #[must_use]
547 pub fn is_enabled(&self) -> bool {
548 self.inner.enabled.load(Ordering::Relaxed)
549 }
550
551 pub fn set_enabled(&self, enabled: bool) {
553 self.inner.enabled.store(enabled, Ordering::Relaxed);
554 }
555
556 #[must_use]
558 pub fn snapshot(&self) -> MetricsSnapshot {
559 let totals = match self.inner.totals.lock() {
560 Ok(guard) => guard,
561 Err(poisoned) => poisoned.into_inner(),
562 };
563
564 let red_by_operation: BTreeMap<String, RedSnapshot> = totals
565 .red_by_operation
566 .iter()
567 .map(|(operation, acc)| (operation.clone(), acc.snapshot()))
568 .collect();
569
570 let red_global = totals.red_global.snapshot();
571
572 let http_by_route: BTreeMap<(String, String), HttpRouteSnapshot> = totals
573 .http_by_route
574 .iter()
575 .map(|(key, acc)| {
576 (
577 key.clone(),
578 HttpRouteSnapshot {
579 requests: acc.requests,
580 errors_4xx: acc.errors_4xx,
581 errors_5xx: acc.errors_5xx,
582 duration_total_ms: acc.duration_micros_total as f64 / 1_000.0,
583 latency_buckets: acc.histogram.cumulative_buckets(),
584 },
585 )
586 })
587 .collect();
588
589 MetricsSnapshot {
590 enabled: self.is_enabled(),
591 orders_created: self.inner.orders_created.load(Ordering::Relaxed),
592 customers_created: self.inner.customers_created.load(Ordering::Relaxed),
593 products_created: self.inner.products_created.load(Ordering::Relaxed),
594 returns_requested: self.inner.returns_requested.load(Ordering::Relaxed),
595 carts_created: self.inner.carts_created.load(Ordering::Relaxed),
596 cart_checkouts_completed: self.inner.cart_checkouts_completed.load(Ordering::Relaxed),
597 shipments_created: self.inner.shipments_created.load(Ordering::Relaxed),
598 shipments_delivered: self.inner.shipments_delivered.load(Ordering::Relaxed),
599 subscriptions_created: self.inner.subscriptions_created.load(Ordering::Relaxed),
600 payments_completed: self.inner.payments_completed.load(Ordering::Relaxed),
601 inventory_adjustments: self.inner.inventory_adjustments.load(Ordering::Relaxed),
602 a2a_quotes_created: self.inner.a2a_quotes_created.load(Ordering::Relaxed),
603 a2a_purchases_created: self.inner.a2a_purchases_created.load(Ordering::Relaxed),
604 x402_intents_created: self.inner.x402_intents_created.load(Ordering::Relaxed),
605 x402_intents_settled: self.inner.x402_intents_settled.load(Ordering::Relaxed),
606 policy_evaluations: self.inner.policy_evaluations.load(Ordering::Relaxed),
607 policy_denials: self.inner.policy_denials.load(Ordering::Relaxed),
608 agent_registrations: self.inner.agent_registrations.load(Ordering::Relaxed),
609 webhook_deliveries: self.inner.webhook_deliveries.load(Ordering::Relaxed),
610 webhook_failures: self.inner.webhook_failures.load(Ordering::Relaxed),
611 pqc_legacy_signatures: self.inner.pqc_legacy_signatures.load(Ordering::Relaxed),
612 pqc_hybrid_signatures: self.inner.pqc_hybrid_signatures.load(Ordering::Relaxed),
613 pqc_strict_signatures: self.inner.pqc_strict_signatures.load(Ordering::Relaxed),
614 pqc_legacy_encryptions: self.inner.pqc_legacy_encryptions.load(Ordering::Relaxed),
615 pqc_hybrid_encryptions: self.inner.pqc_hybrid_encryptions.load(Ordering::Relaxed),
616 pqc_strict_encryptions: self.inner.pqc_strict_encryptions.load(Ordering::Relaxed),
617 order_amount_total: f64::from_bits(
618 self.inner.order_amount_total_bits.load(Ordering::Relaxed),
619 ),
620 payment_amount_total: f64::from_bits(
621 self.inner.payment_amount_total_bits.load(Ordering::Relaxed),
622 ),
623 inventory_delta_total: f64::from_bits(
624 self.inner.inventory_delta_total_bits.load(Ordering::Relaxed),
625 ),
626 red_global,
627 red_by_operation,
628 http_by_route,
629 }
630 }
631
632 pub fn record_order_created(&self, _customer_id: &str, amount: f64) {
634 if !self.is_enabled() {
635 return;
636 }
637 self.inner.orders_created.fetch_add(1, Ordering::Relaxed);
638 if Self::is_finite_metric_value(amount) {
639 Self::atomic_f64_add(&self.inner.order_amount_total_bits, amount);
640 }
641 }
642
643 pub fn record_customer_created(&self, _customer_id: &str) {
645 if !self.is_enabled() {
646 return;
647 }
648 self.inner.customers_created.fetch_add(1, Ordering::Relaxed);
649 }
650
651 pub fn record_product_created(&self, _product_id: &str) {
653 if !self.is_enabled() {
654 return;
655 }
656 self.inner.products_created.fetch_add(1, Ordering::Relaxed);
657 }
658
659 pub fn record_return_requested(&self, _return_id: &str) {
661 if !self.is_enabled() {
662 return;
663 }
664 self.inner.returns_requested.fetch_add(1, Ordering::Relaxed);
665 }
666
667 pub fn record_cart_created(&self, _cart_id: &str) {
669 if !self.is_enabled() {
670 return;
671 }
672 self.inner.carts_created.fetch_add(1, Ordering::Relaxed);
673 }
674
675 pub fn record_cart_checkout_completed(&self, _cart_id: &str, _order_id: &str) {
677 if !self.is_enabled() {
678 return;
679 }
680 self.inner.cart_checkouts_completed.fetch_add(1, Ordering::Relaxed);
681 }
682
683 pub fn record_shipment_created(&self, _shipment_id: &str) {
685 if !self.is_enabled() {
686 return;
687 }
688 self.inner.shipments_created.fetch_add(1, Ordering::Relaxed);
689 }
690
691 pub fn record_shipment_delivered(&self, _shipment_id: &str) {
693 if !self.is_enabled() {
694 return;
695 }
696 self.inner.shipments_delivered.fetch_add(1, Ordering::Relaxed);
697 }
698
699 pub fn record_subscription_created(&self, _subscription_id: &str) {
701 if !self.is_enabled() {
702 return;
703 }
704 self.inner.subscriptions_created.fetch_add(1, Ordering::Relaxed);
705 }
706
707 pub fn record_payment_completed(&self, _payment_id: &str, amount: f64) {
709 if !self.is_enabled() {
710 return;
711 }
712 self.inner.payments_completed.fetch_add(1, Ordering::Relaxed);
713 if Self::is_finite_metric_value(amount) {
714 Self::atomic_f64_add(&self.inner.payment_amount_total_bits, amount);
715 }
716 }
717
718 pub fn record_inventory_adjusted(&self, _sku: &str, delta: f64) {
720 if !self.is_enabled() {
721 return;
722 }
723 self.inner.inventory_adjustments.fetch_add(1, Ordering::Relaxed);
724 if Self::is_finite_metric_value(delta) {
725 Self::atomic_f64_add(&self.inner.inventory_delta_total_bits, delta);
726 }
727 }
728
729 pub fn record_a2a_quote_created(&self) {
731 if !self.is_enabled() {
732 return;
733 }
734 self.inner.a2a_quotes_created.fetch_add(1, Ordering::Relaxed);
735 }
736
737 pub fn record_a2a_purchase_created(&self) {
739 if !self.is_enabled() {
740 return;
741 }
742 self.inner.a2a_purchases_created.fetch_add(1, Ordering::Relaxed);
743 }
744
745 pub fn record_x402_intent_created(&self) {
747 if !self.is_enabled() {
748 return;
749 }
750 self.inner.x402_intents_created.fetch_add(1, Ordering::Relaxed);
751 }
752
753 pub fn record_x402_intent_settled(&self) {
755 if !self.is_enabled() {
756 return;
757 }
758 self.inner.x402_intents_settled.fetch_add(1, Ordering::Relaxed);
759 }
760
761 pub fn record_policy_evaluation(&self, denied: bool) {
763 if !self.is_enabled() {
764 return;
765 }
766 self.inner.policy_evaluations.fetch_add(1, Ordering::Relaxed);
767 if denied {
768 self.inner.policy_denials.fetch_add(1, Ordering::Relaxed);
769 }
770 }
771
772 pub fn record_agent_registration(&self) {
774 if !self.is_enabled() {
775 return;
776 }
777 self.inner.agent_registrations.fetch_add(1, Ordering::Relaxed);
778 }
779
780 pub fn record_webhook_delivery(&self, failed: bool) {
782 if !self.is_enabled() {
783 return;
784 }
785 self.inner.webhook_deliveries.fetch_add(1, Ordering::Relaxed);
786 if failed {
787 self.inner.webhook_failures.fetch_add(1, Ordering::Relaxed);
788 }
789 }
790
791 pub fn record_pqc_signature(&self, profile: &str) {
795 if !self.is_enabled() {
796 return;
797 }
798 match profile {
799 "legacy" => self.inner.pqc_legacy_signatures.fetch_add(1, Ordering::Relaxed),
800 "hybrid" => self.inner.pqc_hybrid_signatures.fetch_add(1, Ordering::Relaxed),
801 "pqc-strict" => self.inner.pqc_strict_signatures.fetch_add(1, Ordering::Relaxed),
802 _ => 0,
803 };
804 }
805
806 pub fn record_pqc_encryption(&self, profile: &str) {
810 if !self.is_enabled() {
811 return;
812 }
813 match profile {
814 "legacy" => self.inner.pqc_legacy_encryptions.fetch_add(1, Ordering::Relaxed),
815 "hybrid" => self.inner.pqc_hybrid_encryptions.fetch_add(1, Ordering::Relaxed),
816 "pqc-strict" => self.inner.pqc_strict_encryptions.fetch_add(1, Ordering::Relaxed),
817 _ => 0,
818 };
819 }
820
821 pub fn record_request(&self, operation: &str, duration: Duration, is_error: bool) {
825 if !self.is_enabled() {
826 return;
827 }
828
829 let op = conventions::operation_metric_label(operation);
830 let duration_micros = duration.as_micros().min(u128::from(u64::MAX)) as u64;
831
832 self.inner.requests_total.fetch_add(1, Ordering::Relaxed);
833 if is_error {
834 self.inner.request_errors_total.fetch_add(1, Ordering::Relaxed);
835 }
836 self.inner.request_duration_micros_total.fetch_add(duration_micros, Ordering::Relaxed);
837
838 let mut totals = match self.inner.totals.lock() {
839 Ok(guard) => guard,
840 Err(poisoned) => poisoned.into_inner(),
841 };
842 totals.red_global.record(duration_micros, is_error);
843 totals.red_by_operation.entry(op).or_default().record(duration_micros, is_error);
844 }
845
846 pub fn record_http_request(&self, method: &str, route: &str, status: u16, duration: Duration) {
853 if !self.is_enabled() {
854 return;
855 }
856
857 let is_5xx = (500..600).contains(&status);
858 let is_4xx = (400..500).contains(&status);
859 let duration_micros = duration.as_micros().min(u128::from(u64::MAX)) as u64;
860
861 self.inner.requests_total.fetch_add(1, Ordering::Relaxed);
862 if is_5xx {
863 self.inner.request_errors_total.fetch_add(1, Ordering::Relaxed);
864 }
865 self.inner.request_duration_micros_total.fetch_add(duration_micros, Ordering::Relaxed);
866
867 let mut totals = match self.inner.totals.lock() {
868 Ok(guard) => guard,
869 Err(poisoned) => poisoned.into_inner(),
870 };
871 totals.red_global.record(duration_micros, is_5xx);
872
873 let acc = totals.http_by_route.entry((method.to_owned(), route.to_owned())).or_default();
874 acc.requests = acc.requests.saturating_add(1);
875 if is_4xx {
876 acc.errors_4xx = acc.errors_4xx.saturating_add(1);
877 }
878 if is_5xx {
879 acc.errors_5xx = acc.errors_5xx.saturating_add(1);
880 }
881 acc.duration_micros_total = acc.duration_micros_total.saturating_add(duration_micros);
882 acc.histogram.record(duration_micros);
883 }
884
885 pub fn record_request_success(&self, operation: &str, duration: Duration) {
887 self.record_request(operation, duration, false);
888 }
889
890 pub fn record_request_error(&self, operation: &str, duration: Duration) {
892 self.record_request(operation, duration, true);
893 }
894}
895
896#[must_use]
898pub fn init_metrics(config: MetricsConfig) -> Metrics {
899 Metrics {
900 inner: Arc::new(MetricsInner {
901 enabled: AtomicBool::new(config.enabled),
902 orders_created: AtomicU64::new(0),
903 customers_created: AtomicU64::new(0),
904 products_created: AtomicU64::new(0),
905 returns_requested: AtomicU64::new(0),
906 carts_created: AtomicU64::new(0),
907 cart_checkouts_completed: AtomicU64::new(0),
908 shipments_created: AtomicU64::new(0),
909 shipments_delivered: AtomicU64::new(0),
910 subscriptions_created: AtomicU64::new(0),
911 payments_completed: AtomicU64::new(0),
912 inventory_adjustments: AtomicU64::new(0),
913 a2a_quotes_created: AtomicU64::new(0),
914 a2a_purchases_created: AtomicU64::new(0),
915 x402_intents_created: AtomicU64::new(0),
916 x402_intents_settled: AtomicU64::new(0),
917 policy_evaluations: AtomicU64::new(0),
918 policy_denials: AtomicU64::new(0),
919 agent_registrations: AtomicU64::new(0),
920 webhook_deliveries: AtomicU64::new(0),
921 webhook_failures: AtomicU64::new(0),
922 pqc_legacy_signatures: AtomicU64::new(0),
923 pqc_hybrid_signatures: AtomicU64::new(0),
924 pqc_strict_signatures: AtomicU64::new(0),
925 pqc_legacy_encryptions: AtomicU64::new(0),
926 pqc_hybrid_encryptions: AtomicU64::new(0),
927 pqc_strict_encryptions: AtomicU64::new(0),
928 requests_total: AtomicU64::new(0),
929 request_errors_total: AtomicU64::new(0),
930 request_duration_micros_total: AtomicU64::new(0),
931 order_amount_total_bits: AtomicU64::new(0u64),
932 payment_amount_total_bits: AtomicU64::new(0u64),
933 inventory_delta_total_bits: AtomicU64::new(0u64),
934 totals: Mutex::new(MetricTotals::default()),
935 }),
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942
943 fn assert_close(a: f64, b: f64) {
944 assert!((a - b).abs() < 1e-9, "expected {a} to be close to {b}");
945 }
946
947 #[test]
948 fn records_values_when_enabled() {
949 let metrics = init_metrics(MetricsConfig { enabled: true });
950 metrics.record_order_created("cust-1", 99.99);
951 metrics.record_customer_created("cust-1");
952 metrics.record_product_created("prod-1");
953 metrics.record_return_requested("ret-1");
954 metrics.record_cart_created("cart-1");
955 metrics.record_cart_checkout_completed("cart-1", "ord-1");
956 metrics.record_shipment_created("shp-1");
957 metrics.record_shipment_delivered("shp-1");
958 metrics.record_subscription_created("sub-1");
959 metrics.record_payment_completed("pay-1", 99.99);
960 metrics.record_inventory_adjusted("sku-1", -2.0);
961 metrics.record_inventory_adjusted("sku-1", 1.0);
962
963 let snapshot = metrics.snapshot();
964 assert!(snapshot.enabled);
965 assert_eq!(snapshot.orders_created, 1);
966 assert_eq!(snapshot.customers_created, 1);
967 assert_eq!(snapshot.products_created, 1);
968 assert_eq!(snapshot.returns_requested, 1);
969 assert_eq!(snapshot.carts_created, 1);
970 assert_eq!(snapshot.cart_checkouts_completed, 1);
971 assert_eq!(snapshot.shipments_created, 1);
972 assert_eq!(snapshot.shipments_delivered, 1);
973 assert_eq!(snapshot.subscriptions_created, 1);
974 assert_eq!(snapshot.payments_completed, 1);
975 assert_eq!(snapshot.inventory_adjustments, 2);
976 assert_close(snapshot.order_amount_total, 99.99);
977 assert_close(snapshot.payment_amount_total, 99.99);
978 assert_close(snapshot.inventory_delta_total, -1.0);
979 assert_eq!(snapshot.red_global.requests, 0);
980 }
981
982 #[test]
983 fn does_not_record_when_disabled() {
984 let metrics = init_metrics(MetricsConfig { enabled: false });
985 metrics.record_order_created("cust-1", 50.0);
986 metrics.record_customer_created("cust-1");
987 metrics.record_product_created("prod-1");
988 metrics.record_return_requested("ret-1");
989 metrics.record_cart_created("cart-1");
990 metrics.record_cart_checkout_completed("cart-1", "ord-1");
991 metrics.record_shipment_created("shp-1");
992 metrics.record_shipment_delivered("shp-1");
993 metrics.record_subscription_created("sub-1");
994 metrics.record_payment_completed("pay-1", 50.0);
995 metrics.record_inventory_adjusted("sku-1", 3.0);
996 metrics.record_request_success("order.create", Duration::from_millis(25));
997
998 let snapshot = metrics.snapshot();
999 assert!(!snapshot.enabled);
1000 assert_eq!(snapshot.orders_created, 0);
1001 assert_eq!(snapshot.customers_created, 0);
1002 assert_eq!(snapshot.products_created, 0);
1003 assert_eq!(snapshot.returns_requested, 0);
1004 assert_eq!(snapshot.carts_created, 0);
1005 assert_eq!(snapshot.cart_checkouts_completed, 0);
1006 assert_eq!(snapshot.shipments_created, 0);
1007 assert_eq!(snapshot.shipments_delivered, 0);
1008 assert_eq!(snapshot.subscriptions_created, 0);
1009 assert_eq!(snapshot.payments_completed, 0);
1010 assert_eq!(snapshot.inventory_adjustments, 0);
1011 assert_close(snapshot.order_amount_total, 0.0);
1012 assert_close(snapshot.payment_amount_total, 0.0);
1013 assert_close(snapshot.inventory_delta_total, 0.0);
1014 assert_eq!(snapshot.red_global.requests, 0);
1015 assert!(snapshot.red_by_operation.is_empty());
1016 }
1017
1018 #[test]
1019 fn can_toggle_collection_runtime() {
1020 let metrics = init_metrics(MetricsConfig { enabled: true });
1021 metrics.record_order_created("cust-1", 10.0);
1022 metrics.record_customer_created("cust-1");
1023 metrics.record_product_created("prod-1");
1024 metrics.record_cart_created("cart-1");
1025 metrics.record_shipment_created("shp-1");
1026 metrics.record_subscription_created("sub-1");
1027
1028 metrics.set_enabled(false);
1029 metrics.record_order_created("cust-2", 20.0);
1030 metrics.record_customer_created("cust-2");
1031 metrics.record_product_created("prod-2");
1032 metrics.record_return_requested("ret-1");
1033 metrics.record_cart_created("cart-2");
1034 metrics.record_cart_checkout_completed("cart-1", "ord-1");
1035 metrics.record_shipment_created("shp-2");
1036 metrics.record_shipment_delivered("shp-1");
1037 metrics.record_subscription_created("sub-2");
1038 metrics.record_payment_completed("pay-2", 20.0);
1039 metrics.record_request_error("order.create", Duration::from_millis(100));
1040
1041 let snapshot = metrics.snapshot();
1042 assert!(!snapshot.enabled);
1043 assert_eq!(snapshot.orders_created, 1);
1044 assert_eq!(snapshot.customers_created, 1);
1045 assert_eq!(snapshot.products_created, 1);
1046 assert_eq!(snapshot.returns_requested, 0);
1047 assert_eq!(snapshot.carts_created, 1);
1048 assert_eq!(snapshot.cart_checkouts_completed, 0);
1049 assert_eq!(snapshot.shipments_created, 1);
1050 assert_eq!(snapshot.shipments_delivered, 0);
1051 assert_eq!(snapshot.subscriptions_created, 1);
1052 assert_eq!(snapshot.payments_completed, 0);
1053 assert_close(snapshot.order_amount_total, 10.0);
1054 assert_eq!(snapshot.red_global.requests, 0);
1055 }
1056
1057 #[test]
1058 fn records_red_metrics_with_normalized_operation_labels() {
1059 let metrics = init_metrics(MetricsConfig { enabled: true });
1060 metrics.record_request_success("Order Created", Duration::from_millis(120));
1061 metrics.record_request_error("Order Created", Duration::from_millis(220));
1062 metrics.record_request_success("payment/authorize", Duration::from_millis(80));
1063
1064 let snapshot = metrics.snapshot();
1065 assert_eq!(snapshot.red_global.requests, 3);
1066 assert_eq!(snapshot.red_global.errors, 1);
1067 assert_close(snapshot.red_global.error_rate, 1.0 / 3.0);
1068 assert_close(snapshot.red_global.avg_duration_ms, 140.0);
1069 assert!(snapshot.red_global.p50_ms > 0.0);
1070 assert!(snapshot.red_global.p95_ms >= snapshot.red_global.p50_ms);
1071
1072 let order = snapshot.red_by_operation.get("order_created").unwrap();
1073 assert_eq!(order.requests, 2);
1074 assert_eq!(order.errors, 1);
1075 assert_close(order.avg_duration_ms, 170.0);
1076
1077 let payment = snapshot.red_by_operation.get("payment_authorize").unwrap();
1078 assert_eq!(payment.requests, 1);
1079 assert_eq!(payment.errors, 0);
1080 assert_close(payment.avg_duration_ms, 80.0);
1081 }
1082
1083 #[test]
1084 fn slo_evaluation_passes_and_fails() {
1085 let metrics = init_metrics(MetricsConfig { enabled: true });
1086 metrics.record_request_success("checkout", Duration::from_millis(100));
1087 metrics.record_request_success("checkout", Duration::from_millis(120));
1088 metrics.record_request_error("checkout", Duration::from_millis(180));
1089
1090 let snapshot = metrics.snapshot();
1091 let pass_target =
1092 SloTarget { min_success_rate: 0.60, max_avg_latency_ms: 200.0, min_requests: 3 };
1093 let pass = snapshot.evaluate_operation_slo("checkout", pass_target).unwrap();
1094 assert!(pass.passed);
1095
1096 let fail_target =
1097 SloTarget { min_success_rate: 0.90, max_avg_latency_ms: 120.0, min_requests: 3 };
1098 let fail = snapshot.evaluate_operation_slo("checkout", fail_target).unwrap();
1099 assert!(!fail.passed);
1100 assert!(fail.reason.is_some());
1101 }
1102
1103 #[test]
1104 fn slo_evaluation_requires_minimum_samples() {
1105 let metrics = init_metrics(MetricsConfig { enabled: true });
1106 metrics.record_request_success("inventory.sync", Duration::from_millis(20));
1107
1108 let snapshot = metrics.snapshot();
1109 let report = snapshot
1110 .evaluate_operation_slo(
1111 "inventory.sync",
1112 SloTarget { min_success_rate: 0.99, max_avg_latency_ms: 50.0, min_requests: 10 },
1113 )
1114 .unwrap();
1115 assert!(!report.passed);
1116 assert!(report.reason.unwrap().contains("insufficient requests"));
1117 }
1118
1119 #[test]
1120 fn agentic_counters_record() {
1121 let metrics = init_metrics(MetricsConfig::default());
1122 metrics.record_a2a_quote_created();
1123 metrics.record_a2a_quote_created();
1124 metrics.record_a2a_purchase_created();
1125 metrics.record_x402_intent_created();
1126 metrics.record_x402_intent_settled();
1127 metrics.record_policy_evaluation(false);
1128 metrics.record_policy_evaluation(true);
1129 metrics.record_agent_registration();
1130 metrics.record_webhook_delivery(false);
1131 metrics.record_webhook_delivery(true);
1132
1133 let snap = metrics.snapshot();
1134 assert_eq!(snap.a2a_quotes_created, 2);
1135 assert_eq!(snap.a2a_purchases_created, 1);
1136 assert_eq!(snap.x402_intents_created, 1);
1137 assert_eq!(snap.x402_intents_settled, 1);
1138 assert_eq!(snap.policy_evaluations, 2);
1139 assert_eq!(snap.policy_denials, 1);
1140 assert_eq!(snap.agent_registrations, 1);
1141 assert_eq!(snap.webhook_deliveries, 2);
1142 assert_eq!(snap.webhook_failures, 1);
1143 }
1144
1145 #[test]
1146 fn histogram_empty() {
1147 let h = LatencyHistogram::new();
1148 assert_eq!(h.count(), 0);
1149 assert_eq!(h.percentile(0.5), 0.0);
1150 assert_eq!(h.percentile(0.99), 0.0);
1151 }
1152
1153 #[test]
1154 fn histogram_single_value() {
1155 let mut h = LatencyHistogram::new();
1156 h.record(5000); assert_eq!(h.count(), 1);
1158 assert_eq!(h.percentile(0.5), 5.0);
1159 assert_eq!(h.percentile(0.99), 5.0);
1160 }
1161
1162 #[test]
1163 fn histogram_percentiles() {
1164 let mut h = LatencyHistogram::new();
1165 for i in 1..=100 {
1166 h.record(i * 1000);
1167 }
1168 assert_eq!(h.count(), 100);
1169 let p50 = h.percentile(0.50);
1170 let p95 = h.percentile(0.95);
1171 let p99 = h.percentile(0.99);
1172 assert!((49.0..=51.0).contains(&p50), "p50 was {p50}");
1173 assert!((94.0..=96.0).contains(&p95), "p95 was {p95}");
1174 assert!((98.0..=100.0).contains(&p99), "p99 was {p99}");
1175 }
1176
1177 #[test]
1178 fn histogram_clamps_percentile() {
1179 let mut h = LatencyHistogram::new();
1180 h.record(1000);
1181 assert_eq!(h.percentile(-1.0), 1.0);
1182 assert_eq!(h.percentile(2.0), 1.0);
1183 }
1184
1185 #[test]
1186 fn histogram_memory_is_bounded_and_count_exact() {
1187 let mut h = LatencyHistogram::new();
1188 for i in 0..100_000u64 {
1189 h.record(i);
1190 }
1191 assert_eq!(h.count(), 100_000);
1193 assert_eq!(h.cumulative_buckets().len(), LATENCY_BUCKET_COUNT);
1194 }
1195
1196 #[test]
1197 fn histogram_bucket_math_known_values() {
1198 let mut h = LatencyHistogram::new();
1199 h.record(500); h.record(1_000); h.record(3_000); h.record(70_000); h.record(20_000_000); assert_eq!(h.count(), 5);
1206 assert_eq!(h.sum_micros(), 500 + 1_000 + 3_000 + 70_000 + 20_000_000);
1207
1208 let buckets = h.cumulative_buckets();
1209 let get = |bound: f64| {
1210 buckets
1211 .iter()
1212 .find(|(b, _)| (*b - bound).abs() < 1e-12)
1213 .map(|(_, c)| *c)
1214 .expect("bucket bound present")
1215 };
1216 assert_eq!(get(0.001), 2);
1217 assert_eq!(get(0.0025), 2);
1218 assert_eq!(get(0.005), 3);
1219 assert_eq!(get(0.05), 3);
1220 assert_eq!(get(0.1), 4);
1221 assert_eq!(get(10.0), 4);
1222 let (last_bound, last_count) = buckets[buckets.len() - 1];
1224 assert!(last_bound.is_infinite());
1225 assert_eq!(last_count, 5);
1226 for pair in buckets.windows(2) {
1228 assert!(pair[1].1 >= pair[0].1);
1229 }
1230 }
1231
1232 #[test]
1233 fn red_snapshot_includes_percentiles() {
1234 let metrics = init_metrics(MetricsConfig::default());
1235 metrics.record_request_success("test.op", Duration::from_millis(10));
1236 metrics.record_request_success("test.op", Duration::from_millis(50));
1237 metrics.record_request_success("test.op", Duration::from_millis(100));
1238
1239 let snap = metrics.snapshot();
1240 if let Some(red) = snap.red_by_operation.get("test_op") {
1241 assert_eq!(red.requests, 3);
1242 assert!(red.p50_ms > 0.0, "p50 should be > 0");
1243 } else {
1244 panic!("Expected test_op in red_by_operation");
1245 }
1246 }
1247
1248 #[test]
1249 fn ignores_non_finite_metric_totals() {
1250 let metrics = init_metrics(MetricsConfig::default());
1251 metrics.record_order_created("cust-1", f64::NAN);
1252 metrics.record_payment_completed("pay-1", f64::INFINITY);
1253 metrics.record_inventory_adjusted("sku-1", f64::NEG_INFINITY);
1254
1255 let snap = metrics.snapshot();
1256 assert_eq!(snap.orders_created, 1);
1257 assert_eq!(snap.payments_completed, 1);
1258 assert_eq!(snap.inventory_adjustments, 1);
1259 assert_close(snap.order_amount_total, 0.0);
1260 assert_close(snap.payment_amount_total, 0.0);
1261 assert_close(snap.inventory_delta_total, 0.0);
1262 }
1263
1264 #[test]
1267 fn metrics_config_default_is_enabled() {
1268 let cfg = MetricsConfig::default();
1269 assert!(cfg.enabled);
1270 }
1271
1272 #[test]
1273 fn metrics_config_disabled() {
1274 let cfg = MetricsConfig { enabled: false };
1275 assert!(!cfg.enabled);
1276 }
1277
1278 #[test]
1279 fn metrics_config_debug() {
1280 let cfg = MetricsConfig::default();
1281 let debug = format!("{cfg:?}");
1282 assert!(debug.contains("MetricsConfig"));
1283 assert!(debug.contains("true"));
1284 }
1285
1286 #[test]
1287 fn metrics_config_clone() {
1288 let cfg = MetricsConfig { enabled: false };
1289 #[allow(clippy::redundant_clone)]
1290 let cloned = cfg.clone();
1291 assert!(!cloned.enabled);
1292 }
1293
1294 #[test]
1297 fn metrics_default_is_enabled() {
1298 let m = Metrics::default();
1299 assert!(m.is_enabled());
1300 }
1301
1302 #[test]
1303 fn metrics_debug_format() {
1304 let m = init_metrics(MetricsConfig::default());
1305 let debug = format!("{m:?}");
1306 assert!(debug.contains("Metrics"));
1307 }
1308
1309 #[test]
1310 fn metrics_clone_shares_state() {
1311 let m = init_metrics(MetricsConfig::default());
1312 let m2 = m.clone();
1313 m.record_order_created("c", 10.0);
1314 let snap = m2.snapshot();
1315 assert_eq!(snap.orders_created, 1);
1316 }
1317
1318 #[test]
1321 fn set_enabled_true_to_false() {
1322 let m = init_metrics(MetricsConfig { enabled: true });
1323 assert!(m.is_enabled());
1324 m.set_enabled(false);
1325 assert!(!m.is_enabled());
1326 }
1327
1328 #[test]
1329 fn set_enabled_false_to_true() {
1330 let m = init_metrics(MetricsConfig { enabled: false });
1331 assert!(!m.is_enabled());
1332 m.set_enabled(true);
1333 assert!(m.is_enabled());
1334 }
1335
1336 #[test]
1337 fn set_enabled_idempotent() {
1338 let m = init_metrics(MetricsConfig { enabled: true });
1339 m.set_enabled(true);
1340 m.set_enabled(true);
1341 assert!(m.is_enabled());
1342 }
1343
1344 #[test]
1347 fn disabled_does_not_record_a2a_counters() {
1348 let m = init_metrics(MetricsConfig { enabled: false });
1349 m.record_a2a_quote_created();
1350 m.record_a2a_purchase_created();
1351 m.record_x402_intent_created();
1352 m.record_x402_intent_settled();
1353 let snap = m.snapshot();
1354 assert_eq!(snap.a2a_quotes_created, 0);
1355 assert_eq!(snap.a2a_purchases_created, 0);
1356 assert_eq!(snap.x402_intents_created, 0);
1357 assert_eq!(snap.x402_intents_settled, 0);
1358 }
1359
1360 #[test]
1361 fn disabled_does_not_record_policy_counters() {
1362 let m = init_metrics(MetricsConfig { enabled: false });
1363 m.record_policy_evaluation(false);
1364 m.record_policy_evaluation(true);
1365 let snap = m.snapshot();
1366 assert_eq!(snap.policy_evaluations, 0);
1367 assert_eq!(snap.policy_denials, 0);
1368 }
1369
1370 #[test]
1371 fn disabled_does_not_record_agent_registration() {
1372 let m = init_metrics(MetricsConfig { enabled: false });
1373 m.record_agent_registration();
1374 assert_eq!(m.snapshot().agent_registrations, 0);
1375 }
1376
1377 #[test]
1378 fn disabled_does_not_record_webhook_delivery() {
1379 let m = init_metrics(MetricsConfig { enabled: false });
1380 m.record_webhook_delivery(false);
1381 m.record_webhook_delivery(true);
1382 let snap = m.snapshot();
1383 assert_eq!(snap.webhook_deliveries, 0);
1384 assert_eq!(snap.webhook_failures, 0);
1385 }
1386
1387 #[test]
1388 fn disabled_does_not_record_request_error() {
1389 let m = init_metrics(MetricsConfig { enabled: false });
1390 m.record_request_error("op", Duration::from_millis(100));
1391 let snap = m.snapshot();
1392 assert_eq!(snap.red_global.requests, 0);
1393 assert_eq!(snap.red_global.errors, 0);
1394 }
1395
1396 #[test]
1399 fn red_snapshot_zero_requests() {
1400 let snap = RedSnapshot::from_counts(0, 0, 0);
1401 assert_eq!(snap.requests, 0);
1402 assert_eq!(snap.errors, 0);
1403 assert_close(snap.error_rate, 0.0);
1404 assert_close(snap.avg_duration_ms, 0.0);
1405 assert_close(snap.duration_total_ms, 0.0);
1406 }
1407
1408 #[test]
1409 fn red_snapshot_all_errors() {
1410 let snap = RedSnapshot::from_counts(5, 5, 500_000);
1411 assert_eq!(snap.requests, 5);
1412 assert_eq!(snap.errors, 5);
1413 assert_close(snap.error_rate, 1.0);
1414 assert_close(snap.avg_duration_ms, 100.0);
1415 }
1416
1417 #[test]
1418 fn red_snapshot_no_errors() {
1419 let snap = RedSnapshot::from_counts(10, 0, 1_000_000);
1420 assert_close(snap.error_rate, 0.0);
1421 assert_close(snap.avg_duration_ms, 100.0);
1422 }
1423
1424 #[test]
1425 fn red_snapshot_partial_errors() {
1426 let snap = RedSnapshot::from_counts(4, 1, 400_000);
1427 assert_close(snap.error_rate, 0.25);
1428 assert_close(snap.avg_duration_ms, 100.0);
1429 }
1430
1431 #[test]
1432 fn red_snapshot_duration_total_ms_conversion() {
1433 let snap = RedSnapshot::from_counts(3, 0, 1_500_000);
1435 assert_close(snap.duration_total_ms, 1500.0);
1436 assert_close(snap.avg_duration_ms, 500.0);
1437 }
1438
1439 #[test]
1440 fn red_snapshot_single_request() {
1441 let snap = RedSnapshot::from_counts(1, 0, 42_000);
1442 assert_close(snap.error_rate, 0.0);
1443 assert_close(snap.avg_duration_ms, 42.0);
1444 }
1445
1446 #[test]
1447 fn red_snapshot_clone_eq() {
1448 let snap = RedSnapshot::from_counts(10, 2, 500_000);
1449 let snap2 = snap.clone();
1450 assert_eq!(snap, snap2);
1451 }
1452
1453 #[test]
1454 fn red_snapshot_debug() {
1455 let snap = RedSnapshot::from_counts(1, 0, 1000);
1456 let debug = format!("{snap:?}");
1457 assert!(debug.contains("RedSnapshot"));
1458 }
1459
1460 #[test]
1461 fn red_snapshot_default_percentiles_zero() {
1462 let snap = RedSnapshot::from_counts(5, 1, 100_000);
1463 assert_close(snap.p50_ms, 0.0);
1464 assert_close(snap.p95_ms, 0.0);
1465 assert_close(snap.p99_ms, 0.0);
1466 }
1467
1468 #[test]
1471 fn slo_evaluation_zero_requests() {
1472 let snap = RedSnapshot::from_counts(0, 0, 0);
1473 let target =
1474 SloTarget { min_success_rate: 0.99, max_avg_latency_ms: 100.0, min_requests: 1 };
1475 let eval = snap.evaluate_slo(target);
1476 assert!(!eval.passed);
1477 assert!(eval.reason.unwrap().contains("insufficient requests"));
1478 }
1479
1480 #[test]
1481 fn slo_evaluation_min_requests_zero_threshold() {
1482 let snap = RedSnapshot::from_counts(0, 0, 0);
1483 let target =
1484 SloTarget { min_success_rate: 0.0, max_avg_latency_ms: f64::MAX, min_requests: 0 };
1485 let eval = snap.evaluate_slo(target);
1486 assert!(eval.passed);
1488 }
1489
1490 #[test]
1491 fn slo_evaluation_exactly_at_threshold() {
1492 let snap = RedSnapshot::from_counts(10, 1, 1_000_000);
1494 let target =
1495 SloTarget { min_success_rate: 0.9, max_avg_latency_ms: 100.0, min_requests: 10 };
1496 let eval = snap.evaluate_slo(target);
1497 assert!(eval.passed, "Should pass when success_rate == min_success_rate");
1498 assert!(eval.reason.is_none());
1499 }
1500
1501 #[test]
1502 fn slo_evaluation_just_below_success_threshold() {
1503 let snap = RedSnapshot::from_counts(100, 11, 10_000_000);
1505 let target =
1506 SloTarget { min_success_rate: 0.9, max_avg_latency_ms: 200.0, min_requests: 10 };
1507 let eval = snap.evaluate_slo(target);
1508 assert!(!eval.passed);
1509 assert!(eval.reason.unwrap().contains("success rate"));
1510 }
1511
1512 #[test]
1513 fn slo_evaluation_latency_just_above_threshold() {
1514 let snap = RedSnapshot::from_counts(10, 0, 2_000_000);
1516 let target =
1517 SloTarget { min_success_rate: 0.9, max_avg_latency_ms: 199.0, min_requests: 5 };
1518 let eval = snap.evaluate_slo(target);
1519 assert!(!eval.passed);
1520 assert!(eval.reason.unwrap().contains("avg latency"));
1521 }
1522
1523 #[test]
1524 fn slo_evaluation_latency_exactly_at_threshold() {
1525 let snap = RedSnapshot::from_counts(10, 0, 1_000_000);
1526 let target =
1527 SloTarget { min_success_rate: 0.9, max_avg_latency_ms: 100.0, min_requests: 5 };
1528 let eval = snap.evaluate_slo(target);
1529 assert!(eval.passed);
1530 }
1531
1532 #[test]
1533 fn slo_evaluation_all_errors() {
1534 let snap = RedSnapshot::from_counts(10, 10, 500_000);
1535 let target =
1536 SloTarget { min_success_rate: 0.01, max_avg_latency_ms: 1000.0, min_requests: 1 };
1537 let eval = snap.evaluate_slo(target);
1538 assert!(!eval.passed);
1539 assert!(eval.reason.unwrap().contains("success rate"));
1540 }
1541
1542 #[test]
1543 fn slo_evaluation_report_fields() {
1544 let snap = RedSnapshot::from_counts(20, 4, 2_000_000);
1545 let target =
1546 SloTarget { min_success_rate: 0.5, max_avg_latency_ms: 200.0, min_requests: 10 };
1547 let eval = snap.evaluate_slo(target);
1548 assert!(eval.passed);
1549 assert_eq!(eval.requests, 20);
1550 assert_close(eval.error_rate, 0.2);
1551 assert_close(eval.success_rate, 0.8);
1552 assert_close(eval.avg_duration_ms, 100.0);
1553 assert!(eval.reason.is_none());
1554 }
1555
1556 #[test]
1557 fn slo_default_target_values() {
1558 let target = SloTarget::default();
1559 assert_close(target.min_success_rate, 0.99);
1560 assert_close(target.max_avg_latency_ms, 250.0);
1561 assert_eq!(target.min_requests, 100);
1562 }
1563
1564 #[test]
1565 fn slo_target_debug() {
1566 let target = SloTarget::default();
1567 let debug = format!("{target:?}");
1568 assert!(debug.contains("SloTarget"));
1569 }
1570
1571 #[test]
1572 fn slo_target_clone() {
1573 let target =
1574 SloTarget { min_success_rate: 0.95, max_avg_latency_ms: 100.0, min_requests: 50 };
1575 let cloned = target;
1576 assert_eq!(target, cloned);
1577 }
1578
1579 #[test]
1580 fn slo_evaluation_debug() {
1581 let snap = RedSnapshot::from_counts(5, 0, 50_000);
1582 let eval = snap.evaluate_slo(SloTarget::default());
1583 let debug = format!("{eval:?}");
1584 assert!(debug.contains("SloEvaluation"));
1585 }
1586
1587 #[test]
1588 fn slo_evaluation_clone_eq() {
1589 let snap = RedSnapshot::from_counts(100, 1, 1_000_000);
1590 let eval1 = snap.evaluate_slo(SloTarget::default());
1591 let eval2 = eval1.clone();
1592 assert_eq!(eval1, eval2);
1593 }
1594
1595 #[test]
1598 fn evaluate_global_slo_passes() {
1599 let m = init_metrics(MetricsConfig::default());
1600 for _ in 0..100 {
1601 m.record_request_success("op", Duration::from_millis(10));
1602 }
1603 let snap = m.snapshot();
1604 let target =
1605 SloTarget { min_success_rate: 0.99, max_avg_latency_ms: 50.0, min_requests: 100 };
1606 let eval = snap.evaluate_global_slo(target);
1607 assert!(eval.passed);
1608 }
1609
1610 #[test]
1611 fn evaluate_global_slo_fails_on_errors() {
1612 let m = init_metrics(MetricsConfig::default());
1613 for _ in 0..90 {
1614 m.record_request_success("op", Duration::from_millis(10));
1615 }
1616 for _ in 0..10 {
1617 m.record_request_error("op", Duration::from_millis(10));
1618 }
1619 let snap = m.snapshot();
1620 let target =
1621 SloTarget { min_success_rate: 0.95, max_avg_latency_ms: 50.0, min_requests: 50 };
1622 let eval = snap.evaluate_global_slo(target);
1623 assert!(!eval.passed);
1624 }
1625
1626 #[test]
1629 fn evaluate_operation_slo_returns_none_for_unknown() {
1630 let m = init_metrics(MetricsConfig::default());
1631 let snap = m.snapshot();
1632 let result = snap.evaluate_operation_slo("nonexistent", SloTarget::default());
1633 assert!(result.is_none());
1634 }
1635
1636 #[test]
1637 fn evaluate_operation_slo_normalizes_key() {
1638 let m = init_metrics(MetricsConfig::default());
1639 m.record_request_success("Order Created", Duration::from_millis(10));
1640 let snap = m.snapshot();
1641 let result = snap.evaluate_operation_slo(
1643 "order-created",
1644 SloTarget { min_success_rate: 0.5, max_avg_latency_ms: 100.0, min_requests: 1 },
1645 );
1646 assert!(result.is_some());
1647 assert!(result.unwrap().passed);
1648 }
1649
1650 #[test]
1653 fn multiple_operations_independent() {
1654 let m = init_metrics(MetricsConfig::default());
1655 m.record_request_success("order.create", Duration::from_millis(50));
1656 m.record_request_error("payment.charge", Duration::from_millis(200));
1657 m.record_request_success("inventory.check", Duration::from_millis(5));
1658
1659 let snap = m.snapshot();
1660 assert_eq!(snap.red_by_operation.len(), 3);
1661
1662 let order = snap.red_by_operation.get("order_create").unwrap();
1663 assert_eq!(order.requests, 1);
1664 assert_eq!(order.errors, 0);
1665
1666 let payment = snap.red_by_operation.get("payment_charge").unwrap();
1667 assert_eq!(payment.requests, 1);
1668 assert_eq!(payment.errors, 1);
1669
1670 let inv = snap.red_by_operation.get("inventory_check").unwrap();
1671 assert_eq!(inv.requests, 1);
1672 assert_eq!(inv.errors, 0);
1673 }
1674
1675 #[test]
1676 fn same_operation_accumulates() {
1677 let m = init_metrics(MetricsConfig::default());
1678 for _ in 0..50 {
1679 m.record_request_success("checkout", Duration::from_millis(20));
1680 }
1681 for _ in 0..5 {
1682 m.record_request_error("checkout", Duration::from_millis(100));
1683 }
1684 let snap = m.snapshot();
1685 let checkout = snap.red_by_operation.get("checkout").unwrap();
1686 assert_eq!(checkout.requests, 55);
1687 assert_eq!(checkout.errors, 5);
1688 assert_close(checkout.error_rate, 5.0 / 55.0);
1689 }
1690
1691 #[test]
1694 fn snapshot_is_point_in_time() {
1695 let m = init_metrics(MetricsConfig::default());
1696 m.record_order_created("c", 10.0);
1697 let snap1 = m.snapshot();
1698
1699 m.record_order_created("c", 20.0);
1701 m.record_order_created("c", 30.0);
1702
1703 assert_eq!(snap1.orders_created, 1);
1705 assert_close(snap1.order_amount_total, 10.0);
1706
1707 let snap2 = m.snapshot();
1709 assert_eq!(snap2.orders_created, 3);
1710 assert_close(snap2.order_amount_total, 60.0);
1711 }
1712
1713 #[test]
1714 fn snapshot_red_isolation() {
1715 let m = init_metrics(MetricsConfig::default());
1716 m.record_request_success("op", Duration::from_millis(10));
1717 let snap1 = m.snapshot();
1718
1719 m.record_request_error("op", Duration::from_millis(200));
1720 let snap2 = m.snapshot();
1721
1722 assert_eq!(snap1.red_global.requests, 1);
1723 assert_eq!(snap1.red_global.errors, 0);
1724 assert_eq!(snap2.red_global.requests, 2);
1725 assert_eq!(snap2.red_global.errors, 1);
1726 }
1727
1728 #[test]
1731 fn record_order_zero_amount() {
1732 let m = init_metrics(MetricsConfig::default());
1733 m.record_order_created("c", 0.0);
1734 let snap = m.snapshot();
1735 assert_eq!(snap.orders_created, 1);
1736 assert_close(snap.order_amount_total, 0.0);
1737 }
1738
1739 #[test]
1740 fn record_order_large_amount() {
1741 let m = init_metrics(MetricsConfig::default());
1742 m.record_order_created("c", 1_000_000.99);
1743 let snap = m.snapshot();
1744 assert_close(snap.order_amount_total, 1_000_000.99);
1745 }
1746
1747 #[test]
1748 fn record_order_negative_amount() {
1749 let m = init_metrics(MetricsConfig::default());
1750 m.record_order_created("c", -50.0);
1751 let snap = m.snapshot();
1752 assert_eq!(snap.orders_created, 1);
1753 assert_close(snap.order_amount_total, -50.0);
1754 }
1755
1756 #[test]
1757 fn record_payment_zero_amount() {
1758 let m = init_metrics(MetricsConfig::default());
1759 m.record_payment_completed("p", 0.0);
1760 let snap = m.snapshot();
1761 assert_eq!(snap.payments_completed, 1);
1762 assert_close(snap.payment_amount_total, 0.0);
1763 }
1764
1765 #[test]
1766 fn record_payment_large_amount() {
1767 let m = init_metrics(MetricsConfig::default());
1768 m.record_payment_completed("p", 999_999.99);
1769 let snap = m.snapshot();
1770 assert_close(snap.payment_amount_total, 999_999.99);
1771 }
1772
1773 #[test]
1774 fn record_inventory_zero_delta() {
1775 let m = init_metrics(MetricsConfig::default());
1776 m.record_inventory_adjusted("sku", 0.0);
1777 let snap = m.snapshot();
1778 assert_eq!(snap.inventory_adjustments, 1);
1779 assert_close(snap.inventory_delta_total, 0.0);
1780 }
1781
1782 #[test]
1783 fn record_inventory_positive_delta() {
1784 let m = init_metrics(MetricsConfig::default());
1785 m.record_inventory_adjusted("sku", 100.0);
1786 let snap = m.snapshot();
1787 assert_close(snap.inventory_delta_total, 100.0);
1788 }
1789
1790 #[test]
1791 fn record_inventory_negative_delta() {
1792 let m = init_metrics(MetricsConfig::default());
1793 m.record_inventory_adjusted("sku", -5.5);
1794 let snap = m.snapshot();
1795 assert_close(snap.inventory_delta_total, -5.5);
1796 }
1797
1798 #[test]
1799 fn record_multiple_inventory_adjustments_accumulate() {
1800 let m = init_metrics(MetricsConfig::default());
1801 m.record_inventory_adjusted("a", 10.0);
1802 m.record_inventory_adjusted("b", -3.0);
1803 m.record_inventory_adjusted("c", 7.5);
1804 let snap = m.snapshot();
1805 assert_eq!(snap.inventory_adjustments, 3);
1806 assert_close(snap.inventory_delta_total, 14.5);
1807 }
1808
1809 #[test]
1810 fn record_multiple_orders_accumulate_amounts() {
1811 let m = init_metrics(MetricsConfig::default());
1812 m.record_order_created("c1", 10.0);
1813 m.record_order_created("c2", 20.0);
1814 m.record_order_created("c3", 30.0);
1815 let snap = m.snapshot();
1816 assert_eq!(snap.orders_created, 3);
1817 assert_close(snap.order_amount_total, 60.0);
1818 }
1819
1820 #[test]
1821 fn record_multiple_payments_accumulate_amounts() {
1822 let m = init_metrics(MetricsConfig::default());
1823 m.record_payment_completed("p1", 100.0);
1824 m.record_payment_completed("p2", 200.0);
1825 let snap = m.snapshot();
1826 assert_eq!(snap.payments_completed, 2);
1827 assert_close(snap.payment_amount_total, 300.0);
1828 }
1829
1830 #[test]
1833 fn record_customer_created_counts() {
1834 let m = init_metrics(MetricsConfig::default());
1835 m.record_customer_created("c1");
1836 m.record_customer_created("c2");
1837 m.record_customer_created("c3");
1838 assert_eq!(m.snapshot().customers_created, 3);
1839 }
1840
1841 #[test]
1842 fn record_product_created_counts() {
1843 let m = init_metrics(MetricsConfig::default());
1844 m.record_product_created("p1");
1845 m.record_product_created("p2");
1846 assert_eq!(m.snapshot().products_created, 2);
1847 }
1848
1849 #[test]
1850 fn record_return_requested_counts() {
1851 let m = init_metrics(MetricsConfig::default());
1852 m.record_return_requested("r1");
1853 m.record_return_requested("r2");
1854 m.record_return_requested("r3");
1855 m.record_return_requested("r4");
1856 assert_eq!(m.snapshot().returns_requested, 4);
1857 }
1858
1859 #[test]
1860 fn record_cart_created_counts() {
1861 let m = init_metrics(MetricsConfig::default());
1862 for _ in 0..10 {
1863 m.record_cart_created("cart");
1864 }
1865 assert_eq!(m.snapshot().carts_created, 10);
1866 }
1867
1868 #[test]
1869 fn record_cart_checkout_completed_counts() {
1870 let m = init_metrics(MetricsConfig::default());
1871 m.record_cart_checkout_completed("cart1", "ord1");
1872 m.record_cart_checkout_completed("cart2", "ord2");
1873 assert_eq!(m.snapshot().cart_checkouts_completed, 2);
1874 }
1875
1876 #[test]
1877 fn record_shipment_created_counts() {
1878 let m = init_metrics(MetricsConfig::default());
1879 m.record_shipment_created("s1");
1880 m.record_shipment_created("s2");
1881 m.record_shipment_created("s3");
1882 assert_eq!(m.snapshot().shipments_created, 3);
1883 }
1884
1885 #[test]
1886 fn record_shipment_delivered_counts() {
1887 let m = init_metrics(MetricsConfig::default());
1888 m.record_shipment_delivered("s1");
1889 assert_eq!(m.snapshot().shipments_delivered, 1);
1890 }
1891
1892 #[test]
1893 fn record_subscription_created_counts() {
1894 let m = init_metrics(MetricsConfig::default());
1895 m.record_subscription_created("sub1");
1896 m.record_subscription_created("sub2");
1897 assert_eq!(m.snapshot().subscriptions_created, 2);
1898 }
1899
1900 #[test]
1901 fn record_a2a_quotes_multiple() {
1902 let m = init_metrics(MetricsConfig::default());
1903 for _ in 0..5 {
1904 m.record_a2a_quote_created();
1905 }
1906 assert_eq!(m.snapshot().a2a_quotes_created, 5);
1907 }
1908
1909 #[test]
1910 fn record_a2a_purchases_multiple() {
1911 let m = init_metrics(MetricsConfig::default());
1912 for _ in 0..3 {
1913 m.record_a2a_purchase_created();
1914 }
1915 assert_eq!(m.snapshot().a2a_purchases_created, 3);
1916 }
1917
1918 #[test]
1919 fn record_x402_intents_created_multiple() {
1920 let m = init_metrics(MetricsConfig::default());
1921 m.record_x402_intent_created();
1922 m.record_x402_intent_created();
1923 assert_eq!(m.snapshot().x402_intents_created, 2);
1924 }
1925
1926 #[test]
1927 fn record_x402_intents_settled_multiple() {
1928 let m = init_metrics(MetricsConfig::default());
1929 m.record_x402_intent_settled();
1930 m.record_x402_intent_settled();
1931 m.record_x402_intent_settled();
1932 assert_eq!(m.snapshot().x402_intents_settled, 3);
1933 }
1934
1935 #[test]
1936 fn record_policy_evaluations_all_allowed() {
1937 let m = init_metrics(MetricsConfig::default());
1938 for _ in 0..10 {
1939 m.record_policy_evaluation(false);
1940 }
1941 let snap = m.snapshot();
1942 assert_eq!(snap.policy_evaluations, 10);
1943 assert_eq!(snap.policy_denials, 0);
1944 }
1945
1946 #[test]
1947 fn record_policy_evaluations_all_denied() {
1948 let m = init_metrics(MetricsConfig::default());
1949 for _ in 0..5 {
1950 m.record_policy_evaluation(true);
1951 }
1952 let snap = m.snapshot();
1953 assert_eq!(snap.policy_evaluations, 5);
1954 assert_eq!(snap.policy_denials, 5);
1955 }
1956
1957 #[test]
1958 fn record_policy_evaluations_mixed() {
1959 let m = init_metrics(MetricsConfig::default());
1960 m.record_policy_evaluation(false);
1961 m.record_policy_evaluation(true);
1962 m.record_policy_evaluation(false);
1963 m.record_policy_evaluation(true);
1964 m.record_policy_evaluation(false);
1965 let snap = m.snapshot();
1966 assert_eq!(snap.policy_evaluations, 5);
1967 assert_eq!(snap.policy_denials, 2);
1968 }
1969
1970 #[test]
1971 fn record_agent_registrations_multiple() {
1972 let m = init_metrics(MetricsConfig::default());
1973 for _ in 0..7 {
1974 m.record_agent_registration();
1975 }
1976 assert_eq!(m.snapshot().agent_registrations, 7);
1977 }
1978
1979 #[test]
1980 fn record_webhook_deliveries_all_success() {
1981 let m = init_metrics(MetricsConfig::default());
1982 for _ in 0..10 {
1983 m.record_webhook_delivery(false);
1984 }
1985 let snap = m.snapshot();
1986 assert_eq!(snap.webhook_deliveries, 10);
1987 assert_eq!(snap.webhook_failures, 0);
1988 }
1989
1990 #[test]
1991 fn record_webhook_deliveries_all_failed() {
1992 let m = init_metrics(MetricsConfig::default());
1993 for _ in 0..3 {
1994 m.record_webhook_delivery(true);
1995 }
1996 let snap = m.snapshot();
1997 assert_eq!(snap.webhook_deliveries, 3);
1998 assert_eq!(snap.webhook_failures, 3);
1999 }
2000
2001 #[test]
2002 fn record_webhook_deliveries_mixed() {
2003 let m = init_metrics(MetricsConfig::default());
2004 m.record_webhook_delivery(false);
2005 m.record_webhook_delivery(true);
2006 m.record_webhook_delivery(false);
2007 let snap = m.snapshot();
2008 assert_eq!(snap.webhook_deliveries, 3);
2009 assert_eq!(snap.webhook_failures, 1);
2010 }
2011
2012 #[test]
2015 fn record_request_success_increments_global() {
2016 let m = init_metrics(MetricsConfig::default());
2017 m.record_request_success("op", Duration::from_millis(10));
2018 let snap = m.snapshot();
2019 assert_eq!(snap.red_global.requests, 1);
2020 assert_eq!(snap.red_global.errors, 0);
2021 }
2022
2023 #[test]
2024 fn record_request_error_increments_global_errors() {
2025 let m = init_metrics(MetricsConfig::default());
2026 m.record_request_error("op", Duration::from_millis(10));
2027 let snap = m.snapshot();
2028 assert_eq!(snap.red_global.requests, 1);
2029 assert_eq!(snap.red_global.errors, 1);
2030 }
2031
2032 #[test]
2033 fn record_request_zero_duration() {
2034 let m = init_metrics(MetricsConfig::default());
2035 m.record_request_success("op", Duration::ZERO);
2036 let snap = m.snapshot();
2037 assert_eq!(snap.red_global.requests, 1);
2038 assert_close(snap.red_global.avg_duration_ms, 0.0);
2039 }
2040
2041 #[test]
2042 fn record_request_large_duration() {
2043 let m = init_metrics(MetricsConfig::default());
2044 m.record_request_success("op", Duration::from_secs(60));
2045 let snap = m.snapshot();
2046 assert_close(snap.red_global.avg_duration_ms, 60_000.0);
2047 }
2048
2049 #[test]
2050 fn record_request_creates_operation_entry() {
2051 let m = init_metrics(MetricsConfig::default());
2052 m.record_request_success("my.custom.op", Duration::from_millis(5));
2053 let snap = m.snapshot();
2054 assert!(snap.red_by_operation.contains_key("my_custom_op"));
2055 }
2056
2057 #[test]
2058 fn record_request_mixed_success_and_error() {
2059 let m = init_metrics(MetricsConfig::default());
2060 m.record_request_success("op", Duration::from_millis(10));
2061 m.record_request_success("op", Duration::from_millis(20));
2062 m.record_request_error("op", Duration::from_millis(30));
2063 let snap = m.snapshot();
2064 let op = snap.red_by_operation.get("op").unwrap();
2065 assert_eq!(op.requests, 3);
2066 assert_eq!(op.errors, 1);
2067 assert_close(op.error_rate, 1.0 / 3.0);
2068 assert_close(op.avg_duration_ms, 20.0);
2069 }
2070
2071 #[test]
2074 fn snapshot_reflects_enabled_true() {
2075 let m = init_metrics(MetricsConfig { enabled: true });
2076 assert!(m.snapshot().enabled);
2077 }
2078
2079 #[test]
2080 fn snapshot_reflects_enabled_false() {
2081 let m = init_metrics(MetricsConfig { enabled: false });
2082 assert!(!m.snapshot().enabled);
2083 }
2084
2085 #[test]
2086 fn snapshot_reflects_runtime_toggle() {
2087 let m = init_metrics(MetricsConfig { enabled: true });
2088 assert!(m.snapshot().enabled);
2089 m.set_enabled(false);
2090 assert!(!m.snapshot().enabled);
2091 m.set_enabled(true);
2092 assert!(m.snapshot().enabled);
2093 }
2094
2095 #[test]
2098 fn metrics_snapshot_debug() {
2099 let m = init_metrics(MetricsConfig::default());
2100 let snap = m.snapshot();
2101 let debug = format!("{snap:?}");
2102 assert!(debug.contains("MetricsSnapshot"));
2103 }
2104
2105 #[test]
2106 fn metrics_snapshot_clone() {
2107 let m = init_metrics(MetricsConfig::default());
2108 m.record_order_created("c", 42.0);
2109 let snap1 = m.snapshot();
2110 let snap2 = snap1.clone();
2111 assert_eq!(snap1.orders_created, snap2.orders_created);
2112 assert_close(snap1.order_amount_total, snap2.order_amount_total);
2113 }
2114
2115 #[test]
2118 fn finite_metric_value_normal() {
2119 assert!(Metrics::is_finite_metric_value(0.0));
2120 assert!(Metrics::is_finite_metric_value(1.0));
2121 assert!(Metrics::is_finite_metric_value(-1.0));
2122 assert!(Metrics::is_finite_metric_value(f64::MAX));
2123 assert!(Metrics::is_finite_metric_value(f64::MIN));
2124 assert!(Metrics::is_finite_metric_value(f64::MIN_POSITIVE));
2125 }
2126
2127 #[test]
2128 fn finite_metric_value_non_finite() {
2129 assert!(!Metrics::is_finite_metric_value(f64::NAN));
2130 assert!(!Metrics::is_finite_metric_value(f64::INFINITY));
2131 assert!(!Metrics::is_finite_metric_value(f64::NEG_INFINITY));
2132 }
2133
2134 #[test]
2137 fn histogram_default_is_empty() {
2138 let h = LatencyHistogram::default();
2139 assert_eq!(h.count(), 0);
2140 }
2141
2142 #[test]
2143 fn histogram_two_values() {
2144 let mut h = LatencyHistogram::new();
2145 h.record(10_000); h.record(20_000); assert_eq!(h.count(), 2);
2148 let p50 = h.percentile(0.5);
2149 assert!((10.0..=20.0).contains(&p50), "p50={p50}");
2150 }
2151
2152 #[test]
2153 fn histogram_identical_values() {
2154 let mut h = LatencyHistogram::new();
2155 for _ in 0..100 {
2156 h.record(5_000);
2157 }
2158 assert_eq!(h.count(), 100);
2159 let p50 = h.percentile(0.5);
2161 let p99 = h.percentile(0.99);
2162 assert!((2.5..=5.0).contains(&p50), "p50={p50}");
2163 assert!((2.5..=5.0).contains(&p99), "p99={p99}");
2164 assert!(p99 >= p50);
2165 }
2166
2167 #[test]
2168 fn histogram_zero_values() {
2169 let mut h = LatencyHistogram::new();
2170 for _ in 0..10 {
2171 h.record(0);
2172 }
2173 assert_eq!(h.count(), 10);
2174 assert!(h.percentile(0.5) <= 1.0);
2177 }
2178
2179 #[test]
2180 fn histogram_large_value() {
2181 let mut h = LatencyHistogram::new();
2182 h.record(u64::MAX);
2183 assert_eq!(h.count(), 1);
2184 let p99 = h.percentile(0.99);
2185 assert!(p99 > 0.0);
2186 }
2187
2188 #[test]
2189 fn histogram_percentile_zero() {
2190 let mut h = LatencyHistogram::new();
2191 h.record(10_000);
2192 h.record(20_000);
2193 let p0 = h.percentile(0.0);
2194 assert_close(p0, 10.0);
2195 }
2196
2197 #[test]
2198 fn histogram_percentile_one() {
2199 let mut h = LatencyHistogram::new();
2200 h.record(10_000);
2201 h.record(20_000);
2202 let p100 = h.percentile(1.0);
2203 assert!((10.0..=25.0).contains(&p100), "p100={p100}");
2206 }
2207
2208 #[test]
2209 fn histogram_insertion_order_irrelevant() {
2210 let mut a = LatencyHistogram::new();
2211 a.record(30_000);
2212 a.record(10_000);
2213 a.record(20_000);
2214 let mut b = LatencyHistogram::new();
2215 b.record(10_000);
2216 b.record(20_000);
2217 b.record(30_000);
2218 assert_eq!(a.count(), 3);
2219 assert_close(a.percentile(0.5), b.percentile(0.5));
2220 assert_eq!(a.cumulative_buckets(), b.cumulative_buckets());
2221 }
2222
2223 #[test]
2224 fn histogram_debug() {
2225 let h = LatencyHistogram::new();
2226 let debug = format!("{h:?}");
2227 assert!(debug.contains("LatencyHistogram"));
2228 }
2229
2230 #[test]
2231 fn histogram_clone() {
2232 let mut h = LatencyHistogram::new();
2233 h.record(5_000);
2234 let h2 = h.clone();
2235 assert_eq!(h.count(), h2.count());
2236 assert_close(h.percentile(0.5), h2.percentile(0.5));
2237 }
2238
2239 #[test]
2240 fn histogram_percentiles_monotonic_under_volume() {
2241 let mut h = LatencyHistogram::new();
2242 for i in 0..=5_000u64 {
2243 h.record(i * 1000);
2244 }
2245 assert_eq!(h.count(), 5_001);
2246 assert!(h.percentile(0.95) >= h.percentile(0.50));
2248 assert!(h.percentile(0.99) >= h.percentile(0.95));
2249 }
2250
2251 #[test]
2254 fn red_accumulator_single_success() {
2255 let m = init_metrics(MetricsConfig::default());
2256 m.record_request_success("test", Duration::from_millis(42));
2257 let snap = m.snapshot();
2258 let op = snap.red_by_operation.get("test").unwrap();
2259 assert_eq!(op.requests, 1);
2260 assert_eq!(op.errors, 0);
2261 assert_close(op.error_rate, 0.0);
2262 assert_close(op.avg_duration_ms, 42.0);
2263 }
2264
2265 #[test]
2266 fn red_accumulator_single_error() {
2267 let m = init_metrics(MetricsConfig::default());
2268 m.record_request_error("test", Duration::from_millis(99));
2269 let snap = m.snapshot();
2270 let op = snap.red_by_operation.get("test").unwrap();
2271 assert_eq!(op.requests, 1);
2272 assert_eq!(op.errors, 1);
2273 assert_close(op.error_rate, 1.0);
2274 assert_close(op.avg_duration_ms, 99.0);
2275 }
2276
2277 #[test]
2278 fn red_accumulator_percentiles_populated() {
2279 let m = init_metrics(MetricsConfig::default());
2280 for i in 1..=100 {
2281 m.record_request_success("perc_test", Duration::from_millis(i));
2282 }
2283 let snap = m.snapshot();
2284 let op = snap.red_by_operation.get("perc_test").unwrap();
2285 assert_eq!(op.requests, 100);
2286 assert!(op.p50_ms > 0.0);
2287 assert!(op.p95_ms > op.p50_ms);
2288 assert!(op.p99_ms >= op.p95_ms);
2289 }
2290
2291 #[test]
2294 fn concurrent_record_from_multiple_threads() {
2295 use std::thread;
2296
2297 let m = init_metrics(MetricsConfig::default());
2298 let handles: Vec<_> = (0..8)
2299 .map(|i| {
2300 let m = m.clone();
2301 thread::spawn(move || {
2302 for _ in 0..100 {
2303 m.record_order_created(&format!("c{i}"), 1.0);
2304 m.record_request_success("concurrent_op", Duration::from_millis(1));
2305 }
2306 })
2307 })
2308 .collect();
2309
2310 for h in handles {
2311 h.join().unwrap();
2312 }
2313
2314 let snap = m.snapshot();
2315 assert_eq!(snap.orders_created, 800);
2316 assert_close(snap.order_amount_total, 800.0);
2317 let op = snap.red_by_operation.get("concurrent_op").unwrap();
2318 assert_eq!(op.requests, 800);
2319 }
2320
2321 #[test]
2322 fn concurrent_toggle_and_record() {
2323 use std::thread;
2324
2325 let m = init_metrics(MetricsConfig::default());
2326
2327 let m_toggle = m.clone();
2329 let toggle_handle = thread::spawn(move || {
2330 for _ in 0..100 {
2331 m_toggle.set_enabled(false);
2332 m_toggle.set_enabled(true);
2333 }
2334 });
2335
2336 let mut record_handles = Vec::new();
2337 for _ in 0..4 {
2338 let m = m.clone();
2339 record_handles.push(thread::spawn(move || {
2340 for _ in 0..100 {
2341 m.record_order_created("c", 1.0);
2342 }
2343 }));
2344 }
2345
2346 toggle_handle.join().unwrap();
2347 for h in record_handles {
2348 h.join().unwrap();
2349 }
2350
2351 let _snap = m.snapshot();
2353 }
2354
2355 #[test]
2356 fn concurrent_snapshot_while_recording() {
2357 use std::thread;
2358
2359 let m = init_metrics(MetricsConfig::default());
2360
2361 let m_record = m.clone();
2362 let record_handle = thread::spawn(move || {
2363 for _ in 0..500 {
2364 m_record.record_order_created("c", 1.0);
2365 m_record.record_request_success("op", Duration::from_millis(1));
2366 }
2367 });
2368
2369 let m_snap = m;
2370 let snap_handle = thread::spawn(move || {
2371 let mut snapshots = Vec::new();
2372 for _ in 0..50 {
2373 snapshots.push(m_snap.snapshot());
2374 }
2375 snapshots
2376 });
2377
2378 record_handle.join().unwrap();
2379 let snapshots = snap_handle.join().unwrap();
2380
2381 for i in 1..snapshots.len() {
2383 assert!(
2384 snapshots[i].orders_created >= snapshots[i - 1].orders_created,
2385 "Snapshot order counts should be monotonically non-decreasing"
2386 );
2387 }
2388 }
2389
2390 #[test]
2393 fn fresh_metrics_all_zero() {
2394 let m = init_metrics(MetricsConfig::default());
2395 let snap = m.snapshot();
2396 assert_eq!(snap.orders_created, 0);
2397 assert_eq!(snap.customers_created, 0);
2398 assert_eq!(snap.products_created, 0);
2399 assert_eq!(snap.returns_requested, 0);
2400 assert_eq!(snap.carts_created, 0);
2401 assert_eq!(snap.cart_checkouts_completed, 0);
2402 assert_eq!(snap.shipments_created, 0);
2403 assert_eq!(snap.shipments_delivered, 0);
2404 assert_eq!(snap.subscriptions_created, 0);
2405 assert_eq!(snap.payments_completed, 0);
2406 assert_eq!(snap.inventory_adjustments, 0);
2407 assert_eq!(snap.a2a_quotes_created, 0);
2408 assert_eq!(snap.a2a_purchases_created, 0);
2409 assert_eq!(snap.x402_intents_created, 0);
2410 assert_eq!(snap.x402_intents_settled, 0);
2411 assert_eq!(snap.policy_evaluations, 0);
2412 assert_eq!(snap.policy_denials, 0);
2413 assert_eq!(snap.agent_registrations, 0);
2414 assert_eq!(snap.webhook_deliveries, 0);
2415 assert_eq!(snap.webhook_failures, 0);
2416 assert_close(snap.order_amount_total, 0.0);
2417 assert_close(snap.payment_amount_total, 0.0);
2418 assert_close(snap.inventory_delta_total, 0.0);
2419 assert_eq!(snap.red_global.requests, 0);
2420 assert!(snap.red_by_operation.is_empty());
2421 }
2422
2423 #[test]
2426 fn red_operation_labels_normalized_spaces() {
2427 let m = init_metrics(MetricsConfig::default());
2428 m.record_request_success(" Order Created ", Duration::from_millis(10));
2429 let snap = m.snapshot();
2430 assert!(snap.red_by_operation.contains_key("order_created"));
2431 }
2432
2433 #[test]
2434 fn red_operation_labels_normalized_dots() {
2435 let m = init_metrics(MetricsConfig::default());
2436 m.record_request_success("order.create.v2", Duration::from_millis(10));
2437 let snap = m.snapshot();
2438 assert!(snap.red_by_operation.contains_key("order_create_v2"));
2439 }
2440
2441 #[test]
2442 fn red_operation_labels_normalized_slashes() {
2443 let m = init_metrics(MetricsConfig::default());
2444 m.record_request_success("api/v1/orders", Duration::from_millis(10));
2445 let snap = m.snapshot();
2446 assert!(snap.red_by_operation.contains_key("api_v1_orders"));
2447 }
2448
2449 #[test]
2450 fn red_operation_labels_normalized_mixed() {
2451 let m = init_metrics(MetricsConfig::default());
2452 m.record_request_success("Order---Created", Duration::from_millis(10));
2453 let snap = m.snapshot();
2454 assert!(snap.red_by_operation.contains_key("order_created"));
2455 }
2456
2457 #[test]
2458 fn red_same_op_different_casing_merges() {
2459 let m = init_metrics(MetricsConfig::default());
2460 m.record_request_success("Order", Duration::from_millis(10));
2461 m.record_request_success("ORDER", Duration::from_millis(20));
2462 m.record_request_success("order", Duration::from_millis(30));
2463 let snap = m.snapshot();
2464 assert_eq!(snap.red_by_operation.len(), 1);
2465 let order = snap.red_by_operation.get("order").unwrap();
2466 assert_eq!(order.requests, 3);
2467 }
2468
2469 #[test]
2472 fn red_by_operation_sorted() {
2473 let m = init_metrics(MetricsConfig::default());
2474 m.record_request_success("zzz_op", Duration::from_millis(1));
2475 m.record_request_success("aaa_op", Duration::from_millis(1));
2476 m.record_request_success("mmm_op", Duration::from_millis(1));
2477 let snap = m.snapshot();
2478 let keys: Vec<&String> = snap.red_by_operation.keys().collect();
2479 assert_eq!(keys, vec!["aaa_op", "mmm_op", "zzz_op"]);
2480 }
2481
2482 #[test]
2485 fn order_nan_amount_does_not_affect_total() {
2486 let m = init_metrics(MetricsConfig::default());
2487 m.record_order_created("c1", 100.0);
2488 m.record_order_created("c2", f64::NAN);
2489 m.record_order_created("c3", 200.0);
2490 let snap = m.snapshot();
2491 assert_eq!(snap.orders_created, 3);
2492 assert_close(snap.order_amount_total, 300.0);
2493 }
2494
2495 #[test]
2496 fn payment_infinity_amount_does_not_affect_total() {
2497 let m = init_metrics(MetricsConfig::default());
2498 m.record_payment_completed("p1", 50.0);
2499 m.record_payment_completed("p2", f64::INFINITY);
2500 let snap = m.snapshot();
2501 assert_eq!(snap.payments_completed, 2);
2502 assert_close(snap.payment_amount_total, 50.0);
2503 }
2504
2505 #[test]
2506 fn inventory_neg_infinity_does_not_affect_total() {
2507 let m = init_metrics(MetricsConfig::default());
2508 m.record_inventory_adjusted("s1", 10.0);
2509 m.record_inventory_adjusted("s2", f64::NEG_INFINITY);
2510 let snap = m.snapshot();
2511 assert_eq!(snap.inventory_adjustments, 2);
2512 assert_close(snap.inventory_delta_total, 10.0);
2513 }
2514
2515 #[test]
2518 fn toggle_during_red_recording() {
2519 let m = init_metrics(MetricsConfig::default());
2520 m.record_request_success("op", Duration::from_millis(10));
2521
2522 m.set_enabled(false);
2523 m.record_request_error("op", Duration::from_millis(200));
2524
2525 m.set_enabled(true);
2526 m.record_request_success("op", Duration::from_millis(30));
2527
2528 let snap = m.snapshot();
2529 let op = snap.red_by_operation.get("op").unwrap();
2530 assert_eq!(op.requests, 2); assert_eq!(op.errors, 0);
2532 }
2533
2534 #[test]
2537 fn large_scale_order_recording() {
2538 let m = init_metrics(MetricsConfig::default());
2539 for i in 0..1000 {
2540 m.record_order_created(&format!("c{i}"), 1.0);
2541 }
2542 let snap = m.snapshot();
2543 assert_eq!(snap.orders_created, 1000);
2544 assert_close(snap.order_amount_total, 1000.0);
2545 }
2546
2547 #[test]
2548 fn large_scale_red_recording() {
2549 let m = init_metrics(MetricsConfig::default());
2550 for i in 0..500 {
2551 let is_error = i % 10 == 0;
2552 m.record_request("bulk_op", Duration::from_millis(10 + (i % 50)), is_error);
2553 }
2554 let snap = m.snapshot();
2555 let op = snap.red_by_operation.get("bulk_op").unwrap();
2556 assert_eq!(op.requests, 500);
2557 assert_eq!(op.errors, 50); assert!(op.avg_duration_ms > 0.0);
2559 assert!(op.p50_ms > 0.0);
2560 assert!(op.p95_ms > 0.0);
2561 assert!(op.p99_ms > 0.0);
2562 }
2563
2564 #[test]
2567 fn record_request_direct_success() {
2568 let m = init_metrics(MetricsConfig::default());
2569 m.record_request("direct", Duration::from_millis(5), false);
2570 let snap = m.snapshot();
2571 let op = snap.red_by_operation.get("direct").unwrap();
2572 assert_eq!(op.requests, 1);
2573 assert_eq!(op.errors, 0);
2574 }
2575
2576 #[test]
2577 fn record_request_direct_error() {
2578 let m = init_metrics(MetricsConfig::default());
2579 m.record_request("direct_err", Duration::from_millis(5), true);
2580 let snap = m.snapshot();
2581 let op = snap.red_by_operation.get("direct_err").unwrap();
2582 assert_eq!(op.requests, 1);
2583 assert_eq!(op.errors, 1);
2584 }
2585
2586 #[test]
2589 fn global_red_aggregates_all_operations() {
2590 let m = init_metrics(MetricsConfig::default());
2591 m.record_request_success("op_a", Duration::from_millis(10));
2592 m.record_request_error("op_b", Duration::from_millis(20));
2593 m.record_request_success("op_c", Duration::from_millis(30));
2594 let snap = m.snapshot();
2595 assert_eq!(snap.red_global.requests, 3);
2596 assert_eq!(snap.red_global.errors, 1);
2597 assert_close(snap.red_global.avg_duration_ms, 20.0);
2598 }
2599
2600 #[test]
2603 fn red_by_operation_btreemap_iteration_order() {
2604 let m = init_metrics(MetricsConfig::default());
2605 m.record_request_success("zebra", Duration::from_millis(1));
2606 m.record_request_success("alpha", Duration::from_millis(1));
2607 m.record_request_success("middle", Duration::from_millis(1));
2608
2609 let snap = m.snapshot();
2610 let keys: Vec<String> = snap.red_by_operation.keys().cloned().collect();
2611 let mut sorted = keys.clone();
2612 sorted.sort();
2613 assert_eq!(keys, sorted);
2614 }
2615
2616 #[test]
2619 fn comprehensive_mixed_recording_session() {
2620 let m = init_metrics(MetricsConfig::default());
2621
2622 m.record_order_created("c1", 100.0);
2624 m.record_order_created("c2", 200.0);
2625 m.record_customer_created("c1");
2626 m.record_product_created("p1");
2627 m.record_return_requested("r1");
2628 m.record_cart_created("cart1");
2629 m.record_cart_checkout_completed("cart1", "ord1");
2630 m.record_shipment_created("s1");
2631 m.record_shipment_delivered("s1");
2632 m.record_subscription_created("sub1");
2633 m.record_payment_completed("pay1", 100.0);
2634 m.record_payment_completed("pay2", 200.0);
2635 m.record_inventory_adjusted("sku1", 50.0);
2636 m.record_inventory_adjusted("sku1", -10.0);
2637
2638 m.record_a2a_quote_created();
2640 m.record_a2a_purchase_created();
2641 m.record_x402_intent_created();
2642 m.record_x402_intent_settled();
2643
2644 m.record_policy_evaluation(false);
2646 m.record_policy_evaluation(true);
2647
2648 m.record_agent_registration();
2650
2651 m.record_webhook_delivery(false);
2653 m.record_webhook_delivery(true);
2654
2655 m.record_request_success("order.create", Duration::from_millis(50));
2657 m.record_request_error("order.create", Duration::from_millis(200));
2658 m.record_request_success("payment.charge", Duration::from_millis(80));
2659
2660 let snap = m.snapshot();
2661
2662 assert_eq!(snap.orders_created, 2);
2664 assert_eq!(snap.customers_created, 1);
2665 assert_eq!(snap.products_created, 1);
2666 assert_eq!(snap.returns_requested, 1);
2667 assert_eq!(snap.carts_created, 1);
2668 assert_eq!(snap.cart_checkouts_completed, 1);
2669 assert_eq!(snap.shipments_created, 1);
2670 assert_eq!(snap.shipments_delivered, 1);
2671 assert_eq!(snap.subscriptions_created, 1);
2672 assert_eq!(snap.payments_completed, 2);
2673 assert_eq!(snap.inventory_adjustments, 2);
2674 assert_eq!(snap.a2a_quotes_created, 1);
2675 assert_eq!(snap.a2a_purchases_created, 1);
2676 assert_eq!(snap.x402_intents_created, 1);
2677 assert_eq!(snap.x402_intents_settled, 1);
2678 assert_eq!(snap.policy_evaluations, 2);
2679 assert_eq!(snap.policy_denials, 1);
2680 assert_eq!(snap.agent_registrations, 1);
2681 assert_eq!(snap.webhook_deliveries, 2);
2682 assert_eq!(snap.webhook_failures, 1);
2683
2684 assert_close(snap.order_amount_total, 300.0);
2686 assert_close(snap.payment_amount_total, 300.0);
2687 assert_close(snap.inventory_delta_total, 40.0);
2688
2689 assert_eq!(snap.red_global.requests, 3);
2691 assert_eq!(snap.red_global.errors, 1);
2692 assert_eq!(snap.red_by_operation.len(), 2);
2693 }
2694
2695 #[test]
2698 fn http_request_records_by_method_and_route() {
2699 let m = init_metrics(MetricsConfig::default());
2700 m.record_http_request("GET", "/api/v1/orders/{id}", 200, Duration::from_millis(20));
2701 m.record_http_request("GET", "/api/v1/orders/{id}", 404, Duration::from_millis(5));
2702 m.record_http_request("GET", "/api/v1/orders/{id}", 500, Duration::from_millis(80));
2703 m.record_http_request("POST", "/api/v1/orders", 201, Duration::from_millis(40));
2704
2705 let snap = m.snapshot();
2706 assert_eq!(snap.http_by_route.len(), 2);
2707
2708 let get =
2709 snap.http_by_route.get(&("GET".to_owned(), "/api/v1/orders/{id}".to_owned())).unwrap();
2710 assert_eq!(get.requests, 3);
2711 assert_eq!(get.errors_4xx, 1);
2712 assert_eq!(get.errors_5xx, 1);
2713 assert_close(get.duration_total_ms, 105.0);
2714 let (last_bound, last_count) = get.latency_buckets[get.latency_buckets.len() - 1];
2715 assert!(last_bound.is_infinite());
2716 assert_eq!(last_count, 3);
2717
2718 let post =
2719 snap.http_by_route.get(&("POST".to_owned(), "/api/v1/orders".to_owned())).unwrap();
2720 assert_eq!(post.requests, 1);
2721 assert_eq!(post.errors_4xx, 0);
2722 assert_eq!(post.errors_5xx, 0);
2723 }
2724
2725 #[test]
2726 fn http_request_feeds_global_red_with_5xx_errors_only() {
2727 let m = init_metrics(MetricsConfig::default());
2728 m.record_http_request("GET", "/a", 200, Duration::from_millis(10));
2729 m.record_http_request("GET", "/a", 404, Duration::from_millis(10));
2730 m.record_http_request("GET", "/a", 503, Duration::from_millis(10));
2731
2732 let snap = m.snapshot();
2733 assert_eq!(snap.red_global.requests, 3);
2734 assert_eq!(snap.red_global.errors, 1); }
2736
2737 #[test]
2738 fn http_request_disabled_is_noop() {
2739 let m = init_metrics(MetricsConfig { enabled: false });
2740 m.record_http_request("GET", "/a", 500, Duration::from_millis(10));
2741 let snap = m.snapshot();
2742 assert!(snap.http_by_route.is_empty());
2743 assert_eq!(snap.red_global.requests, 0);
2744 }
2745}