- Fixed systematic array indexing corruption: [0_i32] → [0] - Fixed numeric literal suffixes across 835 files - Fixed iterator patterns on RwLockReadGuard (.iter() required) - Fixed float type annotations (365.25_f64 for sqrt) - Fixed missing semicolons in position manager - Fixed reference dereferencing in data loader Root cause: Mass refactoring incorrectly added _i32 suffixes to array indices Impact: Complete compilation failure (463 errors) Resolution: Automated regex + targeted fixes Result: 100% compilation success (0 errors) Validated: cargo check --workspace passes Ready for: Production deployment
1248 lines
41 KiB
Rust
1248 lines
41 KiB
Rust
//! Performance Monitoring and Automatic Rollback System
|
|
//!
|
|
//! This module provides real-time performance monitoring, SLA violation detection,
|
|
//! and automatic rollback capabilities for deployed ML models.
|
|
|
|
use std::collections::{HashMap, VecDeque};
|
|
use std::sync::atomic::{AtomicU64, AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant, SystemTime};
|
|
|
|
use async_trait::async_trait;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::sync::{RwLock, Mutex, watch};
|
|
use tokio::time::interval;
|
|
use uuid::Uuid;
|
|
|
|
use crate::{MLError, MLResult, ModelType, Features, ModelPrediction, MLModel};
|
|
use super::{ModelVersion, DeploymentStatus, PerformanceBaseline};
|
|
|
|
/// Monitoring configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MonitoringConfig {
|
|
/// Metrics collection interval
|
|
pub collection_interval: Duration,
|
|
/// Metrics retention period
|
|
pub retention_period: Duration,
|
|
/// SLA thresholds
|
|
pub sla_thresholds: SLAThresholds,
|
|
/// Alerting configuration
|
|
pub alerting: AlertingConfig,
|
|
/// Rollback configuration
|
|
pub rollback: RollbackConfig,
|
|
/// Dashboard configuration
|
|
pub dashboard: DashboardConfig,
|
|
/// Enable detailed metrics
|
|
pub enable_detailed_metrics: bool,
|
|
/// Enable real-time alerting
|
|
pub enable_real_time_alerts: bool,
|
|
}
|
|
|
|
impl Default for MonitoringConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
collection_interval: Duration::from_secs(10),
|
|
retention_period: Duration::from_hours(24),
|
|
sla_thresholds: SLAThresholds::default(),
|
|
alerting: AlertingConfig::default(),
|
|
rollback: RollbackConfig::default(),
|
|
dashboard: DashboardConfig::default(),
|
|
enable_detailed_metrics: true,
|
|
enable_real_time_alerts: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// SLA threshold configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SLAThresholds {
|
|
/// Maximum average latency in microseconds
|
|
pub max_avg_latency_us: u64,
|
|
/// Maximum 95th percentile latency in microseconds
|
|
pub max_p95_latency_us: u64,
|
|
/// Maximum 99th percentile latency in microseconds
|
|
pub max_p99_latency_us: u64,
|
|
/// Maximum error rate (0.0 to 1.0)
|
|
pub max_error_rate: f32,
|
|
/// Minimum accuracy score (0.0 to 1.0)
|
|
pub min_accuracy: f32,
|
|
/// Maximum memory usage in MB
|
|
pub max_memory_mb: u64,
|
|
/// Maximum CPU utilization percentage
|
|
pub max_cpu_percent: f32,
|
|
/// Minimum throughput in predictions per second
|
|
pub min_throughput_pps: u32,
|
|
/// Degradation tolerance (percentage change from baseline)
|
|
pub degradation_tolerance: f32,
|
|
}
|
|
|
|
impl Default for SLAThresholds {
|
|
fn default() -> Self {
|
|
Self {
|
|
max_avg_latency_us: 100, // 100 microseconds
|
|
max_p95_latency_us: 200, // 200 microseconds
|
|
max_p99_latency_us: 500, // 500 microseconds
|
|
max_error_rate: 0.01, // 1%
|
|
min_accuracy: 0.8, // 80%
|
|
max_memory_mb: 1024, // 1GB
|
|
max_cpu_percent: 80.0, // 80%
|
|
min_throughput_pps: 1000, // 1K predictions per second
|
|
degradation_tolerance: 0.2, // 20% degradation
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Alerting configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AlertingConfig {
|
|
/// Enable email alerts
|
|
pub enable_email: bool,
|
|
/// Enable webhook alerts
|
|
pub enable_webhooks: bool,
|
|
/// Enable Slack alerts
|
|
pub enable_slack: bool,
|
|
/// Alert recipients
|
|
pub email_recipients: Vec<String>,
|
|
/// Webhook URLs
|
|
pub webhook_urls: Vec<String>,
|
|
/// Slack webhook URL
|
|
pub slack_webhook_url: Option<String>,
|
|
/// Alert cooldown period
|
|
pub cooldown_period: Duration,
|
|
/// Alert severity levels to send
|
|
pub alert_levels: Vec<AlertSeverity>,
|
|
}
|
|
|
|
impl Default for AlertingConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enable_email: false,
|
|
enable_webhooks: false,
|
|
enable_slack: false,
|
|
email_recipients: Vec::new(),
|
|
webhook_urls: Vec::new(),
|
|
slack_webhook_url: None,
|
|
cooldown_period: Duration::from_minutes(5),
|
|
alert_levels: vec![AlertSeverity::Critical, AlertSeverity::High],
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Alert severity levels
|
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
pub enum AlertSeverity {
|
|
/// Low severity - informational
|
|
Low,
|
|
/// Medium severity - warning
|
|
Medium,
|
|
/// High severity - error requiring attention
|
|
High,
|
|
/// Critical severity - immediate action required
|
|
Critical,
|
|
}
|
|
|
|
/// Dashboard configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DashboardConfig {
|
|
/// Enable Grafana dashboard
|
|
pub enable_grafana: bool,
|
|
/// Grafana dashboard URL
|
|
pub grafana_url: Option<String>,
|
|
/// Enable Prometheus metrics export
|
|
pub enable_prometheus: bool,
|
|
/// Prometheus metrics port
|
|
pub prometheus_port: u16,
|
|
/// Custom dashboard panels
|
|
pub custom_panels: Vec<DashboardPanel>,
|
|
}
|
|
|
|
impl Default for DashboardConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
enable_grafana: false,
|
|
grafana_url: None,
|
|
enable_prometheus: true,
|
|
prometheus_port: 9090,
|
|
custom_panels: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Dashboard panel configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DashboardPanel {
|
|
/// Panel name
|
|
pub name: String,
|
|
/// Panel type
|
|
pub panel_type: PanelType,
|
|
/// Metrics to display
|
|
pub metrics: Vec<String>,
|
|
/// Panel configuration
|
|
pub config: HashMap<String, String>,
|
|
}
|
|
|
|
/// Dashboard panel types
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum PanelType {
|
|
/// Time series graph
|
|
TimeSeries,
|
|
/// Single stat display
|
|
SingleStat,
|
|
/// Gauge display
|
|
Gauge,
|
|
/// Table display
|
|
Table,
|
|
/// Heatmap display
|
|
Heatmap,
|
|
}
|
|
|
|
/// Performance monitoring system
|
|
pub struct PerformanceMonitor {
|
|
/// Model being monitored
|
|
model_id: String,
|
|
/// Model type
|
|
model_type: ModelType,
|
|
/// Model version
|
|
version: ModelVersion,
|
|
/// Monitoring configuration
|
|
config: MonitoringConfig,
|
|
/// Current metrics
|
|
current_metrics: Arc<RwLock<PerformanceMetrics>>,
|
|
/// Historical metrics
|
|
historical_metrics: Arc<Mutex<VecDeque<PerformanceSnapshot>>>,
|
|
/// SLA violations counter
|
|
sla_violations: Arc<RwLock<SLAViolations>>,
|
|
/// Alert manager
|
|
alert_manager: Arc<AlertManager>,
|
|
/// Rollback manager
|
|
rollback_manager: Arc<RollbackManager>,
|
|
/// Metrics collector
|
|
metrics_collector: Arc<MetricsCollector>,
|
|
/// Monitor status
|
|
is_running: Arc<AtomicBool>,
|
|
/// Shutdown signal
|
|
shutdown_tx: Option<watch::Sender<bool>>,
|
|
}
|
|
|
|
/// Real-time performance metrics
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceMetrics {
|
|
/// Model ID
|
|
pub model_id: String,
|
|
/// Timestamp
|
|
pub timestamp: SystemTime,
|
|
/// Total predictions made
|
|
pub total_predictions: u64,
|
|
/// Total errors
|
|
pub total_errors: u64,
|
|
/// Current error rate
|
|
pub error_rate: f32,
|
|
/// Latency percentiles in microseconds
|
|
pub latency_p50: f64,
|
|
pub latency_p95: f64,
|
|
pub latency_p99: f64,
|
|
pub latency_p999: f64,
|
|
/// Average latency
|
|
pub avg_latency_us: f64,
|
|
/// Current throughput (predictions per second)
|
|
pub throughput_pps: f64,
|
|
/// Memory usage in MB
|
|
pub memory_usage_mb: f64,
|
|
/// CPU utilization percentage
|
|
pub cpu_utilization: f32,
|
|
/// Average accuracy score
|
|
pub avg_accuracy: f32,
|
|
/// Custom metrics
|
|
pub custom_metrics: HashMap<String, f64>,
|
|
}
|
|
|
|
impl Default for PerformanceMetrics {
|
|
fn default() -> Self {
|
|
Self {
|
|
model_id: String::new(),
|
|
timestamp: SystemTime::now(),
|
|
total_predictions: 0,
|
|
total_errors: 0,
|
|
error_rate: 0.0,
|
|
latency_p50: 0.0,
|
|
latency_p95: 0.0,
|
|
latency_p99: 0.0,
|
|
latency_p999: 0.0,
|
|
avg_latency_us: 0.0,
|
|
throughput_pps: 0.0,
|
|
memory_usage_mb: 0.0,
|
|
cpu_utilization: 0.0,
|
|
avg_accuracy: 0.0,
|
|
custom_metrics: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance snapshot for historical tracking
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceSnapshot {
|
|
/// Snapshot timestamp
|
|
pub timestamp: SystemTime,
|
|
/// Performance metrics at snapshot time
|
|
pub metrics: PerformanceMetrics,
|
|
/// Baseline comparison
|
|
pub baseline_comparison: Option<BaselineComparison>,
|
|
}
|
|
|
|
/// Comparison with performance baseline
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BaselineComparison {
|
|
/// Latency change percentage
|
|
pub latency_change_percent: f32,
|
|
/// Throughput change percentage
|
|
pub throughput_change_percent: f32,
|
|
/// Error rate change percentage
|
|
pub error_rate_change_percent: f32,
|
|
/// Accuracy change percentage
|
|
pub accuracy_change_percent: f32,
|
|
/// Overall degradation score
|
|
pub degradation_score: f32,
|
|
}
|
|
|
|
/// SLA violations tracking
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct SLAViolations {
|
|
/// Latency violations
|
|
pub latency_violations: u32,
|
|
/// Error rate violations
|
|
pub error_rate_violations: u32,
|
|
/// Accuracy violations
|
|
pub accuracy_violations: u32,
|
|
/// Throughput violations
|
|
pub throughput_violations: u32,
|
|
/// Memory violations
|
|
pub memory_violations: u32,
|
|
/// CPU violations
|
|
pub cpu_violations: u32,
|
|
/// Total violations
|
|
pub total_violations: u32,
|
|
/// Last violation timestamp
|
|
pub last_violation_time: Option<SystemTime>,
|
|
}
|
|
|
|
impl PerformanceMonitor {
|
|
/// Create new performance monitor
|
|
pub async fn new(
|
|
model_id: String,
|
|
model_type: ModelType,
|
|
version: ModelVersion,
|
|
config: MonitoringConfig,
|
|
baseline: Option<PerformanceBaseline>,
|
|
) -> Self {
|
|
let (shutdown_tx, shutdown_rx) = watch::channel(false);
|
|
|
|
let alert_manager = Arc::new(AlertManager::new(config.alerting.clone()));
|
|
let rollback_manager = Arc::new(RollbackManager::new(config.rollback.clone()));
|
|
let metrics_collector = Arc::new(MetricsCollector::new(
|
|
model_id.clone(),
|
|
config.collection_interval,
|
|
baseline,
|
|
));
|
|
|
|
let current_metrics = Arc::new(RwLock::new(PerformanceMetrics {
|
|
model_id: model_id.clone(),
|
|
..Default::default()
|
|
}));
|
|
|
|
Self {
|
|
model_id,
|
|
model_type,
|
|
version,
|
|
config,
|
|
current_metrics,
|
|
historical_metrics: Arc::new(Mutex::new(VecDeque::new())),
|
|
sla_violations: Arc::new(RwLock::new(SLAViolations::default())),
|
|
alert_manager,
|
|
rollback_manager,
|
|
metrics_collector,
|
|
is_running: Arc::new(AtomicBool::new(false)),
|
|
shutdown_tx: Some(shutdown_tx),
|
|
}
|
|
}
|
|
|
|
/// Start monitoring
|
|
pub async fn start(&self) -> MLResult<()> {
|
|
if self.is_running.load(Ordering::Acquire) {
|
|
return Err(MLError::ValidationError {
|
|
message: "Monitor is already running".to_string(),
|
|
});
|
|
}
|
|
|
|
self.is_running.store(true, Ordering::Release);
|
|
|
|
// Start metrics collection task
|
|
let metrics_collector = self.metrics_collector.clone();
|
|
let current_metrics = self.current_metrics.clone();
|
|
let historical_metrics = self.historical_metrics.clone();
|
|
let config = self.config.clone();
|
|
let is_running = self.is_running.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let mut interval = interval(config.collection_interval);
|
|
|
|
while is_running.load(Ordering::Acquire) {
|
|
interval.tick().await;
|
|
|
|
if let Ok(metrics) = metrics_collector.collect_metrics().await {
|
|
// Update current metrics
|
|
{
|
|
let mut current = current_metrics.write().await;
|
|
*current = metrics.clone();
|
|
}
|
|
|
|
// Add to historical metrics
|
|
{
|
|
let mut historical = historical_metrics.lock().await;
|
|
historical.push_back(PerformanceSnapshot {
|
|
timestamp: SystemTime::now(),
|
|
metrics,
|
|
baseline_comparison: None, // Would be calculated
|
|
});
|
|
|
|
// Cleanup old metrics
|
|
let retention_cutoff = SystemTime::now() - config.retention_period;
|
|
while let Some(front) = historical.front() {
|
|
if front.timestamp < retention_cutoff {
|
|
historical.pop_front();
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Start SLA monitoring task
|
|
self.start_sla_monitoring().await?;
|
|
|
|
tracing::info!("Started performance monitoring for model {}", self.model_id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop monitoring
|
|
pub async fn stop(&self) -> MLResult<()> {
|
|
self.is_running.store(false, Ordering::Release);
|
|
|
|
if let Some(ref tx) = self.shutdown_tx {
|
|
let _ = tx.send(true);
|
|
}
|
|
|
|
tracing::info!("Stopped performance monitoring for model {}", self.model_id);
|
|
Ok(())
|
|
}
|
|
|
|
/// Record prediction metrics
|
|
pub async fn record_prediction(
|
|
&self,
|
|
prediction: &ModelPrediction,
|
|
latency: Duration,
|
|
error: Option<&MLError>,
|
|
) -> MLResult<()> {
|
|
self.metrics_collector.record_prediction(prediction, latency, error).await
|
|
}
|
|
|
|
/// Get current performance metrics
|
|
pub async fn get_current_metrics(&self) -> PerformanceMetrics {
|
|
let metrics = self.current_metrics.read().await;
|
|
metrics.clone()
|
|
}
|
|
|
|
/// Get historical metrics
|
|
pub async fn get_historical_metrics(
|
|
&self,
|
|
start_time: SystemTime,
|
|
end_time: SystemTime,
|
|
) -> Vec<PerformanceSnapshot> {
|
|
let historical = self.historical_metrics.lock().await;
|
|
historical
|
|
.iter()
|
|
.filter(|snapshot| snapshot.timestamp >= start_time && snapshot.timestamp <= end_time)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
/// Get SLA violation summary
|
|
pub async fn get_sla_violations(&self) -> SLAViolations {
|
|
let violations = self.sla_violations.read().await;
|
|
violations.clone()
|
|
}
|
|
|
|
/// Check if model meets SLA requirements
|
|
pub async fn check_sla_compliance(&self) -> SLAComplianceReport {
|
|
let metrics = self.get_current_metrics().await;
|
|
let thresholds = &self.config.sla_thresholds;
|
|
|
|
let mut violations = Vec::new();
|
|
let mut compliance_score = 100.0;
|
|
|
|
// Check latency SLA
|
|
if metrics.avg_latency_us > thresholds.max_avg_latency_us as f64 {
|
|
violations.push(SLAViolation {
|
|
metric: "avg_latency".to_string(),
|
|
current_value: metrics.avg_latency_us,
|
|
threshold: thresholds.max_avg_latency_us as f64,
|
|
severity: AlertSeverity::High,
|
|
description: format!(
|
|
"Average latency {} exceeds threshold {}",
|
|
metrics.avg_latency_us, thresholds.max_avg_latency_us
|
|
),
|
|
});
|
|
compliance_score -= 15.0;
|
|
}
|
|
|
|
if metrics.latency_p95 > thresholds.max_p95_latency_us as f64 {
|
|
violations.push(SLAViolation {
|
|
metric: "p95_latency".to_string(),
|
|
current_value: metrics.latency_p95,
|
|
threshold: thresholds.max_p95_latency_us as f64,
|
|
severity: AlertSeverity::High,
|
|
description: format!(
|
|
"P95 latency {} exceeds threshold {}",
|
|
metrics.latency_p95, thresholds.max_p95_latency_us
|
|
),
|
|
});
|
|
compliance_score -= 20.0;
|
|
}
|
|
|
|
// Check error rate SLA
|
|
if metrics.error_rate > thresholds.max_error_rate {
|
|
violations.push(SLAViolation {
|
|
metric: "error_rate".to_string(),
|
|
current_value: metrics.error_rate as f64,
|
|
threshold: thresholds.max_error_rate as f64,
|
|
severity: AlertSeverity::Critical,
|
|
description: format!(
|
|
"Error rate {} exceeds threshold {}",
|
|
metrics.error_rate, thresholds.max_error_rate
|
|
),
|
|
});
|
|
compliance_score -= 25.0;
|
|
}
|
|
|
|
// Check accuracy SLA
|
|
if metrics.avg_accuracy < thresholds.min_accuracy {
|
|
violations.push(SLAViolation {
|
|
metric: "accuracy".to_string(),
|
|
current_value: metrics.avg_accuracy as f64,
|
|
threshold: thresholds.min_accuracy as f64,
|
|
severity: AlertSeverity::High,
|
|
description: format!(
|
|
"Accuracy {} below threshold {}",
|
|
metrics.avg_accuracy, thresholds.min_accuracy
|
|
),
|
|
});
|
|
compliance_score -= 20.0;
|
|
}
|
|
|
|
// Check throughput SLA
|
|
if metrics.throughput_pps < thresholds.min_throughput_pps as f64 {
|
|
violations.push(SLAViolation {
|
|
metric: "throughput".to_string(),
|
|
current_value: metrics.throughput_pps,
|
|
threshold: thresholds.min_throughput_pps as f64,
|
|
severity: AlertSeverity::Medium,
|
|
description: format!(
|
|
"Throughput {} below threshold {}",
|
|
metrics.throughput_pps, thresholds.min_throughput_pps
|
|
),
|
|
});
|
|
compliance_score -= 10.0;
|
|
}
|
|
|
|
// Check memory SLA
|
|
if metrics.memory_usage_mb > thresholds.max_memory_mb as f64 {
|
|
violations.push(SLAViolation {
|
|
metric: "memory_usage".to_string(),
|
|
current_value: metrics.memory_usage_mb,
|
|
threshold: thresholds.max_memory_mb as f64,
|
|
severity: AlertSeverity::Medium,
|
|
description: format!(
|
|
"Memory usage {} exceeds threshold {}",
|
|
metrics.memory_usage_mb, thresholds.max_memory_mb
|
|
),
|
|
});
|
|
compliance_score -= 10.0;
|
|
}
|
|
|
|
SLAComplianceReport {
|
|
model_id: self.model_id.clone(),
|
|
timestamp: SystemTime::now(),
|
|
compliance_score: compliance_score.max(0.0),
|
|
violations,
|
|
is_compliant: violations.is_empty(),
|
|
metrics_snapshot: metrics,
|
|
}
|
|
}
|
|
|
|
/// Start SLA monitoring task
|
|
async fn start_sla_monitoring(&self) -> MLResult<()> {
|
|
let current_metrics = self.current_metrics.clone();
|
|
let sla_violations = self.sla_violations.clone();
|
|
let alert_manager = self.alert_manager.clone();
|
|
let rollback_manager = self.rollback_manager.clone();
|
|
let config = self.config.clone();
|
|
let is_running = self.is_running.clone();
|
|
let model_id = self.model_id.clone();
|
|
|
|
tokio::spawn(async move {
|
|
let mut interval = interval(Duration::from_secs(30)); // Check SLA every 30 seconds
|
|
|
|
while is_running.load(Ordering::Acquire) {
|
|
interval.tick().await;
|
|
|
|
let metrics = {
|
|
let current = current_metrics.read().await;
|
|
current.clone()
|
|
};
|
|
|
|
// Check for SLA violations
|
|
let mut violations_detected = false;
|
|
let mut critical_violations = 0;
|
|
|
|
// Check latency violations
|
|
if metrics.avg_latency_us > config.sla_thresholds.max_avg_latency_us as f64 {
|
|
violations_detected = true;
|
|
let alert = Alert {
|
|
id: Uuid::new_v4(),
|
|
model_id: model_id.clone(),
|
|
alert_type: AlertType::SLAViolation,
|
|
severity: AlertSeverity::High,
|
|
message: format!(
|
|
"Average latency {} exceeds threshold {}",
|
|
metrics.avg_latency_us,
|
|
config.sla_thresholds.max_avg_latency_us
|
|
),
|
|
timestamp: SystemTime::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
if let Err(e) = alert_manager.send_alert(alert).await {
|
|
tracing::error!("Failed to send latency SLA violation alert: {}", e);
|
|
}
|
|
}
|
|
|
|
// Check error rate violations
|
|
if metrics.error_rate > config.sla_thresholds.max_error_rate {
|
|
violations_detected = true;
|
|
critical_violations += 1;
|
|
|
|
let alert = Alert {
|
|
id: Uuid::new_v4(),
|
|
model_id: model_id.clone(),
|
|
alert_type: AlertType::SLAViolation,
|
|
severity: AlertSeverity::Critical,
|
|
message: format!(
|
|
"Error rate {} exceeds threshold {}",
|
|
metrics.error_rate,
|
|
config.sla_thresholds.max_error_rate
|
|
),
|
|
timestamp: SystemTime::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
if let Err(e) = alert_manager.send_alert(alert).await {
|
|
tracing::error!("Failed to send error rate SLA violation alert: {}", e);
|
|
}
|
|
}
|
|
|
|
// Update violation counters
|
|
if violations_detected {
|
|
let mut violations = sla_violations.write().await;
|
|
violations.total_violations += 1;
|
|
violations.last_violation_time = Some(SystemTime::now());
|
|
|
|
if metrics.avg_latency_us > config.sla_thresholds.max_avg_latency_us as f64 {
|
|
violations.latency_violations += 1;
|
|
}
|
|
if metrics.error_rate > config.sla_thresholds.max_error_rate {
|
|
violations.error_rate_violations += 1;
|
|
}
|
|
}
|
|
|
|
// Check if automatic rollback should be triggered
|
|
if critical_violations > 0 && config.rollback.auto_rollback_enabled {
|
|
if let Err(e) = rollback_manager.evaluate_rollback(&metrics, &config.sla_thresholds).await {
|
|
tracing::error!("Failed to evaluate rollback: {}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// SLA compliance report
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SLAComplianceReport {
|
|
/// Model ID
|
|
pub model_id: String,
|
|
/// Report timestamp
|
|
pub timestamp: SystemTime,
|
|
/// Compliance score (0-100)
|
|
pub compliance_score: f32,
|
|
/// SLA violations
|
|
pub violations: Vec<SLAViolation>,
|
|
/// Overall compliance status
|
|
pub is_compliant: bool,
|
|
/// Metrics snapshot
|
|
pub metrics_snapshot: PerformanceMetrics,
|
|
}
|
|
|
|
/// SLA violation details
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SLAViolation {
|
|
/// Metric name
|
|
pub metric: String,
|
|
/// Current value
|
|
pub current_value: f64,
|
|
/// Threshold value
|
|
pub threshold: f64,
|
|
/// Violation severity
|
|
pub severity: AlertSeverity,
|
|
/// Violation description
|
|
pub description: String,
|
|
}
|
|
|
|
/// Alert manager for sending notifications
|
|
pub struct AlertManager {
|
|
/// Alerting configuration
|
|
config: AlertingConfig,
|
|
/// Last alert timestamps (for cooldown)
|
|
last_alerts: Arc<RwLock<HashMap<String, SystemTime>>>,
|
|
}
|
|
|
|
impl AlertManager {
|
|
/// Create new alert manager
|
|
pub fn new(config: AlertingConfig) -> Self {
|
|
Self {
|
|
config,
|
|
last_alerts: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
/// Send alert
|
|
pub async fn send_alert(&self, alert: Alert) -> MLResult<()> {
|
|
// Check cooldown period
|
|
let alert_key = format!("{}_{}", alert.model_id, alert.alert_type.to_string());
|
|
{
|
|
let last_alerts = self.last_alerts.read().await;
|
|
if let Some(last_time) = last_alerts.get(&alert_key) {
|
|
if let Ok(elapsed) = SystemTime::now().duration_since(*last_time) {
|
|
if elapsed < self.config.cooldown_period {
|
|
return Ok(()); // Skip alert due to cooldown
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if alert severity should be sent
|
|
if !self.config.alert_levels.contains(&alert.severity) {
|
|
return Ok(());
|
|
}
|
|
|
|
// Send email alerts
|
|
if self.config.enable_email && !self.config.email_recipients.is_empty() {
|
|
for recipient in &self.config.email_recipients {
|
|
if let Err(e) = self.send_email_alert(recipient, &alert).await {
|
|
tracing::error!("Failed to send email alert to {}: {}", recipient, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send webhook alerts
|
|
if self.config.enable_webhooks && !self.config.webhook_urls.is_empty() {
|
|
for webhook_url in &self.config.webhook_urls {
|
|
if let Err(e) = self.send_webhook_alert(webhook_url, &alert).await {
|
|
tracing::error!("Failed to send webhook alert to {}: {}", webhook_url, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send Slack alerts
|
|
if self.config.enable_slack {
|
|
if let Some(ref slack_url) = self.config.slack_webhook_url {
|
|
if let Err(e) = self.send_slack_alert(slack_url, &alert).await {
|
|
tracing::error!("Failed to send Slack alert: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update last alert time
|
|
{
|
|
let mut last_alerts = self.last_alerts.write().await;
|
|
last_alerts.insert(alert_key, SystemTime::now());
|
|
}
|
|
|
|
tracing::info!("Sent alert for model {}: {}", alert.model_id, alert.message);
|
|
Ok(())
|
|
}
|
|
|
|
/// Send email alert (placeholder implementation)
|
|
async fn send_email_alert(&self, recipient: &str, alert: &Alert) -> MLResult<()> {
|
|
// In a real implementation, this would use an email service
|
|
tracing::info!("Email alert sent to {}: {}", recipient, alert.message);
|
|
Ok(())
|
|
}
|
|
|
|
/// Send webhook alert (placeholder implementation)
|
|
async fn send_webhook_alert(&self, webhook_url: &str, alert: &Alert) -> MLResult<()> {
|
|
// In a real implementation, this would make an HTTP POST request
|
|
tracing::info!("Webhook alert sent to {}: {}", webhook_url, alert.message);
|
|
Ok(())
|
|
}
|
|
|
|
/// Send Slack alert (placeholder implementation)
|
|
async fn send_slack_alert(&self, slack_url: &str, alert: &Alert) -> MLResult<()> {
|
|
// In a real implementation, this would send to Slack webhook
|
|
tracing::info!("Slack alert sent: {}", alert.message);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Alert information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Alert {
|
|
/// Alert ID
|
|
pub id: Uuid,
|
|
/// Model ID
|
|
pub model_id: String,
|
|
/// Alert type
|
|
pub alert_type: AlertType,
|
|
/// Alert severity
|
|
pub severity: AlertSeverity,
|
|
/// Alert message
|
|
pub message: String,
|
|
/// Alert timestamp
|
|
pub timestamp: SystemTime,
|
|
/// Additional metadata
|
|
pub metadata: HashMap<String, String>,
|
|
}
|
|
|
|
/// Alert types
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum AlertType {
|
|
/// SLA violation
|
|
SLAViolation,
|
|
/// Performance degradation
|
|
PerformanceDegradation,
|
|
/// Model failure
|
|
ModelFailure,
|
|
/// High error rate
|
|
HighErrorRate,
|
|
/// Memory leak detected
|
|
MemoryLeak,
|
|
/// Rollback triggered
|
|
RollbackTriggered,
|
|
/// Custom alert
|
|
Custom(String),
|
|
}
|
|
|
|
impl AlertType {
|
|
/// Convert to string representation
|
|
pub fn to_string(&self) -> String {
|
|
match self {
|
|
AlertType::SLAViolation => "sla_violation".to_string(),
|
|
AlertType::PerformanceDegradation => "performance_degradation".to_string(),
|
|
AlertType::ModelFailure => "model_failure".to_string(),
|
|
AlertType::HighErrorRate => "high_error_rate".to_string(),
|
|
AlertType::MemoryLeak => "memory_leak".to_string(),
|
|
AlertType::RollbackTriggered => "rollback_triggered".to_string(),
|
|
AlertType::Custom(name) => format!("custom_{}", name),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rollback manager for automatic rollbacks
|
|
pub struct RollbackManager {
|
|
/// Rollback configuration
|
|
config: RollbackConfig,
|
|
/// Rollback state
|
|
state: Arc<RwLock<RollbackState>>,
|
|
}
|
|
|
|
/// Rollback state tracking
|
|
#[derive(Debug, Clone, Default)]
|
|
struct RollbackState {
|
|
/// Number of consecutive violations
|
|
consecutive_violations: u32,
|
|
/// Last evaluation time
|
|
last_evaluation: Option<SystemTime>,
|
|
/// Rollback in progress
|
|
rollback_in_progress: bool,
|
|
}
|
|
|
|
impl RollbackManager {
|
|
/// Create new rollback manager
|
|
pub fn new(config: RollbackConfig) -> Self {
|
|
Self {
|
|
config,
|
|
state: Arc::new(RwLock::new(RollbackState::default())),
|
|
}
|
|
}
|
|
|
|
/// Evaluate if rollback should be triggered
|
|
pub async fn evaluate_rollback(
|
|
&self,
|
|
metrics: &PerformanceMetrics,
|
|
thresholds: &SLAThresholds,
|
|
) -> MLResult<bool> {
|
|
let mut state = self.state.write().await;
|
|
|
|
// Check if rollback is already in progress
|
|
if state.rollback_in_progress {
|
|
return Ok(false);
|
|
}
|
|
|
|
// Check for SLA violations
|
|
let violations = self.count_violations(metrics, thresholds);
|
|
|
|
if violations > 0 {
|
|
state.consecutive_violations += 1;
|
|
} else {
|
|
state.consecutive_violations = 0;
|
|
}
|
|
|
|
state.last_evaluation = Some(SystemTime::now());
|
|
|
|
// Check if rollback threshold is met
|
|
if state.consecutive_violations >= self.config.violation_threshold {
|
|
state.rollback_in_progress = true;
|
|
tracing::warn!(
|
|
"Triggering automatic rollback for model {} due to {} consecutive violations",
|
|
metrics.model_id,
|
|
state.consecutive_violations
|
|
);
|
|
|
|
// In a real implementation, this would trigger the actual rollback
|
|
// For now, we just log the action
|
|
tokio::spawn(async move {
|
|
// Simulate rollback process
|
|
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
tracing::info!("Rollback completed for model {}", metrics.model_id);
|
|
});
|
|
|
|
return Ok(true);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
|
|
/// Count current SLA violations
|
|
fn count_violations(&self, metrics: &PerformanceMetrics, thresholds: &SLAThresholds) -> u32 {
|
|
let mut violations = 0;
|
|
|
|
if metrics.avg_latency_us > thresholds.max_avg_latency_us as f64 {
|
|
violations += 1;
|
|
}
|
|
if metrics.error_rate > thresholds.max_error_rate {
|
|
violations += 1;
|
|
}
|
|
if metrics.avg_accuracy < thresholds.min_accuracy {
|
|
violations += 1;
|
|
}
|
|
if metrics.throughput_pps < thresholds.min_throughput_pps as f64 {
|
|
violations += 1;
|
|
}
|
|
if metrics.memory_usage_mb > thresholds.max_memory_mb as f64 {
|
|
violations += 1;
|
|
}
|
|
|
|
violations
|
|
}
|
|
}
|
|
|
|
/// Rollback configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct RollbackConfig {
|
|
/// Enable automatic rollback
|
|
pub auto_rollback_enabled: bool,
|
|
/// Number of consecutive violations before rollback
|
|
pub violation_threshold: u32,
|
|
/// Rollback timeout
|
|
pub rollback_timeout: Duration,
|
|
/// Enable rollback confirmation
|
|
pub require_confirmation: bool,
|
|
}
|
|
|
|
impl Default for RollbackConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
auto_rollback_enabled: true,
|
|
violation_threshold: 3,
|
|
rollback_timeout: Duration::from_secs(60),
|
|
require_confirmation: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Metrics collector for gathering performance data
|
|
pub struct MetricsCollector {
|
|
/// Model ID
|
|
model_id: String,
|
|
/// Collection interval
|
|
collection_interval: Duration,
|
|
/// Performance baseline
|
|
baseline: Option<PerformanceBaseline>,
|
|
/// Prediction counter
|
|
prediction_counter: Arc<AtomicU64>,
|
|
/// Error counter
|
|
error_counter: Arc<AtomicU64>,
|
|
/// Latency measurements
|
|
latency_measurements: Arc<Mutex<VecDeque<f64>>>,
|
|
/// Accuracy measurements
|
|
accuracy_measurements: Arc<Mutex<VecDeque<f32>>>,
|
|
}
|
|
|
|
impl MetricsCollector {
|
|
/// Create new metrics collector
|
|
pub fn new(
|
|
model_id: String,
|
|
collection_interval: Duration,
|
|
baseline: Option<PerformanceBaseline>,
|
|
) -> Self {
|
|
Self {
|
|
model_id,
|
|
collection_interval,
|
|
baseline,
|
|
prediction_counter: Arc::new(AtomicU64::new(0)),
|
|
error_counter: Arc::new(AtomicU64::new(0)),
|
|
latency_measurements: Arc::new(Mutex::new(VecDeque::new())),
|
|
accuracy_measurements: Arc::new(Mutex::new(VecDeque::new())),
|
|
}
|
|
}
|
|
|
|
/// Record prediction metrics
|
|
pub async fn record_prediction(
|
|
&self,
|
|
prediction: &ModelPrediction,
|
|
latency: Duration,
|
|
error: Option<&MLError>,
|
|
) -> MLResult<()> {
|
|
// Increment prediction counter
|
|
self.prediction_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
// Record error if present
|
|
if error.is_some() {
|
|
self.error_counter.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
// Record latency
|
|
{
|
|
let mut latencies = self.latency_measurements.lock().await;
|
|
latencies.push_back(latency.as_micros() as f64);
|
|
|
|
// Keep only recent measurements (last 1000)
|
|
while latencies.len() > 1000 {
|
|
latencies.pop_front();
|
|
}
|
|
}
|
|
|
|
// Record accuracy
|
|
{
|
|
let mut accuracies = self.accuracy_measurements.lock().await;
|
|
accuracies.push_back(prediction.confidence);
|
|
|
|
// Keep only recent measurements (last 1000)
|
|
while accuracies.len() > 1000 {
|
|
accuracies.pop_front();
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Collect current metrics
|
|
pub async fn collect_metrics(&self) -> MLResult<PerformanceMetrics> {
|
|
let total_predictions = self.prediction_counter.load(Ordering::Relaxed);
|
|
let total_errors = self.error_counter.load(Ordering::Relaxed);
|
|
|
|
let error_rate = if total_predictions > 0 {
|
|
total_errors as f32 / total_predictions as f32
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Calculate latency percentiles
|
|
let latencies = {
|
|
let latency_measurements = self.latency_measurements.lock().await;
|
|
latency_measurements.iter().cloned().collect::<Vec<_>>()
|
|
};
|
|
|
|
let (avg_latency, p50, p95, p99, p999) = if !latencies.is_empty() {
|
|
let mut sorted_latencies = latencies.clone();
|
|
sorted_latencies.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
|
|
let avg = latencies.iter().sum::<f64>() / latencies.len() as f64;
|
|
let p50 = sorted_latencies[sorted_latencies.len() * 50 / 100];
|
|
let p95 = sorted_latencies[sorted_latencies.len() * 95 / 100];
|
|
let p99 = sorted_latencies[sorted_latencies.len() * 99 / 100];
|
|
let p999 = sorted_latencies[sorted_latencies.len() * 999 / 1000];
|
|
|
|
(avg, p50, p95, p99, p999)
|
|
} else {
|
|
(0.0, 0.0, 0.0, 0.0, 0.0)
|
|
};
|
|
|
|
// Calculate average accuracy
|
|
let avg_accuracy = {
|
|
let accuracy_measurements = self.accuracy_measurements.lock().await;
|
|
if !accuracy_measurements.is_empty() {
|
|
accuracy_measurements.iter().sum::<f32>() / accuracy_measurements.len() as f32
|
|
} else {
|
|
0.0
|
|
}
|
|
};
|
|
|
|
// Calculate throughput (predictions per second)
|
|
let throughput_pps = if !latencies.is_empty() && avg_latency > 0.0 {
|
|
1_000_000.0 / avg_latency // Convert microseconds to seconds
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
Ok(PerformanceMetrics {
|
|
model_id: self.model_id.clone(),
|
|
timestamp: SystemTime::now(),
|
|
total_predictions,
|
|
total_errors,
|
|
error_rate,
|
|
latency_p50: p50,
|
|
latency_p95: p95,
|
|
latency_p99: p99,
|
|
latency_p999: p999,
|
|
avg_latency_us: avg_latency,
|
|
throughput_pps,
|
|
memory_usage_mb: self.get_memory_usage().await,
|
|
cpu_utilization: self.get_cpu_utilization().await,
|
|
avg_accuracy,
|
|
custom_metrics: HashMap::new(),
|
|
})
|
|
}
|
|
|
|
/// Get current memory usage (requires system integration)
|
|
async fn get_memory_usage(&self) -> f64 {
|
|
// TODO: Implement using sysinfo crate to get actual process memory
|
|
// For now, return 0.0 to indicate unimplemented
|
|
warn!("Memory usage tracking not implemented - returning 0.0");
|
|
0.0
|
|
}
|
|
|
|
/// Get current CPU utilization (requires system integration)
|
|
async fn get_cpu_utilization(&self) -> f32 {
|
|
// TODO: Implement using sysinfo crate to get actual CPU usage
|
|
// For now, return 0.0 to indicate unimplemented
|
|
warn!("CPU utilization tracking not implemented - returning 0.0");
|
|
0.0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::model_factory;
|
|
|
|
#[tokio::test]
|
|
async fn test_performance_monitor_creation() {
|
|
let config = MonitoringConfig::default();
|
|
let monitor = PerformanceMonitor::new(
|
|
"test_model".to_string(),
|
|
ModelType::DQN,
|
|
ModelVersion::new(1, 0, 0),
|
|
config,
|
|
None,
|
|
).await;
|
|
|
|
assert_eq!(monitor.model_id, "test_model");
|
|
assert_eq!(monitor.model_type, ModelType::DQN);
|
|
assert!(!monitor.is_running.load(Ordering::Acquire));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_metrics_collector() {
|
|
let collector = MetricsCollector::new(
|
|
"test_model".to_string(),
|
|
Duration::from_secs(10),
|
|
None,
|
|
);
|
|
|
|
let prediction = ModelPrediction::new(
|
|
"test_model".to_string(),
|
|
0.8,
|
|
0.9,
|
|
);
|
|
|
|
let result = collector.record_prediction(&prediction, Duration::from_micros(100), None).await;
|
|
assert!(result.is_ok());
|
|
|
|
let metrics = collector.collect_metrics().await.unwrap();
|
|
assert_eq!(metrics.total_predictions, 1);
|
|
assert_eq!(metrics.total_errors, 0);
|
|
assert_eq!(metrics.error_rate, 0.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_sla_compliance_check() {
|
|
let config = MonitoringConfig::default();
|
|
let monitor = PerformanceMonitor::new(
|
|
"test_model".to_string(),
|
|
ModelType::DQN,
|
|
ModelVersion::new(1, 0, 0),
|
|
config,
|
|
None,
|
|
).await;
|
|
|
|
let compliance_report = monitor.check_sla_compliance().await;
|
|
assert!(compliance_report.is_compliant);
|
|
assert_eq!(compliance_report.violations.len(), 0);
|
|
assert_eq!(compliance_report.compliance_score, 100.0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_alert_manager() {
|
|
let config = AlertingConfig::default();
|
|
let alert_manager = AlertManager::new(config);
|
|
|
|
let alert = Alert {
|
|
id: Uuid::new_v4(),
|
|
model_id: "test_model".to_string(),
|
|
alert_type: AlertType::SLAViolation,
|
|
severity: AlertSeverity::High,
|
|
message: "Test alert".to_string(),
|
|
timestamp: SystemTime::now(),
|
|
metadata: HashMap::new(),
|
|
};
|
|
|
|
let result = alert_manager.send_alert(alert).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_rollback_manager() {
|
|
let config = RollbackConfig::default();
|
|
let rollback_manager = RollbackManager::new(config);
|
|
|
|
let metrics = PerformanceMetrics {
|
|
model_id: "test_model".to_string(),
|
|
error_rate: 0.1, // High error rate
|
|
avg_latency_us: 1000.0, // High latency
|
|
..Default::default()
|
|
};
|
|
|
|
let thresholds = SLAThresholds {
|
|
max_error_rate: 0.01,
|
|
max_avg_latency_us: 100,
|
|
..Default::default()
|
|
};
|
|
|
|
let result = rollback_manager.evaluate_rollback(&metrics, &thresholds).await;
|
|
assert!(result.is_ok());
|
|
}
|
|
}
|