use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertRule {
pub id: String,
pub name: String,
pub description: String,
pub metric_name: String,
pub condition: AlertCondition,
pub severity: AlertSeverity,
pub enabled: bool,
pub cooldown_seconds: u64,
}
impl AlertRule {
pub fn new(
id: String,
name: String,
metric_name: String,
condition: AlertCondition,
severity: AlertSeverity,
) -> Self {
Self {
id,
name,
description: String::new(),
metric_name,
condition,
severity,
enabled: true,
cooldown_seconds: 300, }
}
pub fn with_description(mut self, description: String) -> Self {
self.description = description;
self
}
pub fn with_cooldown(mut self, cooldown_seconds: u64) -> Self {
self.cooldown_seconds = cooldown_seconds;
self
}
pub fn set_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertCondition {
pub operator: ComparisonOperator,
pub threshold: f64,
pub duration_seconds: u64,
}
impl AlertCondition {
pub fn new(operator: ComparisonOperator, threshold: f64, duration_seconds: u64) -> Self {
Self {
operator,
threshold,
duration_seconds,
}
}
pub fn is_satisfied(&self, value: f64) -> bool {
match self.operator {
ComparisonOperator::GreaterThan => value > self.threshold,
ComparisonOperator::GreaterThanOrEqual => value >= self.threshold,
ComparisonOperator::LessThan => value < self.threshold,
ComparisonOperator::LessThanOrEqual => value <= self.threshold,
ComparisonOperator::Equal => (value - self.threshold).abs() < f64::EPSILON,
ComparisonOperator::NotEqual => (value - self.threshold).abs() >= f64::EPSILON,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComparisonOperator {
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Equal,
NotEqual,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlertSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Alert {
pub id: String,
pub rule_id: String,
pub severity: AlertSeverity,
pub message: String,
pub triggered_at: u64,
pub resolved_at: Option<u64>,
pub is_active: bool,
pub context: HashMap<String, String>,
}
impl Alert {
pub fn new(
id: String,
rule_id: String,
severity: AlertSeverity,
message: String,
) -> Self {
Self {
id,
rule_id,
severity,
message,
triggered_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
resolved_at: None,
is_active: true,
context: HashMap::new(),
}
}
pub fn with_context(mut self, context: HashMap<String, String>) -> Self {
self.context = context;
self
}
pub fn resolve(&mut self) {
self.is_active = false;
self.resolved_at = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
);
}
pub fn duration(&self) -> Duration {
let end_time = self.resolved_at.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
});
Duration::from_secs(end_time - self.triggered_at)
}
}
#[derive(Debug, Clone)]
pub struct AlertManager {
rules: HashMap<String, AlertRule>,
active_alerts: HashMap<String, Alert>,
alert_history: Vec<Alert>,
max_history_size: usize,
last_trigger_times: HashMap<String, u64>,
}
impl AlertManager {
pub fn new() -> Self {
Self {
rules: HashMap::new(),
active_alerts: HashMap::new(),
alert_history: Vec::new(),
max_history_size: 1000,
last_trigger_times: HashMap::new(),
}
}
pub fn with_config(config: AlertConfig) -> Self {
Self {
rules: HashMap::new(),
active_alerts: HashMap::new(),
alert_history: Vec::new(),
max_history_size: config.max_history_size,
last_trigger_times: HashMap::new(),
}
}
pub fn add_rule(&mut self, rule: AlertRule) {
self.rules.insert(rule.id.clone(), rule);
}
pub fn remove_rule(&mut self, rule_id: &str) {
self.rules.remove(rule_id);
self.active_alerts.retain(|_, alert| alert.rule_id != rule_id);
}
pub fn update_rule(&mut self, rule: AlertRule) {
self.rules.insert(rule.id.clone(), rule);
}
pub fn get_rules(&self) -> Vec<&AlertRule> {
self.rules.values().collect()
}
pub fn get_rule(&self, rule_id: &str) -> Option<&AlertRule> {
self.rules.get(rule_id)
}
pub fn check_metric(&mut self, metric_name: &str, value: f64) -> Vec<Alert> {
let mut new_alerts = Vec::new();
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let rule_ids: Vec<String> = self.rules.keys().cloned().collect();
let mut rules_to_resolve = Vec::new();
for rule_id in rule_ids {
if let Some(rule) = self.rules.get(&rule_id) {
if !rule.enabled || rule.metric_name != metric_name {
continue;
}
if let Some(last_trigger) = self.last_trigger_times.get(&rule.id) {
if current_time - last_trigger < rule.cooldown_seconds {
continue;
}
}
if rule.condition.is_satisfied(value) {
let has_active_alert = self.active_alerts.values()
.any(|alert| alert.rule_id == rule.id && alert.is_active);
if !has_active_alert {
let alert_id = format!("{}_{}", rule.id, current_time);
let message = format!(
"Alert triggered: {} (value: {}, threshold: {})",
rule.name, value, rule.condition.threshold
);
let mut alert = Alert::new(
alert_id,
rule.id.clone(),
rule.severity.clone(),
message,
);
let mut context = HashMap::new();
context.insert("metric_name".to_string(), metric_name.to_string());
context.insert("value".to_string(), value.to_string());
context.insert("threshold".to_string(), rule.condition.threshold.to_string());
alert.context = context;
self.active_alerts.insert(alert.id.clone(), alert.clone());
self.alert_history.push(alert.clone());
self.last_trigger_times.insert(rule.id.clone(), current_time);
new_alerts.push(alert);
}
} else {
rules_to_resolve.push(rule.id.clone());
}
}
}
for rule_id in rules_to_resolve {
self.resolve_alerts_for_rule(&rule_id);
}
self.cleanup_history();
new_alerts
}
pub fn get_active_alerts(&self) -> Vec<&Alert> {
self.active_alerts.values().filter(|alert| alert.is_active).collect()
}
pub fn get_alerts_by_severity(&self, severity: &AlertSeverity) -> Vec<&Alert> {
self.active_alerts
.values()
.filter(|alert| alert.is_active && &alert.severity == severity)
.collect()
}
pub fn resolve_alert(&mut self, alert_id: &str) -> bool {
if let Some(alert) = self.active_alerts.get_mut(alert_id) {
alert.resolve();
true
} else {
false
}
}
fn resolve_alerts_for_rule(&mut self, rule_id: &str) {
for alert in self.active_alerts.values_mut() {
if alert.rule_id == rule_id && alert.is_active {
alert.resolve();
}
}
}
pub fn get_alert_history(&self, limit: Option<usize>) -> Vec<&Alert> {
let limit = limit.unwrap_or(self.alert_history.len());
self.alert_history
.iter()
.rev()
.take(limit)
.collect()
}
pub fn clear_history(&mut self) {
self.alert_history.clear();
}
fn cleanup_history(&mut self) {
if self.alert_history.len() > self.max_history_size {
let excess = self.alert_history.len() - self.max_history_size;
self.alert_history.drain(0..excess);
}
}
pub fn get_stats(&self) -> AlertStats {
let active_count = self.active_alerts.values().filter(|a| a.is_active).count();
let critical_count = self.get_alerts_by_severity(&AlertSeverity::Critical).len();
let high_count = self.get_alerts_by_severity(&AlertSeverity::High).len();
let medium_count = self.get_alerts_by_severity(&AlertSeverity::Medium).len();
let low_count = self.get_alerts_by_severity(&AlertSeverity::Low).len();
AlertStats {
total_rules: self.rules.len(),
active_alerts: active_count,
critical_alerts: critical_count,
high_alerts: high_count,
medium_alerts: medium_count,
low_alerts: low_count,
total_history: self.alert_history.len(),
}
}
}
impl Default for AlertManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertStats {
pub total_rules: usize,
pub active_alerts: usize,
pub critical_alerts: usize,
pub high_alerts: usize,
pub medium_alerts: usize,
pub low_alerts: usize,
pub total_history: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertConfig {
pub max_history_size: usize,
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
max_history_size: 1000,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_alert_rule_creation() {
let condition = AlertCondition::new(ComparisonOperator::GreaterThan, 80.0, 60);
let rule = AlertRule::new(
"cpu_high".to_string(),
"High CPU Usage".to_string(),
"cpu_usage".to_string(),
condition,
AlertSeverity::High,
);
assert_eq!(rule.id, "cpu_high");
assert_eq!(rule.metric_name, "cpu_usage");
assert!(rule.enabled);
}
#[test]
fn test_alert_condition() {
let condition = AlertCondition::new(ComparisonOperator::GreaterThan, 80.0, 60);
assert!(condition.is_satisfied(85.0));
assert!(!condition.is_satisfied(75.0));
assert!(!condition.is_satisfied(80.0));
}
#[test]
fn test_alert_creation() {
let alert = Alert::new(
"alert_1".to_string(),
"rule_1".to_string(),
AlertSeverity::High,
"Test alert".to_string(),
);
assert_eq!(alert.id, "alert_1");
assert!(alert.is_active);
assert!(alert.resolved_at.is_none());
}
#[test]
fn test_alert_resolution() {
let mut alert = Alert::new(
"alert_1".to_string(),
"rule_1".to_string(),
AlertSeverity::High,
"Test alert".to_string(),
);
assert!(alert.is_active);
alert.resolve();
assert!(!alert.is_active);
assert!(alert.resolved_at.is_some());
}
#[test]
fn test_alert_manager() {
let mut manager = AlertManager::new();
let condition = AlertCondition::new(ComparisonOperator::GreaterThan, 80.0, 60);
let rule = AlertRule::new(
"cpu_high".to_string(),
"High CPU Usage".to_string(),
"cpu_usage".to_string(),
condition,
AlertSeverity::High,
);
manager.add_rule(rule);
let alerts = manager.check_metric("cpu_usage", 85.0);
assert_eq!(alerts.len(), 1);
assert_eq!(alerts[0].severity, AlertSeverity::High);
let active_alerts = manager.get_active_alerts();
assert_eq!(active_alerts.len(), 1);
let alerts = manager.check_metric("cpu_usage", 75.0);
assert_eq!(alerts.len(), 0);
let active_alerts = manager.get_active_alerts();
assert_eq!(active_alerts.len(), 0);
}
#[test]
fn test_alert_cooldown() {
let mut manager = AlertManager::new();
let condition = AlertCondition::new(ComparisonOperator::GreaterThan, 80.0, 60);
let rule = AlertRule::new(
"cpu_high".to_string(),
"High CPU Usage".to_string(),
"cpu_usage".to_string(),
condition,
AlertSeverity::High,
).with_cooldown(300); manager.add_rule(rule);
let alerts = manager.check_metric("cpu_usage", 85.0);
assert_eq!(alerts.len(), 1);
let alerts = manager.check_metric("cpu_usage", 90.0);
assert_eq!(alerts.len(), 0);
}
}