Skip to main content

llm_optimizer_processor/actuator/
health.rs

1//! Health monitoring for deployments
2//!
3//! This module provides comprehensive health monitoring capabilities for canary deployments,
4//! including statistical analysis, metric tracking, and rollback decision logic.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use std::collections::HashMap;
9use std::sync::{Arc, RwLock};
10
11use super::error::{ActuatorError, ActuatorResult};
12use super::traits::HealthMonitor as HealthMonitorTrait;
13use super::types::{
14    DeploymentMetrics, DeploymentStatus, HealthCheckResult, HealthStatus, StatisticalAnalysis,
15    SuccessCriteria,
16};
17use crate::decision::SystemMetrics;
18
19/// Default number of consecutive failures before rollback
20const DEFAULT_MAX_CONSECUTIVE_FAILURES: usize = 3;
21
22/// Default minimum observation period in seconds
23const DEFAULT_MIN_OBSERVATION_SECS: u64 = 60;
24
25/// Health monitor configuration
26#[derive(Debug, Clone)]
27pub struct HealthMonitorConfig {
28    /// Success criteria for deployments
29    pub success_criteria: SuccessCriteria,
30
31    /// Maximum consecutive health check failures before rollback
32    pub max_consecutive_failures: usize,
33
34    /// Minimum observation period before making decisions (seconds)
35    pub min_observation_secs: u64,
36
37    /// Enable statistical significance testing
38    pub enable_statistical_testing: bool,
39
40    /// Minimum effect size for practical significance
41    pub min_effect_size: f64,
42
43    /// Alert on warnings (not just failures)
44    pub alert_on_warnings: bool,
45}
46
47impl Default for HealthMonitorConfig {
48    fn default() -> Self {
49        Self {
50            success_criteria: SuccessCriteria::default(),
51            max_consecutive_failures: DEFAULT_MAX_CONSECUTIVE_FAILURES,
52            min_observation_secs: DEFAULT_MIN_OBSERVATION_SECS,
53            enable_statistical_testing: true,
54            min_effect_size: 0.05, // 5% minimum effect size
55            alert_on_warnings: true,
56        }
57    }
58}
59
60/// Health check type
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum HealthCheckType {
63    SuccessRate,
64    ErrorRate,
65    P95Latency,
66    P99Latency,
67    QualityScore,
68    CostIncrease,
69    SampleSize,
70    Statistical,
71}
72
73impl HealthCheckType {
74    fn as_str(&self) -> &'static str {
75        match self {
76            Self::SuccessRate => "success_rate",
77            Self::ErrorRate => "error_rate",
78            Self::P95Latency => "p95_latency",
79            Self::P99Latency => "p99_latency",
80            Self::QualityScore => "quality_score",
81            Self::CostIncrease => "cost_increase",
82            Self::SampleSize => "sample_size",
83            Self::Statistical => "statistical_significance",
84        }
85    }
86}
87
88/// Health history entry
89#[derive(Debug, Clone)]
90struct HealthHistoryEntry {
91    timestamp: DateTime<Utc>,
92    health_status: HealthStatus,
93    metrics: DeploymentMetrics,
94}
95
96/// Internal state for health monitoring
97#[derive(Debug)]
98struct HealthMonitorState {
99    /// Configuration
100    config: HealthMonitorConfig,
101
102    /// Health history per deployment
103    health_history: HashMap<String, Vec<HealthHistoryEntry>>,
104
105    /// Consecutive failure counts per deployment
106    consecutive_failures: HashMap<String, usize>,
107
108    /// Deployment start times
109    deployment_start_times: HashMap<String, DateTime<Utc>>,
110
111    /// Metrics history for trend analysis
112    metrics_history: HashMap<String, Vec<(DateTime<Utc>, DeploymentMetrics)>>,
113}
114
115impl HealthMonitorState {
116    fn new(config: HealthMonitorConfig) -> Self {
117        Self {
118            config,
119            health_history: HashMap::new(),
120            consecutive_failures: HashMap::new(),
121            deployment_start_times: HashMap::new(),
122            metrics_history: HashMap::new(),
123        }
124    }
125
126    fn record_health(&mut self, deployment_id: &str, status: HealthStatus, metrics: DeploymentMetrics) {
127        let entry = HealthHistoryEntry {
128            timestamp: Utc::now(),
129            health_status: status.clone(),
130            metrics: metrics.clone(),
131        };
132
133        self.health_history
134            .entry(deployment_id.to_string())
135            .or_default()
136            .push(entry);
137
138        // Update consecutive failures
139        if status.healthy {
140            self.consecutive_failures.insert(deployment_id.to_string(), 0);
141        } else {
142            *self.consecutive_failures
143                .entry(deployment_id.to_string())
144                .or_insert(0) += 1;
145        }
146
147        // Record metrics history
148        self.metrics_history
149            .entry(deployment_id.to_string())
150            .or_default()
151            .push((Utc::now(), metrics));
152    }
153
154    fn get_consecutive_failures(&self, deployment_id: &str) -> usize {
155        self.consecutive_failures
156            .get(deployment_id)
157            .copied()
158            .unwrap_or(0)
159    }
160
161    fn register_deployment(&mut self, deployment_id: &str) {
162        self.deployment_start_times
163            .insert(deployment_id.to_string(), Utc::now());
164    }
165
166    fn get_observation_duration(&self, deployment_id: &str) -> Option<i64> {
167        self.deployment_start_times
168            .get(deployment_id)
169            .map(|start| (Utc::now() - *start).num_seconds())
170    }
171
172    fn cleanup_old_data(&mut self, deployment_id: &str) {
173        self.health_history.remove(deployment_id);
174        self.consecutive_failures.remove(deployment_id);
175        self.deployment_start_times.remove(deployment_id);
176        self.metrics_history.remove(deployment_id);
177    }
178}
179
180/// Production-ready health monitor for deployments
181pub struct ProductionHealthMonitor {
182    state: Arc<RwLock<HealthMonitorState>>,
183}
184
185impl ProductionHealthMonitor {
186    /// Create a new health monitor with default configuration
187    pub fn new() -> Self {
188        Self::with_config(HealthMonitorConfig::default())
189    }
190
191    /// Create a new health monitor with custom configuration
192    pub fn with_config(config: HealthMonitorConfig) -> Self {
193        Self {
194            state: Arc::new(RwLock::new(HealthMonitorState::new(config))),
195        }
196    }
197
198    /// Register a new deployment for monitoring
199    pub fn register_deployment(&self, deployment_id: &str) {
200        if let Ok(mut state) = self.state.write() {
201            state.register_deployment(deployment_id);
202        }
203    }
204
205    /// Remove deployment data after completion or rollback
206    pub fn cleanup_deployment(&self, deployment_id: &str) {
207        if let Ok(mut state) = self.state.write() {
208            state.cleanup_old_data(deployment_id);
209        }
210    }
211
212    /// Get health history for a deployment
213    pub fn get_health_history(&self, deployment_id: &str) -> Vec<HealthHistoryEntry> {
214        self.state
215            .read()
216            .ok()
217            .and_then(|state| state.health_history.get(deployment_id).cloned())
218            .unwrap_or_default()
219    }
220
221    /// Perform a single health check
222    fn perform_health_check(
223        &self,
224        check_type: HealthCheckType,
225        metrics: &DeploymentMetrics,
226        criteria: &SuccessCriteria,
227    ) -> HealthCheckResult {
228        let timestamp = Utc::now();
229
230        match check_type {
231            HealthCheckType::SuccessRate => {
232                let value = metrics.canary_success_rate;
233                let threshold = criteria.min_success_rate;
234                let passed = value >= threshold;
235
236                HealthCheckResult {
237                    name: check_type.as_str().to_string(),
238                    passed,
239                    details: format!(
240                        "Canary success rate: {:.2}% (threshold: {:.2}%, control: {:.2}%)",
241                        value, threshold, metrics.control_success_rate
242                    ),
243                    value: Some(value),
244                    threshold: Some(threshold),
245                    timestamp,
246                }
247            }
248            HealthCheckType::ErrorRate => {
249                let value = metrics.canary_error_rate;
250                let threshold = criteria.max_error_rate;
251                let passed = value <= threshold;
252
253                HealthCheckResult {
254                    name: check_type.as_str().to_string(),
255                    passed,
256                    details: format!(
257                        "Canary error rate: {:.2}% (threshold: {:.2}%, control: {:.2}%)",
258                        value, threshold, metrics.control_error_rate
259                    ),
260                    value: Some(value),
261                    threshold: Some(threshold),
262                    timestamp,
263                }
264            }
265            HealthCheckType::P95Latency => {
266                let value = metrics.canary_p95_latency_ms;
267                let threshold = criteria.max_p95_latency_ms;
268                let passed = value <= threshold;
269
270                HealthCheckResult {
271                    name: check_type.as_str().to_string(),
272                    passed,
273                    details: format!(
274                        "Canary P95 latency: {:.2}ms (threshold: {:.2}ms, control: {:.2}ms)",
275                        value, threshold, metrics.control_p95_latency_ms
276                    ),
277                    value: Some(value),
278                    threshold: Some(threshold),
279                    timestamp,
280                }
281            }
282            HealthCheckType::P99Latency => {
283                let value = metrics.canary_p99_latency_ms;
284                let threshold = criteria.max_p99_latency_ms;
285                let passed = value <= threshold;
286
287                HealthCheckResult {
288                    name: check_type.as_str().to_string(),
289                    passed,
290                    details: format!(
291                        "Canary P99 latency: {:.2}ms (threshold: {:.2}ms, control: {:.2}ms)",
292                        value, threshold, metrics.control_p99_latency_ms
293                    ),
294                    value: Some(value),
295                    threshold: Some(threshold),
296                    timestamp,
297                }
298            }
299            HealthCheckType::QualityScore => {
300                // Quality score check is optional
301                if let Some(min_quality) = criteria.min_quality_score {
302                    // In a real system, we'd extract quality scores from metrics
303                    // For now, we'll consider it passing if not available
304                    HealthCheckResult {
305                        name: check_type.as_str().to_string(),
306                        passed: true,
307                        details: format!(
308                            "Quality score check (threshold: {:.2})",
309                            min_quality
310                        ),
311                        value: Some(min_quality),
312                        threshold: Some(min_quality),
313                        timestamp,
314                    }
315                } else {
316                    HealthCheckResult {
317                        name: check_type.as_str().to_string(),
318                        passed: true,
319                        details: "Quality score check disabled".to_string(),
320                        value: None,
321                        threshold: None,
322                        timestamp,
323                    }
324                }
325            }
326            HealthCheckType::CostIncrease => {
327                let canary_cost = metrics.canary_cost_per_request;
328                let control_cost = metrics.control_cost_per_request;
329
330                let cost_increase_pct = if control_cost > 0.0 {
331                    ((canary_cost - control_cost) / control_cost) * 100.0
332                } else {
333                    0.0
334                };
335
336                let threshold = criteria.max_cost_increase_pct;
337                let passed = cost_increase_pct <= threshold;
338
339                HealthCheckResult {
340                    name: check_type.as_str().to_string(),
341                    passed,
342                    details: format!(
343                        "Cost increase: {:.2}% (threshold: {:.2}%, canary: ${:.6}, control: ${:.6})",
344                        cost_increase_pct, threshold, canary_cost, control_cost
345                    ),
346                    value: Some(cost_increase_pct),
347                    threshold: Some(threshold),
348                    timestamp,
349                }
350            }
351            HealthCheckType::SampleSize => {
352                let canary_samples = metrics.canary_requests as usize;
353                let control_samples = metrics.control_requests as usize;
354                let min_samples = criteria.min_sample_size;
355
356                let passed = canary_samples >= min_samples && control_samples >= min_samples;
357
358                HealthCheckResult {
359                    name: check_type.as_str().to_string(),
360                    passed,
361                    details: format!(
362                        "Sample sizes - canary: {}, control: {} (minimum: {})",
363                        canary_samples, control_samples, min_samples
364                    ),
365                    value: Some(canary_samples.min(control_samples) as f64),
366                    threshold: Some(min_samples as f64),
367                    timestamp,
368                }
369            }
370            HealthCheckType::Statistical => {
371                if let Some(analysis) = &metrics.statistical_analysis {
372                    let passed = !analysis.significant_difference
373                        || metrics.canary_success_rate >= metrics.control_success_rate;
374
375                    HealthCheckResult {
376                        name: check_type.as_str().to_string(),
377                        passed,
378                        details: format!(
379                            "Statistical significance - success rate p-value: {:.4}, latency p-value: {:.4} (threshold: {:.2})",
380                            analysis.success_rate_p_value,
381                            analysis.latency_p_value,
382                            criteria.significance_level
383                        ),
384                        value: Some(analysis.success_rate_p_value),
385                        threshold: Some(criteria.significance_level),
386                        timestamp,
387                    }
388                } else {
389                    HealthCheckResult {
390                        name: check_type.as_str().to_string(),
391                        passed: true,
392                        details: "Statistical analysis not yet available".to_string(),
393                        value: None,
394                        threshold: None,
395                        timestamp,
396                    }
397                }
398            }
399        }
400    }
401
402    /// Perform statistical analysis comparing canary vs control
403    fn perform_statistical_analysis(
404        &self,
405        metrics: &DeploymentMetrics,
406        criteria: &SuccessCriteria,
407    ) -> Option<StatisticalAnalysis> {
408        let canary_samples = metrics.canary_requests as usize;
409        let control_samples = metrics.control_requests as usize;
410
411        // Need minimum sample size
412        if canary_samples < criteria.min_sample_size
413            || control_samples < criteria.min_sample_size
414        {
415            return None;
416        }
417
418        // Calculate success rate p-value using z-test for proportions
419        let success_rate_p_value = self.calculate_proportion_test_p_value(
420            metrics.canary_success_rate / 100.0,
421            canary_samples,
422            metrics.control_success_rate / 100.0,
423            control_samples,
424        );
425
426        // Calculate latency p-value (simplified - in production use proper t-test)
427        let latency_diff_pct = if metrics.control_p95_latency_ms > 0.0 {
428            ((metrics.canary_p95_latency_ms - metrics.control_p95_latency_ms)
429                / metrics.control_p95_latency_ms)
430                .abs()
431        } else {
432            0.0
433        };
434
435        // Simplified p-value based on effect size
436        let latency_p_value = if latency_diff_pct > 0.2 {
437            0.01
438        } else if latency_diff_pct > 0.1 {
439            0.05
440        } else {
441            0.5
442        };
443
444        let significant_difference = success_rate_p_value < criteria.significance_level
445            || latency_p_value < criteria.significance_level;
446
447        Some(StatisticalAnalysis {
448            success_rate_p_value,
449            latency_p_value,
450            significant_difference,
451            confidence_level: 1.0 - criteria.significance_level,
452            canary_sample_size: canary_samples,
453            control_sample_size: control_samples,
454        })
455    }
456
457    /// Calculate p-value for two-proportion z-test
458    fn calculate_proportion_test_p_value(
459        &self,
460        p1: f64,
461        n1: usize,
462        p2: f64,
463        n2: usize,
464    ) -> f64 {
465        // Pooled proportion
466        let p_pool = ((p1 * n1 as f64) + (p2 * n2 as f64)) / (n1 + n2) as f64;
467
468        // Standard error
469        let se = (p_pool * (1.0 - p_pool) * ((1.0 / n1 as f64) + (1.0 / n2 as f64))).sqrt();
470
471        if se == 0.0 {
472            return 1.0;
473        }
474
475        // Z-score
476        let z = ((p1 - p2) / se).abs();
477
478        // Approximate p-value using normal distribution
479        // This is a simplification - in production, use proper statistical library
480        if z > 2.576 {
481            0.01
482        } else if z > 1.96 {
483            0.05
484        } else if z > 1.645 {
485            0.1
486        } else {
487            0.5
488        }
489    }
490
491    /// Check if sufficient observation time has passed
492    fn has_sufficient_observation_time(&self, deployment_id: &str, min_secs: u64) -> bool {
493        self.state
494            .read()
495            .ok()
496            .and_then(|state| state.get_observation_duration(deployment_id))
497            .map(|duration| duration >= min_secs as i64)
498            .unwrap_or(false)
499    }
500
501    /// Build comprehensive health status from check results
502    fn build_health_status(&self, check_results: Vec<HealthCheckResult>) -> HealthStatus {
503        let checks_passed = check_results.iter().filter(|r| r.passed).count();
504        let checks_failed = check_results.len() - checks_passed;
505        let healthy = checks_failed == 0;
506
507        HealthStatus {
508            healthy,
509            checks_passed,
510            checks_failed,
511            check_results,
512            last_check_time: Utc::now(),
513        }
514    }
515}
516
517impl Default for ProductionHealthMonitor {
518    fn default() -> Self {
519        Self::new()
520    }
521}
522
523#[async_trait]
524impl HealthMonitorTrait for ProductionHealthMonitor {
525    async fn check_health(
526        &self,
527        deployment_id: &str,
528        metrics: &SystemMetrics,
529    ) -> ActuatorResult<Vec<HealthCheckResult>> {
530        let state = self.state.read().map_err(|e| {
531            ActuatorError::Internal(format!("Failed to acquire read lock: {}", e))
532        })?;
533
534        let criteria = state.config.success_criteria.clone();
535
536        // Convert SystemMetrics to DeploymentMetrics for this check
537        // In a real system, we'd track separate canary/control metrics
538        let deployment_metrics = DeploymentMetrics {
539            total_requests: metrics.request_count,
540            canary_requests: metrics.request_count / 2, // Simplified
541            control_requests: metrics.request_count / 2,
542            canary_success_rate: metrics.success_rate_pct,
543            control_success_rate: metrics.success_rate_pct,
544            canary_error_rate: metrics.error_rate_pct,
545            control_error_rate: metrics.error_rate_pct,
546            canary_p95_latency_ms: metrics.p95_latency_ms,
547            control_p95_latency_ms: metrics.p95_latency_ms,
548            canary_p99_latency_ms: metrics.p99_latency_ms,
549            control_p99_latency_ms: metrics.p99_latency_ms,
550            canary_cost_per_request: 0.0,
551            control_cost_per_request: 0.0,
552            statistical_analysis: None,
553        };
554
555        drop(state); // Release read lock before write operations
556
557        // Perform all health checks
558        let mut results = Vec::new();
559
560        results.push(self.perform_health_check(
561            HealthCheckType::SuccessRate,
562            &deployment_metrics,
563            &criteria,
564        ));
565
566        results.push(self.perform_health_check(
567            HealthCheckType::ErrorRate,
568            &deployment_metrics,
569            &criteria,
570        ));
571
572        results.push(self.perform_health_check(
573            HealthCheckType::P95Latency,
574            &deployment_metrics,
575            &criteria,
576        ));
577
578        results.push(self.perform_health_check(
579            HealthCheckType::P99Latency,
580            &deployment_metrics,
581            &criteria,
582        ));
583
584        results.push(self.perform_health_check(
585            HealthCheckType::QualityScore,
586            &deployment_metrics,
587            &criteria,
588        ));
589
590        results.push(self.perform_health_check(
591            HealthCheckType::CostIncrease,
592            &deployment_metrics,
593            &criteria,
594        ));
595
596        results.push(self.perform_health_check(
597            HealthCheckType::SampleSize,
598            &deployment_metrics,
599            &criteria,
600        ));
601
602        // Perform statistical analysis if enabled
603        if self.state.read().unwrap().config.enable_statistical_testing {
604            if let Some(analysis) = self.perform_statistical_analysis(&deployment_metrics, &criteria)
605            {
606                let mut metrics_with_analysis = deployment_metrics;
607                metrics_with_analysis.statistical_analysis = Some(analysis);
608
609                results.push(self.perform_health_check(
610                    HealthCheckType::Statistical,
611                    &metrics_with_analysis,
612                    &criteria,
613                ));
614            }
615        }
616
617        Ok(results)
618    }
619
620    async fn should_rollback(
621        &self,
622        deployment_id: &str,
623        status: &DeploymentStatus,
624    ) -> ActuatorResult<bool> {
625        let state = self.state.read().map_err(|e| {
626            ActuatorError::Internal(format!("Failed to acquire read lock: {}", e))
627        })?;
628
629        // Check if we have enough observation time
630        if !self.has_sufficient_observation_time(
631            deployment_id,
632            state.config.min_observation_secs,
633        ) {
634            return Ok(false); // Too early to make a decision
635        }
636
637        // Check if deployment is unhealthy
638        if !status.health.healthy {
639            let consecutive_failures = state.get_consecutive_failures(deployment_id);
640
641            // Rollback if we've exceeded the failure threshold
642            if consecutive_failures >= state.config.max_consecutive_failures {
643                return Ok(true);
644            }
645        }
646
647        // Check for critical failures in health checks
648        for check in &status.health.check_results {
649            if !check.passed {
650                match check.name.as_str() {
651                    "success_rate" | "error_rate" => {
652                        // Critical checks - consider immediate rollback
653                        if let (Some(value), Some(threshold)) = (check.value, check.threshold) {
654                            // If the deviation is severe (>2x threshold violation)
655                            let deviation = if check.name == "success_rate" {
656                                (threshold - value) / threshold
657                            } else {
658                                (value - threshold) / threshold
659                            };
660
661                            if deviation > 2.0 {
662                                return Ok(true);
663                            }
664                        }
665                    }
666                    _ => {}
667                }
668            }
669        }
670
671        // Check statistical significance
672        if let Some(analysis) = &status.metrics.statistical_analysis {
673            if analysis.significant_difference {
674                // If there's a statistically significant degradation
675                if status.metrics.canary_success_rate < status.metrics.control_success_rate {
676                    let degradation_pct = ((status.metrics.control_success_rate
677                        - status.metrics.canary_success_rate)
678                        / status.metrics.control_success_rate)
679                        * 100.0;
680
681                    if degradation_pct > state.config.min_effect_size * 100.0 {
682                        return Ok(true);
683                    }
684                }
685            }
686        }
687
688        Ok(false)
689    }
690
691    async fn get_health_summary(
692        &self,
693        deployment_id: &str,
694    ) -> ActuatorResult<HealthStatus> {
695        let state = self.state.read().map_err(|e| {
696            ActuatorError::Internal(format!("Failed to acquire read lock: {}", e))
697        })?;
698
699        // Get the latest health status from history
700        if let Some(history) = state.health_history.get(deployment_id) {
701            if let Some(latest) = history.last() {
702                return Ok(latest.health_status.clone());
703            }
704        }
705
706        // Return default healthy status if no history
707        Ok(HealthStatus {
708            healthy: true,
709            checks_passed: 0,
710            checks_failed: 0,
711            check_results: Vec::new(),
712            last_check_time: Utc::now(),
713        })
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use std::time::Duration as StdDuration;
721
722    fn create_test_metrics(
723        success_rate: f64,
724        error_rate: f64,
725        p95_latency: f64,
726        p99_latency: f64,
727        request_count: u64,
728    ) -> SystemMetrics {
729        SystemMetrics {
730            avg_latency_ms: p95_latency * 0.7,
731            p95_latency_ms: p95_latency,
732            p99_latency_ms: p99_latency,
733            success_rate_pct: success_rate,
734            error_rate_pct: error_rate,
735            throughput_rps: 100.0,
736            total_cost_usd: 10.0,
737            daily_cost_usd: 10.0,
738            avg_rating: Some(4.5),
739            cache_hit_rate_pct: Some(80.0),
740            request_count,
741            timestamp: Utc::now(),
742        }
743    }
744
745    fn create_test_deployment_status(
746        deployment_id: &str,
747        healthy: bool,
748        metrics: DeploymentMetrics,
749    ) -> DeploymentStatus {
750        use super::super::error::DeploymentState;
751
752        DeploymentStatus {
753            deployment_id: deployment_id.to_string(),
754            state: DeploymentState::Monitoring,
755            current_phase: Some(0),
756            traffic_percent: 10.0,
757            start_time: Utc::now() - chrono::Duration::minutes(10),
758            phase_start_time: Some(Utc::now() - chrono::Duration::minutes(5)),
759            health: HealthStatus {
760                healthy,
761                checks_passed: if healthy { 7 } else { 5 },
762                checks_failed: if healthy { 0 } else { 2 },
763                check_results: Vec::new(),
764                last_check_time: Utc::now(),
765            },
766            metrics,
767            errors: Vec::new(),
768            last_updated: Utc::now(),
769        }
770    }
771
772    #[tokio::test]
773    async fn test_health_monitor_creation() {
774        let monitor = ProductionHealthMonitor::new();
775        let deployment_id = "test-deployment-1";
776
777        monitor.register_deployment(deployment_id);
778
779        let history = monitor.get_health_history(deployment_id);
780        assert!(history.is_empty());
781    }
782
783    #[tokio::test]
784    async fn test_health_check_success_rate_pass() {
785        let monitor = ProductionHealthMonitor::new();
786        let deployment_id = "test-deployment-2";
787
788        monitor.register_deployment(deployment_id);
789
790        let metrics = create_test_metrics(99.5, 0.5, 500.0, 800.0, 1000);
791
792        let results = monitor
793            .check_health(deployment_id, &metrics)
794            .await
795            .unwrap();
796
797        assert!(!results.is_empty());
798
799        let success_rate_check = results
800            .iter()
801            .find(|r| r.name == "success_rate")
802            .unwrap();
803        assert!(success_rate_check.passed);
804        assert_eq!(success_rate_check.value, Some(99.5));
805    }
806
807    #[tokio::test]
808    async fn test_health_check_success_rate_fail() {
809        let monitor = ProductionHealthMonitor::new();
810        let deployment_id = "test-deployment-3";
811
812        monitor.register_deployment(deployment_id);
813
814        let metrics = create_test_metrics(98.0, 2.0, 500.0, 800.0, 1000);
815
816        let results = monitor
817            .check_health(deployment_id, &metrics)
818            .await
819            .unwrap();
820
821        let success_rate_check = results
822            .iter()
823            .find(|r| r.name == "success_rate")
824            .unwrap();
825        assert!(!success_rate_check.passed);
826        assert_eq!(success_rate_check.value, Some(98.0));
827    }
828
829    #[tokio::test]
830    async fn test_health_check_error_rate() {
831        let monitor = ProductionHealthMonitor::new();
832        let deployment_id = "test-deployment-4";
833
834        monitor.register_deployment(deployment_id);
835
836        // Pass: error rate within threshold
837        let metrics_pass = create_test_metrics(99.5, 0.5, 500.0, 800.0, 1000);
838        let results_pass = monitor
839            .check_health(deployment_id, &metrics_pass)
840            .await
841            .unwrap();
842        let error_rate_check_pass = results_pass
843            .iter()
844            .find(|r| r.name == "error_rate")
845            .unwrap();
846        assert!(error_rate_check_pass.passed);
847
848        // Fail: error rate exceeds threshold
849        let metrics_fail = create_test_metrics(97.0, 3.0, 500.0, 800.0, 1000);
850        let results_fail = monitor
851            .check_health(deployment_id, &metrics_fail)
852            .await
853            .unwrap();
854        let error_rate_check_fail = results_fail
855            .iter()
856            .find(|r| r.name == "error_rate")
857            .unwrap();
858        assert!(!error_rate_check_fail.passed);
859    }
860
861    #[tokio::test]
862    async fn test_health_check_latency() {
863        let monitor = ProductionHealthMonitor::new();
864        let deployment_id = "test-deployment-5";
865
866        monitor.register_deployment(deployment_id);
867
868        // Pass: latency within thresholds
869        let metrics_pass = create_test_metrics(99.5, 0.5, 800.0, 1500.0, 1000);
870        let results_pass = monitor
871            .check_health(deployment_id, &metrics_pass)
872            .await
873            .unwrap();
874        let p95_check = results_pass
875            .iter()
876            .find(|r| r.name == "p95_latency")
877            .unwrap();
878        assert!(p95_check.passed);
879
880        // Fail: P95 latency exceeds threshold
881        let metrics_fail_p95 = create_test_metrics(99.5, 0.5, 1500.0, 1800.0, 1000);
882        let results_fail_p95 = monitor
883            .check_health(deployment_id, &metrics_fail_p95)
884            .await
885            .unwrap();
886        let p95_check_fail = results_fail_p95
887            .iter()
888            .find(|r| r.name == "p95_latency")
889            .unwrap();
890        assert!(!p95_check_fail.passed);
891
892        // Fail: P99 latency exceeds threshold
893        let metrics_fail_p99 = create_test_metrics(99.5, 0.5, 800.0, 3000.0, 1000);
894        let results_fail_p99 = monitor
895            .check_health(deployment_id, &metrics_fail_p99)
896            .await
897            .unwrap();
898        let p99_check_fail = results_fail_p99
899            .iter()
900            .find(|r| r.name == "p99_latency")
901            .unwrap();
902        assert!(!p99_check_fail.passed);
903    }
904
905    #[tokio::test]
906    async fn test_health_check_sample_size() {
907        let monitor = ProductionHealthMonitor::new();
908        let deployment_id = "test-deployment-6";
909
910        monitor.register_deployment(deployment_id);
911
912        // Pass: sufficient samples
913        let metrics_pass = create_test_metrics(99.5, 0.5, 500.0, 800.0, 1000);
914        let results_pass = monitor
915            .check_health(deployment_id, &metrics_pass)
916            .await
917            .unwrap();
918        let sample_check = results_pass
919            .iter()
920            .find(|r| r.name == "sample_size")
921            .unwrap();
922        assert!(sample_check.passed);
923
924        // Fail: insufficient samples
925        let metrics_fail = create_test_metrics(99.5, 0.5, 500.0, 800.0, 50);
926        let results_fail = monitor
927            .check_health(deployment_id, &metrics_fail)
928            .await
929            .unwrap();
930        let sample_check_fail = results_fail
931            .iter()
932            .find(|r| r.name == "sample_size")
933            .unwrap();
934        assert!(!sample_check_fail.passed);
935    }
936
937    #[tokio::test]
938    async fn test_should_rollback_healthy() {
939        let config = HealthMonitorConfig {
940            min_observation_secs: 0, // Disable min observation time for test
941            ..Default::default()
942        };
943        let monitor = ProductionHealthMonitor::with_config(config);
944        let deployment_id = "test-deployment-7";
945
946        monitor.register_deployment(deployment_id);
947
948        let metrics = DeploymentMetrics {
949            canary_success_rate: 99.5,
950            control_success_rate: 99.0,
951            ..Default::default()
952        };
953
954        let status = create_test_deployment_status(deployment_id, true, metrics);
955
956        let should_rollback = monitor
957            .should_rollback(deployment_id, &status)
958            .await
959            .unwrap();
960        assert!(!should_rollback);
961    }
962
963    #[tokio::test]
964    async fn test_should_rollback_consecutive_failures() {
965        let config = HealthMonitorConfig {
966            max_consecutive_failures: 3,
967            min_observation_secs: 0,
968            ..Default::default()
969        };
970        let monitor = ProductionHealthMonitor::with_config(config);
971        let deployment_id = "test-deployment-8";
972
973        monitor.register_deployment(deployment_id);
974
975        let metrics = DeploymentMetrics {
976            canary_success_rate: 95.0,
977            control_success_rate: 99.0,
978            ..Default::default()
979        };
980
981        // Record multiple failures
982        for _ in 0..3 {
983            let status = create_test_deployment_status(deployment_id, false, metrics.clone());
984            let _ = monitor
985                .should_rollback(deployment_id, &status)
986                .await
987                .unwrap();
988
989            // Record the failure
990            if let Ok(mut state) = monitor.state.write() {
991                state.record_health(
992                    deployment_id,
993                    HealthStatus {
994                        healthy: false,
995                        checks_passed: 5,
996                        checks_failed: 2,
997                        check_results: Vec::new(),
998                        last_check_time: Utc::now(),
999                    },
1000                    metrics.clone(),
1001                );
1002            }
1003        }
1004
1005        let status = create_test_deployment_status(deployment_id, false, metrics);
1006        let should_rollback = monitor
1007            .should_rollback(deployment_id, &status)
1008            .await
1009            .unwrap();
1010        assert!(should_rollback);
1011    }
1012
1013    #[tokio::test]
1014    async fn test_should_rollback_critical_failure() {
1015        let config = HealthMonitorConfig {
1016            min_observation_secs: 0,
1017            ..Default::default()
1018        };
1019        let monitor = ProductionHealthMonitor::with_config(config);
1020        let deployment_id = "test-deployment-9";
1021
1022        monitor.register_deployment(deployment_id);
1023
1024        let mut metrics = DeploymentMetrics {
1025            canary_success_rate: 90.0, // Very low
1026            control_success_rate: 99.0,
1027            ..Default::default()
1028        };
1029
1030        let mut status = create_test_deployment_status(deployment_id, false, metrics.clone());
1031
1032        // Add a critical failure
1033        status.health.check_results.push(HealthCheckResult {
1034            name: "success_rate".to_string(),
1035            passed: false,
1036            details: "Critical failure".to_string(),
1037            value: Some(90.0),
1038            threshold: Some(99.0),
1039            timestamp: Utc::now(),
1040        });
1041
1042        let should_rollback = monitor
1043            .should_rollback(deployment_id, &status)
1044            .await
1045            .unwrap();
1046        assert!(should_rollback);
1047    }
1048
1049    #[tokio::test]
1050    async fn test_statistical_analysis() {
1051        let monitor = ProductionHealthMonitor::new();
1052
1053        let metrics = DeploymentMetrics {
1054            canary_requests: 1000,
1055            control_requests: 1000,
1056            canary_success_rate: 99.5,
1057            control_success_rate: 99.0,
1058            canary_p95_latency_ms: 500.0,
1059            control_p95_latency_ms: 480.0,
1060            ..Default::default()
1061        };
1062
1063        let criteria = SuccessCriteria::default();
1064
1065        let analysis = monitor.perform_statistical_analysis(&metrics, &criteria);
1066        assert!(analysis.is_some());
1067
1068        let analysis = analysis.unwrap();
1069        assert_eq!(analysis.canary_sample_size, 1000);
1070        assert_eq!(analysis.control_sample_size, 1000);
1071        assert!(analysis.success_rate_p_value >= 0.0);
1072        assert!(analysis.success_rate_p_value <= 1.0);
1073    }
1074
1075    #[tokio::test]
1076    async fn test_statistical_analysis_insufficient_samples() {
1077        let monitor = ProductionHealthMonitor::new();
1078
1079        let metrics = DeploymentMetrics {
1080            canary_requests: 50,
1081            control_requests: 50,
1082            canary_success_rate: 99.5,
1083            control_success_rate: 99.0,
1084            ..Default::default()
1085        };
1086
1087        let criteria = SuccessCriteria::default();
1088
1089        let analysis = monitor.perform_statistical_analysis(&metrics, &criteria);
1090        assert!(analysis.is_none());
1091    }
1092
1093    #[tokio::test]
1094    async fn test_get_health_summary_no_history() {
1095        let monitor = ProductionHealthMonitor::new();
1096        let deployment_id = "test-deployment-10";
1097
1098        let summary = monitor
1099            .get_health_summary(deployment_id)
1100            .await
1101            .unwrap();
1102
1103        assert!(summary.healthy);
1104        assert_eq!(summary.checks_passed, 0);
1105        assert_eq!(summary.checks_failed, 0);
1106    }
1107
1108    #[tokio::test]
1109    async fn test_get_health_summary_with_history() {
1110        let monitor = ProductionHealthMonitor::new();
1111        let deployment_id = "test-deployment-11";
1112
1113        monitor.register_deployment(deployment_id);
1114
1115        // Record some health data
1116        if let Ok(mut state) = monitor.state.write() {
1117            state.record_health(
1118                deployment_id,
1119                HealthStatus {
1120                    healthy: true,
1121                    checks_passed: 7,
1122                    checks_failed: 0,
1123                    check_results: Vec::new(),
1124                    last_check_time: Utc::now(),
1125                },
1126                DeploymentMetrics::default(),
1127            );
1128        }
1129
1130        let summary = monitor
1131            .get_health_summary(deployment_id)
1132            .await
1133            .unwrap();
1134
1135        assert!(summary.healthy);
1136        assert_eq!(summary.checks_passed, 7);
1137        assert_eq!(summary.checks_failed, 0);
1138    }
1139
1140    #[tokio::test]
1141    async fn test_cleanup_deployment() {
1142        let monitor = ProductionHealthMonitor::new();
1143        let deployment_id = "test-deployment-12";
1144
1145        monitor.register_deployment(deployment_id);
1146
1147        // Record some data
1148        if let Ok(mut state) = monitor.state.write() {
1149            state.record_health(
1150                deployment_id,
1151                HealthStatus {
1152                    healthy: true,
1153                    checks_passed: 7,
1154                    checks_failed: 0,
1155                    check_results: Vec::new(),
1156                    last_check_time: Utc::now(),
1157                },
1158                DeploymentMetrics::default(),
1159            );
1160        }
1161
1162        let history_before = monitor.get_health_history(deployment_id);
1163        assert!(!history_before.is_empty());
1164
1165        monitor.cleanup_deployment(deployment_id);
1166
1167        let history_after = monitor.get_health_history(deployment_id);
1168        assert!(history_after.is_empty());
1169    }
1170
1171    #[tokio::test]
1172    async fn test_health_monitor_with_custom_config() {
1173        let config = HealthMonitorConfig {
1174            success_criteria: SuccessCriteria {
1175                min_success_rate: 95.0,
1176                max_error_rate: 5.0,
1177                ..Default::default()
1178            },
1179            max_consecutive_failures: 5,
1180            min_observation_secs: 30,
1181            ..Default::default()
1182        };
1183
1184        let monitor = ProductionHealthMonitor::with_config(config);
1185        let deployment_id = "test-deployment-13";
1186
1187        monitor.register_deployment(deployment_id);
1188
1189        let metrics = create_test_metrics(96.0, 4.0, 500.0, 800.0, 1000);
1190
1191        let results = monitor
1192            .check_health(deployment_id, &metrics)
1193            .await
1194            .unwrap();
1195
1196        let success_rate_check = results
1197            .iter()
1198            .find(|r| r.name == "success_rate")
1199            .unwrap();
1200        assert!(success_rate_check.passed); // Passes with custom threshold
1201
1202        let error_rate_check = results
1203            .iter()
1204            .find(|r| r.name == "error_rate")
1205            .unwrap();
1206        assert!(error_rate_check.passed); // Passes with custom threshold
1207    }
1208
1209    #[tokio::test]
1210    async fn test_proportion_test_p_value() {
1211        let monitor = ProductionHealthMonitor::new();
1212
1213        // Test with identical proportions
1214        let p_value = monitor.calculate_proportion_test_p_value(0.99, 1000, 0.99, 1000);
1215        assert!(p_value > 0.05); // Should not be significant
1216
1217        // Test with very different proportions
1218        let p_value = monitor.calculate_proportion_test_p_value(0.95, 1000, 0.99, 1000);
1219        assert!(p_value <= 0.05); // Should be significant
1220    }
1221
1222    #[tokio::test]
1223    async fn test_cost_increase_check() {
1224        let monitor = ProductionHealthMonitor::new();
1225        let deployment_id = "test-deployment-14";
1226
1227        monitor.register_deployment(deployment_id);
1228
1229        let state = monitor.state.read().unwrap();
1230        let criteria = &state.config.success_criteria;
1231
1232        // Test with acceptable cost increase
1233        let metrics_pass = DeploymentMetrics {
1234            canary_cost_per_request: 0.105,
1235            control_cost_per_request: 0.10,
1236            ..Default::default()
1237        };
1238
1239        let check_pass = monitor.perform_health_check(
1240            HealthCheckType::CostIncrease,
1241            &metrics_pass,
1242            criteria,
1243        );
1244        assert!(check_pass.passed);
1245
1246        // Test with excessive cost increase
1247        let metrics_fail = DeploymentMetrics {
1248            canary_cost_per_request: 0.15,
1249            control_cost_per_request: 0.10,
1250            ..Default::default()
1251        };
1252
1253        let check_fail = monitor.perform_health_check(
1254            HealthCheckType::CostIncrease,
1255            &metrics_fail,
1256            criteria,
1257        );
1258        assert!(!check_fail.passed);
1259    }
1260}