- ml-observability (1.2K lines): alerts, dashboards, metrics modules. Depends on ml-core + common (ModelType). 4 tests passing. - ml-stress-testing (1.3K lines): load_generator, market_simulator, performance_analyzer modules. Depends on ml-core + common + config. 5 tests passing. - ml-security (1.4K lines): anomaly_detector, prediction_validator modules. Depends on ml-core + ml-ensemble (EnsembleDecision, ModelVote, TradingAction). 17 tests passing. Total: 18 sub-crates extracted from ml monolith. Workspace: 0 errors, ml tests 876 + 26 in new sub-crates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
311 lines
9.5 KiB
Rust
311 lines
9.5 KiB
Rust
//! Alert management system for ML production monitoring
|
|
|
|
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, SystemTime};
|
|
use tokio::sync::RwLock;
|
|
|
|
/// Alert severity levels
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum AlertSeverity {
|
|
Info,
|
|
Warning,
|
|
Critical,
|
|
Emergency,
|
|
}
|
|
|
|
/// Alert channels for notification delivery
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AlertChannel {
|
|
Slack { webhook_url: String },
|
|
Email { recipients: Vec<String> },
|
|
PagerDuty { service_key: String },
|
|
Webhook { url: String },
|
|
Console,
|
|
}
|
|
|
|
/// Alert rule configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AlertRule {
|
|
pub name: String,
|
|
pub metric_name: String,
|
|
pub threshold: f64,
|
|
pub comparison: AlertComparison,
|
|
pub severity: AlertSeverity,
|
|
pub cooldown_minutes: u64,
|
|
pub channels: Vec<AlertChannel>,
|
|
pub labels: HashMap<String, String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub enum AlertComparison {
|
|
GreaterThan,
|
|
LessThan,
|
|
Equal,
|
|
NotEqual,
|
|
}
|
|
|
|
/// Active alert
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Alert {
|
|
pub id: String,
|
|
pub rule_name: String,
|
|
pub metric_name: String,
|
|
pub current_value: f64,
|
|
pub threshold: f64,
|
|
pub severity: AlertSeverity,
|
|
pub message: String,
|
|
pub labels: HashMap<String, String>,
|
|
pub triggered_at: SystemTime,
|
|
pub acknowledged: bool,
|
|
pub resolved: bool,
|
|
}
|
|
|
|
/// Alert management system
|
|
#[derive(Debug)]
|
|
pub struct AlertManager {
|
|
rules: Arc<RwLock<Vec<AlertRule>>>,
|
|
active_alerts: Arc<RwLock<HashMap<String, Alert>>>,
|
|
cooldown_tracker: Arc<RwLock<HashMap<String, SystemTime>>>,
|
|
}
|
|
|
|
impl AlertManager {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
rules: Arc::new(RwLock::new(Vec::new())),
|
|
active_alerts: Arc::new(RwLock::new(HashMap::new())),
|
|
cooldown_tracker: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Add alert rule
|
|
pub async fn add_rule(&self, rule: AlertRule) {
|
|
let mut rules = self.rules.write().await;
|
|
rules.push(rule);
|
|
}
|
|
|
|
/// Evaluate metric against all rules
|
|
pub async fn evaluate_metric(
|
|
&self,
|
|
metric_name: &str,
|
|
value: f64,
|
|
labels: HashMap<String, String>,
|
|
) -> Result<()> {
|
|
let rules = self.rules.read().await;
|
|
|
|
for rule in rules.iter() {
|
|
if rule.metric_name == metric_name {
|
|
self.evaluate_rule(rule, value, labels.clone()).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn evaluate_rule(
|
|
&self,
|
|
rule: &AlertRule,
|
|
value: f64,
|
|
labels: HashMap<String, String>,
|
|
) -> Result<()> {
|
|
let should_trigger = match rule.comparison {
|
|
AlertComparison::GreaterThan => value > rule.threshold,
|
|
AlertComparison::LessThan => value < rule.threshold,
|
|
AlertComparison::Equal => (value - rule.threshold).abs() < f64::EPSILON,
|
|
AlertComparison::NotEqual => (value - rule.threshold).abs() > f64::EPSILON,
|
|
};
|
|
|
|
if should_trigger {
|
|
self.trigger_alert(rule, value, labels).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn trigger_alert(
|
|
&self,
|
|
rule: &AlertRule,
|
|
value: f64,
|
|
labels: HashMap<String, String>,
|
|
) -> Result<()> {
|
|
let alert_id = format!(
|
|
"{}_{}",
|
|
rule.name,
|
|
SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)?
|
|
.as_secs()
|
|
);
|
|
|
|
// Check cooldown
|
|
{
|
|
let cooldown = self.cooldown_tracker.read().await;
|
|
if let Some(last_trigger) = cooldown.get(&rule.name) {
|
|
let elapsed = SystemTime::now().duration_since(*last_trigger)?;
|
|
if elapsed < Duration::from_secs(rule.cooldown_minutes * 60) {
|
|
return Ok(()); // Still in cooldown
|
|
}
|
|
}
|
|
}
|
|
|
|
let alert = Alert {
|
|
id: alert_id.clone(),
|
|
rule_name: rule.name.clone(),
|
|
metric_name: rule.metric_name.clone(),
|
|
current_value: value,
|
|
threshold: rule.threshold,
|
|
severity: rule.severity,
|
|
message: format!(
|
|
"Alert: {} - {} {} {} (current: {})",
|
|
rule.name,
|
|
rule.metric_name,
|
|
match rule.comparison {
|
|
AlertComparison::GreaterThan => ">",
|
|
AlertComparison::LessThan => "<",
|
|
AlertComparison::Equal => "==",
|
|
AlertComparison::NotEqual => "!=",
|
|
},
|
|
rule.threshold,
|
|
value
|
|
),
|
|
labels,
|
|
triggered_at: SystemTime::now(),
|
|
acknowledged: false,
|
|
resolved: false,
|
|
};
|
|
|
|
// Store alert
|
|
{
|
|
let mut alerts = self.active_alerts.write().await;
|
|
alerts.insert(alert_id, alert.clone());
|
|
}
|
|
|
|
// Update cooldown
|
|
{
|
|
let mut cooldown = self.cooldown_tracker.write().await;
|
|
cooldown.insert(rule.name.clone(), SystemTime::now());
|
|
}
|
|
|
|
// Send notifications
|
|
for channel in &rule.channels {
|
|
self.send_notification(channel, &alert).await?;
|
|
}
|
|
|
|
tracing::warn!("Alert triggered: {}", alert.message);
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_notification(&self, channel: &AlertChannel, alert: &Alert) -> Result<()> {
|
|
match channel {
|
|
AlertChannel::Console => {
|
|
println!("ALERT: {} - {}", alert.severity as u8, alert.message);
|
|
},
|
|
AlertChannel::Slack { webhook_url: _ } => {
|
|
// Implement Slack webhook notification
|
|
tracing::info!("Would send Slack alert: {}", alert.message);
|
|
},
|
|
AlertChannel::Email { recipients: _ } => {
|
|
// Implement email notification
|
|
tracing::info!("Would send email alert: {}", alert.message);
|
|
},
|
|
AlertChannel::PagerDuty { service_key: _ } => {
|
|
// Implement PagerDuty notification
|
|
tracing::info!("Would send PagerDuty alert: {}", alert.message);
|
|
},
|
|
AlertChannel::Webhook { url: _ } => {
|
|
// Implement webhook notification
|
|
tracing::info!("Would send webhook alert: {}", alert.message);
|
|
},
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get active alerts
|
|
pub async fn get_active_alerts(&self) -> HashMap<String, Alert> {
|
|
self.active_alerts.read().await.clone()
|
|
}
|
|
|
|
/// Acknowledge alert
|
|
pub async fn acknowledge_alert(&self, alert_id: &str) -> Result<()> {
|
|
let mut alerts = self.active_alerts.write().await;
|
|
if let Some(alert) = alerts.get_mut(alert_id) {
|
|
alert.acknowledged = true;
|
|
tracing::info!("Alert acknowledged: {}", alert_id);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve alert
|
|
pub async fn resolve_alert(&self, alert_id: &str) -> Result<()> {
|
|
let mut alerts = self.active_alerts.write().await;
|
|
if let Some(alert) = alerts.get_mut(alert_id) {
|
|
alert.resolved = true;
|
|
tracing::info!("Alert resolved: {}", alert_id);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Create default HFT alert rules
|
|
pub fn create_hft_alert_rules() -> Vec<AlertRule> {
|
|
vec![
|
|
AlertRule {
|
|
name: "high_inference_latency".to_owned(),
|
|
metric_name: "ml_inference_latency_microseconds".to_owned(),
|
|
threshold: 100.0, // 100us
|
|
comparison: AlertComparison::GreaterThan,
|
|
severity: AlertSeverity::Warning,
|
|
cooldown_minutes: 5,
|
|
channels: vec![AlertChannel::Console],
|
|
labels: HashMap::new(),
|
|
},
|
|
AlertRule {
|
|
name: "critical_inference_latency".to_owned(),
|
|
metric_name: "ml_inference_latency_microseconds".to_owned(),
|
|
threshold: 500.0, // 500us
|
|
comparison: AlertComparison::GreaterThan,
|
|
severity: AlertSeverity::Critical,
|
|
cooldown_minutes: 1,
|
|
channels: vec![AlertChannel::Console],
|
|
labels: HashMap::new(),
|
|
},
|
|
AlertRule {
|
|
name: "high_error_rate".to_owned(),
|
|
metric_name: "ml_error_rate".to_owned(),
|
|
threshold: 0.05, // 5%
|
|
comparison: AlertComparison::GreaterThan,
|
|
severity: AlertSeverity::Warning,
|
|
cooldown_minutes: 10,
|
|
channels: vec![AlertChannel::Console],
|
|
labels: HashMap::new(),
|
|
},
|
|
AlertRule {
|
|
name: "low_model_confidence".to_owned(),
|
|
metric_name: "ml_model_confidence".to_owned(),
|
|
threshold: 0.6, // 60%
|
|
comparison: AlertComparison::LessThan,
|
|
severity: AlertSeverity::Warning,
|
|
cooldown_minutes: 15,
|
|
channels: vec![AlertChannel::Console],
|
|
labels: HashMap::new(),
|
|
},
|
|
AlertRule {
|
|
name: "model_drift_detected".to_owned(),
|
|
metric_name: "ml_drift_detection_score".to_owned(),
|
|
threshold: 0.3,
|
|
comparison: AlertComparison::GreaterThan,
|
|
severity: AlertSeverity::Warning,
|
|
cooldown_minutes: 30,
|
|
channels: vec![AlertChannel::Console],
|
|
labels: HashMap::new(),
|
|
},
|
|
]
|
|
}
|
|
|
|
impl Default for AlertManager {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|