1use 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
19const DEFAULT_MAX_CONSECUTIVE_FAILURES: usize = 3;
21
22const DEFAULT_MIN_OBSERVATION_SECS: u64 = 60;
24
25#[derive(Debug, Clone)]
27pub struct HealthMonitorConfig {
28 pub success_criteria: SuccessCriteria,
30
31 pub max_consecutive_failures: usize,
33
34 pub min_observation_secs: u64,
36
37 pub enable_statistical_testing: bool,
39
40 pub min_effect_size: f64,
42
43 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, alert_on_warnings: true,
56 }
57 }
58}
59
60#[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#[derive(Debug, Clone)]
90struct HealthHistoryEntry {
91 timestamp: DateTime<Utc>,
92 health_status: HealthStatus,
93 metrics: DeploymentMetrics,
94}
95
96#[derive(Debug)]
98struct HealthMonitorState {
99 config: HealthMonitorConfig,
101
102 health_history: HashMap<String, Vec<HealthHistoryEntry>>,
104
105 consecutive_failures: HashMap<String, usize>,
107
108 deployment_start_times: HashMap<String, DateTime<Utc>>,
110
111 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 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 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
180pub struct ProductionHealthMonitor {
182 state: Arc<RwLock<HealthMonitorState>>,
183}
184
185impl ProductionHealthMonitor {
186 pub fn new() -> Self {
188 Self::with_config(HealthMonitorConfig::default())
189 }
190
191 pub fn with_config(config: HealthMonitorConfig) -> Self {
193 Self {
194 state: Arc::new(RwLock::new(HealthMonitorState::new(config))),
195 }
196 }
197
198 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 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 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 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 if let Some(min_quality) = criteria.min_quality_score {
302 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 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 if canary_samples < criteria.min_sample_size
413 || control_samples < criteria.min_sample_size
414 {
415 return None;
416 }
417
418 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 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 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 fn calculate_proportion_test_p_value(
459 &self,
460 p1: f64,
461 n1: usize,
462 p2: f64,
463 n2: usize,
464 ) -> f64 {
465 let p_pool = ((p1 * n1 as f64) + (p2 * n2 as f64)) / (n1 + n2) as f64;
467
468 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 let z = ((p1 - p2) / se).abs();
477
478 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 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 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 let deployment_metrics = DeploymentMetrics {
539 total_requests: metrics.request_count,
540 canary_requests: metrics.request_count / 2, 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); 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 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 if !self.has_sufficient_observation_time(
631 deployment_id,
632 state.config.min_observation_secs,
633 ) {
634 return Ok(false); }
636
637 if !status.health.healthy {
639 let consecutive_failures = state.get_consecutive_failures(deployment_id);
640
641 if consecutive_failures >= state.config.max_consecutive_failures {
643 return Ok(true);
644 }
645 }
646
647 for check in &status.health.check_results {
649 if !check.passed {
650 match check.name.as_str() {
651 "success_rate" | "error_rate" => {
652 if let (Some(value), Some(threshold)) = (check.value, check.threshold) {
654 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 if let Some(analysis) = &status.metrics.statistical_analysis {
673 if analysis.significant_difference {
674 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 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 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 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 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 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 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 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 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 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, ..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 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 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, 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 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 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 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); let error_rate_check = results
1203 .iter()
1204 .find(|r| r.name == "error_rate")
1205 .unwrap();
1206 assert!(error_rate_check.passed); }
1208
1209 #[tokio::test]
1210 async fn test_proportion_test_p_value() {
1211 let monitor = ProductionHealthMonitor::new();
1212
1213 let p_value = monitor.calculate_proportion_test_p_value(0.99, 1000, 0.99, 1000);
1215 assert!(p_value > 0.05); let p_value = monitor.calculate_proportion_test_p_value(0.95, 1000, 0.99, 1000);
1219 assert!(p_value <= 0.05); }
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 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 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}