1use crate::alert_storage::StoredAlert;
2use crate::alert_analyzer::AlertPattern;
3use smartcore::ensemble::random_forest_classifier::RandomForestClassifier;
4use smartcore::linalg::basic::matrix::DenseMatrix;
5use std::collections::HashMap;
6use smartcore::cluster::dbscan::{DBSCAN, DBSCANParameters};
7use smartcore::metrics::distance::euclidian::Euclidian;
8use chrono::{DateTime, Utc, Duration, Timelike, Datelike};
9use std::collections::HashSet;
10
11pub struct AlertMLAnalyzer {
12 model: Option<RandomForestClassifier<f64, i32, DenseMatrix<f64>, Vec<i32>>>,
13 #[allow(dead_code)]
14 feature_names: Vec<String>,
15 #[allow(dead_code)]
16 pattern_history: HashMap<String, Vec<AlertPattern>>,
17}
18
19impl AlertMLAnalyzer {
20 pub fn new() -> Self {
21 Self {
22 model: None,
23 feature_names: vec![
24 "hour_of_day".to_string(),
25 "day_of_week".to_string(),
26 "severity_level".to_string(),
27 "metric_type".to_string(),
28 "threshold_ratio".to_string(),
29 ],
30 pattern_history: HashMap::new(),
31 }
32 }
33
34 pub fn train(&mut self, alerts: &[StoredAlert], patterns: &[AlertPattern]) {
35 let (features, labels) = self.prepare_training_data(alerts, patterns);
36
37 if features.is_empty() {
38 return;
39 }
40
41 let x = DenseMatrix::from_2d_vec(&features);
42 let y = labels;
43
44 let model = RandomForestClassifier::fit(
45 &x, &y,
46 Default::default()
47 ).unwrap();
48
49 self.model = Some(model);
50 }
51
52 fn prepare_training_data(&self, alerts: &[StoredAlert], patterns: &[AlertPattern])
53 -> (Vec<Vec<f64>>, Vec<i32>)
54 {
55 let mut features = Vec::new();
56 let mut labels = Vec::new();
57
58 for alert in alerts {
59 features.push(self.extract_features(alert));
60
61 let is_pattern = patterns.iter().any(|p| {
62 p.affected_metrics.contains(&alert.metric_name)
63 });
64
65 labels.push(if is_pattern { 1 } else { 0 });
66 }
67
68 (features, labels)
69 }
70
71 fn extract_features(&self, alert: &StoredAlert) -> Vec<f64> {
72 vec![
73 alert.created_at.hour() as f64,
74 alert.created_at.weekday().num_days_from_monday() as f64,
75 self.severity_to_number(&alert.severity),
76 self.hash_metric_type(&alert.metric_name),
77 alert.current_value / alert.threshold,
78 ]
79 }
80
81 fn severity_to_number(&self, severity: &str) -> f64 {
82 match severity {
83 "Critical" => 3.0,
84 "Warning" => 2.0,
85 "Info" => 1.0,
86 _ => 0.0,
87 }
88 }
89
90 fn hash_metric_type(&self, metric: &str) -> f64 {
91 let hash = metric.chars().fold(0u64, |acc, c| {
92 acc.wrapping_add(c as u64)
93 });
94 (hash % 100) as f64 / 100.0
95 }
96
97 pub fn analyze_patterns(&mut self, alerts: &[StoredAlert]) -> Vec<AlertCluster> {
98 let features = alerts.iter()
99 .map(|alert| self.extract_features(alert))
100 .collect::<Vec<_>>();
101
102 if features.is_empty() {
103 return Vec::new();
104 }
105
106 let features_matrix = DenseMatrix::from_2d_vec(&features);
107 let params = DBSCANParameters::default()
108 .with_eps(0.3)
109 .with_min_samples(3)
110 .with_distance(Euclidian::default());
111
112 let dbscan: DBSCAN<f64, i32, DenseMatrix<f64>, Vec<i32>, Euclidian<f64>> =
113 DBSCAN::fit(&features_matrix, params).unwrap();
114 let labels = dbscan.predict(&features_matrix).unwrap().to_vec();
115
116 self.create_alert_clusters(alerts, &labels)
117 }
118
119 fn create_alert_clusters(&self, alerts: &[StoredAlert], labels: &[i32]) -> Vec<AlertCluster> {
120 let mut clusters = HashMap::new();
121
122 for (idx, &label) in labels.iter().enumerate() {
123 if label >= 0 { clusters.entry(label)
125 .or_insert_with(Vec::new)
126 .push(alerts[idx].clone());
127 }
128 }
129
130 clusters.into_iter()
131 .map(|(label, cluster_alerts)| {
132 let (center, radius) = self.calculate_cluster_metrics(&cluster_alerts);
133 AlertCluster {
134 id: format!("cluster_{}", label),
135 alerts: cluster_alerts.clone(),
136 center,
137 radius,
138 characteristics: self.analyze_cluster_characteristics(&cluster_alerts),
139 }
140 })
141 .collect()
142 }
143
144 fn calculate_cluster_metrics(&self, alerts: &[StoredAlert]) -> (ClusterCenter, f64) {
145 let mut avg_time = Duration::zero();
146 let mut avg_value = 0.0;
147 let mut avg_threshold = 0.0;
148
149 for alert in alerts {
150 avg_time = avg_time + (alert.created_at - alerts[0].created_at);
151 avg_value += alert.current_value;
152 avg_threshold += alert.threshold;
153 }
154
155 let count = alerts.len() as f64;
156 let center = ClusterCenter {
157 time_offset: avg_time / alerts.len() as i32,
158 value: avg_value / count,
159 threshold: avg_threshold / count,
160 };
161
162 let radius = alerts.iter()
163 .map(|alert| {
164 let time_diff = (alert.created_at - alerts[0].created_at - center.time_offset)
165 .num_seconds() as f64;
166 let value_diff = alert.current_value - center.value;
167 let threshold_diff = alert.threshold - center.threshold;
168
169 (time_diff.powi(2) + value_diff.powi(2) + threshold_diff.powi(2)).sqrt()
170 })
171 .max_by(|a, b| a.partial_cmp(b).unwrap())
172 .unwrap_or(0.0);
173
174 (center, radius)
175 }
176
177 fn analyze_cluster_characteristics(&self, alerts: &[StoredAlert]) -> ClusterCharacteristics {
178 let mut metrics = HashSet::new();
179 let mut severities = HashSet::new();
180 let mut time_range: Option<(DateTime<Utc>, DateTime<Utc>)> = None;
181
182 for alert in alerts {
183 metrics.insert(alert.metric_name.clone());
184 severities.insert(alert.severity.clone());
185
186 time_range = Some(match time_range {
187 Some((start, end)) => (
188 start.min(alert.created_at),
189 end.max(alert.created_at)
190 ),
191 None => (alert.created_at, alert.created_at)
192 });
193 }
194
195 let (start_time, end_time) = time_range.unwrap_or((Utc::now(), Utc::now()));
196 let duration = end_time - start_time;
197
198 ClusterCharacteristics {
199 unique_metrics: metrics.len(),
200 unique_severities: severities.len(),
201 duration,
202 alert_count: alerts.len(),
203 severity_distribution: self.calculate_severity_distribution(alerts),
204 }
205 }
206
207 fn calculate_severity_distribution(&self, alerts: &[StoredAlert]) -> HashMap<String, f64> {
208 let mut distribution = HashMap::new();
209 let total = alerts.len() as f64;
210
211 for alert in alerts {
212 *distribution.entry(alert.severity.clone()).or_insert(0.0) += 1.0;
213 }
214
215 for count in distribution.values_mut() {
216 *count /= total;
217 }
218
219 distribution
220 }
221
222 pub fn predict_cluster(&self, alert: &StoredAlert) -> Option<String> {
223 if let Some(model) = &self.model {
224 let features = self.extract_features(alert);
225 let x = DenseMatrix::from_2d_vec(&vec![features]);
226
227 if let Ok(prediction) = model.predict(&x) {
228 if prediction[0] == 1 {
229 return Some("cluster_id".to_string());
230 }
231 }
232 }
233 None
234 }
235}
236
237#[derive(Debug)]
238pub struct AlertCluster {
239 pub id: String,
240 pub alerts: Vec<StoredAlert>,
241 pub center: ClusterCenter,
242 pub radius: f64,
243 pub characteristics: ClusterCharacteristics,
244}
245
246#[derive(Debug)]
247pub struct ClusterCenter {
248 pub time_offset: Duration,
249 pub value: f64,
250 pub threshold: f64,
251}
252
253#[derive(Debug)]
254pub struct ClusterCharacteristics {
255 pub unique_metrics: usize,
256 pub unique_severities: usize,
257 pub duration: Duration,
258 pub alert_count: usize,
259 pub severity_distribution: HashMap<String, f64>,
260}