spawn_access_control/
alert_analyzer.rs

1use crate::alert_storage::{AlertStorage, StoredAlert};
2use crate::alert_system::{AlertSeverity};
3use chrono::{DateTime, Utc, Duration, Timelike, Datelike};
4use serde::{Serialize, Deserialize};
5use std::collections::{HashMap, HashSet};
6use std::str::FromStr;
7
8impl FromStr for AlertSeverity {
9    type Err = String;
10
11    fn from_str(s: &str) -> Result<Self, Self::Err> {
12        match s.to_lowercase().as_str() {
13            "critical" => Ok(AlertSeverity::Critical),
14            "warning" => Ok(AlertSeverity::Warning),
15            "info" => Ok(AlertSeverity::Info),
16            _ => Err(format!("Invalid severity: {}", s))
17        }
18    }
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct AlertAnalysis {
23    pub time_range: TimeRange,
24    pub total_alerts: usize,
25    pub severity_distribution: HashMap<AlertSeverity, usize>,
26    pub resolution_metrics: ResolutionMetrics,
27    pub patterns: Vec<AlertPattern>,
28    pub recommendations: Vec<AlertRecommendation>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TimeRange {
33    pub start: DateTime<Utc>,
34    pub end: DateTime<Utc>,
35    #[serde(with = "duration_serde")]
36    pub duration: Duration,
37}
38
39mod duration_serde {
40    use serde::{Deserialize, Deserializer, Serialize, Serializer};
41    use chrono::Duration;
42
43    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
44    where
45        S: Serializer,
46    {
47        duration.num_seconds().serialize(serializer)
48    }
49
50    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let secs = i64::deserialize(deserializer)?;
55        Ok(Duration::seconds(secs))
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ResolutionMetrics {
61    #[serde(with = "duration_serde")]
62    pub avg_resolution_time: Duration,
63    pub resolution_rate: f64,
64    pub unresolved_critical: usize,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct AlertPattern {
69    pub pattern_type: PatternType,
70    pub frequency: usize,
71    pub confidence: f64,
72    pub affected_metrics: Vec<String>,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub enum PatternType {
77    Periodic,
78    Cascading,
79    Correlated,
80    Seasonal,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct AlertRecommendation {
85    pub recommendation_type: RecommendationType,
86    pub description: String,
87    pub priority: u8,
88    pub estimated_impact: f64,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum RecommendationType {
93    ThresholdAdjustment,
94    MonitoringEnhancement,
95    AutoRemediation,
96    ProcessImprovement,
97}
98
99pub struct AlertAnalyzer {
100    storage: AlertStorage,
101    config: AnalyzerConfig,
102}
103
104#[derive(Clone)]
105pub struct AnalyzerConfig {
106    pub analysis_window: Duration,
107    pub pattern_detection_threshold: f64,
108    pub min_correlation_strength: f64,
109}
110
111impl AlertAnalyzer {
112    pub fn new(storage: AlertStorage, config: AnalyzerConfig) -> Self {
113        Self { storage, config }
114    }
115
116    pub async fn analyze_alerts(&self, time_range: TimeRange) -> Result<AlertAnalysis, Box<dyn std::error::Error>> {
117        let alerts = self.storage.get_alerts_in_range(time_range.start, time_range.end).await?;
118        
119        let analysis = AlertAnalysis {
120            time_range,
121            total_alerts: alerts.len(),
122            severity_distribution: self.calculate_severity_distribution(&alerts),
123            resolution_metrics: self.calculate_resolution_metrics(&alerts),
124            patterns: self.detect_patterns(&alerts),
125            recommendations: self.generate_recommendations(&alerts),
126        };
127
128        Ok(analysis)
129    }
130
131    fn calculate_severity_distribution(&self, alerts: &[StoredAlert]) -> HashMap<AlertSeverity, usize> {
132        let mut distribution = HashMap::new();
133        
134        for alert in alerts {
135            let severity = AlertSeverity::from_str(&alert.severity)
136                .unwrap_or(AlertSeverity::Info);
137            *distribution.entry(severity).or_insert(0) += 1;
138        }
139
140        distribution
141    }
142
143    fn calculate_resolution_metrics(&self, alerts: &[StoredAlert]) -> ResolutionMetrics {
144        let mut total_resolution_time = Duration::zero();
145        let mut resolved_count = 0;
146        let mut unresolved_critical = 0;
147
148        for alert in alerts {
149            if let Some(resolved_at) = alert.resolved_at {
150                let resolution_time = resolved_at - alert.created_at;
151                total_resolution_time = total_resolution_time + resolution_time;
152                resolved_count += 1;
153            } else if alert.severity == AlertSeverity::Critical.to_string() {
154                unresolved_critical += 1;
155            }
156        }
157
158        let avg_resolution_time = if resolved_count > 0 {
159            total_resolution_time / resolved_count as i32
160        } else {
161            Duration::zero()
162        };
163
164        let resolution_rate = resolved_count as f64 / alerts.len() as f64;
165
166        ResolutionMetrics {
167            avg_resolution_time,
168            resolution_rate,
169            unresolved_critical,
170        }
171    }
172
173    fn detect_patterns(&self, alerts: &[StoredAlert]) -> Vec<AlertPattern> {
174        let mut patterns = Vec::new();
175
176        if let Some(pattern) = self.detect_periodic_pattern(alerts) {
177            patterns.push(pattern);
178        }
179
180        if let Some(pattern) = self.detect_cascade_pattern(alerts) {
181            patterns.push(pattern);
182        }
183
184        patterns.extend(self.detect_correlated_patterns(alerts));
185
186        if let Some(pattern) = self.detect_seasonal_pattern(alerts) {
187            patterns.push(pattern);
188        }
189
190        patterns
191    }
192
193    fn detect_periodic_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
194        let mut timeline = alerts.iter()
195            .map(|a| (a.created_at, a.metric_name.clone()))
196            .collect::<Vec<_>>();
197        timeline.sort_by_key(|k| k.0);
198
199        let intervals = vec![
200            chrono::Duration::hours(1),
201            chrono::Duration::hours(6),
202            chrono::Duration::hours(12),
203            chrono::Duration::hours(24),
204        ];
205
206        let mut best_period = None;
207        let mut best_confidence = 0.0;
208
209        for interval in intervals {
210            if let Some((period, confidence)) = self.analyze_periodicity(&timeline, interval) {
211                if confidence > best_confidence && confidence > self.config.pattern_detection_threshold {
212                    best_period = Some(period);
213                    best_confidence = confidence;
214                }
215            }
216        }
217
218        best_period.map(|period| AlertPattern {
219            pattern_type: PatternType::Periodic,
220            frequency: period.num_hours() as usize,
221            confidence: best_confidence,
222            affected_metrics: self.get_affected_metrics(alerts),
223        })
224    }
225
226    fn analyze_periodicity(
227        &self,
228        timeline: &[(DateTime<Utc>, String)],
229        interval: Duration
230    ) -> Option<(Duration, f64)> {
231        let mut interval_counts = HashMap::new();
232        let mut prev_time = None;
233
234        for (time, _) in timeline {
235            if let Some(prev) = prev_time {
236                let diff: Duration = *time - prev;
237                let normalized_diff = Duration::hours(
238                    (diff.num_seconds() as f64 / interval.num_seconds() as f64).round() as i64
239                );
240                
241                *interval_counts.entry(normalized_diff).or_insert(0) += 1;
242            }
243            prev_time = Some(*time);
244        }
245
246        interval_counts.iter()
247            .max_by_key(|(_, &count)| count)
248            .map(|(period, count)| {
249                let total_intervals = interval_counts.values().sum::<i32>();
250                let confidence = *count as f64 / total_intervals as f64;
251                (*period, confidence)
252            })
253    }
254
255    fn detect_cascade_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
256        let mut timeline = alerts.iter()
257            .map(|a| (a.created_at, a.metric_name.clone()))
258            .collect::<Vec<_>>();
259        timeline.sort_by_key(|k| k.0);
260
261        let window_size = chrono::Duration::minutes(5);
262        let mut cascade_groups = Vec::new();
263        let mut current_group = Vec::new();
264
265        for i in 0..timeline.len() {
266            if current_group.is_empty() {
267                current_group.push(timeline[i].clone());
268                continue;
269            }
270
271            let time_diff = timeline[i].0 - current_group.last().unwrap().0;
272            if time_diff <= window_size {
273                current_group.push(timeline[i].clone());
274            } else {
275                if current_group.len() >= 3 {
276                    cascade_groups.push(current_group.clone());
277                }
278                current_group.clear();
279                current_group.push(timeline[i].clone());
280            }
281        }
282
283        if current_group.len() >= 3 {
284            cascade_groups.push(current_group);
285        }
286
287        if let Some(largest_cascade) = cascade_groups.iter().max_by_key(|g| g.len()) {
288            if largest_cascade.len() >= 3 {
289                let affected_metrics = largest_cascade.iter()
290                    .map(|(_, metric)| metric.clone())
291                    .collect();
292
293                Some(AlertPattern {
294                    pattern_type: PatternType::Cascading,
295                    frequency: largest_cascade.len(),
296                    confidence: largest_cascade.len() as f64 / alerts.len() as f64,
297                    affected_metrics,
298                })
299            } else {
300                None
301            }
302        } else {
303            None
304        }
305    }
306
307    fn detect_correlated_patterns(&self, alerts: &[StoredAlert]) -> Vec<AlertPattern> {
308        let mut patterns = Vec::new();
309        let mut metric_groups = HashMap::new();
310
311        let window_size = chrono::Duration::minutes(15);
312        for alert in alerts {
313            let window_start = alert.created_at.timestamp() / window_size.num_seconds();
314            metric_groups
315                .entry(window_start)
316                .or_insert_with(HashSet::new)
317                .insert(alert.metric_name.clone());
318        }
319
320        let mut co_occurrences = HashMap::new();
321        for metrics in metric_groups.values() {
322            for m1 in metrics.iter() {
323                for m2 in metrics.iter() {
324                    if m1 < m2 {
325                        *co_occurrences.entry((m1.clone(), m2.clone())).or_insert(0) += 1;
326                    }
327                }
328            }
329        }
330
331        let min_occurrences = (metric_groups.len() as f64 * self.config.min_correlation_strength) as usize;
332        let mut correlated_metrics = HashSet::new();
333
334        for ((m1, m2), count) in co_occurrences {
335            if count >= min_occurrences {
336                let confidence = count as f64 / metric_groups.len() as f64;
337                
338                if !correlated_metrics.contains(&m1) && !correlated_metrics.contains(&m2) {
339                    patterns.push(AlertPattern {
340                        pattern_type: PatternType::Correlated,
341                        frequency: count,
342                        confidence,
343                        affected_metrics: vec![m1.clone(), m2.clone()],
344                    });
345                    correlated_metrics.insert(m1);
346                    correlated_metrics.insert(m2);
347                }
348            }
349        }
350
351        patterns
352    }
353
354    fn detect_seasonal_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
355        let mut hourly_distribution = vec![0; 24];
356        let mut daily_distribution = vec![0; 7];
357
358        for alert in alerts {
359            let hour = alert.created_at.hour() as usize;
360            let day = alert.created_at.weekday().num_days_from_monday() as usize;
361            
362            hourly_distribution[hour] += 1;
363            daily_distribution[day] += 1;
364        }
365
366        let hourly_variance = self.calculate_distribution_variance(&hourly_distribution);
367        let daily_variance = self.calculate_distribution_variance(&daily_distribution);
368
369        if hourly_variance > self.config.pattern_detection_threshold {
370            Some(AlertPattern {
371                pattern_type: PatternType::Seasonal,
372                frequency: self.find_peak_frequency(&hourly_distribution),
373                confidence: hourly_variance,
374                affected_metrics: self.get_affected_metrics(alerts),
375            })
376        } else if daily_variance > self.config.pattern_detection_threshold {
377            Some(AlertPattern {
378                pattern_type: PatternType::Seasonal,
379                frequency: self.find_peak_frequency(&daily_distribution) * 24,
380                confidence: daily_variance,
381                affected_metrics: self.get_affected_metrics(alerts),
382            })
383        } else {
384            None
385        }
386    }
387
388    fn calculate_distribution_variance(&self, distribution: &[i32]) -> f64 {
389        let mean = distribution.iter().sum::<i32>() as f64 / distribution.len() as f64;
390        let variance = distribution.iter()
391            .map(|&x| {
392                let diff = x as f64 - mean;
393                diff * diff
394            })
395            .sum::<f64>() / distribution.len() as f64;
396        
397        variance.sqrt() / mean
398    }
399
400    fn find_peak_frequency(&self, distribution: &[i32]) -> usize {
401        let mut max_val = 0;
402        let mut max_idx = 0;
403
404        for (idx, &val) in distribution.iter().enumerate() {
405            if val > max_val {
406                max_val = val;
407                max_idx = idx;
408            }
409        }
410
411        max_idx
412    }
413
414    fn get_affected_metrics(&self, alerts: &[StoredAlert]) -> Vec<String> {
415        let mut metrics = std::collections::HashSet::new();
416        for alert in alerts {
417            metrics.insert(alert.metric_name.clone());
418        }
419        metrics.into_iter().collect()
420    }
421
422    fn generate_recommendations(&self, alerts: &[StoredAlert]) -> Vec<AlertRecommendation> {
423        let mut recommendations = Vec::new();
424
425        if let Some(rec) = self.recommend_threshold_adjustments(alerts) {
426            recommendations.push(rec);
427        }
428
429        if let Some(rec) = self.recommend_monitoring_improvements(alerts) {
430            recommendations.push(rec);
431        }
432
433        if let Some(rec) = self.recommend_auto_remediation(alerts) {
434            recommendations.push(rec);
435        }
436
437        recommendations
438    }
439
440    fn recommend_auto_remediation(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
441        let auto_resolvable_count = alerts.iter()
442            .filter(|a| self.is_auto_resolvable(a))
443            .count();
444
445        let total_alerts = alerts.len();
446        if auto_resolvable_count > total_alerts / 3 {
447            Some(AlertRecommendation {
448                recommendation_type: RecommendationType::AutoRemediation,
449                description: "Implement automatic resolution for common alert patterns".to_string(),
450                priority: 8,
451                estimated_impact: 0.7,
452            })
453        } else {
454            None
455        }
456    }
457
458    fn is_auto_resolvable(&self, alert: &StoredAlert) -> bool {
459        match alert.severity.as_str() {
460            "Info" | "Warning" => true,
461            "Critical" => false,
462            _ => false,
463        }
464    }
465
466    fn recommend_threshold_adjustments(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
467        let mut threshold_alerts = 0;
468        for alert in alerts {
469            if alert.current_value > alert.threshold * 0.9 
470                && alert.current_value < alert.threshold * 1.1 {
471                threshold_alerts += 1;
472            }
473        }
474
475        if threshold_alerts > alerts.len() / 4 {
476            Some(AlertRecommendation {
477                recommendation_type: RecommendationType::ThresholdAdjustment,
478                description: "Consider adjusting thresholds based on recent alert patterns".to_string(),
479                priority: 7,
480                estimated_impact: 0.6,
481            })
482        } else {
483            None
484        }
485    }
486
487    fn recommend_monitoring_improvements(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
488        let mut metric_frequencies = HashMap::new();
489        for alert in alerts {
490            *metric_frequencies.entry(&alert.metric_name).or_insert(0) += 1;
491        }
492
493        let high_frequency_metrics = metric_frequencies.iter()
494            .filter(|(_, &count)| count > alerts.len() / 10)
495            .map(|(metric, _)| (*metric).clone())
496            .collect::<Vec<String>>();
497
498        if !high_frequency_metrics.is_empty() {
499            Some(AlertRecommendation {
500                recommendation_type: RecommendationType::MonitoringEnhancement,
501                description: format!(
502                    "Enhance monitoring for metrics: {}",
503                    high_frequency_metrics.join(", ")
504                ),
505                priority: 6,
506                estimated_impact: 0.5,
507            })
508        } else {
509            None
510        }
511    }
512
513    #[allow(dead_code)]
514    fn analyze_alert_similarity(&self, a1: &StoredAlert, a2: &StoredAlert) -> f64 {
515        let time_diff = (a1.created_at - a2.created_at).num_seconds().abs() as f64;
516        let time_similarity = (-time_diff / 3600.0).exp();
517
518        let metric_similarity = if a1.metric_name == a2.metric_name { 1.0 } else { 0.0 };
519        let severity_similarity = if a1.severity == a2.severity { 1.0 } else { 0.0 };
520        
521        let threshold_diff = (a1.threshold - a2.threshold).abs();
522        let threshold_similarity = (-threshold_diff / a1.threshold).exp();
523
524        0.4 * time_similarity +
525        0.3 * metric_similarity +
526        0.2 * severity_similarity +
527        0.1 * threshold_similarity
528    }
529}