use crate::alert_storage::{AlertStorage, StoredAlert};
use crate::alert_system::{AlertSeverity};
use chrono::{DateTime, Utc, Duration, Timelike, Datelike};
use serde::{Serialize, Deserialize};
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
impl FromStr for AlertSeverity {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"critical" => Ok(AlertSeverity::Critical),
"warning" => Ok(AlertSeverity::Warning),
"info" => Ok(AlertSeverity::Info),
_ => Err(format!("Invalid severity: {}", s))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertAnalysis {
pub time_range: TimeRange,
pub total_alerts: usize,
pub severity_distribution: HashMap<AlertSeverity, usize>,
pub resolution_metrics: ResolutionMetrics,
pub patterns: Vec<AlertPattern>,
pub recommendations: Vec<AlertRecommendation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeRange {
pub start: DateTime<Utc>,
pub end: DateTime<Utc>,
#[serde(with = "duration_serde")]
pub duration: Duration,
}
mod duration_serde {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use chrono::Duration;
pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
duration.num_seconds().serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs = i64::deserialize(deserializer)?;
Ok(Duration::seconds(secs))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolutionMetrics {
#[serde(with = "duration_serde")]
pub avg_resolution_time: Duration,
pub resolution_rate: f64,
pub unresolved_critical: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertPattern {
pub pattern_type: PatternType,
pub frequency: usize,
pub confidence: f64,
pub affected_metrics: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PatternType {
Periodic,
Cascading,
Correlated,
Seasonal,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertRecommendation {
pub recommendation_type: RecommendationType,
pub description: String,
pub priority: u8,
pub estimated_impact: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecommendationType {
ThresholdAdjustment,
MonitoringEnhancement,
AutoRemediation,
ProcessImprovement,
}
pub struct AlertAnalyzer {
storage: AlertStorage,
config: AnalyzerConfig,
}
#[derive(Clone)]
pub struct AnalyzerConfig {
pub analysis_window: Duration,
pub pattern_detection_threshold: f64,
pub min_correlation_strength: f64,
}
impl AlertAnalyzer {
pub fn new(storage: AlertStorage, config: AnalyzerConfig) -> Self {
Self { storage, config }
}
pub async fn analyze_alerts(&self, time_range: TimeRange) -> Result<AlertAnalysis, Box<dyn std::error::Error>> {
let alerts = self.storage.get_alerts_in_range(time_range.start, time_range.end).await?;
let analysis = AlertAnalysis {
time_range,
total_alerts: alerts.len(),
severity_distribution: self.calculate_severity_distribution(&alerts),
resolution_metrics: self.calculate_resolution_metrics(&alerts),
patterns: self.detect_patterns(&alerts),
recommendations: self.generate_recommendations(&alerts),
};
Ok(analysis)
}
fn calculate_severity_distribution(&self, alerts: &[StoredAlert]) -> HashMap<AlertSeverity, usize> {
let mut distribution = HashMap::new();
for alert in alerts {
let severity = AlertSeverity::from_str(&alert.severity)
.unwrap_or(AlertSeverity::Info);
*distribution.entry(severity).or_insert(0) += 1;
}
distribution
}
fn calculate_resolution_metrics(&self, alerts: &[StoredAlert]) -> ResolutionMetrics {
let mut total_resolution_time = Duration::zero();
let mut resolved_count = 0;
let mut unresolved_critical = 0;
for alert in alerts {
if let Some(resolved_at) = alert.resolved_at {
let resolution_time = resolved_at - alert.created_at;
total_resolution_time = total_resolution_time + resolution_time;
resolved_count += 1;
} else if alert.severity == AlertSeverity::Critical.to_string() {
unresolved_critical += 1;
}
}
let avg_resolution_time = if resolved_count > 0 {
total_resolution_time / resolved_count as i32
} else {
Duration::zero()
};
let resolution_rate = resolved_count as f64 / alerts.len() as f64;
ResolutionMetrics {
avg_resolution_time,
resolution_rate,
unresolved_critical,
}
}
fn detect_patterns(&self, alerts: &[StoredAlert]) -> Vec<AlertPattern> {
let mut patterns = Vec::new();
if let Some(pattern) = self.detect_periodic_pattern(alerts) {
patterns.push(pattern);
}
if let Some(pattern) = self.detect_cascade_pattern(alerts) {
patterns.push(pattern);
}
patterns.extend(self.detect_correlated_patterns(alerts));
if let Some(pattern) = self.detect_seasonal_pattern(alerts) {
patterns.push(pattern);
}
patterns
}
fn detect_periodic_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
let mut timeline = alerts.iter()
.map(|a| (a.created_at, a.metric_name.clone()))
.collect::<Vec<_>>();
timeline.sort_by_key(|k| k.0);
let intervals = vec![
chrono::Duration::hours(1),
chrono::Duration::hours(6),
chrono::Duration::hours(12),
chrono::Duration::hours(24),
];
let mut best_period = None;
let mut best_confidence = 0.0;
for interval in intervals {
if let Some((period, confidence)) = self.analyze_periodicity(&timeline, interval) {
if confidence > best_confidence && confidence > self.config.pattern_detection_threshold {
best_period = Some(period);
best_confidence = confidence;
}
}
}
best_period.map(|period| AlertPattern {
pattern_type: PatternType::Periodic,
frequency: period.num_hours() as usize,
confidence: best_confidence,
affected_metrics: self.get_affected_metrics(alerts),
})
}
fn analyze_periodicity(
&self,
timeline: &[(DateTime<Utc>, String)],
interval: Duration
) -> Option<(Duration, f64)> {
let mut interval_counts = HashMap::new();
let mut prev_time = None;
for (time, _) in timeline {
if let Some(prev) = prev_time {
let diff: Duration = *time - prev;
let normalized_diff = Duration::hours(
(diff.num_seconds() as f64 / interval.num_seconds() as f64).round() as i64
);
*interval_counts.entry(normalized_diff).or_insert(0) += 1;
}
prev_time = Some(*time);
}
interval_counts.iter()
.max_by_key(|(_, &count)| count)
.map(|(period, count)| {
let total_intervals = interval_counts.values().sum::<i32>();
let confidence = *count as f64 / total_intervals as f64;
(*period, confidence)
})
}
fn detect_cascade_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
let mut timeline = alerts.iter()
.map(|a| (a.created_at, a.metric_name.clone()))
.collect::<Vec<_>>();
timeline.sort_by_key(|k| k.0);
let window_size = chrono::Duration::minutes(5);
let mut cascade_groups = Vec::new();
let mut current_group = Vec::new();
for i in 0..timeline.len() {
if current_group.is_empty() {
current_group.push(timeline[i].clone());
continue;
}
let time_diff = timeline[i].0 - current_group.last().unwrap().0;
if time_diff <= window_size {
current_group.push(timeline[i].clone());
} else {
if current_group.len() >= 3 {
cascade_groups.push(current_group.clone());
}
current_group.clear();
current_group.push(timeline[i].clone());
}
}
if current_group.len() >= 3 {
cascade_groups.push(current_group);
}
if let Some(largest_cascade) = cascade_groups.iter().max_by_key(|g| g.len()) {
if largest_cascade.len() >= 3 {
let affected_metrics = largest_cascade.iter()
.map(|(_, metric)| metric.clone())
.collect();
Some(AlertPattern {
pattern_type: PatternType::Cascading,
frequency: largest_cascade.len(),
confidence: largest_cascade.len() as f64 / alerts.len() as f64,
affected_metrics,
})
} else {
None
}
} else {
None
}
}
fn detect_correlated_patterns(&self, alerts: &[StoredAlert]) -> Vec<AlertPattern> {
let mut patterns = Vec::new();
let mut metric_groups = HashMap::new();
let window_size = chrono::Duration::minutes(15);
for alert in alerts {
let window_start = alert.created_at.timestamp() / window_size.num_seconds();
metric_groups
.entry(window_start)
.or_insert_with(HashSet::new)
.insert(alert.metric_name.clone());
}
let mut co_occurrences = HashMap::new();
for metrics in metric_groups.values() {
for m1 in metrics.iter() {
for m2 in metrics.iter() {
if m1 < m2 {
*co_occurrences.entry((m1.clone(), m2.clone())).or_insert(0) += 1;
}
}
}
}
let min_occurrences = (metric_groups.len() as f64 * self.config.min_correlation_strength) as usize;
let mut correlated_metrics = HashSet::new();
for ((m1, m2), count) in co_occurrences {
if count >= min_occurrences {
let confidence = count as f64 / metric_groups.len() as f64;
if !correlated_metrics.contains(&m1) && !correlated_metrics.contains(&m2) {
patterns.push(AlertPattern {
pattern_type: PatternType::Correlated,
frequency: count,
confidence,
affected_metrics: vec![m1.clone(), m2.clone()],
});
correlated_metrics.insert(m1);
correlated_metrics.insert(m2);
}
}
}
patterns
}
fn detect_seasonal_pattern(&self, alerts: &[StoredAlert]) -> Option<AlertPattern> {
let mut hourly_distribution = vec![0; 24];
let mut daily_distribution = vec![0; 7];
for alert in alerts {
let hour = alert.created_at.hour() as usize;
let day = alert.created_at.weekday().num_days_from_monday() as usize;
hourly_distribution[hour] += 1;
daily_distribution[day] += 1;
}
let hourly_variance = self.calculate_distribution_variance(&hourly_distribution);
let daily_variance = self.calculate_distribution_variance(&daily_distribution);
if hourly_variance > self.config.pattern_detection_threshold {
Some(AlertPattern {
pattern_type: PatternType::Seasonal,
frequency: self.find_peak_frequency(&hourly_distribution),
confidence: hourly_variance,
affected_metrics: self.get_affected_metrics(alerts),
})
} else if daily_variance > self.config.pattern_detection_threshold {
Some(AlertPattern {
pattern_type: PatternType::Seasonal,
frequency: self.find_peak_frequency(&daily_distribution) * 24,
confidence: daily_variance,
affected_metrics: self.get_affected_metrics(alerts),
})
} else {
None
}
}
fn calculate_distribution_variance(&self, distribution: &[i32]) -> f64 {
let mean = distribution.iter().sum::<i32>() as f64 / distribution.len() as f64;
let variance = distribution.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>() / distribution.len() as f64;
variance.sqrt() / mean
}
fn find_peak_frequency(&self, distribution: &[i32]) -> usize {
let mut max_val = 0;
let mut max_idx = 0;
for (idx, &val) in distribution.iter().enumerate() {
if val > max_val {
max_val = val;
max_idx = idx;
}
}
max_idx
}
fn get_affected_metrics(&self, alerts: &[StoredAlert]) -> Vec<String> {
let mut metrics = std::collections::HashSet::new();
for alert in alerts {
metrics.insert(alert.metric_name.clone());
}
metrics.into_iter().collect()
}
fn generate_recommendations(&self, alerts: &[StoredAlert]) -> Vec<AlertRecommendation> {
let mut recommendations = Vec::new();
if let Some(rec) = self.recommend_threshold_adjustments(alerts) {
recommendations.push(rec);
}
if let Some(rec) = self.recommend_monitoring_improvements(alerts) {
recommendations.push(rec);
}
if let Some(rec) = self.recommend_auto_remediation(alerts) {
recommendations.push(rec);
}
recommendations
}
fn recommend_auto_remediation(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
let auto_resolvable_count = alerts.iter()
.filter(|a| self.is_auto_resolvable(a))
.count();
let total_alerts = alerts.len();
if auto_resolvable_count > total_alerts / 3 {
Some(AlertRecommendation {
recommendation_type: RecommendationType::AutoRemediation,
description: "Implement automatic resolution for common alert patterns".to_string(),
priority: 8,
estimated_impact: 0.7,
})
} else {
None
}
}
fn is_auto_resolvable(&self, alert: &StoredAlert) -> bool {
match alert.severity.as_str() {
"Info" | "Warning" => true,
"Critical" => false,
_ => false,
}
}
fn recommend_threshold_adjustments(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
let mut threshold_alerts = 0;
for alert in alerts {
if alert.current_value > alert.threshold * 0.9
&& alert.current_value < alert.threshold * 1.1 {
threshold_alerts += 1;
}
}
if threshold_alerts > alerts.len() / 4 {
Some(AlertRecommendation {
recommendation_type: RecommendationType::ThresholdAdjustment,
description: "Consider adjusting thresholds based on recent alert patterns".to_string(),
priority: 7,
estimated_impact: 0.6,
})
} else {
None
}
}
fn recommend_monitoring_improvements(&self, alerts: &[StoredAlert]) -> Option<AlertRecommendation> {
let mut metric_frequencies = HashMap::new();
for alert in alerts {
*metric_frequencies.entry(&alert.metric_name).or_insert(0) += 1;
}
let high_frequency_metrics = metric_frequencies.iter()
.filter(|(_, &count)| count > alerts.len() / 10)
.map(|(metric, _)| (*metric).clone())
.collect::<Vec<String>>();
if !high_frequency_metrics.is_empty() {
Some(AlertRecommendation {
recommendation_type: RecommendationType::MonitoringEnhancement,
description: format!(
"Enhance monitoring for metrics: {}",
high_frequency_metrics.join(", ")
),
priority: 6,
estimated_impact: 0.5,
})
} else {
None
}
}
#[allow(dead_code)]
fn analyze_alert_similarity(&self, a1: &StoredAlert, a2: &StoredAlert) -> f64 {
let time_diff = (a1.created_at - a2.created_at).num_seconds().abs() as f64;
let time_similarity = (-time_diff / 3600.0).exp();
let metric_similarity = if a1.metric_name == a2.metric_name { 1.0 } else { 0.0 };
let severity_similarity = if a1.severity == a2.severity { 1.0 } else { 0.0 };
let threshold_diff = (a1.threshold - a2.threshold).abs();
let threshold_similarity = (-threshold_diff / a1.threshold).exp();
0.4 * time_similarity +
0.3 * metric_similarity +
0.2 * severity_similarity +
0.1 * threshold_similarity
}
}