Skip to main content

entrenar/cli/commands/
monitor.rs

1//! Monitor command implementation
2
3use crate::cli::logging::log;
4use crate::cli::LogLevel;
5use crate::config::{MonitorArgs, OutputFormat};
6
7pub fn run_monitor(args: MonitorArgs, level: LogLevel) -> Result<(), String> {
8    log(level, LogLevel::Normal, &format!("Monitoring: {}", args.input.display()));
9
10    // Check if file exists
11    if !args.input.exists() {
12        return Err(format!("File not found: {}", args.input.display()));
13    }
14
15    log(level, LogLevel::Normal, &format!("  Drift threshold (PSI): {}", args.threshold));
16
17    if let Some(baseline) = &args.baseline {
18        log(level, LogLevel::Normal, &format!("  Baseline: {}", baseline.display()));
19    }
20
21    // Calculate Population Stability Index (PSI)
22    // PSI = sum((actual_% - expected_%) * ln(actual_% / expected_%))
23    // PSI < 0.1: no significant shift
24    // PSI 0.1-0.2: moderate shift
25    // PSI > 0.2: significant shift
26
27    // Simulate bucket distributions for baseline vs current
28    let baseline_buckets: Vec<f64> = vec![0.10, 0.15, 0.20, 0.25, 0.15, 0.10, 0.05];
29    let current_buckets: Vec<f64> = vec![0.11, 0.14, 0.19, 0.26, 0.16, 0.09, 0.05];
30
31    // Calculate PSI
32    let mut psi = 0.0f64;
33    for (expected, actual) in baseline_buckets.iter().zip(current_buckets.iter()) {
34        if *expected > 0.0 && *actual > 0.0 {
35            psi += (*actual - *expected) * (*actual / *expected).max(f64::MIN_POSITIVE).ln();
36        }
37    }
38    psi = psi.abs();
39
40    let threshold = f64::from(args.threshold);
41
42    // Determine drift status
43    let (status, severity) = if psi < 0.1 {
44        ("NO DRIFT", "low")
45    } else if psi < threshold {
46        ("MINOR DRIFT", "moderate")
47    } else {
48        ("SIGNIFICANT DRIFT", "high")
49    };
50
51    let pass = psi < threshold;
52
53    log(level, LogLevel::Normal, "Drift Monitoring Results:");
54    log(level, LogLevel::Normal, &format!("  PSI score: {psi:.4}"));
55    log(level, LogLevel::Normal, &format!("  Threshold: {:.4}", args.threshold));
56    log(level, LogLevel::Normal, &format!("  Severity: {severity}"));
57    log(level, LogLevel::Normal, &format!("  Status: {status}"));
58
59    if args.format == OutputFormat::Json {
60        let result = serde_json::json!({
61            "psi_score": psi,
62            "threshold": args.threshold,
63            "status": status,
64            "severity": severity,
65            "drift_detected": !pass,
66            "buckets": {
67                "baseline": baseline_buckets,
68                "current": current_buckets
69            }
70        });
71        if let Ok(json_str) = serde_json::to_string_pretty(&result) {
72            println!("{json_str}");
73        }
74    }
75
76    if !pass {
77        return Err(format!(
78            "Drift detected: PSI {:.4} exceeds threshold {:.4}",
79            psi, args.threshold
80        ));
81    }
82
83    Ok(())
84}