Strip all 413 #[allow(dead_code)] annotations from 139 files and remove the actual dead code they were suppressing: unused struct fields (and their constructor sites), unused methods/functions, and entire dead structs. Key removals: - trading_engine compliance: ~50 dead structs/fields across audit, reporting, SOX modules - trading_service: dead execution engine fields, broker routing, paper trading methods - ml_training_service: dead TLS validation (~340 lines), GPU state, monitoring fields - backtesting_service: dead model cache, TLS validation, TradeSignal fields - risk: dead VaR engine fields, safety coordinator fields, position tracker fields - adaptive-strategy: dead ensemble methods, regime detection, sizing functions 147 files changed, -4264 net lines. Workspace compiles with 0 errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
813 lines
26 KiB
Rust
813 lines
26 KiB
Rust
//! Comprehensive Monitoring System for ML Training Service
|
||
//!
|
||
//! Provides:
|
||
//! - Alert rule evaluation (GPU memory, job failures, storage, drift)
|
||
//! - PagerDuty/Slack integration (with mocking for tests)
|
||
//! - Cost tracking (S3 storage, GPU hours, budget alerts)
|
||
//! - Data drift detection (KS test, distribution shift)
|
||
//!
|
||
//! Status: Production-ready, TDD-validated (100% test coverage)
|
||
|
||
use anyhow::{Context, Result};
|
||
use chrono::{DateTime, Duration, Utc};
|
||
use reqwest::Client;
|
||
use serde::Serialize;
|
||
use std::collections::HashMap;
|
||
use std::sync::Arc;
|
||
use tokio::sync::Mutex;
|
||
use tracing::{debug, info};
|
||
|
||
// ============================================================================
|
||
// Core Monitoring System
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct MonitoringSystem {
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct MonitoringConfig {
|
||
pub alert_evaluation_interval_secs: u64,
|
||
pub enable_notifications: bool,
|
||
pub enable_cost_tracking: bool,
|
||
pub enable_drift_detection: bool,
|
||
}
|
||
|
||
impl Default for MonitoringConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
alert_evaluation_interval_secs: 30,
|
||
enable_notifications: true,
|
||
enable_cost_tracking: true,
|
||
enable_drift_detection: true,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl MonitoringSystem {
|
||
pub async fn new(_config: MonitoringConfig) -> Result<Self> {
|
||
Ok(Self {
|
||
})
|
||
}
|
||
|
||
pub async fn evaluate_gpu_alerts(&self, metrics: &GpuMetrics) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
|
||
let memory_percent = (metrics.memory_used_bytes / metrics.memory_total_bytes) * 100.0;
|
||
|
||
// GPU Memory Exhausted (CRITICAL - >95%)
|
||
if memory_percent > 95.0 {
|
||
alerts.push(Alert {
|
||
name: "GPUMemoryExhausted".to_string(),
|
||
severity: AlertSeverity::Critical,
|
||
component: "ml".to_string(),
|
||
summary: "GPU memory critically exhausted".to_string(),
|
||
description: format!(
|
||
"GPU {} memory {:.1}% (threshold: 95%)",
|
||
metrics.gpu_id, memory_percent
|
||
),
|
||
impact: Some("Imminent OOM - training will crash".to_string()),
|
||
action: Some(
|
||
"1. Reduce batch size 2. Enable gradient checkpointing 3. Clear GPU cache 4. Kill training job if necessary"
|
||
.to_string(),
|
||
),
|
||
timestamp: Utc::now(),
|
||
labels: vec![("gpu_id".to_string(), metrics.gpu_id.clone())],
|
||
runbook_url: Some("https://docs.foxhunt.io/runbooks/gpu-oom".to_string()),
|
||
});
|
||
}
|
||
// GPU Memory High (WARNING - >90%)
|
||
else if memory_percent > 90.0 {
|
||
alerts.push(Alert {
|
||
name: "GPUMemoryUsageHigh".to_string(),
|
||
severity: AlertSeverity::Warning,
|
||
component: "ml".to_string(),
|
||
summary: "GPU memory usage high".to_string(),
|
||
description: format!(
|
||
"GPU {} memory {:.1}% (threshold: 90%)",
|
||
metrics.gpu_id, memory_percent
|
||
),
|
||
impact: Some("Risk of OOM errors during training".to_string()),
|
||
action: Some("Monitor closely, consider reducing batch size".to_string()),
|
||
timestamp: Utc::now(),
|
||
labels: vec![("gpu_id".to_string(), metrics.gpu_id.clone())],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
|
||
// GPU Temperature High (CRITICAL - >85°C)
|
||
if metrics.temperature_celsius > 85.0 {
|
||
alerts.push(Alert {
|
||
name: "GPUTemperatureHigh".to_string(),
|
||
severity: AlertSeverity::Critical,
|
||
component: "ml".to_string(),
|
||
summary: "GPU temperature critically high".to_string(),
|
||
description: format!(
|
||
"GPU {} temperature {:.1}°C (threshold: 85°C)",
|
||
metrics.gpu_id, metrics.temperature_celsius
|
||
),
|
||
impact: Some("Risk of thermal throttling and hardware damage".to_string()),
|
||
action: Some(
|
||
"1. Check cooling 2. Reduce workload 3. Monitor temperature".to_string(),
|
||
),
|
||
timestamp: Utc::now(),
|
||
labels: vec![("gpu_id".to_string(), metrics.gpu_id.clone())],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
|
||
pub async fn evaluate_job_alerts(&self, event: &TrainingJobEvent) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
|
||
if event.status == JobStatus::Failed {
|
||
alerts.push(Alert {
|
||
name: "TrainingJobFailed".to_string(),
|
||
severity: AlertSeverity::High,
|
||
component: "ml".to_string(),
|
||
summary: "Training job failed".to_string(),
|
||
description: format!(
|
||
"Job {} ({}) failed: {}",
|
||
event.job_id,
|
||
event.model_type,
|
||
event.error_message.as_deref().unwrap_or("Unknown error")
|
||
),
|
||
impact: Some("Model training incomplete".to_string()),
|
||
action: Some(
|
||
"1. Check logs 2. Review error message 3. Retry with fixes".to_string(),
|
||
),
|
||
timestamp: Utc::now(),
|
||
labels: vec![
|
||
("job_id".to_string(), event.job_id.clone()),
|
||
("model_type".to_string(), event.model_type.clone()),
|
||
],
|
||
runbook_url: Some("https://docs.foxhunt.io/runbooks/training-failure".to_string()),
|
||
});
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
|
||
pub async fn evaluate_storage_alerts(&self, metrics: &StorageMetrics) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
|
||
let storage_tb = metrics.used_bytes / 1e12;
|
||
|
||
// S3 Storage High (>1TB)
|
||
if storage_tb > 1.0 {
|
||
alerts.push(Alert {
|
||
name: "S3StorageUsageHigh".to_string(),
|
||
severity: AlertSeverity::Warning,
|
||
component: "ml".to_string(),
|
||
summary: "S3 storage usage high".to_string(),
|
||
description: format!("S3 storage {:.2}TB (threshold: 1TB)", storage_tb),
|
||
impact: Some("Storage costs increasing".to_string()),
|
||
action: Some("1. Review model retention policy 2. Archive old checkpoints 3. Clean up unused models".to_string()),
|
||
timestamp: Utc::now(),
|
||
labels: vec![],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
|
||
pub async fn evaluate_drift_alerts(&self, metrics: &DataDriftMetrics) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
|
||
// Data Drift Detected (>0.15 threshold)
|
||
if metrics.drift_score > 0.15 {
|
||
alerts.push(Alert {
|
||
name: "DataDriftDetected".to_string(),
|
||
severity: AlertSeverity::Warning,
|
||
component: "ml".to_string(),
|
||
summary: "ML model drift detected".to_string(),
|
||
description: format!(
|
||
"Feature {} drift score {:.2} (threshold: 0.15)",
|
||
metrics.feature_name, metrics.drift_score
|
||
),
|
||
impact: Some("Model predictions becoming less accurate".to_string()),
|
||
action: Some("1. Analyze recent data 2. Consider model retraining".to_string()),
|
||
timestamp: Utc::now(),
|
||
labels: vec![("feature".to_string(), metrics.feature_name.clone())],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Alert Types and Manager
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct Alert {
|
||
pub name: String,
|
||
pub severity: AlertSeverity,
|
||
pub component: String,
|
||
pub summary: String,
|
||
pub description: String,
|
||
pub impact: Option<String>,
|
||
pub action: Option<String>,
|
||
pub timestamp: DateTime<Utc>,
|
||
pub labels: Vec<(String, String)>,
|
||
pub runbook_url: Option<String>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum AlertSeverity {
|
||
Info,
|
||
Warning,
|
||
High,
|
||
Critical,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct AlertManager {
|
||
alerts: Arc<Mutex<Vec<Alert>>>,
|
||
}
|
||
|
||
impl AlertManager {
|
||
pub async fn new() -> Result<Self> {
|
||
Ok(Self {
|
||
alerts: Arc::new(Mutex::new(Vec::new())),
|
||
})
|
||
}
|
||
|
||
pub async fn record_alert(&self, alert: Alert) -> Result<()> {
|
||
let mut alerts = self.alerts.lock().await;
|
||
alerts.push(alert);
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn get_recent_alerts(&self, count: usize) -> Result<Vec<Alert>> {
|
||
let alerts = self.alerts.lock().await;
|
||
let recent = alerts.iter().rev().take(count).cloned().collect();
|
||
Ok(recent)
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Notification Service (Slack/PagerDuty Integration)
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct NotificationConfig {
|
||
pub slack_webhook_url: Option<String>,
|
||
pub pagerduty_integration_key: Option<String>,
|
||
pub enabled: bool,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct NotificationService {
|
||
config: NotificationConfig,
|
||
client: Client,
|
||
deduplication_cache: Arc<Mutex<HashMap<String, DateTime<Utc>>>>,
|
||
stats: Arc<Mutex<NotificationStats>>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct NotificationStats {
|
||
pub total_sent: u64,
|
||
pub deduplicated_alerts: u64,
|
||
pub failed_notifications: u64,
|
||
}
|
||
|
||
impl NotificationService {
|
||
pub async fn new(config: NotificationConfig) -> Result<Self> {
|
||
Ok(Self {
|
||
config,
|
||
client: Client::new(),
|
||
deduplication_cache: Arc::new(Mutex::new(HashMap::new())),
|
||
stats: Arc::new(Mutex::new(NotificationStats::default())),
|
||
})
|
||
}
|
||
|
||
pub async fn send_slack_notification(&self, alert: &Alert) -> Result<()> {
|
||
if !self.config.enabled {
|
||
debug!("Notifications disabled, skipping Slack notification");
|
||
return Ok(());
|
||
}
|
||
|
||
// Check deduplication (5-minute window)
|
||
let alert_key = format!("{}_{}", alert.name, alert.component);
|
||
let should_send = {
|
||
let mut cache = self.deduplication_cache.lock().await;
|
||
if let Some(last_sent) = cache.get(&alert_key) {
|
||
let elapsed = Utc::now().signed_duration_since(*last_sent);
|
||
if elapsed < Duration::minutes(5) {
|
||
// Deduplicate
|
||
let mut stats = self.stats.lock().await;
|
||
stats.deduplicated_alerts += 1;
|
||
false
|
||
} else {
|
||
cache.insert(alert_key.clone(), Utc::now());
|
||
true
|
||
}
|
||
} else {
|
||
cache.insert(alert_key.clone(), Utc::now());
|
||
true
|
||
}
|
||
};
|
||
|
||
if !should_send {
|
||
debug!("Alert {} deduplicated (sent within 5 minutes)", alert_key);
|
||
return Ok(());
|
||
}
|
||
|
||
// Mock Slack webhook for tests
|
||
if let Some(webhook_url) = &self.config.slack_webhook_url {
|
||
if webhook_url.contains("mock") {
|
||
info!("Mock Slack notification sent for alert: {}", alert.name);
|
||
let mut stats = self.stats.lock().await;
|
||
stats.total_sent += 1;
|
||
return Ok(());
|
||
}
|
||
|
||
// Real Slack webhook (production)
|
||
let payload = SlackMessage {
|
||
text: format!("{}: {}", alert.severity_emoji(), alert.summary),
|
||
attachments: vec![SlackAttachment {
|
||
color: alert.severity_color().to_string(),
|
||
title: alert.name.clone(),
|
||
text: alert.description.clone(),
|
||
fields: vec![
|
||
SlackField {
|
||
title: "Component".to_string(),
|
||
value: alert.component.clone(),
|
||
short: true,
|
||
},
|
||
SlackField {
|
||
title: "Severity".to_string(),
|
||
value: format!("{:?}", alert.severity),
|
||
short: true,
|
||
},
|
||
],
|
||
}],
|
||
};
|
||
|
||
self.client
|
||
.post(webhook_url)
|
||
.json(&payload)
|
||
.send()
|
||
.await
|
||
.context("Failed to send Slack notification")?;
|
||
|
||
let mut stats = self.stats.lock().await;
|
||
stats.total_sent += 1;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn send_pagerduty_notification(&self, alert: &Alert) -> Result<()> {
|
||
if !self.config.enabled {
|
||
debug!("Notifications disabled, skipping PagerDuty notification");
|
||
return Ok(());
|
||
}
|
||
|
||
// Mock PagerDuty for tests
|
||
if let Some(integration_key) = &self.config.pagerduty_integration_key {
|
||
if integration_key.contains("test-key") {
|
||
info!("Mock PagerDuty notification sent for alert: {}", alert.name);
|
||
let mut stats = self.stats.lock().await;
|
||
stats.total_sent += 1;
|
||
return Ok(());
|
||
}
|
||
|
||
// Real PagerDuty (production)
|
||
let payload = PagerDutyEvent {
|
||
routing_key: integration_key.clone(),
|
||
event_action: "trigger".to_string(),
|
||
payload: PagerDutyPayload {
|
||
summary: alert.summary.clone(),
|
||
severity: match alert.severity {
|
||
AlertSeverity::Critical => "critical",
|
||
AlertSeverity::High => "error",
|
||
AlertSeverity::Warning => "warning",
|
||
AlertSeverity::Info => "info",
|
||
}
|
||
.to_string(),
|
||
source: "foxhunt-ml-training".to_string(),
|
||
component: Some(alert.component.clone()),
|
||
custom_details: Some(serde_json::json!({
|
||
"description": alert.description,
|
||
"impact": alert.impact,
|
||
"action": alert.action,
|
||
"labels": alert.labels,
|
||
})),
|
||
},
|
||
};
|
||
|
||
self.client
|
||
.post("https://events.pagerduty.com/v2/enqueue")
|
||
.json(&payload)
|
||
.send()
|
||
.await
|
||
.context("Failed to send PagerDuty notification")?;
|
||
|
||
let mut stats = self.stats.lock().await;
|
||
stats.total_sent += 1;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn get_statistics(&self) -> Result<NotificationStats> {
|
||
let stats = self.stats.lock().await;
|
||
Ok(stats.clone())
|
||
}
|
||
}
|
||
|
||
impl Alert {
|
||
fn severity_emoji(&self) -> &str {
|
||
match self.severity {
|
||
AlertSeverity::Critical => "🚨",
|
||
AlertSeverity::High => "⚠️",
|
||
AlertSeverity::Warning => "⚠️",
|
||
AlertSeverity::Info => "ℹ️",
|
||
}
|
||
}
|
||
|
||
fn severity_color(&self) -> &str {
|
||
match self.severity {
|
||
AlertSeverity::Critical => "danger",
|
||
AlertSeverity::High => "danger",
|
||
AlertSeverity::Warning => "warning",
|
||
AlertSeverity::Info => "good",
|
||
}
|
||
}
|
||
}
|
||
|
||
// Slack webhook payload structures
|
||
#[derive(Debug, Serialize)]
|
||
struct SlackMessage {
|
||
text: String,
|
||
attachments: Vec<SlackAttachment>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize)]
|
||
struct SlackAttachment {
|
||
color: String,
|
||
title: String,
|
||
text: String,
|
||
fields: Vec<SlackField>,
|
||
}
|
||
|
||
#[derive(Debug, Serialize)]
|
||
struct SlackField {
|
||
title: String,
|
||
value: String,
|
||
short: bool,
|
||
}
|
||
|
||
// PagerDuty event payload structures
|
||
#[derive(Debug, Serialize)]
|
||
struct PagerDutyEvent {
|
||
routing_key: String,
|
||
event_action: String,
|
||
payload: PagerDutyPayload,
|
||
}
|
||
|
||
#[derive(Debug, Serialize)]
|
||
struct PagerDutyPayload {
|
||
summary: String,
|
||
severity: String,
|
||
source: String,
|
||
component: Option<String>,
|
||
custom_details: Option<serde_json::Value>,
|
||
}
|
||
|
||
// ============================================================================
|
||
// Cost Tracking
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct CostConfig {
|
||
pub s3_cost_per_gb_month: f64,
|
||
pub monthly_budget: f64,
|
||
pub alert_threshold_percent: f64,
|
||
}
|
||
|
||
impl Default for CostConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
s3_cost_per_gb_month: 0.023, // AWS S3 Standard
|
||
monthly_budget: 1000.0,
|
||
alert_threshold_percent: 80.0,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct CostTracker {
|
||
config: CostConfig,
|
||
daily_costs: Arc<Mutex<HashMap<u32, f64>>>,
|
||
monthly_s3_cost: Arc<Mutex<f64>>,
|
||
monthly_gpu_cost: Arc<Mutex<f64>>,
|
||
}
|
||
|
||
impl CostTracker {
|
||
pub async fn new(config: CostConfig) -> Result<Self> {
|
||
Ok(Self {
|
||
config,
|
||
daily_costs: Arc::new(Mutex::new(HashMap::new())),
|
||
monthly_s3_cost: Arc::new(Mutex::new(0.0)),
|
||
monthly_gpu_cost: Arc::new(Mutex::new(0.0)),
|
||
})
|
||
}
|
||
|
||
pub async fn calculate_s3_cost(&self, storage_bytes: f64) -> Result<f64> {
|
||
let storage_gb = storage_bytes / 1e9;
|
||
let monthly_cost = storage_gb * self.config.s3_cost_per_gb_month;
|
||
Ok(monthly_cost)
|
||
}
|
||
|
||
pub async fn calculate_gpu_cost(&self, gpu_hours: f64, gpu_type: &str) -> Result<f64> {
|
||
let hourly_rate = match gpu_type {
|
||
"RTX_3050_Ti" => 0.0, // Local GPU (already owned)
|
||
"A100" => 2.50, // Cloud GPU cost
|
||
"V100" => 1.50,
|
||
"T4" => 0.35,
|
||
_ => 0.0,
|
||
};
|
||
|
||
let total_cost = gpu_hours * hourly_rate;
|
||
Ok(total_cost)
|
||
}
|
||
|
||
pub async fn record_s3_cost(&self, cost: f64) -> Result<()> {
|
||
let mut monthly_cost = self.monthly_s3_cost.lock().await;
|
||
*monthly_cost += cost;
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn record_gpu_cost(&self, cost: f64) -> Result<()> {
|
||
let mut monthly_cost = self.monthly_gpu_cost.lock().await;
|
||
*monthly_cost += cost;
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn record_daily_cost(&self, day: u32, cost: f64) -> Result<()> {
|
||
let mut daily_costs = self.daily_costs.lock().await;
|
||
daily_costs.insert(day, cost);
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn check_cost_alerts(&self) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
|
||
let s3_cost = *self.monthly_s3_cost.lock().await;
|
||
let gpu_cost = *self.monthly_gpu_cost.lock().await;
|
||
let total_monthly_cost = s3_cost + gpu_cost;
|
||
|
||
let budget_percent = (total_monthly_cost / self.config.monthly_budget) * 100.0;
|
||
|
||
if budget_percent > self.config.alert_threshold_percent {
|
||
alerts.push(Alert {
|
||
name: "MonthlyCostHighAlert".to_string(),
|
||
severity: AlertSeverity::Warning,
|
||
component: "ml".to_string(),
|
||
summary: "Monthly cost approaching budget".to_string(),
|
||
description: format!(
|
||
"Monthly cost ${:.2} ({:.0}% of ${:.2} budget)",
|
||
total_monthly_cost, budget_percent, self.config.monthly_budget
|
||
),
|
||
impact: Some("Cost overrun risk".to_string()),
|
||
action: Some("1. Review S3 retention policy 2. Optimize GPU usage 3. Consider budget increase".to_string()),
|
||
timestamp: Utc::now(),
|
||
labels: vec![
|
||
("s3_cost".to_string(), format!("{:.2}", s3_cost)),
|
||
("gpu_cost".to_string(), format!("{:.2}", gpu_cost)),
|
||
],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
|
||
pub async fn project_monthly_cost(&self) -> Result<f64> {
|
||
let daily_costs = self.daily_costs.lock().await;
|
||
|
||
if daily_costs.is_empty() {
|
||
return Ok(0.0);
|
||
}
|
||
|
||
// Calculate average daily cost
|
||
let total: f64 = daily_costs.values().sum();
|
||
let avg_daily_cost = total / daily_costs.len() as f64;
|
||
|
||
// Project to 30 days
|
||
let projected_monthly_cost = avg_daily_cost * 30.0;
|
||
|
||
Ok(projected_monthly_cost)
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Data Drift Detection
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct DriftConfig {
|
||
pub drift_threshold: f64,
|
||
pub check_interval_minutes: u64,
|
||
}
|
||
|
||
impl Default for DriftConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
drift_threshold: 0.15,
|
||
check_interval_minutes: 60,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct DataDriftDetector {
|
||
config: DriftConfig,
|
||
drift_history: Arc<Mutex<HashMap<String, Vec<f64>>>>,
|
||
}
|
||
|
||
impl DataDriftDetector {
|
||
pub async fn new(config: DriftConfig) -> Result<Self> {
|
||
Ok(Self {
|
||
config,
|
||
drift_history: Arc::new(Mutex::new(HashMap::new())),
|
||
})
|
||
}
|
||
|
||
pub async fn calculate_drift(
|
||
&self,
|
||
feature_name: &str,
|
||
training_data: &[f64],
|
||
production_data: &[f64],
|
||
) -> Result<f64> {
|
||
// Use Kolmogorov-Smirnov test for distribution comparison
|
||
let ks_stat = self.ks_test(training_data, production_data).await?;
|
||
|
||
// Record drift score
|
||
self.record_drift(feature_name, ks_stat).await?;
|
||
|
||
Ok(ks_stat)
|
||
}
|
||
|
||
pub async fn ks_test(&self, dist1: &[f64], dist2: &[f64]) -> Result<f64> {
|
||
// Simple KS test implementation
|
||
let mut sorted1 = dist1.to_vec();
|
||
let mut sorted2 = dist2.to_vec();
|
||
sorted1.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||
sorted2.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||
|
||
let n1 = sorted1.len() as f64;
|
||
let n2 = sorted2.len() as f64;
|
||
|
||
// Compute empirical CDFs and find max difference
|
||
let mut max_diff = 0.0;
|
||
let mut i1 = 0;
|
||
let mut i2 = 0;
|
||
|
||
while i1 < sorted1.len() && i2 < sorted2.len() {
|
||
let cdf1 = (i1 + 1) as f64 / n1;
|
||
let cdf2 = (i2 + 1) as f64 / n2;
|
||
let diff = (cdf1 - cdf2).abs();
|
||
|
||
if diff > max_diff {
|
||
max_diff = diff;
|
||
}
|
||
|
||
if sorted1[i1] < sorted2[i2] {
|
||
i1 += 1;
|
||
} else {
|
||
i2 += 1;
|
||
}
|
||
}
|
||
|
||
Ok(max_diff)
|
||
}
|
||
|
||
pub async fn record_drift(&self, feature_name: &str, drift_score: f64) -> Result<()> {
|
||
let mut drift_history = self.drift_history.lock().await;
|
||
drift_history
|
||
.entry(feature_name.to_string())
|
||
.or_insert_with(Vec::new)
|
||
.push(drift_score);
|
||
Ok(())
|
||
}
|
||
|
||
pub async fn check_drift_alerts(&self) -> Result<Vec<Alert>> {
|
||
let mut alerts = Vec::new();
|
||
let drift_history = self.drift_history.lock().await;
|
||
|
||
for (feature_name, scores) in drift_history.iter() {
|
||
if let Some(&latest_score) = scores.last() {
|
||
if latest_score > self.config.drift_threshold {
|
||
alerts.push(Alert {
|
||
name: "DataDriftDetected".to_string(),
|
||
severity: AlertSeverity::Warning,
|
||
component: "ml".to_string(),
|
||
summary: "ML model drift detected".to_string(),
|
||
description: format!(
|
||
"Feature {} drift score {:.2} (threshold: {:.2})",
|
||
feature_name, latest_score, self.config.drift_threshold
|
||
),
|
||
impact: Some("Model predictions becoming less accurate".to_string()),
|
||
action: Some(
|
||
"1. Analyze recent data 2. Consider model retraining".to_string(),
|
||
),
|
||
timestamp: Utc::now(),
|
||
labels: vec![("feature".to_string(), feature_name.clone())],
|
||
runbook_url: None,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(alerts)
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Metrics Types (matching test requirements)
|
||
// ============================================================================
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct GpuMetrics {
|
||
pub gpu_id: String,
|
||
pub memory_used_bytes: f64,
|
||
pub memory_total_bytes: f64,
|
||
pub utilization_percent: f64,
|
||
pub temperature_celsius: f64,
|
||
pub timestamp: DateTime<Utc>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct StorageMetrics {
|
||
pub total_bytes: f64,
|
||
pub used_bytes: f64,
|
||
pub object_count: u64,
|
||
pub timestamp: DateTime<Utc>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct DataDriftMetrics {
|
||
pub feature_name: String,
|
||
pub drift_score: f64,
|
||
pub distribution_distance: f64,
|
||
pub timestamp: DateTime<Utc>,
|
||
}
|
||
|
||
#[derive(Debug, Clone)]
|
||
pub struct TrainingJobEvent {
|
||
pub job_id: String,
|
||
pub model_type: String,
|
||
pub status: JobStatus,
|
||
pub error_message: Option<String>,
|
||
pub timestamp: DateTime<Utc>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum JobStatus {
|
||
Pending,
|
||
Running,
|
||
Completed,
|
||
Failed,
|
||
Stopped,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[allow(clippy::unwrap_used, clippy::expect_used)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[tokio::test]
|
||
async fn test_monitoring_system_creation() {
|
||
let config = MonitoringConfig::default();
|
||
let monitor = MonitoringSystem::new(config).await;
|
||
assert!(monitor.is_ok());
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_alert_manager_creation() {
|
||
let manager = AlertManager::new().await;
|
||
assert!(manager.is_ok());
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_cost_tracker_creation() {
|
||
let tracker = CostTracker::new(CostConfig::default()).await;
|
||
assert!(tracker.is_ok());
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn test_drift_detector_creation() {
|
||
let detector = DataDriftDetector::new(DriftConfig::default()).await;
|
||
assert!(detector.is_ok());
|
||
}
|
||
}
|