1#![allow(dead_code)]
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, VecDeque};
14use std::time::SystemTime;
15use tracing::info;
16use uuid::Uuid;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RegressionDetectionConfig {
21 pub enable_detection: bool,
23 pub min_data_points: usize,
25 pub significance_threshold: f64,
27 pub min_degradation_threshold: f64,
29 pub max_history_hours: u64,
31 pub ema_smoothing_factor: f64,
33 pub enable_ml_detection: bool,
35 pub ml_confidence_threshold: f64,
37 pub enable_seasonal_adjustment: bool,
39 pub enable_outlier_filtering: bool,
41}
42
43impl Default for RegressionDetectionConfig {
44 fn default() -> Self {
45 Self {
46 enable_detection: true,
47 min_data_points: 10,
48 significance_threshold: 0.05,
49 min_degradation_threshold: 5.0, max_history_hours: 24,
51 ema_smoothing_factor: 0.3,
52 enable_ml_detection: true,
53 ml_confidence_threshold: 0.8,
54 enable_seasonal_adjustment: true,
55 enable_outlier_filtering: true,
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
62pub enum MetricType {
63 Latency,
65 MemoryUsage,
67 CpuUtilization,
69 GpuUtilization,
71 Throughput,
73 ModelAccuracy,
75 Custom(String),
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MetricDataPoint {
82 pub metric_type: MetricType,
83 pub value: f64,
84 pub timestamp: SystemTime,
85 pub session_id: Uuid,
86 pub metadata: HashMap<String, String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MetricSeries {
92 pub metric_type: MetricType,
93 pub data_points: VecDeque<MetricDataPoint>,
94 pub baseline_statistics: BaselineStatistics,
95 pub last_updated: SystemTime,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct BaselineStatistics {
101 pub mean: f64,
102 pub std_dev: f64,
103 pub median: f64,
104 pub percentile_95: f64,
105 pub percentile_99: f64,
106 pub trend_slope: f64,
107 pub seasonal_pattern: Option<Vec<f64>>,
108 pub sample_count: usize,
109 pub last_computed: SystemTime,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RegressionDetection {
115 pub detection_id: Uuid,
116 pub metric_type: MetricType,
117 pub regression_type: RegressionType,
118 pub severity: RegressionSeverity,
119 pub confidence: f64,
120 pub degradation_percentage: f64,
121 pub statistical_significance: f64,
122 pub affected_period: (SystemTime, SystemTime),
123 pub root_cause_analysis: RootCauseAnalysis,
124 pub recommendations: Vec<String>,
125 pub detected_at: SystemTime,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130pub enum RegressionType {
131 StepChange,
133 GradualDegradation,
135 VarianceIncrease,
137 PeriodicRegression,
139 OutlierRegression,
141 ComplexRegression,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
147pub enum RegressionSeverity {
148 Low,
149 Medium,
150 High,
151 Critical,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RootCauseAnalysis {
157 pub likely_causes: Vec<PotentialCause>,
158 pub correlated_metrics: Vec<String>,
159 pub environmental_factors: Vec<String>,
160 pub change_points: Vec<SystemTime>,
161 pub anomaly_score: f64,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct PotentialCause {
167 pub cause_type: CauseType,
168 pub description: String,
169 pub confidence: f64,
170 pub supporting_evidence: Vec<String>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub enum CauseType {
176 CodeChange,
177 DataChange,
178 ResourceContention,
179 HardwareIssue,
180 ConfigurationChange,
181 EnvironmentalFactor,
182 ModelDrift,
183 Unknown,
184}
185
186pub struct RegressionDetector {
188 config: RegressionDetectionConfig,
189 metric_series: HashMap<MetricType, MetricSeries>,
190 anomaly_detector: AnomalyDetector,
191 trend_analyzer: TrendAnalyzer,
192 change_point_detector: ChangePointDetector,
193 seasonal_decomposer: SeasonalDecomposer,
194 ml_predictor: Option<MLPredictor>,
195 detection_history: VecDeque<RegressionDetection>,
196}
197
198#[derive(Debug)]
200struct AnomalyDetector {
201 z_score_threshold: f64,
202 iqr_multiplier: f64,
203 isolation_forest_threshold: f64,
204}
205
206impl AnomalyDetector {
207 fn new() -> Self {
208 Self {
209 z_score_threshold: 3.0,
210 iqr_multiplier: 1.5,
211 isolation_forest_threshold: 0.1,
212 }
213 }
214
215 fn detect_outliers(&self, values: &[f64]) -> Vec<bool> {
217 if values.is_empty() {
218 return vec![];
219 }
220
221 let z_score_outliers = self.detect_z_score_outliers(values);
222 let iqr_outliers = self.detect_iqr_outliers(values);
223
224 z_score_outliers
226 .iter()
227 .zip(iqr_outliers.iter())
228 .map(|(&z_outlier, &iqr_outlier)| z_outlier || iqr_outlier)
229 .collect()
230 }
231
232 fn detect_z_score_outliers(&self, values: &[f64]) -> Vec<bool> {
233 let mean = values.iter().sum::<f64>() / values.len() as f64;
234 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
235 let std_dev = variance.sqrt();
236
237 values
238 .iter()
239 .map(|&value| {
240 if std_dev > 0.0 {
241 ((value - mean) / std_dev).abs() > self.z_score_threshold
242 } else {
243 false
244 }
245 })
246 .collect()
247 }
248
249 fn detect_iqr_outliers(&self, values: &[f64]) -> Vec<bool> {
250 let mut sorted_values = values.to_vec();
251 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
252
253 let q1 = Self::percentile(&sorted_values, 25.0);
254 let q3 = Self::percentile(&sorted_values, 75.0);
255 let iqr = q3 - q1;
256
257 let lower_bound = q1 - self.iqr_multiplier * iqr;
258 let upper_bound = q3 + self.iqr_multiplier * iqr;
259
260 values.iter().map(|&value| value < lower_bound || value > upper_bound).collect()
261 }
262
263 fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
264 if sorted_values.is_empty() {
265 return 0.0;
266 }
267
268 let index = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
269 let lower = index.floor() as usize;
270 let upper = index.ceil() as usize;
271
272 if lower == upper {
273 sorted_values[lower]
274 } else {
275 let weight = index - lower as f64;
276 sorted_values[lower] * (1.0 - weight) + sorted_values[upper] * weight
277 }
278 }
279}
280
281#[derive(Debug)]
283struct TrendAnalyzer {
284 window_size: usize,
285 significance_threshold: f64,
286}
287
288impl TrendAnalyzer {
289 fn new(window_size: usize, significance_threshold: f64) -> Self {
290 Self {
291 window_size,
292 significance_threshold,
293 }
294 }
295
296 fn detect_trend_change(&self, values: &[f64]) -> Option<TrendChangeResult> {
298 if values.len() < self.window_size {
299 return None;
300 }
301
302 let recent_values = &values[values.len() - self.window_size..];
303 let baseline_values = if values.len() >= 2 * self.window_size {
304 &values[values.len() - 2 * self.window_size..values.len() - self.window_size]
305 } else {
306 &values[0..values.len() - self.window_size]
307 };
308
309 let recent_slope = self.calculate_slope(recent_values);
310 let baseline_slope = self.calculate_slope(baseline_values);
311
312 let slope_change = recent_slope - baseline_slope;
313 let significance = self.calculate_trend_significance(recent_values, recent_slope);
314
315 if significance < self.significance_threshold {
316 Some(TrendChangeResult {
317 slope_change,
318 recent_slope,
319 baseline_slope,
320 significance,
321 is_regression: slope_change > 0.0, })
323 } else {
324 None
325 }
326 }
327
328 fn calculate_slope(&self, values: &[f64]) -> f64 {
329 if values.len() < 2 {
330 return 0.0;
331 }
332
333 let n = values.len() as f64;
334 let sum_x = (0..values.len()).sum::<usize>() as f64;
335 let sum_y = values.iter().sum::<f64>();
336 let sum_xy = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum::<f64>();
337 let sum_x_squared = (0..values.len()).map(|i| (i as f64).powi(2)).sum::<f64>();
338
339 let denominator = n * sum_x_squared - sum_x.powi(2);
340 if denominator.abs() < 1e-10 {
341 0.0
342 } else {
343 (n * sum_xy - sum_x * sum_y) / denominator
344 }
345 }
346
347 fn calculate_trend_significance(&self, values: &[f64], slope: f64) -> f64 {
348 if values.len() < 3 {
350 return 1.0;
351 }
352
353 let n = values.len() as f64;
354 let mean_x = (values.len() - 1) as f64 / 2.0;
355 let ss_x = (0..values.len()).map(|i| (i as f64 - mean_x).powi(2)).sum::<f64>();
356
357 let mean_y = values.iter().sum::<f64>() / n;
359 let intercept = mean_y - slope * mean_x;
360 let predicted: Vec<f64> = (0..values.len()).map(|i| intercept + slope * i as f64).collect();
361
362 let residuals: Vec<f64> = values
363 .iter()
364 .zip(predicted.iter())
365 .map(|(&actual, &pred)| actual - pred)
366 .collect();
367
368 let mse = residuals.iter().map(|&r| r.powi(2)).sum::<f64>() / (n - 2.0);
369 let se_slope = (mse / ss_x).sqrt();
370
371 if se_slope > 0.0 {
372 let t_stat = slope / se_slope;
373 let df = n - 2.0;
375 if df > 0.0 {
376 2.0 * (1.0 - Self::t_distribution_cdf(t_stat.abs(), df))
377 } else {
378 1.0
379 }
380 } else {
381 1.0
382 }
383 }
384
385 fn t_distribution_cdf(t: f64, df: f64) -> f64 {
386 let x = t / (df + t.powi(2)).sqrt();
389 0.5 + 0.5 * x.atan() * (2.0 / std::f64::consts::PI)
390 }
391}
392
393#[derive(Debug)]
394struct TrendChangeResult {
395 slope_change: f64,
396 recent_slope: f64,
397 baseline_slope: f64,
398 significance: f64,
399 is_regression: bool,
400}
401
402#[derive(Debug)]
404struct ChangePointDetector {
405 min_segment_length: usize,
406 penalty_factor: f64,
407}
408
409impl ChangePointDetector {
410 fn new(min_segment_length: usize, penalty_factor: f64) -> Self {
411 Self {
412 min_segment_length,
413 penalty_factor,
414 }
415 }
416
417 fn detect_change_points(&self, values: &[f64]) -> Vec<usize> {
419 if values.len() < 2 * self.min_segment_length {
420 return vec![];
421 }
422
423 let mut change_points = vec![];
424 let mut current_start = 0;
425
426 while current_start + 2 * self.min_segment_length <= values.len() {
427 if let Some(change_point) = self.find_next_change_point(&values[current_start..]) {
428 let absolute_change_point = current_start + change_point;
429 change_points.push(absolute_change_point);
430 current_start = absolute_change_point + self.min_segment_length;
431 } else {
432 break;
433 }
434 }
435
436 change_points
437 }
438
439 fn find_next_change_point(&self, values: &[f64]) -> Option<usize> {
440 let n = values.len();
441 if n < 2 * self.min_segment_length {
442 return None;
443 }
444
445 let mut max_statistic = 0.0;
446 let mut best_change_point = None;
447
448 for t in self.min_segment_length..n - self.min_segment_length {
449 let statistic = self.cusum_statistic(values, t);
450 if statistic > max_statistic {
451 max_statistic = statistic;
452 best_change_point = Some(t);
453 }
454 }
455
456 let threshold = self.penalty_factor * (n as f64).ln();
458 if max_statistic > threshold {
459 best_change_point
460 } else {
461 None
462 }
463 }
464
465 fn cusum_statistic(&self, values: &[f64], change_point: usize) -> f64 {
466 let segment1 = &values[0..change_point];
467 let segment2 = &values[change_point..];
468
469 let mean1 = segment1.iter().sum::<f64>() / segment1.len() as f64;
470 let mean2 = segment2.iter().sum::<f64>() / segment2.len() as f64;
471 let overall_mean = values.iter().sum::<f64>() / values.len() as f64;
472
473 let n1 = segment1.len() as f64;
474 let n2 = segment2.len() as f64;
475 let n = values.len() as f64;
476
477 let variance = values.iter().map(|&x| (x - overall_mean).powi(2)).sum::<f64>() / (n - 1.0);
479
480 if variance > 0.0 {
481 (n1 * (mean1 - overall_mean).powi(2) + n2 * (mean2 - overall_mean).powi(2)) / variance
482 } else {
483 0.0
484 }
485 }
486}
487
488#[derive(Debug)]
490struct SeasonalDecomposer {
491 period: usize,
492 enable_decomposition: bool,
493}
494
495impl SeasonalDecomposer {
496 fn new(period: usize) -> Self {
497 Self {
498 period,
499 enable_decomposition: true,
500 }
501 }
502
503 fn decompose(&self, values: &[f64]) -> Option<SeasonalComponents> {
505 if !self.enable_decomposition || values.len() < 2 * self.period {
506 return None;
507 }
508
509 let trend = self.extract_trend(values);
510 let detrended = self.subtract_series(values, &trend);
511 let seasonal = self.extract_seasonal(&detrended);
512 let residual = self.subtract_series(&detrended, &seasonal);
513
514 Some(SeasonalComponents {
515 trend,
516 seasonal,
517 residual,
518 })
519 }
520
521 fn extract_trend(&self, values: &[f64]) -> Vec<f64> {
522 let window_size = self.period;
524 let mut trend = vec![0.0; values.len()];
525
526 for i in 0..values.len() {
527 let start = i.saturating_sub(window_size / 2);
528 let end = std::cmp::min(i + window_size / 2 + 1, values.len());
529
530 let sum: f64 = values[start..end].iter().sum();
531 trend[i] = sum / (end - start) as f64;
532 }
533
534 trend
535 }
536
537 fn extract_seasonal(&self, detrended: &[f64]) -> Vec<f64> {
538 let mut seasonal = vec![0.0; detrended.len()];
539 let mut seasonal_pattern = vec![0.0; self.period];
540 let mut pattern_counts = vec![0usize; self.period];
541
542 for (i, &value) in detrended.iter().enumerate() {
544 let season_index = i % self.period;
545 seasonal_pattern[season_index] += value;
546 pattern_counts[season_index] += 1;
547 }
548
549 for i in 0..self.period {
551 if pattern_counts[i] > 0 {
552 seasonal_pattern[i] /= pattern_counts[i] as f64;
553 }
554 }
555
556 for (i, seasonal_value) in seasonal.iter_mut().enumerate() {
558 *seasonal_value = seasonal_pattern[i % self.period];
559 }
560
561 seasonal
562 }
563
564 fn subtract_series(&self, series1: &[f64], series2: &[f64]) -> Vec<f64> {
565 series1.iter().zip(series2.iter()).map(|(&a, &b)| a - b).collect()
566 }
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
570struct SeasonalComponents {
571 trend: Vec<f64>,
572 seasonal: Vec<f64>,
573 residual: Vec<f64>,
574}
575
576#[derive(Debug)]
578struct MLPredictor {
579 model_type: MLModelType,
580 feature_extractor: FeatureExtractor,
581 prediction_threshold: f64,
582}
583
584#[derive(Debug)]
585enum MLModelType {
586 IsolationForest,
587 LSTM,
588 AutoEncoder,
589}
590
591#[derive(Debug)]
592struct FeatureExtractor {
593 window_size: usize,
594 statistical_features: bool,
595 frequency_features: bool,
596}
597
598impl MLPredictor {
599 fn new(model_type: MLModelType, prediction_threshold: f64) -> Self {
600 Self {
601 model_type,
602 feature_extractor: FeatureExtractor {
603 window_size: 50,
604 statistical_features: true,
605 frequency_features: true,
606 },
607 prediction_threshold,
608 }
609 }
610
611 fn predict_regression(&self, values: &[f64]) -> Option<MLPrediction> {
613 if values.len() < self.feature_extractor.window_size {
614 return None;
615 }
616
617 let features = self.feature_extractor.extract_features(values);
618
619 let anomaly_score = self.calculate_anomaly_score(&features);
621 let confidence = self.calculate_confidence(&features);
622
623 if anomaly_score > self.prediction_threshold {
624 Some(MLPrediction {
625 anomaly_score,
626 confidence,
627 feature_importance: self.calculate_feature_importance(&features),
628 predicted_severity: self.predict_severity(anomaly_score),
629 })
630 } else {
631 None
632 }
633 }
634
635 fn calculate_anomaly_score(&self, features: &[f64]) -> f64 {
636 let mean = features.iter().sum::<f64>() / features.len() as f64;
638 let variance =
639 features.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / features.len() as f64;
640
641 variance.sqrt() / (mean.abs() + 1e-6)
642 }
643
644 fn calculate_confidence(&self, features: &[f64]) -> f64 {
645 let feature_consistency = 1.0
647 - (features.iter().map(|&x| (x - features[0]).abs()).sum::<f64>()
648 / (features.len() as f64 * features[0].abs() + 1e-6));
649
650 feature_consistency.max(0.0).min(1.0)
651 }
652
653 fn calculate_feature_importance(&self, features: &[f64]) -> Vec<f64> {
654 let max_magnitude = features.iter().map(|x| x.abs()).fold(0.0, f64::max);
656
657 if max_magnitude > 0.0 {
658 features.iter().map(|&x| x.abs() / max_magnitude).collect()
659 } else {
660 vec![0.0; features.len()]
661 }
662 }
663
664 fn predict_severity(&self, anomaly_score: f64) -> RegressionSeverity {
665 if anomaly_score > 0.8 {
666 RegressionSeverity::Critical
667 } else if anomaly_score > 0.6 {
668 RegressionSeverity::High
669 } else if anomaly_score > 0.4 {
670 RegressionSeverity::Medium
671 } else {
672 RegressionSeverity::Low
673 }
674 }
675}
676
677impl FeatureExtractor {
678 fn extract_features(&self, values: &[f64]) -> Vec<f64> {
679 let mut features = Vec::new();
680
681 if self.statistical_features {
682 features.extend(self.extract_statistical_features(values));
683 }
684
685 if self.frequency_features {
686 features.extend(self.extract_frequency_features(values));
687 }
688
689 features
690 }
691
692 fn extract_statistical_features(&self, values: &[f64]) -> Vec<f64> {
693 let mean = values.iter().sum::<f64>() / values.len() as f64;
694 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
695 let std_dev = variance.sqrt();
696
697 let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
698 let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
699 let range = max - min;
700
701 let skewness = if std_dev > 0.0 {
703 values.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f64>() / values.len() as f64
704 } else {
705 0.0
706 };
707
708 let kurtosis = if std_dev > 0.0 {
710 values.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f64>() / values.len() as f64
711 - 3.0
712 } else {
713 0.0
714 };
715
716 vec![mean, std_dev, min, max, range, skewness, kurtosis]
717 }
718
719 fn extract_frequency_features(&self, values: &[f64]) -> Vec<f64> {
720 let mut features = Vec::new();
722
723 let differences: Vec<f64> = values.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
725
726 if !differences.is_empty() {
727 let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
728 let max_diff = differences.iter().fold(0.0f64, |a, &b| a.max(b));
729 features.extend([mean_diff, max_diff]);
730 }
731
732 features
733 }
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
737struct MLPrediction {
738 anomaly_score: f64,
739 confidence: f64,
740 feature_importance: Vec<f64>,
741 predicted_severity: RegressionSeverity,
742}
743
744impl RegressionDetector {
745 pub fn new(config: RegressionDetectionConfig) -> Self {
747 let ml_predictor = if config.enable_ml_detection {
748 Some(MLPredictor::new(
749 MLModelType::IsolationForest,
750 config.ml_confidence_threshold,
751 ))
752 } else {
753 None
754 };
755
756 let trend_analyzer =
757 TrendAnalyzer::new(config.min_data_points, config.significance_threshold);
758
759 Self {
760 config,
761 metric_series: HashMap::new(),
762 anomaly_detector: AnomalyDetector::new(),
763 trend_analyzer,
764 change_point_detector: ChangePointDetector::new(5, 2.0),
765 seasonal_decomposer: SeasonalDecomposer::new(24), ml_predictor,
767 detection_history: VecDeque::new(),
768 }
769 }
770
771 pub fn add_metric_data_point(&mut self, data_point: MetricDataPoint) -> Result<()> {
773 let metric_type = data_point.metric_type.clone();
774 let max_data_points = (self.config.max_history_hours * 60) as usize; let min_data_points = self.config.min_data_points;
776
777 let data_points_len = {
779 let series =
780 self.metric_series.entry(metric_type.clone()).or_insert_with(|| MetricSeries {
781 metric_type: metric_type.clone(),
782 data_points: VecDeque::new(),
783 baseline_statistics: BaselineStatistics::default(),
784 last_updated: SystemTime::now(),
785 });
786
787 series.data_points.push_back(data_point);
789 series.last_updated = SystemTime::now();
790
791 while series.data_points.len() > max_data_points {
793 series.data_points.pop_front();
794 }
795
796 series.data_points.len()
797 };
798
799 self.update_baseline_statistics(&metric_type)?;
801
802 if data_points_len >= min_data_points {
804 if let Some(detection) = self.detect_regression(&metric_type)? {
805 self.detection_history.push_back(detection);
806
807 while self.detection_history.len() > 1000 {
809 self.detection_history.pop_front();
810 }
811 }
812 }
813
814 Ok(())
815 }
816
817 pub fn detect_regression(
819 &mut self,
820 metric_type: &MetricType,
821 ) -> Result<Option<RegressionDetection>> {
822 let series = match self.metric_series.get(metric_type) {
823 Some(series) => series,
824 None => return Ok(None),
825 };
826
827 if series.data_points.len() < self.config.min_data_points {
828 return Ok(None);
829 }
830
831 let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
832
833 let filtered_values = if self.config.enable_outlier_filtering {
835 self.filter_outliers(&values)
836 } else {
837 values.clone()
838 };
839
840 let mut detections = Vec::new();
842
843 if let Some(trend_result) = self.trend_analyzer.detect_trend_change(&filtered_values) {
845 if trend_result.is_regression {
846 let severity = self.calculate_severity(trend_result.slope_change);
847 detections.push(RegressionDetection {
848 detection_id: Uuid::new_v4(),
849 metric_type: metric_type.clone(),
850 regression_type: RegressionType::GradualDegradation,
851 severity,
852 confidence: 1.0 - trend_result.significance,
853 degradation_percentage: trend_result.slope_change * 100.0,
854 statistical_significance: trend_result.significance,
855 affected_period: self.calculate_affected_period(series),
856 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
857 recommendations: self.generate_recommendations(
858 &RegressionType::GradualDegradation,
859 trend_result.slope_change,
860 ),
861 detected_at: SystemTime::now(),
862 });
863 }
864 }
865
866 let change_points = self.change_point_detector.detect_change_points(&filtered_values);
868 if let Some(latest_change_point) = change_points.last() {
869 let before = &filtered_values[0..*latest_change_point];
870 let after = &filtered_values[*latest_change_point..];
871
872 if !before.is_empty() && !after.is_empty() {
873 let before_mean = before.iter().sum::<f64>() / before.len() as f64;
874 let after_mean = after.iter().sum::<f64>() / after.len() as f64;
875 let degradation = ((after_mean - before_mean) / before_mean) * 100.0;
876
877 if degradation > self.config.min_degradation_threshold {
878 detections.push(RegressionDetection {
879 detection_id: Uuid::new_v4(),
880 metric_type: metric_type.clone(),
881 regression_type: RegressionType::StepChange,
882 severity: self.calculate_severity(degradation / 100.0),
883 confidence: 0.8,
884 degradation_percentage: degradation,
885 statistical_significance: 0.01, affected_period: self.calculate_affected_period(series),
887 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
888 recommendations: self.generate_recommendations(
889 &RegressionType::StepChange,
890 degradation / 100.0,
891 ),
892 detected_at: SystemTime::now(),
893 });
894 }
895 }
896 }
897
898 if let Some(ref ml_predictor) = self.ml_predictor {
900 if let Some(ml_prediction) = ml_predictor.predict_regression(&filtered_values) {
901 detections.push(RegressionDetection {
902 detection_id: Uuid::new_v4(),
903 metric_type: metric_type.clone(),
904 regression_type: RegressionType::ComplexRegression,
905 severity: ml_prediction.predicted_severity,
906 confidence: ml_prediction.confidence,
907 degradation_percentage: ml_prediction.anomaly_score * 100.0,
908 statistical_significance: 1.0 - ml_prediction.confidence,
909 affected_period: self.calculate_affected_period(series),
910 root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
911 recommendations: self.generate_recommendations(
912 &RegressionType::ComplexRegression,
913 ml_prediction.anomaly_score,
914 ),
915 detected_at: SystemTime::now(),
916 });
917 }
918 }
919
920 if let Some(detection) = detections.into_iter().max_by_key(|d| d.severity.clone()) {
922 info!(
923 "Regression detected for {:?}: {:.2}% degradation",
924 metric_type, detection.degradation_percentage
925 );
926 Ok(Some(detection))
927 } else {
928 Ok(None)
929 }
930 }
931
932 pub fn get_recent_detections(&self, limit: usize) -> Vec<RegressionDetection> {
934 self.detection_history.iter().rev().take(limit).cloned().collect()
935 }
936
937 pub fn get_detections_for_metric(&self, metric_type: &MetricType) -> Vec<RegressionDetection> {
939 self.detection_history
940 .iter()
941 .filter(|d| &d.metric_type == metric_type)
942 .cloned()
943 .collect()
944 }
945
946 fn update_baseline_statistics(&mut self, metric_type: &MetricType) -> Result<()> {
948 let series = self.metric_series.get_mut(metric_type).ok_or_else(|| {
949 anyhow::anyhow!("Metric type {:?} not found in metric_series", metric_type)
950 })?;
951 let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
952
953 if values.is_empty() {
954 return Ok(());
955 }
956
957 let mean = values.iter().sum::<f64>() / values.len() as f64;
958 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
959 let std_dev = variance.sqrt();
960
961 let mut sorted_values = values.clone();
962 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
963
964 let median = AnomalyDetector::percentile(&sorted_values, 50.0);
965 let percentile_95 = AnomalyDetector::percentile(&sorted_values, 95.0);
966 let percentile_99 = AnomalyDetector::percentile(&sorted_values, 99.0);
967
968 let trend_slope = self.trend_analyzer.calculate_slope(&values);
969
970 let seasonal_pattern = if self.config.enable_seasonal_adjustment {
971 self.seasonal_decomposer
972 .decompose(&values)
973 .map(|components| components.seasonal)
974 } else {
975 None
976 };
977
978 series.baseline_statistics = BaselineStatistics {
979 mean,
980 std_dev,
981 median,
982 percentile_95,
983 percentile_99,
984 trend_slope,
985 seasonal_pattern,
986 sample_count: values.len(),
987 last_computed: SystemTime::now(),
988 };
989
990 Ok(())
991 }
992
993 fn filter_outliers(&self, values: &[f64]) -> Vec<f64> {
994 let outlier_mask = self.anomaly_detector.detect_outliers(values);
995 values
996 .iter()
997 .zip(outlier_mask.iter())
998 .filter(|(_, &is_outlier)| !is_outlier)
999 .map(|(&value, _)| value)
1000 .collect()
1001 }
1002
1003 fn calculate_severity(&self, degradation_ratio: f64) -> RegressionSeverity {
1004 let degradation_percentage = degradation_ratio.abs() * 100.0;
1005
1006 if degradation_percentage > 50.0 {
1007 RegressionSeverity::Critical
1008 } else if degradation_percentage > 25.0 {
1009 RegressionSeverity::High
1010 } else if degradation_percentage > 10.0 {
1011 RegressionSeverity::Medium
1012 } else {
1013 RegressionSeverity::Low
1014 }
1015 }
1016
1017 fn calculate_affected_period(&self, series: &MetricSeries) -> (SystemTime, SystemTime) {
1018 let start = series.data_points.front().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1019 let end = series.data_points.back().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1020 (start, end)
1021 }
1022
1023 fn analyze_root_causes(&self, series: &MetricSeries, values: &[f64]) -> RootCauseAnalysis {
1024 let mut likely_causes = Vec::new();
1025 let correlated_metrics = Vec::new();
1026 let environmental_factors = Vec::new();
1027
1028 let change_points = self.change_point_detector.detect_change_points(values);
1030 let change_point_timestamps: Vec<SystemTime> = change_points
1031 .iter()
1032 .filter_map(|&idx| series.data_points.get(idx).map(|dp| dp.timestamp))
1033 .collect();
1034
1035 if !change_points.is_empty() {
1037 likely_causes.push(PotentialCause {
1038 cause_type: CauseType::CodeChange,
1039 description: "Sudden performance change detected, possibly due to code deployment"
1040 .to_string(),
1041 confidence: 0.7,
1042 supporting_evidence: vec![format!(
1043 "Change point detected at {} locations",
1044 change_points.len()
1045 )],
1046 });
1047 }
1048
1049 let trend_slope = self.trend_analyzer.calculate_slope(values);
1051 if trend_slope > 0.01 {
1052 likely_causes.push(PotentialCause {
1053 cause_type: CauseType::ResourceContention,
1054 description:
1055 "Gradual performance degradation suggests resource contention or memory leaks"
1056 .to_string(),
1057 confidence: 0.6,
1058 supporting_evidence: vec![format!("Positive trend slope: {:.4}", trend_slope)],
1059 });
1060 }
1061
1062 let mean = values.iter().sum::<f64>() / values.len() as f64;
1064 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1065 let anomaly_score = variance.sqrt() / (mean + 1e-6);
1066
1067 RootCauseAnalysis {
1068 likely_causes,
1069 correlated_metrics,
1070 environmental_factors,
1071 change_points: change_point_timestamps,
1072 anomaly_score,
1073 }
1074 }
1075
1076 fn generate_recommendations(
1077 &self,
1078 regression_type: &RegressionType,
1079 degradation: f64,
1080 ) -> Vec<String> {
1081 let mut recommendations = Vec::new();
1082
1083 match regression_type {
1084 RegressionType::StepChange => {
1085 recommendations
1086 .push("Investigate recent deployments or configuration changes".to_string());
1087 recommendations
1088 .push("Review system logs around the time of performance change".to_string());
1089 recommendations
1090 .push("Consider rolling back recent changes if possible".to_string());
1091 },
1092 RegressionType::GradualDegradation => {
1093 recommendations
1094 .push("Monitor resource utilization (CPU, memory, disk)".to_string());
1095 recommendations.push("Check for memory leaks or resource exhaustion".to_string());
1096 recommendations
1097 .push("Review long-running processes and background tasks".to_string());
1098 },
1099 RegressionType::VarianceIncrease => {
1100 recommendations
1101 .push("Investigate system stability and hardware issues".to_string());
1102 recommendations.push("Check for intermittent network or I/O problems".to_string());
1103 },
1104 RegressionType::ComplexRegression => {
1105 recommendations.push("Perform detailed profiling and analysis".to_string());
1106 recommendations
1107 .push("Investigate multiple potential causes simultaneously".to_string());
1108 },
1109 _ => {
1110 recommendations.push("Perform comprehensive system analysis".to_string());
1111 },
1112 }
1113
1114 if degradation > 0.5 {
1115 recommendations.push("URGENT: Consider immediate mitigation actions".to_string());
1116 recommendations.push("Alert on-call team for immediate investigation".to_string());
1117 } else if degradation > 0.25 {
1118 recommendations.push("Schedule investigation within 24 hours".to_string());
1119 }
1120
1121 recommendations
1122 }
1123}
1124
1125impl Default for BaselineStatistics {
1126 fn default() -> Self {
1127 Self {
1128 mean: 0.0,
1129 std_dev: 0.0,
1130 median: 0.0,
1131 percentile_95: 0.0,
1132 percentile_99: 0.0,
1133 trend_slope: 0.0,
1134 seasonal_pattern: None,
1135 sample_count: 0,
1136 last_computed: SystemTime::now(),
1137 }
1138 }
1139}
1140
1141impl crate::DebugSession {
1143 pub async fn enable_regression_detection(
1145 &mut self,
1146 config: RegressionDetectionConfig,
1147 ) -> Result<RegressionDetector> {
1148 let detector = RegressionDetector::new(config);
1149 info!(
1150 "Enabled regression detection for debug session {}",
1151 self.id()
1152 );
1153 Ok(detector)
1154 }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use super::*;
1160
1161 #[tokio::test]
1162 async fn test_regression_detector_creation() {
1163 let config = RegressionDetectionConfig::default();
1164 let detector = RegressionDetector::new(config);
1165
1166 assert!(detector.metric_series.is_empty());
1167 assert!(detector.detection_history.is_empty());
1168 }
1169
1170 #[tokio::test]
1171 async fn test_add_metric_data_point() {
1172 let config = RegressionDetectionConfig::default();
1173 let mut detector = RegressionDetector::new(config);
1174
1175 let data_point = MetricDataPoint {
1176 metric_type: MetricType::Latency,
1177 value: 100.0,
1178 timestamp: SystemTime::now(),
1179 session_id: Uuid::new_v4(),
1180 metadata: HashMap::new(),
1181 };
1182
1183 assert!(detector.add_metric_data_point(data_point).is_ok());
1184 assert_eq!(detector.metric_series.len(), 1);
1185 }
1186
1187 #[test]
1188 fn test_anomaly_detection() {
1189 let detector = AnomalyDetector::new();
1190 let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 100.0]; let outliers = detector.detect_outliers(&values);
1193 assert_eq!(outliers.len(), values.len());
1194 assert!(outliers[5]); }
1196
1197 #[test]
1198 fn test_trend_analysis() {
1199 let analyzer = TrendAnalyzer::new(3, 0.9);
1200 let values = [1.0, 1.1, 1.2, 10.0, 20.0, 30.0];
1201
1202 let recent_values = &values[3..6]; let baseline_values = &values[0..3]; let recent_slope = analyzer.calculate_slope(recent_values);
1207 let baseline_slope = analyzer.calculate_slope(baseline_values);
1208
1209 assert!(recent_slope > baseline_slope);
1211 assert!(recent_slope > 0.0);
1212 }
1213
1214 #[test]
1215 fn test_change_point_detection() {
1216 let detector = ChangePointDetector::new(3, 2.0);
1217 let values = vec![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0]; let change_points = detector.detect_change_points(&values);
1220 assert!(!change_points.is_empty());
1221 }
1222
1223 #[test]
1224 fn test_seasonal_decomposition() {
1225 let decomposer = SeasonalDecomposer::new(4);
1226 let values = vec![1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0];
1227
1228 let components = decomposer.decompose(&values);
1229 assert!(components.is_some());
1230
1231 let comp = components.expect("operation failed in test");
1232 assert_eq!(comp.trend.len(), values.len());
1233 assert_eq!(comp.seasonal.len(), values.len());
1234 assert_eq!(comp.residual.len(), values.len());
1235 }
1236
1237 #[test]
1238 fn test_feature_extraction() {
1239 let extractor = FeatureExtractor {
1240 window_size: 10,
1241 statistical_features: true,
1242 frequency_features: true,
1243 };
1244
1245 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0];
1246 let features = extractor.extract_features(&values);
1247
1248 assert!(!features.is_empty());
1249 assert!(features.len() >= 7); }
1251}