spawn_access_control/
alert_ml.rs

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 { // -1 noise points
124                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}