Skip to main content

stateset_observability/
metrics.rs

1//! Metrics helpers for StateSet iCommerce.
2//!
3//! This module provides a minimal, dependency-light metrics surface using
4//! lock-free atomic counters. Downstream applications can wrap these hooks
5//! with their preferred metrics exporter (Prometheus, `StatsD`, etc.).
6//!
7//! It includes RED primitives (rate/errors/duration) and SLO evaluation
8//! helpers for operation-level telemetry.
9//!
10//! # Example
11//!
12//! ```rust
13//! use std::time::Duration;
14//! use stateset_observability::{init_metrics, MetricsConfig, SloTarget};
15//!
16//! let metrics = init_metrics(MetricsConfig::default());
17//! assert!(metrics.is_enabled());
18//!
19//! metrics.record_order_created("cust-1", 49.99);
20//! metrics.record_request_success("order.create", Duration::from_millis(42));
21//! metrics.record_request_error("order.create", Duration::from_millis(120));
22//!
23//! let snap = metrics.snapshot();
24//! let report = snap.evaluate_operation_slo("order.create", SloTarget {
25//!     min_success_rate: 0.95,
26//!     max_avg_latency_ms: 150.0,
27//!     min_requests: 2,
28//! });
29//! assert!(report.is_some());
30//! ```
31
32use 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/// Configuration for metrics initialization.
40#[derive(Debug, Clone)]
41pub struct MetricsConfig {
42    /// Whether metrics are enabled.
43    pub enabled: bool,
44}
45
46impl Default for MetricsConfig {
47    fn default() -> Self {
48        Self { enabled: true }
49    }
50}
51
52/// RED snapshot for a single operation or global aggregate.
53#[derive(Debug, Clone, PartialEq)]
54pub struct RedSnapshot {
55    /// Request count (rate denominator).
56    pub requests: u64,
57    /// Error count.
58    pub errors: u64,
59    /// Total request duration in milliseconds.
60    pub duration_total_ms: f64,
61    /// Error rate (`errors / requests`) in [0, 1].
62    pub error_rate: f64,
63    /// Average request duration in milliseconds.
64    pub avg_duration_ms: f64,
65    /// p50 latency in milliseconds (0.0 if no histogram data).
66    pub p50_ms: f64,
67    /// p95 latency in milliseconds (0.0 if no histogram data).
68    pub p95_ms: f64,
69    /// p99 latency in milliseconds (0.0 if no histogram data).
70    pub p99_ms: f64,
71    /// Cumulative latency buckets as `(upper_bound_seconds, cumulative_count)`
72    /// pairs (Prometheus exposition style, final bound is `+Inf`). Empty if no
73    /// histogram data.
74    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    /// Evaluate this RED snapshot against an SLO target.
97    #[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/// SLO target for RED metrics.
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub struct SloTarget {
136    /// Minimum required success rate, in [0, 1].
137    pub min_success_rate: f64,
138    /// Maximum allowed average latency in milliseconds.
139    pub max_avg_latency_ms: f64,
140    /// Minimum number of requests required before evaluating pass/fail.
141    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/// Result of evaluating RED metrics against an SLO target.
151#[derive(Debug, Clone, PartialEq)]
152pub struct SloEvaluation {
153    /// Whether the SLO target passed.
154    pub passed: bool,
155    /// Number of requests used for evaluation.
156    pub requests: u64,
157    /// Success rate (`1 - error_rate`).
158    pub success_rate: f64,
159    /// Error rate (`errors / requests`).
160    pub error_rate: f64,
161    /// Average request latency in milliseconds.
162    pub avg_duration_ms: f64,
163    /// Explanation for failure.
164    pub reason: Option<String>,
165}
166
167/// Snapshot of recorded metrics values.
168#[derive(Debug, Clone)]
169pub struct MetricsSnapshot {
170    /// Whether metrics collection is currently enabled.
171    pub enabled: bool,
172    /// Number of order creation events.
173    pub orders_created: u64,
174    /// Number of customer creation events.
175    pub customers_created: u64,
176    /// Number of product creation events.
177    pub products_created: u64,
178    /// Number of return request events.
179    pub returns_requested: u64,
180    /// Number of cart creation events.
181    pub carts_created: u64,
182    /// Number of completed cart checkouts.
183    pub cart_checkouts_completed: u64,
184    /// Number of shipment creation events.
185    pub shipments_created: u64,
186    /// Number of delivered shipments.
187    pub shipments_delivered: u64,
188    /// Number of created subscriptions.
189    pub subscriptions_created: u64,
190    /// Number of completed payments.
191    pub payments_completed: u64,
192    /// Number of inventory adjustment events.
193    pub inventory_adjustments: u64,
194    /// Number of A2A quotes created.
195    pub a2a_quotes_created: u64,
196    /// Number of A2A purchases created.
197    pub a2a_purchases_created: u64,
198    /// Number of x402 payment intents created.
199    pub x402_intents_created: u64,
200    /// Number of x402 payment intents settled.
201    pub x402_intents_settled: u64,
202    /// Number of policy evaluations performed.
203    pub policy_evaluations: u64,
204    /// Number of policy denials.
205    pub policy_denials: u64,
206    /// Number of agent registrations.
207    pub agent_registrations: u64,
208    /// Number of webhook deliveries attempted.
209    pub webhook_deliveries: u64,
210    /// Number of webhook delivery failures.
211    pub webhook_failures: u64,
212    /// Number of legacy (Ed25519) signature operations.
213    pub pqc_legacy_signatures: u64,
214    /// Number of hybrid (Ed25519 + ML-DSA-65) signature operations.
215    pub pqc_hybrid_signatures: u64,
216    /// Number of PQC-strict (ML-DSA-65) signature operations.
217    pub pqc_strict_signatures: u64,
218    /// Number of legacy (X25519) encryption operations.
219    pub pqc_legacy_encryptions: u64,
220    /// Number of hybrid (X25519 + ML-KEM-768) encryption operations.
221    pub pqc_hybrid_encryptions: u64,
222    /// Number of PQC-strict (ML-KEM-768) encryption operations.
223    pub pqc_strict_encryptions: u64,
224    /// Sum of recorded order amounts.
225    pub order_amount_total: f64,
226    /// Sum of recorded payment amounts.
227    pub payment_amount_total: f64,
228    /// Sum of recorded inventory deltas.
229    pub inventory_delta_total: f64,
230    /// Global RED aggregate across all operations.
231    pub red_global: RedSnapshot,
232    /// Operation-level RED aggregates keyed by normalized operation label.
233    pub red_by_operation: BTreeMap<String, RedSnapshot>,
234    /// HTTP RED aggregates keyed by `(method, route_pattern)`.
235    ///
236    /// Route patterns are matched-path templates (e.g. `/api/v1/orders/{id}`)
237    /// to keep label cardinality bounded.
238    pub http_by_route: BTreeMap<(String, String), HttpRouteSnapshot>,
239}
240
241impl MetricsSnapshot {
242    /// Evaluate global RED metrics against an SLO target.
243    #[must_use]
244    pub fn evaluate_global_slo(&self, target: SloTarget) -> SloEvaluation {
245        self.red_global.evaluate_slo(target)
246    }
247
248    /// Evaluate an operation's RED metrics against an SLO target.
249    ///
250    /// Returns `None` when the operation has no recorded requests.
251    #[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
262/// Upper bounds (inclusive) of the fixed latency buckets, in microseconds.
263///
264/// Prometheus-style bounds covering 1ms through 10s; an implicit `+Inf`
265/// overflow bucket captures anything slower.
266const LATENCY_BUCKET_BOUNDS_MICROS: [u64; 13] = [
267    1_000,      // 1ms
268    2_500,      // 2.5ms
269    5_000,      // 5ms
270    10_000,     // 10ms
271    25_000,     // 25ms
272    50_000,     // 50ms
273    100_000,    // 100ms
274    250_000,    // 250ms
275    500_000,    // 500ms
276    1_000_000,  // 1s
277    2_500_000,  // 2.5s
278    5_000_000,  // 5s
279    10_000_000, // 10s
280];
281
282/// Number of buckets including the `+Inf` overflow bucket.
283const LATENCY_BUCKET_COUNT: usize = LATENCY_BUCKET_BOUNDS_MICROS.len() + 1;
284
285/// Fixed-bucket latency histogram (Prometheus-style).
286///
287/// Values are recorded into a fixed set of cumulative-style buckets spanning
288/// 1ms to 10s plus a `+Inf` overflow bucket, giving O(1) recording and
289/// constant, bounded memory regardless of traffic volume. Percentiles are
290/// estimated by linear interpolation within the containing bucket (the same
291/// approach as Prometheus's `histogram_quantile`).
292///
293/// # Example
294///
295/// ```rust
296/// use stateset_observability::LatencyHistogram;
297///
298/// let mut h = LatencyHistogram::new();
299/// for i in 1..=100 {
300///     h.record(i * 1000); // 1ms to 100ms in micros
301/// }
302/// assert!(h.percentile(0.50) > 0.0);
303/// assert!(h.percentile(0.95) > h.percentile(0.50));
304/// ```
305#[derive(Debug, Clone)]
306pub struct LatencyHistogram {
307    /// Per-bucket (non-cumulative) counts; last slot is the `+Inf` bucket.
308    bucket_counts: [u64; LATENCY_BUCKET_COUNT],
309    /// Total number of recorded values.
310    count: u64,
311    /// Sum of all recorded values in microseconds.
312    sum_micros: u64,
313}
314
315impl Default for LatencyHistogram {
316    fn default() -> Self {
317        Self::new()
318    }
319}
320
321impl LatencyHistogram {
322    /// Create a new empty histogram.
323    #[must_use]
324    pub const fn new() -> Self {
325        Self { bucket_counts: [0; LATENCY_BUCKET_COUNT], count: 0, sum_micros: 0 }
326    }
327
328    /// Record a latency value in microseconds. O(1) with bounded memory.
329    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    /// Estimate a percentile (0.0 to 1.0) in milliseconds. Returns 0.0 if empty.
340    ///
341    /// Uses linear interpolation within the containing bucket. Values in the
342    /// `+Inf` overflow bucket are reported as the largest finite bound (10s).
343    #[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                    // +Inf overflow bucket: clamp to the largest finite bound.
358                    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    /// Number of recorded values.
377    #[must_use]
378    #[allow(clippy::cast_possible_truncation)]
379    pub const fn count(&self) -> usize {
380        self.count as usize
381    }
382
383    /// Sum of all recorded values in microseconds.
384    #[must_use]
385    pub const fn sum_micros(&self) -> u64 {
386        self.sum_micros
387    }
388
389    /// Cumulative bucket counts as `(upper_bound_seconds, cumulative_count)`
390    /// pairs, Prometheus exposition style. The final entry's bound is
391    /// [`f64::INFINITY`] and its count equals [`Self::count`].
392    #[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, |&micros| 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/// Per-route HTTP RED accumulator (requests, 4xx/5xx errors, latency).
437#[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/// Snapshot of HTTP RED metrics for a single `(method, route)` pair.
447#[derive(Debug, Clone, PartialEq)]
448pub struct HttpRouteSnapshot {
449    /// Total requests observed.
450    pub requests: u64,
451    /// Responses with a 4xx status.
452    pub errors_4xx: u64,
453    /// Responses with a 5xx status.
454    pub errors_5xx: u64,
455    /// Total request duration in milliseconds.
456    pub duration_total_ms: f64,
457    /// Cumulative latency buckets as `(upper_bound_seconds, cumulative_count)`
458    /// pairs (Prometheus exposition style, final bound is `+Inf`).
459    pub latency_buckets: Vec<(f64, u64)>,
460}
461
462#[derive(Debug, Default)]
463struct MetricTotals {
464    // f64 totals (order_amount, payment_amount, inventory_delta) moved to
465    // lock-free AtomicU64 fields on MetricsInner for contention-free hot paths.
466    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    // Lock-free f64 accumulators (stored as u64 bits via to_bits/from_bits)
504    order_amount_total_bits: AtomicU64,
505    payment_amount_total_bits: AtomicU64,
506    inventory_delta_total_bits: AtomicU64,
507    totals: Mutex<MetricTotals>,
508}
509
510/// Thread-safe metrics handle for in-process telemetry.
511#[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    /// Lock-free f64 addition using CAS loop on `AtomicU64` bit representation.
528    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    /// Whether this metrics instance currently records values.
546    #[must_use]
547    pub fn is_enabled(&self) -> bool {
548        self.inner.enabled.load(Ordering::Relaxed)
549    }
550
551    /// Enable or disable metrics collection at runtime.
552    pub fn set_enabled(&self, enabled: bool) {
553        self.inner.enabled.store(enabled, Ordering::Relaxed);
554    }
555
556    /// Return a point-in-time snapshot of current metrics values.
557    #[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    /// Record a new order creation event.
633    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    /// Record a new customer creation event.
644    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    /// Record a new product creation event.
652    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    /// Record a new return request event.
660    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    /// Record a new cart creation event.
668    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    /// Record a completed cart checkout event.
676    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    /// Record a new shipment creation event.
684    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    /// Record a shipment delivery event.
692    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    /// Record a new subscription creation event.
700    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    /// Record a completed payment.
708    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    /// Record an inventory adjustment.
719    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    /// Record an A2A quote creation.
730    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    /// Record an A2A purchase creation.
738    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    /// Record an x402 payment intent creation.
746    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    /// Record an x402 payment intent settlement.
754    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    /// Record a policy evaluation.
762    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    /// Record an agent registration.
773    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    /// Record a webhook delivery attempt.
781    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    /// Record a PQC signature operation by security profile.
792    ///
793    /// `profile` should be `"legacy"`, `"hybrid"`, or `"pqc-strict"`.
794    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    /// Record a PQC encryption operation by security profile.
807    ///
808    /// `profile` should be `"legacy"`, `"hybrid"`, or `"pqc-strict"`.
809    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    /// Record a request in RED metrics.
822    ///
823    /// `operation` is normalized to a low-cardinality label.
824    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    /// Record an HTTP request in per-route RED metrics.
847    ///
848    /// `route` should be a low-cardinality route pattern (e.g. axum's
849    /// `MatchedPath` such as `/api/v1/orders/{id}`), never a raw request path.
850    /// 4xx and 5xx responses are counted separately; only 5xx responses are
851    /// treated as errors for the global RED aggregate.
852    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    /// Record a successful request in RED metrics.
886    pub fn record_request_success(&self, operation: &str, duration: Duration) {
887        self.record_request(operation, duration, false);
888    }
889
890    /// Record a failed request in RED metrics.
891    pub fn record_request_error(&self, operation: &str, duration: Duration) {
892        self.record_request(operation, duration, true);
893    }
894}
895
896/// Initialize metrics and return a handle.
897#[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); // 5ms
1157        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        // Fixed buckets: count stays exact regardless of volume.
1192        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); // 0.5ms  -> le 0.001
1200        h.record(1_000); // 1ms   -> le 0.001 (inclusive upper bound)
1201        h.record(3_000); // 3ms   -> le 0.005
1202        h.record(70_000); // 70ms -> le 0.1
1203        h.record(20_000_000); // 20s -> +Inf
1204
1205        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        // +Inf bucket is last and equals total count.
1223        let (last_bound, last_count) = buckets[buckets.len() - 1];
1224        assert!(last_bound.is_infinite());
1225        assert_eq!(last_count, 5);
1226        // Cumulative counts are monotonically non-decreasing.
1227        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    // ── MetricsConfig ──────────────────────────────────────────────────
1265
1266    #[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    // ── Metrics default / debug / clone ────────────────────────────────
1295
1296    #[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    // ── set_enabled toggle ─────────────────────────────────────────────
1319
1320    #[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    // ── Disabled mode: all record_* are no-ops ─────────────────────────
1345
1346    #[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    // ── RedSnapshot calculations ───────────────────────────────────────
1397
1398    #[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        // 1_500_000 micros = 1500.0 ms
1434        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    // ── SLO evaluation edge cases ──────────────────────────────────────
1469
1470    #[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        // 0 >= 0 min_requests, 0.0 error_rate -> success 1.0 >= 0.0, avg 0.0 <= MAX
1487        assert!(eval.passed);
1488    }
1489
1490    #[test]
1491    fn slo_evaluation_exactly_at_threshold() {
1492        // 10 requests, 1 error -> success_rate = 0.9 exactly
1493        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        // 100 requests, 11 errors -> success_rate = 0.89
1504        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        // 10 requests, 0 errors, total 2_000_000 micros -> avg = 200ms
1515        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    // ── Global SLO evaluation ──────────────────────────────────────────
1596
1597    #[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    // ── evaluate_operation_slo ─────────────────────────────────────────
1627
1628    #[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        // Query with different casing/separator — should still resolve via normalization
1642        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    // ── Multiple operations tracked independently ──────────────────────
1651
1652    #[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    // ── Snapshot isolation ─────────────────────────────────────────────
1692
1693    #[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        // Record more after snapshot
1700        m.record_order_created("c", 20.0);
1701        m.record_order_created("c", 30.0);
1702
1703        // snap1 should still reflect original state
1704        assert_eq!(snap1.orders_created, 1);
1705        assert_close(snap1.order_amount_total, 10.0);
1706
1707        // New snapshot should reflect new state
1708        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    // ── record_* with various values ───────────────────────────────────
1729
1730    #[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    // ── Individual counter tests ───────────────────────────────────────
1831
1832    #[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    // ── RED request recording ──────────────────────────────────────────
2013
2014    #[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    // ── Snapshot enabled field ─────────────────────────────────────────
2072
2073    #[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    // ── MetricsSnapshot debug/clone ────────────────────────────────────
2096
2097    #[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    // ── is_finite_metric_value ─────────────────────────────────────────
2116
2117    #[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    // ── Histogram additional tests ─────────────────────────────────────
2135
2136    #[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); // 10ms
2146        h.record(20_000); // 20ms
2147        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        // Bucketed estimate: values fall in the (2.5ms, 5ms] bucket.
2160        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        // Zero values land in the first bucket (0, 1ms]; the estimate is
2175        // bounded by the first bucket's upper edge.
2176        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        // 20ms falls in the (10ms, 25ms] bucket; the estimate is bounded by
2204        // that bucket's edges.
2205        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        // Percentiles should be monotonically non-decreasing
2247        assert!(h.percentile(0.95) >= h.percentile(0.50));
2248        assert!(h.percentile(0.99) >= h.percentile(0.95));
2249    }
2250
2251    // ── RedAccumulator (tested through Metrics) ────────────────────────
2252
2253    #[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    // ── Thread safety (basic concurrent access) ────────────────────────
2292
2293    #[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        // One thread toggles, others record
2328        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        // Just verify no panics occurred and we can take a snapshot
2352        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        // Each snapshot should have monotonically non-decreasing order counts
2382        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    // ── Fresh metrics have all-zero snapshot ───────────────────────────
2391
2392    #[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    // ── Operation label normalization in RED ────────────────────────────
2424
2425    #[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    // ── red_by_operation is BTreeMap (sorted) ──────────────────────────
2470
2471    #[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    // ── Comprehensive non-finite amount tests ──────────────────────────
2483
2484    #[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    // ── Enable/disable during RED recording ────────────────────────────
2516
2517    #[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); // Only 2 recorded (skipped the disabled one)
2531        assert_eq!(op.errors, 0);
2532    }
2533
2534    // ── Large-scale recording ──────────────────────────────────────────
2535
2536    #[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); // every 10th
2558        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    // ── record_request directly (not via success/error helpers) ────────
2565
2566    #[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    // ── Global RED aggregates all operations ───────────────────────────
2587
2588    #[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    // ── MetricsSnapshot red_by_operation BTreeMap ordering ─────────────
2601
2602    #[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    // ── Multiple metric types in one session ───────────────────────────
2617
2618    #[test]
2619    fn comprehensive_mixed_recording_session() {
2620        let m = init_metrics(MetricsConfig::default());
2621
2622        // Commerce events
2623        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        // A2A events
2639        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        // Policy events
2645        m.record_policy_evaluation(false);
2646        m.record_policy_evaluation(true);
2647
2648        // Agent events
2649        m.record_agent_registration();
2650
2651        // Webhook events
2652        m.record_webhook_delivery(false);
2653        m.record_webhook_delivery(true);
2654
2655        // RED events
2656        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        // Verify all counters
2663        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        // Verify amounts
2685        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        // Verify RED
2690        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    // ── HTTP per-route RED metrics ─────────────────────────────────────
2696
2697    #[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); // only the 5xx
2735    }
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}