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