Skip to main content

cbtop/anomaly_detection/
types.rs

1//! Anomaly detection types: severity, classification, anomalies, and reports.
2
3/// Default Z-score threshold for outlier detection
4pub const DEFAULT_ZSCORE_THRESHOLD: f64 = 3.0;
5
6/// Default IQR multiplier for outlier detection
7pub const DEFAULT_IQR_MULTIPLIER: f64 = 1.5;
8
9/// Minimum samples for statistical analysis
10pub const MIN_SAMPLES_FOR_DETECTION: usize = 10;
11
12/// Anomaly severity level
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum AnomalySeverity {
15    /// Informational - minor deviation
16    Info,
17    /// Warning - notable deviation
18    Warning,
19    /// Critical - severe deviation requiring attention
20    Critical,
21}
22
23impl AnomalySeverity {
24    /// Get severity name
25    pub fn name(&self) -> &'static str {
26        match self {
27            Self::Info => "info",
28            Self::Warning => "warning",
29            Self::Critical => "critical",
30        }
31    }
32
33    /// Get severity from deviation magnitude
34    pub fn from_deviation(deviation: f64) -> Self {
35        if deviation >= 5.0 {
36            Self::Critical
37        } else if deviation >= 3.0 {
38            Self::Warning
39        } else {
40            Self::Info
41        }
42    }
43}
44
45/// Anomaly type classification
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum AnomalyType {
48    /// Statistical outlier (Z-score based)
49    Outlier,
50    /// Performance spike (sudden increase)
51    Spike,
52    /// Performance drop (sudden decrease)
53    Drop,
54    /// Change point (sustained shift)
55    ChangePoint,
56    /// Periodic anomaly (recurring pattern)
57    Periodic,
58    /// Correlated anomaly (multi-metric)
59    Correlated,
60}
61
62impl AnomalyType {
63    /// Get anomaly type name
64    pub fn name(&self) -> &'static str {
65        match self {
66            Self::Outlier => "outlier",
67            Self::Spike => "spike",
68            Self::Drop => "drop",
69            Self::ChangePoint => "change_point",
70            Self::Periodic => "periodic",
71            Self::Correlated => "correlated",
72        }
73    }
74}
75
76/// Detected anomaly
77#[derive(Debug, Clone)]
78pub struct Anomaly {
79    /// Index in the data series
80    pub index: usize,
81    /// The anomalous value
82    pub value: f64,
83    /// Expected value (mean or predicted)
84    pub expected: f64,
85    /// Deviation from expected (in standard deviations)
86    pub deviation: f64,
87    /// Anomaly type
88    pub anomaly_type: AnomalyType,
89    /// Severity level
90    pub severity: AnomalySeverity,
91    /// Optional description
92    pub description: Option<String>,
93}
94
95impl Anomaly {
96    /// Create new anomaly
97    pub fn new(
98        index: usize,
99        value: f64,
100        expected: f64,
101        deviation: f64,
102        anomaly_type: AnomalyType,
103    ) -> Self {
104        Self {
105            index,
106            value,
107            expected,
108            deviation,
109            anomaly_type,
110            severity: AnomalySeverity::from_deviation(deviation.abs()),
111            description: None,
112        }
113    }
114
115    /// Set description
116    pub fn with_description(mut self, desc: &str) -> Self {
117        self.description = Some(desc.to_string());
118        self
119    }
120
121    /// Check if anomaly is critical
122    pub fn is_critical(&self) -> bool {
123        self.severity == AnomalySeverity::Critical
124    }
125
126    /// Export to JSON format
127    pub fn to_json(&self) -> String {
128        format!(
129            r#"{{"index":{},"value":{},"expected":{},"deviation":{},"type":"{}","severity":"{}"}}"#,
130            self.index,
131            self.value,
132            self.expected,
133            self.deviation,
134            self.anomaly_type.name(),
135            self.severity.name()
136        )
137    }
138}
139
140/// Change point in data series
141#[derive(Debug, Clone)]
142pub struct ChangePoint {
143    /// Index where change occurs
144    pub index: usize,
145    /// Mean before change
146    pub mean_before: f64,
147    /// Mean after change
148    pub mean_after: f64,
149    /// Magnitude of change
150    pub magnitude: f64,
151    /// Direction of change (positive = increase)
152    pub direction: f64,
153}
154
155impl ChangePoint {
156    /// Create new change point
157    pub fn new(index: usize, mean_before: f64, mean_after: f64) -> Self {
158        let magnitude = (mean_after - mean_before).abs();
159        let direction = mean_after - mean_before;
160        Self {
161            index,
162            mean_before,
163            mean_after,
164            magnitude,
165            direction,
166        }
167    }
168
169    /// Check if change is significant (>10% shift)
170    pub fn is_significant(&self) -> bool {
171        if self.mean_before.abs() < 1e-10 {
172            return self.magnitude > 1e-10;
173        }
174        (self.magnitude / self.mean_before.abs()) > 0.1
175    }
176}
177
178/// Anomaly detection result summary
179#[derive(Debug, Clone)]
180pub struct AnomalyReport {
181    /// Total data points analyzed
182    pub total_points: usize,
183    /// Detected anomalies
184    pub anomalies: Vec<Anomaly>,
185    /// Detected change points
186    pub change_points: Vec<ChangePoint>,
187    /// Data mean
188    pub mean: f64,
189    /// Data standard deviation
190    pub std_dev: f64,
191    /// Detection method used
192    pub method: &'static str,
193}
194
195impl AnomalyReport {
196    /// Count anomalies by severity
197    pub fn count_by_severity(&self, severity: AnomalySeverity) -> usize {
198        self.anomalies
199            .iter()
200            .filter(|a| a.severity == severity)
201            .count()
202    }
203
204    /// Get critical anomalies
205    pub fn critical_anomalies(&self) -> Vec<&Anomaly> {
206        self.anomalies.iter().filter(|a| a.is_critical()).collect()
207    }
208
209    /// Check if any critical anomalies exist
210    pub fn has_critical(&self) -> bool {
211        self.anomalies.iter().any(|a| a.is_critical())
212    }
213
214    /// Export report to JSON
215    pub fn to_json(&self) -> String {
216        let anomalies_json: Vec<String> = self.anomalies.iter().map(|a| a.to_json()).collect();
217        format!(
218            r#"{{"total_points":{},"anomaly_count":{},"critical_count":{},"mean":{},"std_dev":{},"method":"{}","anomalies":[{}]}}"#,
219            self.total_points,
220            self.anomalies.len(),
221            self.count_by_severity(AnomalySeverity::Critical),
222            self.mean,
223            self.std_dev,
224            self.method,
225            anomalies_json.join(",")
226        )
227    }
228}