use crate::aiops::core::data_collector::{Metric, MetricType};
use crate::aiops::core::error::{AIOpsError, Result};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AnomalyType {
Spike,
Drop,
LevelShiftUp,
LevelShiftDown,
TrendUp,
TrendDown,
Outlier,
PatternDeviation,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Anomaly {
pub anomaly_type: AnomalyType,
pub severity: f64,
pub confidence: f64,
pub metric: Metric,
pub expected_range: (f64, f64),
pub actual_value: f64,
pub deviation: f64,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyResult {
pub is_anomaly: bool,
pub anomaly: Option<Anomaly>,
pub stats: AnomalyStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyStats {
pub mean: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
pub count: usize,
}
#[derive(Debug, Clone)]
pub struct AnomalyDetectorConfig {
pub threshold_std_dev: f64,
pub min_samples: usize,
pub window_size: usize,
pub enable_trend: bool,
pub enable_pattern: bool,
}
impl Default for AnomalyDetectorConfig {
fn default() -> Self {
Self {
threshold_std_dev: 3.0,
min_samples: 10,
window_size: 30,
enable_trend: true,
enable_pattern: false,
}
}
}
pub trait AnomalyDetector {
async fn detect_anomaly(&self, metric: &Metric, history: &[Metric]) -> Result<AnomalyResult>;
async fn detect_batch_anomalies(&self, metrics: &[Metric]) -> Result<Vec<AnomalyResult>>;
async fn update_baseline(&mut self, metrics: &[Metric]) -> Result<()>;
fn get_stats(&self) -> &AnomalyStats;
}
#[derive(Debug)]
pub struct StatisticalAnomalyDetector {
config: AnomalyDetectorConfig,
stats: AnomalyStats,
}
impl StatisticalAnomalyDetector {
pub fn new(config: AnomalyDetectorConfig) -> Self {
Self {
config,
stats: AnomalyStats {
mean: 0.0,
std_dev: 0.0,
min: f64::MAX,
max: f64::MIN,
count: 0,
},
}
}
fn calculate_stats(metrics: &[Metric]) -> AnomalyStats {
if metrics.is_empty() {
return AnomalyStats {
mean: 0.0,
std_dev: 0.0,
min: f64::MAX,
max: f64::MIN,
count: 0,
};
}
let count: _ = metrics.len();
let mean: f64 = metrics.iter().map(|m| m.value).sum::<f64>() / count as f64;
let variance: f64 = metrics
.iter()
.map(|m| {
let diff: _ = m.value - mean;
diff * diff
})
.sum::<f64>()
/ count as f64;
let std_dev: _ = variance.sqrt();
let min: _ = metrics.iter().map(|m| m.value).fold(f64::MAX, f64::min);
let max: _ = metrics.iter().map(|m| m.value).fold(f64::MIN, f64::max);
AnomalyStats {
mean,
std_dev,
min,
max,
count,
}
}
fn detect_anomaly_type(&self, metric: &Metric, stats: &AnomalyStats) -> Option<Anomaly> {
let threshold: _ = self.config.threshold_std_dev * stats.std_dev;
if stats.std_dev == 0.0 {
return None;
}
let z_score: _ = (metric.value - stats.mean).abs() / stats.std_dev;
if z_score > self.config.threshold_std_dev {
let is_spike: _ = metric.value > stats.mean;
let anomaly_type: _ = if is_spike {
AnomalyType::Spike
} else {
AnomalyType::Drop
};
let severity: _ = (z_score / self.config.threshold_std_dev).min(1.0);
let confidence: _ = (z_score / (self.config.threshold_std_dev * 2.0)).min(1.0);
let expected_range: _ = (stats.mean - threshold, stats.mean + threshold);
let deviation: _ = metric.value - stats.mean;
Some(Anomaly {
anomaly_type,
severity,
confidence,
metric: metric.clone(),
expected_range,
actual_value: metric.value,
deviation,
description: format!(
"{} detected: value={:.2}, expected=[{:.2}, {:.2}]",
if is_spike { "Spike" } else { "Drop" },
metric.value,
expected_range.0,
expected_range.1
),
})
} else {
None
}
}
}
impl AnomalyDetector for StatisticalAnomalyDetector {
async fn detect_anomaly(&self, metric: &Metric, history: &[Metric]) -> Result<AnomalyResult> {
if history.len() < self.config.min_samples {
return Ok(AnomalyResult {
is_anomaly: false,
anomaly: None,
stats: self.stats.clone(),
});
}
let stats: _ = Self::calculate_stats(history);
let anomaly: _ = self.detect_anomaly_type(metric, &stats);
Ok(AnomalyResult {
is_anomaly: anomaly.is_some(),
anomaly,
stats,
})
}
async fn detect_batch_anomalies(&self, metrics: &[Metric]) -> Result<Vec<AnomalyResult>> {
let mut results = Vec::new();
for (i, metric) in metrics.iter().enumerate() {
let history: _ = if i >= self.config.min_samples {
&metrics[i - self.config.min_samples..i]
} else {
&metrics[..i]
};
let result: _ = self.detect_anomaly(metric, history).await?;
results.push(result);
}
Ok(results)
}
async fn update_baseline(&mut self, metrics: &[Metric]) -> Result<()> {
if !metrics.is_empty() {
self.stats = Self::calculate_stats(metrics);
}
Ok(())
}
fn get_stats(&self) -> &AnomalyStats {
&self.stats
}
}
#[cfg(test)]
mod tests {
fn create_test_metric(value: f64) -> Metric {
Metric {
metric_type: MetricType::CpuUsage,
value,
timestamp: Duration::from_secs(0),
labels: HashMap::new(),
}
}
fn create_test_metrics(values: Vec<f64>) -> Vec<Metric> {
values.into_iter().map(|v| create_test_metric(v)).collect()
}
#[tokio::test]
async fn test_no_anomaly_normal_values() {
let detector: _ = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let history: _ = create_test_metrics(vec![50.0, 51.0, 49.0, 50.5, 50.2]);
let metric: _ = create_test_metric(50.3);
let result: _ = detector.detect_anomaly(&metric, &history).await.unwrap();
assert!(!result.is_anomaly);
assert!(result.anomaly.is_none());
}
#[tokio::test]
async fn test_detect_spike_anomaly() {
let detector: _ = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let history: _ = create_test_metrics(vec![50.0, 51.0, 49.0, 50.5, 50.2]);
let metric: _ = create_test_metric(200.0);
let result: _ = detector.detect_anomaly(&metric, &history).await.unwrap();
assert!(result.is_anomaly);
assert!(result.anomaly.is_some());
let anomaly: _ = result.anomaly.unwrap();
assert_eq!(anomaly.anomaly_type, AnomalyType::Spike);
assert!(anomaly.severity > 0.0);
assert!(anomaly.confidence > 0.0);
}
#[tokio::test]
async fn test_detect_drop_anomaly() {
let detector: _ = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let history: _ = create_test_metrics(vec![50.0, 51.0, 49.0, 50.5, 50.2]);
let metric: _ = create_test_metric(5.0);
let result: _ = detector.detect_anomaly(&metric, &history).await.unwrap();
assert!(result.is_anomaly);
assert!(result.anomaly.is_some());
let anomaly: _ = result.anomaly.unwrap();
assert_eq!(anomaly.anomaly_type, AnomalyType::Drop);
assert!(anomaly.severity > 0.0);
}
#[tokio::test]
async fn test_insufficient_history() {
let detector: _ = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let history: _ = create_test_metrics(vec![50.0]);
let metric: _ = create_test_metric(200.0);
let result: _ = detector.detect_anomaly(&metric, &history).await.unwrap();
assert!(!result.is_anomaly);
assert!(result.anomaly.is_none());
}
#[tokio::test]
async fn test_batch_anomaly_detection() {
let detector: _ = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let metrics: _ = create_test_metrics(vec![50.0, 51.0, 49.0, 200.0, 50.0]);
let results: _ = detector.detect_batch_anomalies(&metrics).await.unwrap();
assert_eq!(results.len(), 5);
assert!(!results[0].is_anomaly);
assert!(!results[1].is_anomaly);
assert!(!results[2].is_anomaly);
assert!(results[3].is_anomaly); assert!(!results[4].is_anomaly);
}
#[tokio::test]
async fn test_update_baseline() {
let mut detector = StatisticalAnomalyDetector::new(AnomalyDetectorConfig::default());
let metrics: _ = create_test_metrics(vec![60.0, 61.0, 59.0, 60.5, 60.2]);
detector.update_baseline(&metrics).await.unwrap();
let stats: _ = detector.get_stats();
assert!(stats.count > 0);
assert!(stats.mean > 0.0);
}
#[tokio::test]
async fn test_custom_threshold() {
let mut config = AnomalyDetectorConfig::default();
config.threshold_std_dev = 2.0;
let detector: _ = StatisticalAnomalyDetector::new(config);
let history: _ = create_test_metrics(vec![50.0, 51.0, 49.0, 50.5, 50.2]);
let metric: _ = create_test_metric(100.0);
let result: _ = detector.detect_anomaly(&metric, &history).await.unwrap();
assert!(result.is_anomaly);
}
}