Skip to main content

optirs_core/streaming/
streaming_metrics.rs

1// Comprehensive metrics and monitoring for streaming optimization
2//
3// This module provides detailed performance metrics, monitoring capabilities,
4// and analytics for streaming optimization systems.
5//
6// Honesty contract for this module (findings M1-M4): every field below is
7// either derived from data the caller actually supplied, or it is an
8// `Option` that stays `None` until a caller feeds the missing measurement
9// through one of the `record_*`/`set_*` hooks. No field is ever populated
10// with an invented constant.
11
12use scirs2_core::numeric::Float;
13use std::collections::{BTreeMap, HashMap};
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16use crate::error::Result;
17
18mod accumulator;
19mod aggregation;
20mod alerts;
21mod export;
22
23#[cfg(test)]
24mod regression_tests;
25
26pub use accumulator::{MetricsAccumulator, ResourceProbe, RobustnessProbe};
27pub use aggregation::AggregatedSeries;
28pub use alerts::KNOWN_METRIC_PATHS;
29
30/// Seconds since the Unix epoch, saturating instead of panicking for times
31/// before the epoch (M4: this used to be `duration_since(..).expect(..)`).
32pub(crate) fn unix_timestamp(time: SystemTime) -> u64 {
33    time.duration_since(UNIX_EPOCH)
34        .unwrap_or_default()
35        .as_secs()
36}
37
38/// Microseconds per second, for converting between the second-resolution
39/// `MetricsSnapshot::timestamp` and the microsecond-resolution key
40/// `HistoricalMetrics::time_series` is indexed by.
41pub(crate) const MICROS_PER_SEC: u64 = 1_000_000;
42
43/// Microseconds since the Unix epoch, saturating at zero for pre-epoch times.
44///
45/// This is the resolution `HistoricalMetrics` keys its time series by. Keying by
46/// whole seconds (as it used to) silently dropped every sample but the last
47/// within each second, which is most of them under sub-second streaming rates —
48/// exactly the regime the retention and compression logic is there to bound.
49/// `u64` microseconds spans ~584 000 years, so it cannot overflow in practice.
50pub(crate) fn unix_timestamp_micros(time: SystemTime) -> u64 {
51    let elapsed = time.duration_since(UNIX_EPOCH).unwrap_or_default();
52    elapsed
53        .as_secs()
54        .saturating_mul(MICROS_PER_SEC)
55        .saturating_add(u64::from(elapsed.subsec_micros()))
56}
57
58/// Elapsed time between two instants, saturating at zero when `later`
59/// precedes `earlier` (clocks can and do step backwards).
60pub(crate) fn saturating_elapsed(later: SystemTime, earlier: SystemTime) -> Duration {
61    later.duration_since(earlier).unwrap_or_default()
62}
63
64/// Streaming metrics collector and analyzer
65#[derive(Debug)]
66pub struct StreamingMetricsCollector<A: Float + Send + Sync> {
67    /// Performance metrics
68    performance_metrics: PerformanceMetrics<A>,
69
70    /// Resource utilization metrics
71    resource_metrics: ResourceMetrics,
72
73    /// Quality metrics
74    quality_metrics: QualityMetrics<A>,
75
76    /// Business metrics
77    business_metrics: BusinessMetrics<A>,
78
79    /// Historical data storage
80    historical_data: HistoricalMetrics<A>,
81
82    /// Real-time dashboards
83    dashboards: Vec<Dashboard>,
84
85    /// Alert system
86    alert_system: AlertSystem<A>,
87
88    /// Metric aggregation settings
89    aggregation_config: AggregationConfig,
90
91    /// Export configuration
92    export_config: ExportConfig,
93
94    /// Rolling raw observations the aggregate metrics are derived from
95    accumulator: MetricsAccumulator<A>,
96
97    /// Service level objectives, when the operator configured any
98    slo: Option<SloTargets>,
99
100    /// Cost model, when the operator configured one
101    cost_model: Option<CostModel<A>>,
102
103    /// When the last export ran, used to honour `ExportConfig::frequency`
104    last_export: Option<SystemTime>,
105}
106
107/// Service level objectives used to compute a real SLO compliance ratio.
108#[derive(Debug, Clone, Default)]
109pub struct SloTargets {
110    /// Maximum acceptable end-to-end processing time per sample
111    pub max_processing_time: Option<Duration>,
112
113    /// Maximum acceptable loss value
114    pub max_loss: Option<f64>,
115
116    /// Maximum acceptable memory usage in bytes
117    pub max_memory_bytes: Option<u64>,
118}
119
120impl SloTargets {
121    /// Whether this target set constrains anything at all.
122    pub fn is_empty(&self) -> bool {
123        self.max_processing_time.is_none()
124            && self.max_loss.is_none()
125            && self.max_memory_bytes.is_none()
126    }
127}
128
129/// Operator-supplied cost model. Without one, cost metrics stay `None`
130/// rather than being invented.
131#[derive(Debug, Clone)]
132pub struct CostModel<A: Float + Send + Sync> {
133    /// Currency cost of one second of compute
134    pub compute_cost_per_second: A,
135
136    /// Currency cost of holding one gigabyte for one hour
137    pub memory_cost_per_gb_hour: A,
138
139    /// Currency cost of one joule of energy
140    pub energy_cost_per_joule: A,
141
142    /// Currency value of reducing the loss by one unit
143    pub value_per_loss_unit: A,
144}
145
146/// Performance-related metrics
147#[derive(Debug, Clone)]
148pub struct PerformanceMetrics<A: Float + Send + Sync> {
149    /// Throughput measurements
150    pub throughput: ThroughputMetrics,
151
152    /// Latency measurements
153    pub latency: LatencyMetrics,
154
155    /// Accuracy and convergence metrics
156    pub accuracy: AccuracyMetrics<A>,
157
158    /// Stability metrics
159    pub stability: StabilityMetrics<A>,
160
161    /// Efficiency metrics
162    pub efficiency: EfficiencyMetrics<A>,
163}
164
165/// Throughput measurements
166#[derive(Debug, Clone)]
167pub struct ThroughputMetrics {
168    /// Samples processed per second
169    pub samples_per_second: f64,
170
171    /// Updates per second
172    pub updates_per_second: f64,
173
174    /// Gradient computations per second
175    pub gradients_per_second: f64,
176
177    /// Peak throughput achieved
178    pub peak_throughput: f64,
179
180    /// Minimum throughput observed
181    pub min_throughput: f64,
182
183    /// Throughput variance
184    pub throughput_variance: f64,
185
186    /// Throughput trend (positive = increasing)
187    pub throughput_trend: f64,
188}
189
190/// Latency measurements
191#[derive(Debug, Clone)]
192pub struct LatencyMetrics {
193    /// End-to-end latency statistics
194    pub end_to_end: LatencyStats,
195
196    /// Gradient computation latency, when the caller reports it
197    pub gradient_computation: Option<LatencyStats>,
198
199    /// Update application latency, when the caller reports it
200    pub update_application: Option<LatencyStats>,
201
202    /// Communication latency (for distributed), when the caller reports it
203    pub communication: Option<LatencyStats>,
204
205    /// Queue waiting time, when the caller reports it
206    pub queue_wait_time: Option<LatencyStats>,
207
208    /// Processing jitter (mean absolute successive difference)
209    pub jitter: f64,
210}
211
212/// Detailed latency statistics
213#[derive(Debug, Clone)]
214pub struct LatencyStats {
215    /// Mean latency
216    pub mean: Duration,
217
218    /// Median latency
219    pub median: Duration,
220
221    /// 95th percentile
222    pub p95: Duration,
223
224    /// 99th percentile
225    pub p99: Duration,
226
227    /// 99.9th percentile
228    pub p999: Duration,
229
230    /// Maximum latency observed
231    pub max: Duration,
232
233    /// Minimum latency observed
234    pub min: Duration,
235
236    /// Standard deviation
237    pub std_dev: Duration,
238}
239
240/// Accuracy and convergence metrics
241#[derive(Debug, Clone)]
242pub struct AccuracyMetrics<A: Float + Send + Sync> {
243    /// Current loss value
244    pub current_loss: A,
245
246    /// Loss reduction per second over the retained window
247    pub loss_reduction_rate: A,
248
249    /// Convergence rate (negative ordinary-least-squares slope of the loss)
250    pub convergence_rate: A,
251
252    /// Prediction accuracy, when the caller reports an `accuracy` custom metric
253    pub prediction_accuracy: Option<A>,
254
255    /// Gradient magnitude
256    pub gradient_magnitude: A,
257
258    /// Parameter stability derived from the gradient-magnitude spread
259    pub parameter_stability: A,
260
261    /// Learning progress relative to the first observed loss
262    pub learning_progress: A,
263}
264
265/// Model stability metrics
266#[derive(Debug, Clone)]
267pub struct StabilityMetrics<A: Float + Send + Sync> {
268    /// Loss variance
269    pub loss_variance: A,
270
271    /// Gradient variance
272    pub gradient_variance: A,
273
274    /// Mean gradient magnitude, i.e. how far the parameters move per step
275    pub parameter_drift: A,
276
277    /// Fraction of successive loss differences that changed sign
278    pub oscillation_score: A,
279
280    /// Empirical fraction of steps where the loss increased
281    pub divergence_probability: A,
282
283    /// Confidence in the stability estimate (falls with relative loss spread)
284    pub stability_confidence: A,
285}
286
287/// Efficiency metrics
288#[derive(Debug, Clone)]
289pub struct EfficiencyMetrics<A: Float + Send + Sync> {
290    /// Loss reduction per second of processing time
291    pub computational_efficiency: Option<A>,
292
293    /// Mean-to-peak memory usage ratio
294    pub memory_efficiency: Option<A>,
295
296    /// Share of processing time not spent communicating; requires the caller
297    /// to report communication times
298    pub communication_efficiency: Option<A>,
299
300    /// Energy efficiency; requires an external energy meter
301    pub energy_efficiency: Option<A>,
302
303    /// Fraction of wall-clock time spent processing
304    pub resource_utilization: A,
305
306    /// Business value per unit cost; requires a configured cost model
307    pub cost_efficiency: Option<A>,
308}
309
310/// Resource utilization metrics.
311///
312/// Every field except `memory_usage` needs an OS-level probe this collector
313/// does not perform itself; feed them with
314/// [`StreamingMetricsCollector::record_resource_probe`].
315#[derive(Debug, Clone, Default)]
316pub struct ResourceMetrics {
317    /// CPU utilization percentage
318    pub cpu_utilization: Option<f64>,
319
320    /// Memory usage
321    pub memory_usage: MemoryUsage,
322
323    /// GPU utilization (if applicable)
324    pub gpu_utilization: Option<f64>,
325
326    /// Network bandwidth usage in MB/s
327    pub network_bandwidth: Option<f64>,
328
329    /// Disk I/O usage in MB/s
330    pub disk_io: Option<f64>,
331
332    /// Thread utilization
333    pub thread_utilization: Option<f64>,
334}
335
336/// Memory usage breakdown
337#[derive(Debug, Clone, Default)]
338pub struct MemoryUsage {
339    /// Total allocated memory (bytes); needs an allocator probe
340    pub total_allocated: Option<u64>,
341
342    /// Currently used memory (bytes)
343    pub current_used: u64,
344
345    /// Peak memory usage (bytes)
346    pub peak_usage: u64,
347
348    /// Memory fragmentation ratio; needs an allocator probe
349    pub fragmentation_ratio: Option<f64>,
350
351    /// Garbage collection overhead. Rust has no garbage collector, so this is
352    /// permanently `None` for in-process measurements.
353    pub gc_overhead: Option<f64>,
354
355    /// Mean-to-peak usage ratio
356    pub efficiency: Option<f64>,
357}
358
359/// Quality metrics for streaming optimization
360#[derive(Debug, Clone)]
361pub struct QualityMetrics<A: Float + Send + Sync> {
362    /// Fraction of recent samples carrying finite, non-negative measurements
363    pub data_quality: A,
364
365    /// Model quality metrics
366    pub model_quality: ModelQuality<A>,
367
368    /// Concept drift metrics
369    pub concept_drift: ConceptDriftMetrics<A>,
370
371    /// Anomaly detection metrics
372    pub anomaly_detection: AnomalyMetrics<A>,
373
374    /// Robustness metrics
375    pub robustness: RobustnessMetrics<A>,
376}
377
378/// Model quality assessment
379#[derive(Debug, Clone)]
380pub struct ModelQuality<A: Float + Send + Sync> {
381    /// Relative loss improvement since the first observed sample
382    pub training_quality: A,
383
384    /// Generalization ability; requires a `val_loss` custom metric
385    pub generalization_score: Option<A>,
386
387    /// Overfitting detection; requires a `val_loss` custom metric
388    pub overfitting_score: Option<A>,
389
390    /// Underfitting detection; requires a `val_loss` custom metric
391    pub underfitting_score: Option<A>,
392
393    /// Model complexity; requires a `parameter_count` custom metric
394    pub complexity_score: Option<A>,
395}
396
397/// Concept drift monitoring metrics
398#[derive(Debug, Clone)]
399pub struct ConceptDriftMetrics<A: Float + Send + Sync> {
400    /// Confidence of the most recent reported drift
401    pub drift_confidence: Option<A>,
402
403    /// Magnitude of the most recent reported drift
404    pub drift_magnitude: Option<A>,
405
406    /// Reported drift events per second over the observed window
407    pub drift_frequency: f64,
408
409    /// Adaptation effectiveness reported alongside a drift event
410    pub adaptation_effectiveness: Option<A>,
411
412    /// Detection latency of the most recent reported drift
413    pub detection_latency: Option<Duration>,
414}
415
416/// Anomaly detection metrics
417#[derive(Debug, Clone)]
418pub struct AnomalyMetrics<A: Float + Send + Sync> {
419    /// Robust z-score of the most recent loss against the running statistics
420    pub anomaly_score: A,
421
422    /// False positive rate; requires labelled ground truth
423    pub false_positive_rate: Option<A>,
424
425    /// False negative rate; requires labelled ground truth
426    pub false_negative_rate: Option<A>,
427
428    /// Detection accuracy; requires labelled ground truth
429    pub detection_accuracy: Option<A>,
430
431    /// Fraction of the retained window flagged as anomalous
432    pub anomaly_frequency: f64,
433}
434
435/// Model robustness metrics. These require deliberate perturbation
436/// experiments; feed them with
437/// [`StreamingMetricsCollector::record_robustness_probe`].
438#[derive(Debug, Clone, Default)]
439pub struct RobustnessMetrics<A: Float + Send + Sync> {
440    /// Noise tolerance
441    pub noise_tolerance: Option<A>,
442
443    /// Adversarial robustness
444    pub adversarial_robustness: Option<A>,
445
446    /// Input perturbation sensitivity
447    pub perturbation_sensitivity: Option<A>,
448
449    /// Recovery capability
450    pub recovery_capability: Option<A>,
451
452    /// Fault tolerance
453    pub fault_tolerance: Option<A>,
454}
455
456/// Business and operational metrics
457#[derive(Debug, Clone)]
458pub struct BusinessMetrics<A: Float + Send + Sync> {
459    /// System availability; requires reported outages
460    pub availability: Option<f64>,
461
462    /// Service level objective compliance; requires configured SLO targets
463    pub slo_compliance: Option<f64>,
464
465    /// Cost metrics
466    pub cost_metrics: CostMetrics<A>,
467
468    /// User satisfaction; requires a `user_satisfaction` custom metric
469    pub user_satisfaction: Option<A>,
470
471    /// Business value; requires a configured cost model
472    pub business_value: Option<A>,
473}
474
475/// Cost-related metrics. All `None` unless a [`CostModel`] is configured.
476#[derive(Debug, Clone, Default)]
477pub struct CostMetrics<A: Float + Send + Sync> {
478    /// Computational cost
479    pub computational_cost: Option<A>,
480
481    /// Infrastructure cost
482    pub infrastructure_cost: Option<A>,
483
484    /// Energy cost
485    pub energy_cost: Option<A>,
486
487    /// Opportunity cost
488    pub opportunity_cost: Option<A>,
489
490    /// Total cost of ownership
491    pub total_cost: Option<A>,
492}
493
494/// Historical metrics storage.
495#[derive(Debug)]
496pub struct HistoricalMetrics<A: Float + Send + Sync> {
497    /// Time-series data storage, keyed by **microseconds** since the Unix epoch
498    /// (`MetricsSnapshot::timestamp_micros`).
499    ///
500    /// This key used to be whole seconds, so two samples taken inside the same
501    /// second silently overwrote each other and the retention/compression logic
502    /// could only ever retain one sample per second no matter how it was
503    /// configured. Microsecond resolution matches what the source
504    /// `SystemTime` actually carries.
505    pub(crate) time_series: BTreeMap<u64, MetricsSnapshot<A>>,
506
507    /// Retention policy
508    pub(crate) retention_policy: RetentionPolicy,
509
510    /// Compression settings
511    pub(crate) compression_config: CompressionConfig,
512}
513
514/// Point-in-time metrics snapshot
515#[derive(Debug, Clone)]
516pub struct MetricsSnapshot<A: Float + Send + Sync> {
517    /// Whole seconds since the Unix epoch.
518    ///
519    /// Kept at second resolution because that is the granularity the
520    /// aggregation buckets (`Minute`/`Hour`/`Day`) are defined over. Use
521    /// [`Self::timestamp_micros`] when full resolution matters.
522    pub timestamp: u64,
523
524    /// Microseconds since the Unix epoch: the full resolution of the sample
525    /// this snapshot was taken from, and the key it is stored under in
526    /// `HistoricalMetrics::time_series`.
527    pub timestamp_micros: u64,
528
529    /// Performance metrics at this time
530    pub performance: PerformanceMetrics<A>,
531
532    /// Resource metrics at this time
533    pub resource: ResourceMetrics,
534
535    /// Quality metrics at this time
536    pub quality: QualityMetrics<A>,
537
538    /// Business metrics at this time
539    pub business: BusinessMetrics<A>,
540}
541
542/// Aggregation periods for historical data
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544pub enum AggregationPeriod {
545    Minute,
546    Hour,
547    Day,
548    Week,
549    Month,
550}
551
552impl AggregationPeriod {
553    /// Length of the period in seconds.
554    pub fn seconds(self) -> u64 {
555        match self {
556            AggregationPeriod::Minute => 60,
557            AggregationPeriod::Hour => 3_600,
558            AggregationPeriod::Day => 86_400,
559            AggregationPeriod::Week => 604_800,
560            AggregationPeriod::Month => 2_592_000, // 30 days
561        }
562    }
563}
564
565/// Aggregated metrics over a time period.
566///
567/// M2: this used to be four whole `MetricsSnapshot`s, which forced the
568/// aggregator to invent values for every field it could not reduce (and the
569/// aggregator simply returned an empty `Vec` instead). It now carries one
570/// [`AggregatedSeries`] per metric path that actually had data in the bucket,
571/// so an absent metric is absent rather than reported as zero.
572#[derive(Debug, Clone)]
573pub struct AggregatedMetrics {
574    /// Aggregation period this bucket belongs to
575    pub period: AggregationPeriod,
576
577    /// Time period start (seconds since the Unix epoch)
578    pub period_start: u64,
579
580    /// Time period end (seconds since the Unix epoch, exclusive)
581    pub period_end: u64,
582
583    /// Number of raw snapshots that went into this bucket
584    pub sample_count: usize,
585
586    /// One entry per metric path that had at least one observation
587    pub series: BTreeMap<String, AggregatedSeries>,
588}
589
590/// Data retention policy
591#[derive(Debug, Clone)]
592pub struct RetentionPolicy {
593    /// Raw data retention (seconds)
594    pub raw_data_retention: u64,
595
596    /// Aggregated data retention by period
597    pub aggregated_retention: HashMap<AggregationPeriod, u64>,
598
599    /// Automatic cleanup enabled
600    pub auto_cleanup: bool,
601
602    /// Maximum storage size (bytes)
603    pub max_storage_size: u64,
604}
605
606/// Data compression configuration
607#[derive(Debug, Clone)]
608pub struct CompressionConfig {
609    /// Enable compression
610    pub enabled: bool,
611
612    /// Compression algorithm
613    pub algorithm: CompressionAlgorithm,
614
615    /// Compression ratio target (retained fraction of raw points)
616    pub target_ratio: f64,
617
618    /// Lossy compression tolerance
619    pub lossy_tolerance: f64,
620}
621
622/// Compression algorithms.
623///
624/// The historical store applies *temporal* compression (dropping snapshots
625/// that are redundant within `lossy_tolerance`). Byte-level codecs are a
626/// property of the export path; requesting one there yields an explicit
627/// unsupported-operation error rather than silently writing uncompressed
628/// bytes.
629#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum CompressionAlgorithm {
631    None,
632    Gzip,
633    Lz4,
634    Zstd,
635    Custom,
636}
637
638/// Real-time dashboard
639#[derive(Debug)]
640pub struct Dashboard {
641    /// Dashboard name
642    pub name: String,
643
644    /// Dashboard widgets
645    pub widgets: Vec<Widget>,
646
647    /// Update frequency
648    pub update_frequency: Duration,
649
650    /// Auto-refresh enabled
651    pub auto_refresh: bool,
652}
653
654/// Dashboard widget
655#[derive(Debug)]
656pub struct Widget {
657    /// Widget type
658    pub widget_type: WidgetType,
659
660    /// Metrics to display
661    pub metrics: Vec<String>,
662
663    /// Display configuration
664    pub config: WidgetConfig,
665}
666
667/// Types of dashboard widgets
668#[derive(Debug, Clone)]
669pub enum WidgetType {
670    LineChart,
671    BarChart,
672    Gauge,
673    Table,
674    Heatmap,
675    Histogram,
676    ScatterPlot,
677    TextDisplay,
678}
679
680/// Widget configuration
681#[derive(Debug, Clone)]
682pub struct WidgetConfig {
683    /// Widget title
684    pub title: String,
685
686    /// Time range to display
687    pub time_range: Duration,
688
689    /// Refresh rate
690    pub refresh_rate: Duration,
691
692    /// Color scheme
693    pub color_scheme: String,
694
695    /// Size and position
696    pub layout: WidgetLayout,
697}
698
699/// Widget layout information
700#[derive(Debug, Clone)]
701pub struct WidgetLayout {
702    /// X position
703    pub x: u32,
704
705    /// Y position
706    pub y: u32,
707
708    /// Width
709    pub width: u32,
710
711    /// Height
712    pub height: u32,
713}
714
715/// Alert system for monitoring
716#[derive(Debug)]
717pub struct AlertSystem<A: Float + Send + Sync> {
718    /// Alert rules
719    pub rules: Vec<AlertRule<A>>,
720
721    /// Active alerts
722    pub active_alerts: Vec<Alert<A>>,
723
724    /// Alert history
725    pub alert_history: Vec<Alert<A>>,
726
727    /// Notification channels
728    pub notification_channels: Vec<NotificationChannel>,
729
730    /// Per-rule bookkeeping used by the evaluator
731    pub(crate) rule_state: HashMap<String, alerts::RuleState>,
732
733    /// Monotonic counter backing collision-free alert identifiers
734    pub(crate) next_alert_id: u64,
735
736    /// Maximum retained resolved alerts
737    pub(crate) max_history: usize,
738}
739
740/// Alert rule definition
741#[derive(Debug, Clone)]
742pub struct AlertRule<A: Float + Send + Sync> {
743    /// Rule name
744    pub name: String,
745
746    /// Metric to monitor
747    pub metric_path: String,
748
749    /// Condition
750    pub condition: AlertCondition<A>,
751
752    /// Severity level
753    pub severity: AlertSeverity,
754
755    /// Evaluation frequency
756    pub evaluation_frequency: Duration,
757
758    /// Notification settings
759    pub notifications: Vec<String>,
760}
761
762/// Alert conditions
763#[derive(Debug, Clone)]
764pub enum AlertCondition<A: Float + Send + Sync> {
765    /// Threshold crossing
766    Threshold {
767        operator: ComparisonOperator,
768        value: A,
769    },
770
771    /// Rate of change
772    RateOfChange { threshold: A, time_window: Duration },
773
774    /// Anomaly detection
775    Anomaly { sensitivity: A },
776
777    /// Custom condition
778    Custom { expression: String },
779}
780
781/// Comparison operators for alerts
782#[derive(Debug, Clone, Copy)]
783pub enum ComparisonOperator {
784    GreaterThan,
785    LessThan,
786    GreaterThanOrEqual,
787    LessThanOrEqual,
788    Equal,
789    NotEqual,
790}
791
792/// Alert severity levels
793#[derive(Debug, Clone, Copy, PartialEq, Eq)]
794pub enum AlertSeverity {
795    Critical,
796    Warning,
797    Info,
798}
799
800/// Active or historical alert
801#[derive(Debug, Clone)]
802pub struct Alert<A: Float + Send + Sync> {
803    /// Alert ID
804    pub id: String,
805
806    /// Rule that triggered the alert
807    pub rule_name: String,
808
809    /// Timestamp when alert was triggered
810    pub triggered_at: SystemTime,
811
812    /// Timestamp when alert was resolved (if applicable)
813    pub resolved_at: Option<SystemTime>,
814
815    /// Current metric value
816    pub current_value: A,
817
818    /// Threshold that was breached
819    pub threshold: A,
820
821    /// Alert severity
822    pub severity: AlertSeverity,
823
824    /// Alert message
825    pub message: String,
826}
827
828/// Notification channels
829#[derive(Debug, Clone)]
830pub enum NotificationChannel {
831    Email {
832        addresses: Vec<String>,
833    },
834    Webhook {
835        url: String,
836        headers: HashMap<String, String>,
837    },
838    Slack {
839        webhook_url: String,
840        channel: String,
841    },
842    PagerDuty {
843        integration_key: String,
844    },
845    Custom {
846        config: HashMap<String, String>,
847    },
848}
849
850/// Metrics aggregation configuration
851#[derive(Debug, Clone)]
852pub struct AggregationConfig {
853    /// Default aggregation functions
854    pub default_functions: Vec<AggregationFunction>,
855
856    /// Custom aggregations by metric
857    pub custom_aggregations: HashMap<String, Vec<AggregationFunction>>,
858
859    /// Aggregation intervals
860    pub intervals: Vec<Duration>,
861
862    /// Maximum aggregation window
863    pub max_window: Duration,
864}
865
866/// Aggregation functions
867#[derive(Debug, Clone, Copy, PartialEq, Eq)]
868pub enum AggregationFunction {
869    Mean,
870    Median,
871    Min,
872    Max,
873    Sum,
874    Count,
875    StdDev,
876    Percentile(u8), // e.g., Percentile(95) for P95
877}
878
879/// Export configuration for metrics
880#[derive(Debug, Clone)]
881pub struct ExportConfig {
882    /// Export formats
883    pub formats: Vec<ExportFormat>,
884
885    /// Export destinations
886    pub destinations: Vec<ExportDestination>,
887
888    /// Export frequency
889    pub frequency: Duration,
890
891    /// Batch size for exports
892    pub batch_size: usize,
893}
894
895/// Export formats
896#[derive(Debug, Clone, PartialEq, Eq)]
897pub enum ExportFormat {
898    Json,
899    Csv,
900    Parquet,
901    Prometheus,
902    InfluxDB,
903    Custom { format: String },
904}
905
906/// Export destinations
907#[derive(Debug, Clone)]
908pub enum ExportDestination {
909    File {
910        path: String,
911    },
912    Database {
913        connection_string: String,
914    },
915    S3 {
916        bucket: String,
917        prefix: String,
918    },
919    Http {
920        endpoint: String,
921        headers: HashMap<String, String>,
922    },
923    Kafka {
924        topic: String,
925        brokers: Vec<String>,
926    },
927}
928
929impl<A: Float + Default + Clone + std::fmt::Debug + Send + Sync> StreamingMetricsCollector<A> {
930    /// Create a new metrics collector
931    pub fn new() -> Self {
932        Self::with_window(512)
933    }
934
935    /// Create a collector retaining `window` raw observations per series.
936    pub fn with_window(window: usize) -> Self {
937        Self {
938            performance_metrics: PerformanceMetrics::default(),
939            resource_metrics: ResourceMetrics::default(),
940            quality_metrics: QualityMetrics::default(),
941            business_metrics: BusinessMetrics::default(),
942            historical_data: HistoricalMetrics::new(),
943            dashboards: Vec::new(),
944            alert_system: AlertSystem::new(),
945            aggregation_config: AggregationConfig::default(),
946            export_config: ExportConfig::default(),
947            accumulator: MetricsAccumulator::new(window),
948            slo: None,
949            cost_model: None,
950            last_export: None,
951        }
952    }
953
954    /// Configure service level objectives so `slo_compliance` becomes a real
955    /// measurement instead of `None`.
956    pub fn set_slo_targets(&mut self, targets: SloTargets) {
957        self.slo = if targets.is_empty() {
958            None
959        } else {
960            Some(targets)
961        };
962    }
963
964    /// Configure a cost model so the cost metrics become real.
965    pub fn set_cost_model(&mut self, model: CostModel<A>) {
966        self.cost_model = Some(model);
967    }
968
969    /// Feed OS-level resource measurements this collector cannot take itself.
970    pub fn record_resource_probe(&mut self, probe: ResourceProbe) {
971        self.accumulator.record_resource_probe(probe);
972    }
973
974    /// Feed robustness measurements obtained from perturbation experiments.
975    pub fn record_robustness_probe(&mut self, probe: RobustnessProbe<A>) {
976        self.accumulator.record_robustness_probe(probe);
977    }
978
979    /// Report an observed outage so `availability` becomes a real ratio.
980    pub fn record_outage(&mut self, downtime: Duration) {
981        self.accumulator.record_outage(downtime);
982    }
983
984    /// Report a detected concept-drift event.
985    pub fn record_drift_event(
986        &mut self,
987        magnitude: A,
988        confidence: A,
989        detection_latency: Duration,
990        adaptation_effectiveness: Option<A>,
991    ) {
992        self.accumulator.record_drift_event(
993            magnitude,
994            confidence,
995            detection_latency,
996            adaptation_effectiveness,
997        );
998    }
999
1000    /// Report measured energy consumption for the most recent samples.
1001    pub fn record_energy(&mut self, joules: f64) {
1002        self.accumulator.record_energy(joules);
1003    }
1004
1005    /// Record a new metrics sample
1006    pub fn record_sample(&mut self, sample: MetricsSample<A>) -> Result<()> {
1007        self.accumulator.ingest(&sample);
1008
1009        // Update current metrics from the accumulated observations.
1010        self.update_performance_metrics(&sample)?;
1011        self.update_resource_metrics(&sample)?;
1012        self.update_quality_metrics(&sample)?;
1013        self.update_business_metrics(&sample)?;
1014
1015        // Store historical data (M4: saturating, never panicking).
1016        let timestamp = unix_timestamp(sample.timestamp);
1017        let timestamp_micros = unix_timestamp_micros(sample.timestamp);
1018
1019        let snapshot = MetricsSnapshot {
1020            timestamp,
1021            timestamp_micros,
1022            performance: self.performance_metrics.clone(),
1023            resource: self.resource_metrics.clone(),
1024            quality: self.quality_metrics.clone(),
1025            business: self.business_metrics.clone(),
1026        };
1027
1028        self.historical_data.store_snapshot(snapshot)?;
1029
1030        // Check alerts
1031        let summary = self.current_summary();
1032        self.alert_system.evaluate_rules(&sample, &summary)?;
1033
1034        Ok(())
1035    }
1036
1037    /// Get current metrics summary
1038    pub fn get_current_metrics(&self) -> MetricsSummary<A> {
1039        self.current_summary()
1040    }
1041
1042    pub(crate) fn current_summary(&self) -> MetricsSummary<A> {
1043        MetricsSummary {
1044            performance: self.performance_metrics.clone(),
1045            resource: self.resource_metrics.clone(),
1046            quality: self.quality_metrics.clone(),
1047            business: self.business_metrics.clone(),
1048            timestamp: SystemTime::now(),
1049        }
1050    }
1051
1052    /// Get historical metrics for a time range
1053    pub fn get_historical_metrics(
1054        &self,
1055        start_time: SystemTime,
1056        end_time: SystemTime,
1057    ) -> Result<Vec<MetricsSnapshot<A>>> {
1058        self.historical_data.get_range(start_time, end_time)
1059    }
1060
1061    /// Number of raw snapshots currently retained.
1062    pub fn retained_snapshot_count(&self) -> usize {
1063        self.historical_data.time_series.len()
1064    }
1065
1066    /// Register an alert rule. Rules whose condition cannot be evaluated by
1067    /// this crate are rejected here instead of being silently ignored at
1068    /// evaluation time.
1069    pub fn add_alert_rule(&mut self, rule: AlertRule<A>) -> Result<()> {
1070        self.alert_system.add_rule(rule)
1071    }
1072
1073    /// Currently firing alerts.
1074    pub fn active_alerts(&self) -> &[Alert<A>] {
1075        &self.alert_system.active_alerts
1076    }
1077
1078    /// Resolved alerts, most recent last.
1079    pub fn alert_history(&self) -> &[Alert<A>] {
1080        &self.alert_system.alert_history
1081    }
1082
1083    /// Register a dashboard definition.
1084    pub fn add_dashboard(&mut self, dashboard: Dashboard) {
1085        self.dashboards.push(dashboard);
1086    }
1087
1088    /// Resolve every metric path referenced by a dashboard's widgets against
1089    /// the current metrics.
1090    pub fn render_dashboard(&self, name: &str) -> Option<Vec<(String, Option<f64>)>> {
1091        let dashboard = self.dashboards.iter().find(|d| d.name == name)?;
1092        let summary = self.current_summary();
1093        let mut resolved = Vec::new();
1094        for widget in &dashboard.widgets {
1095            for metric in &widget.metrics {
1096                resolved.push((metric.clone(), alerts::resolve_metric(&summary, metric)));
1097            }
1098        }
1099        Some(resolved)
1100    }
1101
1102    /// Access to the aggregation configuration.
1103    pub fn aggregation_config(&self) -> &AggregationConfig {
1104        &self.aggregation_config
1105    }
1106
1107    /// Replace the aggregation configuration.
1108    pub fn set_aggregation_config(&mut self, config: AggregationConfig) {
1109        self.aggregation_config = config;
1110    }
1111
1112    /// Replace the export configuration.
1113    pub fn set_export_config(&mut self, config: ExportConfig) {
1114        self.export_config = config;
1115    }
1116
1117    /// Replace the retention policy governing the raw time series and the
1118    /// per-period aggregated roll-ups.
1119    ///
1120    /// `raw_data_retention` prunes the stored snapshots immediately;
1121    /// `aggregated_retention` is applied when a roll-up is requested, since the
1122    /// roll-up is computed on demand rather than stored.
1123    pub fn set_retention_policy(&mut self, policy: RetentionPolicy) {
1124        self.historical_data.retention_policy = policy;
1125        self.historical_data.prune();
1126    }
1127
1128    /// Replace the temporal-compression configuration.
1129    pub fn set_compression_config(&mut self, config: CompressionConfig) {
1130        self.historical_data.compression_config = config;
1131    }
1132}
1133
1134impl<A: Float + Default + Clone + std::fmt::Debug + Send + Sync> Default
1135    for StreamingMetricsCollector<A>
1136{
1137    fn default() -> Self {
1138        Self::new()
1139    }
1140}
1141
1142/// Individual metrics sample
1143#[derive(Debug, Clone)]
1144pub struct MetricsSample<A: Float + Send + Sync> {
1145    /// Timestamp of the sample
1146    pub timestamp: SystemTime,
1147
1148    /// Loss value
1149    pub loss: A,
1150
1151    /// Gradient magnitude
1152    pub gradient_magnitude: A,
1153
1154    /// Processing time
1155    pub processing_time: Duration,
1156
1157    /// Memory usage
1158    pub memory_usage: u64,
1159
1160    /// Gradient computation time, when the caller measured it separately
1161    pub gradient_computation_time: Option<Duration>,
1162
1163    /// Update application time, when the caller measured it separately
1164    pub update_application_time: Option<Duration>,
1165
1166    /// Communication time, when the caller measured it separately
1167    pub communication_time: Option<Duration>,
1168
1169    /// Queue waiting time, when the caller measured it separately
1170    pub queue_wait_time: Option<Duration>,
1171
1172    /// Additional custom metrics
1173    pub custom_metrics: HashMap<String, A>,
1174}
1175
1176impl<A: Float + Send + Sync> MetricsSample<A> {
1177    /// Build a sample from the measurements every caller has.
1178    pub fn new(
1179        timestamp: SystemTime,
1180        loss: A,
1181        gradient_magnitude: A,
1182        processing_time: Duration,
1183        memory_usage: u64,
1184    ) -> Self {
1185        Self {
1186            timestamp,
1187            loss,
1188            gradient_magnitude,
1189            processing_time,
1190            memory_usage,
1191            gradient_computation_time: None,
1192            update_application_time: None,
1193            communication_time: None,
1194            queue_wait_time: None,
1195            custom_metrics: HashMap::new(),
1196        }
1197    }
1198}
1199
1200/// Complete metrics summary
1201#[derive(Debug, Clone)]
1202pub struct MetricsSummary<A: Float + Send + Sync> {
1203    /// Performance metrics
1204    pub performance: PerformanceMetrics<A>,
1205
1206    /// Resource metrics
1207    pub resource: ResourceMetrics,
1208
1209    /// Quality metrics
1210    pub quality: QualityMetrics<A>,
1211
1212    /// Business metrics
1213    pub business: BusinessMetrics<A>,
1214
1215    /// Summary timestamp
1216    pub timestamp: SystemTime,
1217}
1218
1219// Implement default traits for metrics structs
1220impl<A: Float + Default + Send + Sync> Default for PerformanceMetrics<A> {
1221    fn default() -> Self {
1222        Self {
1223            throughput: ThroughputMetrics::default(),
1224            latency: LatencyMetrics::default(),
1225            accuracy: AccuracyMetrics::default(),
1226            stability: StabilityMetrics::default(),
1227            efficiency: EfficiencyMetrics::default(),
1228        }
1229    }
1230}
1231
1232impl Default for ThroughputMetrics {
1233    fn default() -> Self {
1234        Self {
1235            samples_per_second: 0.0,
1236            updates_per_second: 0.0,
1237            gradients_per_second: 0.0,
1238            peak_throughput: 0.0,
1239            min_throughput: f64::MAX,
1240            throughput_variance: 0.0,
1241            throughput_trend: 0.0,
1242        }
1243    }
1244}
1245
1246impl Default for LatencyMetrics {
1247    fn default() -> Self {
1248        Self {
1249            end_to_end: LatencyStats::default(),
1250            gradient_computation: None,
1251            update_application: None,
1252            communication: None,
1253            queue_wait_time: None,
1254            jitter: 0.0,
1255        }
1256    }
1257}
1258
1259impl Default for LatencyStats {
1260    fn default() -> Self {
1261        Self {
1262            mean: Duration::from_micros(0),
1263            median: Duration::from_micros(0),
1264            p95: Duration::from_micros(0),
1265            p99: Duration::from_micros(0),
1266            p999: Duration::from_micros(0),
1267            max: Duration::from_micros(0),
1268            min: Duration::from_micros(u64::MAX),
1269            std_dev: Duration::from_micros(0),
1270        }
1271    }
1272}
1273
1274impl<A: Float + Default + Send + Sync> Default for AccuracyMetrics<A> {
1275    fn default() -> Self {
1276        Self {
1277            current_loss: A::default(),
1278            loss_reduction_rate: A::default(),
1279            convergence_rate: A::default(),
1280            prediction_accuracy: None,
1281            gradient_magnitude: A::default(),
1282            parameter_stability: A::default(),
1283            learning_progress: A::default(),
1284        }
1285    }
1286}
1287
1288impl<A: Float + Default + Send + Sync> Default for StabilityMetrics<A> {
1289    fn default() -> Self {
1290        Self {
1291            loss_variance: A::default(),
1292            gradient_variance: A::default(),
1293            parameter_drift: A::default(),
1294            oscillation_score: A::default(),
1295            divergence_probability: A::default(),
1296            stability_confidence: A::default(),
1297        }
1298    }
1299}
1300
1301impl<A: Float + Default + Send + Sync> Default for EfficiencyMetrics<A> {
1302    fn default() -> Self {
1303        Self {
1304            computational_efficiency: None,
1305            memory_efficiency: None,
1306            communication_efficiency: None,
1307            energy_efficiency: None,
1308            resource_utilization: A::default(),
1309            cost_efficiency: None,
1310        }
1311    }
1312}
1313
1314impl<A: Float + Default + Send + Sync> Default for QualityMetrics<A> {
1315    fn default() -> Self {
1316        Self {
1317            data_quality: A::default(),
1318            model_quality: ModelQuality::default(),
1319            concept_drift: ConceptDriftMetrics::default(),
1320            anomaly_detection: AnomalyMetrics::default(),
1321            robustness: RobustnessMetrics::default(),
1322        }
1323    }
1324}
1325
1326impl<A: Float + Default + Send + Sync> Default for ModelQuality<A> {
1327    fn default() -> Self {
1328        Self {
1329            training_quality: A::default(),
1330            generalization_score: None,
1331            overfitting_score: None,
1332            underfitting_score: None,
1333            complexity_score: None,
1334        }
1335    }
1336}
1337
1338impl<A: Float + Default + Send + Sync> Default for ConceptDriftMetrics<A> {
1339    fn default() -> Self {
1340        Self {
1341            drift_confidence: None,
1342            drift_magnitude: None,
1343            drift_frequency: 0.0,
1344            adaptation_effectiveness: None,
1345            detection_latency: None,
1346        }
1347    }
1348}
1349
1350impl<A: Float + Default + Send + Sync> Default for AnomalyMetrics<A> {
1351    fn default() -> Self {
1352        Self {
1353            anomaly_score: A::default(),
1354            false_positive_rate: None,
1355            false_negative_rate: None,
1356            detection_accuracy: None,
1357            anomaly_frequency: 0.0,
1358        }
1359    }
1360}
1361
1362impl<A: Float + Default + Send + Sync> Default for BusinessMetrics<A> {
1363    fn default() -> Self {
1364        Self {
1365            availability: None,
1366            slo_compliance: None,
1367            cost_metrics: CostMetrics::default(),
1368            user_satisfaction: None,
1369            business_value: None,
1370        }
1371    }
1372}
1373
1374impl<A: Float + Send + Sync> HistoricalMetrics<A> {
1375    fn new() -> Self {
1376        Self {
1377            time_series: BTreeMap::new(),
1378            retention_policy: RetentionPolicy::default(),
1379            compression_config: CompressionConfig::default(),
1380        }
1381    }
1382}
1383
1384impl Default for RetentionPolicy {
1385    fn default() -> Self {
1386        let mut aggregated_retention = HashMap::new();
1387        aggregated_retention.insert(AggregationPeriod::Minute, 3600 * 24); // 1 day
1388        aggregated_retention.insert(AggregationPeriod::Hour, 3600 * 24 * 7); // 1 week
1389        aggregated_retention.insert(AggregationPeriod::Day, 3600 * 24 * 30); // 1 month
1390        aggregated_retention.insert(AggregationPeriod::Week, 3600 * 24 * 365); // 1 year
1391        aggregated_retention.insert(AggregationPeriod::Month, 3600 * 24 * 365 * 5); // 5 years
1392
1393        Self {
1394            raw_data_retention: 3600 * 24, // 1 day
1395            aggregated_retention,
1396            auto_cleanup: true,
1397            max_storage_size: 1024 * 1024 * 1024 * 10, // 10GB
1398        }
1399    }
1400}
1401
1402impl Default for CompressionConfig {
1403    /// Temporal compression is **opt-in**.
1404    ///
1405    /// The previous default was `enabled: true` with `algorithm: Zstd`, which
1406    /// was a no-op because no byte codec was ever applied. Now that the
1407    /// temporal pass is real, leaving it on by default would silently discard
1408    /// ~70% of the retained history (`target_ratio: 0.3`) — a surprising
1409    /// default for a metrics store, and a behaviour change relative to what
1410    /// callers actually observed before. Retention and the storage cap are what
1411    /// bound the series by default; downsampling is something an operator asks
1412    /// for through [`StreamingMetricsCollector::set_compression_config`].
1413    fn default() -> Self {
1414        Self {
1415            enabled: false,
1416            algorithm: CompressionAlgorithm::None,
1417            target_ratio: 0.3,
1418            lossy_tolerance: 0.01,
1419        }
1420    }
1421}
1422
1423impl Default for AggregationConfig {
1424    fn default() -> Self {
1425        Self {
1426            default_functions: vec![
1427                AggregationFunction::Mean,
1428                AggregationFunction::Min,
1429                AggregationFunction::Max,
1430                AggregationFunction::StdDev,
1431            ],
1432            custom_aggregations: HashMap::new(),
1433            intervals: vec![
1434                Duration::from_secs(60),    // 1 minute
1435                Duration::from_secs(3600),  // 1 hour
1436                Duration::from_secs(86400), // 1 day
1437            ],
1438            max_window: Duration::from_secs(86400 * 30), // 30 days
1439        }
1440    }
1441}
1442
1443impl Default for ExportConfig {
1444    fn default() -> Self {
1445        Self {
1446            formats: vec![ExportFormat::Json],
1447            destinations: vec![ExportDestination::File {
1448                path: std::env::temp_dir()
1449                    .join("streaming_metrics")
1450                    .to_string_lossy()
1451                    .into_owned(),
1452            }],
1453            frequency: Duration::from_secs(300), // 5 minutes
1454            batch_size: 1000,
1455        }
1456    }
1457}
1458
1459#[cfg(test)]
1460mod tests {
1461    use super::*;
1462
1463    #[test]
1464    fn test_metrics_collector_creation() {
1465        let collector = StreamingMetricsCollector::<f64>::new();
1466        assert_eq!(
1467            collector.performance_metrics.throughput.samples_per_second,
1468            0.0
1469        );
1470        assert!(collector.dashboards.is_empty());
1471    }
1472
1473    #[test]
1474    fn test_metrics_sample() {
1475        let sample = MetricsSample::new(
1476            SystemTime::now(),
1477            0.5f64,
1478            0.1f64,
1479            Duration::from_millis(10),
1480            1024,
1481        );
1482
1483        assert_eq!(sample.loss, 0.5f64);
1484        assert_eq!(sample.gradient_magnitude, 0.1f64);
1485    }
1486
1487    #[test]
1488    fn test_latency_stats_default() {
1489        let stats = LatencyStats::default();
1490        assert_eq!(stats.mean, Duration::from_micros(0));
1491        assert_eq!(stats.min, Duration::from_micros(u64::MAX));
1492    }
1493
1494    #[test]
1495    fn test_aggregation_period() {
1496        let periods = [
1497            AggregationPeriod::Minute,
1498            AggregationPeriod::Hour,
1499            AggregationPeriod::Day,
1500            AggregationPeriod::Week,
1501            AggregationPeriod::Month,
1502        ];
1503
1504        assert_eq!(periods.len(), 5);
1505    }
1506
1507    #[test]
1508    fn test_alert_severity() {
1509        let severities = [
1510            AlertSeverity::Critical,
1511            AlertSeverity::Warning,
1512            AlertSeverity::Info,
1513        ];
1514
1515        assert_eq!(severities.len(), 3);
1516    }
1517}