1use std::collections::HashMap;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MonitorHealthStatus {
28 Healthy,
30 Degraded,
32 Unhealthy,
34 Unknown,
36}
37
38impl MonitorHealthStatus {
39 fn severity(self) -> u8 {
41 match self {
42 Self::Healthy => 0,
43 Self::Unknown => 1,
44 Self::Degraded => 2,
45 Self::Unhealthy => 3,
46 }
47 }
48
49 fn worst(self, other: Self) -> Self {
51 if self.severity() >= other.severity() {
52 self
53 } else {
54 other
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct MonitorHealthCheck {
62 pub name: String,
64 pub status: MonitorHealthStatus,
66 pub message: String,
68 pub last_check_tick: u64,
70}
71
72#[derive(Debug, Clone)]
74pub struct HealthMonitorConfig {
75 pub check_interval_ticks: u64,
77 pub degraded_threshold: f64,
79 pub unhealthy_threshold: f64,
81}
82
83impl Default for HealthMonitorConfig {
84 fn default() -> Self {
85 Self {
86 check_interval_ticks: 50,
87 degraded_threshold: 0.8,
88 unhealthy_threshold: 0.95,
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct HealthMonitorStats {
96 pub check_count: u64,
98 pub overall_status: MonitorHealthStatus,
100 pub healthy: usize,
102 pub degraded: usize,
104 pub unhealthy: usize,
106}
107
108pub struct StorageHealthMonitor {
113 config: HealthMonitorConfig,
114 checks: HashMap<String, MonitorHealthCheck>,
115 current_tick: u64,
116 last_check_tick: u64,
117 overall_status: MonitorHealthStatus,
118 check_count: u64,
119}
120
121impl StorageHealthMonitor {
122 pub fn new(config: HealthMonitorConfig) -> Self {
124 Self {
125 config,
126 checks: HashMap::new(),
127 current_tick: 0,
128 last_check_tick: 0,
129 overall_status: MonitorHealthStatus::Healthy,
130 check_count: 0,
131 }
132 }
133
134 pub fn register_check(&mut self, name: &str) {
136 self.checks.insert(
137 name.to_string(),
138 MonitorHealthCheck {
139 name: name.to_string(),
140 status: MonitorHealthStatus::Unknown,
141 message: String::new(),
142 last_check_tick: 0,
143 },
144 );
145 }
146
147 pub fn update_check(
151 &mut self,
152 name: &str,
153 status: MonitorHealthStatus,
154 message: &str,
155 ) -> Result<(), String> {
156 let check = self
157 .checks
158 .get_mut(name)
159 .ok_or_else(|| format!("check '{}' not found", name))?;
160 check.status = status;
161 check.message = message.to_string();
162 check.last_check_tick = self.current_tick;
163 Ok(())
164 }
165
166 pub fn evaluate_utilization(&self, current: u64, capacity: u64) -> MonitorHealthStatus {
170 if capacity == 0 {
171 return MonitorHealthStatus::Unhealthy;
172 }
173 let ratio = current as f64 / capacity as f64;
174 if ratio >= self.config.unhealthy_threshold {
175 MonitorHealthStatus::Unhealthy
176 } else if ratio >= self.config.degraded_threshold {
177 MonitorHealthStatus::Degraded
178 } else {
179 MonitorHealthStatus::Healthy
180 }
181 }
182
183 pub fn overall_health(&self) -> MonitorHealthStatus {
187 self.checks
188 .values()
189 .fold(MonitorHealthStatus::Healthy, |acc, c| acc.worst(c.status))
190 }
191
192 pub fn get_check(&self, name: &str) -> Option<&MonitorHealthCheck> {
194 self.checks.get(name)
195 }
196
197 pub fn should_check(&self) -> bool {
199 self.current_tick.saturating_sub(self.last_check_tick) >= self.config.check_interval_ticks
200 }
201
202 pub fn run_checks(&mut self) {
204 self.last_check_tick = self.current_tick;
205 self.check_count += 1;
206 self.overall_status = self.overall_health();
207 }
208
209 pub fn healthy_count(&self) -> usize {
211 self.checks
212 .values()
213 .filter(|c| c.status == MonitorHealthStatus::Healthy)
214 .count()
215 }
216
217 pub fn degraded_count(&self) -> usize {
219 self.checks
220 .values()
221 .filter(|c| c.status == MonitorHealthStatus::Degraded)
222 .count()
223 }
224
225 pub fn unhealthy_count(&self) -> usize {
227 self.checks
228 .values()
229 .filter(|c| c.status == MonitorHealthStatus::Unhealthy)
230 .count()
231 }
232
233 pub fn tick(&mut self) {
235 self.current_tick += 1;
236 }
237
238 pub fn all_checks(&self) -> Vec<&MonitorHealthCheck> {
240 self.checks.values().collect()
241 }
242
243 pub fn stats(&self) -> HealthMonitorStats {
245 HealthMonitorStats {
246 check_count: self.check_count,
247 overall_status: self.overall_status,
248 healthy: self.healthy_count(),
249 degraded: self.degraded_count(),
250 unhealthy: self.unhealthy_count(),
251 }
252 }
253}
254
255#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn default_monitor() -> StorageHealthMonitor {
264 StorageHealthMonitor::new(HealthMonitorConfig::default())
265 }
266
267 #[test]
270 fn test_default_config_values() {
271 let cfg = HealthMonitorConfig::default();
272 assert_eq!(cfg.check_interval_ticks, 50);
273 assert!((cfg.degraded_threshold - 0.8).abs() < f64::EPSILON);
274 assert!((cfg.unhealthy_threshold - 0.95).abs() < f64::EPSILON);
275 }
276
277 #[test]
278 fn test_new_monitor_is_healthy() {
279 let m = default_monitor();
280 assert_eq!(m.overall_health(), MonitorHealthStatus::Healthy);
281 assert_eq!(m.overall_status, MonitorHealthStatus::Healthy);
282 }
283
284 #[test]
285 fn test_new_monitor_empty_checks() {
286 let m = default_monitor();
287 assert!(m.all_checks().is_empty());
288 assert_eq!(m.healthy_count(), 0);
289 assert_eq!(m.degraded_count(), 0);
290 assert_eq!(m.unhealthy_count(), 0);
291 }
292
293 #[test]
296 fn test_register_check_unknown() {
297 let mut m = default_monitor();
298 m.register_check("disk");
299 let c = m.get_check("disk").expect("should exist");
300 assert_eq!(c.status, MonitorHealthStatus::Unknown);
301 assert_eq!(c.name, "disk");
302 assert!(c.message.is_empty());
303 }
304
305 #[test]
306 fn test_register_multiple_checks() {
307 let mut m = default_monitor();
308 m.register_check("disk");
309 m.register_check("memory");
310 m.register_check("network");
311 assert_eq!(m.all_checks().len(), 3);
312 }
313
314 #[test]
315 fn test_get_check_nonexistent() {
316 let m = default_monitor();
317 assert!(m.get_check("nope").is_none());
318 }
319
320 #[test]
323 fn test_update_check_success() {
324 let mut m = default_monitor();
325 m.register_check("disk");
326 let res = m.update_check("disk", MonitorHealthStatus::Healthy, "all good");
327 assert!(res.is_ok());
328 let c = m.get_check("disk").expect("should exist");
329 assert_eq!(c.status, MonitorHealthStatus::Healthy);
330 assert_eq!(c.message, "all good");
331 }
332
333 #[test]
334 fn test_update_check_not_found() {
335 let mut m = default_monitor();
336 let res = m.update_check("missing", MonitorHealthStatus::Healthy, "");
337 assert!(res.is_err());
338 }
339
340 #[test]
341 fn test_update_check_records_tick() {
342 let mut m = default_monitor();
343 m.register_check("disk");
344 for _ in 0..10 {
345 m.tick();
346 }
347 m.update_check("disk", MonitorHealthStatus::Healthy, "ok")
348 .expect("update should succeed");
349 let c = m.get_check("disk").expect("should exist");
350 assert_eq!(c.last_check_tick, 10);
351 }
352
353 #[test]
356 fn test_evaluate_healthy() {
357 let m = default_monitor();
358 assert_eq!(
359 m.evaluate_utilization(50, 100),
360 MonitorHealthStatus::Healthy
361 );
362 }
363
364 #[test]
365 fn test_evaluate_degraded() {
366 let m = default_monitor();
367 assert_eq!(
368 m.evaluate_utilization(85, 100),
369 MonitorHealthStatus::Degraded
370 );
371 }
372
373 #[test]
374 fn test_evaluate_unhealthy() {
375 let m = default_monitor();
376 assert_eq!(
377 m.evaluate_utilization(96, 100),
378 MonitorHealthStatus::Unhealthy
379 );
380 }
381
382 #[test]
383 fn test_evaluate_zero_capacity() {
384 let m = default_monitor();
385 assert_eq!(m.evaluate_utilization(0, 0), MonitorHealthStatus::Unhealthy);
386 }
387
388 #[test]
389 fn test_evaluate_exact_degraded_boundary() {
390 let m = default_monitor();
391 assert_eq!(
393 m.evaluate_utilization(80, 100),
394 MonitorHealthStatus::Degraded
395 );
396 }
397
398 #[test]
399 fn test_evaluate_just_below_degraded() {
400 let m = default_monitor();
401 assert_eq!(
402 m.evaluate_utilization(79, 100),
403 MonitorHealthStatus::Healthy
404 );
405 }
406
407 #[test]
408 fn test_evaluate_exact_unhealthy_boundary() {
409 let m = default_monitor();
410 assert_eq!(
412 m.evaluate_utilization(95, 100),
413 MonitorHealthStatus::Unhealthy
414 );
415 }
416
417 #[test]
418 fn test_evaluate_just_below_unhealthy() {
419 let m = default_monitor();
420 assert_eq!(
422 m.evaluate_utilization(94, 100),
423 MonitorHealthStatus::Degraded
424 );
425 }
426
427 #[test]
428 fn test_evaluate_full_capacity() {
429 let m = default_monitor();
430 assert_eq!(
431 m.evaluate_utilization(100, 100),
432 MonitorHealthStatus::Unhealthy
433 );
434 }
435
436 #[test]
437 fn test_evaluate_custom_thresholds() {
438 let cfg = HealthMonitorConfig {
439 check_interval_ticks: 10,
440 degraded_threshold: 0.5,
441 unhealthy_threshold: 0.7,
442 };
443 let m = StorageHealthMonitor::new(cfg);
444 assert_eq!(
445 m.evaluate_utilization(40, 100),
446 MonitorHealthStatus::Healthy
447 );
448 assert_eq!(
449 m.evaluate_utilization(60, 100),
450 MonitorHealthStatus::Degraded
451 );
452 assert_eq!(
453 m.evaluate_utilization(75, 100),
454 MonitorHealthStatus::Unhealthy
455 );
456 }
457
458 #[test]
461 fn test_overall_health_all_healthy() {
462 let mut m = default_monitor();
463 m.register_check("a");
464 m.register_check("b");
465 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
466 m.update_check("b", MonitorHealthStatus::Healthy, "").ok();
467 assert_eq!(m.overall_health(), MonitorHealthStatus::Healthy);
468 }
469
470 #[test]
471 fn test_overall_health_one_degraded() {
472 let mut m = default_monitor();
473 m.register_check("a");
474 m.register_check("b");
475 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
476 m.update_check("b", MonitorHealthStatus::Degraded, "").ok();
477 assert_eq!(m.overall_health(), MonitorHealthStatus::Degraded);
478 }
479
480 #[test]
481 fn test_overall_health_one_unhealthy() {
482 let mut m = default_monitor();
483 m.register_check("a");
484 m.register_check("b");
485 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
486 m.update_check("b", MonitorHealthStatus::Unhealthy, "").ok();
487 assert_eq!(m.overall_health(), MonitorHealthStatus::Unhealthy);
488 }
489
490 #[test]
491 fn test_overall_health_unknown_worse_than_healthy() {
492 let mut m = default_monitor();
493 m.register_check("a");
494 m.register_check("b");
495 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
496 assert_eq!(m.overall_health(), MonitorHealthStatus::Unknown);
498 }
499
500 #[test]
501 fn test_overall_health_unhealthy_worst() {
502 let mut m = default_monitor();
503 m.register_check("a");
504 m.register_check("b");
505 m.register_check("c");
506 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
507 m.update_check("b", MonitorHealthStatus::Degraded, "").ok();
508 m.update_check("c", MonitorHealthStatus::Unhealthy, "").ok();
509 assert_eq!(m.overall_health(), MonitorHealthStatus::Unhealthy);
510 }
511
512 #[test]
515 fn test_should_check_initially_true() {
516 let m = default_monitor();
519 assert!(!m.should_check());
520 }
521
522 #[test]
523 fn test_should_check_after_enough_ticks() {
524 let mut m = default_monitor();
525 for _ in 0..50 {
526 m.tick();
527 }
528 assert!(m.should_check());
529 }
530
531 #[test]
532 fn test_should_check_resets_after_run() {
533 let mut m = default_monitor();
534 for _ in 0..50 {
535 m.tick();
536 }
537 assert!(m.should_check());
538 m.run_checks();
539 assert!(!m.should_check());
540 }
541
542 #[test]
543 fn test_tick_increments() {
544 let mut m = default_monitor();
545 m.tick();
546 m.tick();
547 m.tick();
548 assert_eq!(m.current_tick, 3);
549 }
550
551 #[test]
554 fn test_run_checks_increments_count() {
555 let mut m = default_monitor();
556 assert_eq!(m.check_count, 0);
557 m.run_checks();
558 assert_eq!(m.check_count, 1);
559 m.run_checks();
560 assert_eq!(m.check_count, 2);
561 }
562
563 #[test]
564 fn test_run_checks_updates_overall() {
565 let mut m = default_monitor();
566 m.register_check("a");
567 m.update_check("a", MonitorHealthStatus::Degraded, "warn")
568 .ok();
569 m.run_checks();
570 assert_eq!(m.overall_status, MonitorHealthStatus::Degraded);
571 }
572
573 #[test]
576 fn test_healthy_count() {
577 let mut m = default_monitor();
578 m.register_check("a");
579 m.register_check("b");
580 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
581 m.update_check("b", MonitorHealthStatus::Healthy, "").ok();
582 assert_eq!(m.healthy_count(), 2);
583 }
584
585 #[test]
586 fn test_degraded_count() {
587 let mut m = default_monitor();
588 m.register_check("a");
589 m.register_check("b");
590 m.update_check("a", MonitorHealthStatus::Degraded, "").ok();
591 m.update_check("b", MonitorHealthStatus::Degraded, "").ok();
592 assert_eq!(m.degraded_count(), 2);
593 }
594
595 #[test]
596 fn test_unhealthy_count() {
597 let mut m = default_monitor();
598 m.register_check("a");
599 m.update_check("a", MonitorHealthStatus::Unhealthy, "").ok();
600 assert_eq!(m.unhealthy_count(), 1);
601 }
602
603 #[test]
604 fn test_mixed_counts() {
605 let mut m = default_monitor();
606 m.register_check("a");
607 m.register_check("b");
608 m.register_check("c");
609 m.register_check("d");
610 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
611 m.update_check("b", MonitorHealthStatus::Degraded, "").ok();
612 m.update_check("c", MonitorHealthStatus::Unhealthy, "").ok();
613 assert_eq!(m.healthy_count(), 1);
615 assert_eq!(m.degraded_count(), 1);
616 assert_eq!(m.unhealthy_count(), 1);
617 }
618
619 #[test]
622 fn test_stats_snapshot() {
623 let mut m = default_monitor();
624 m.register_check("a");
625 m.register_check("b");
626 m.update_check("a", MonitorHealthStatus::Healthy, "").ok();
627 m.update_check("b", MonitorHealthStatus::Degraded, "").ok();
628 m.run_checks();
629 let s = m.stats();
630 assert_eq!(s.check_count, 1);
631 assert_eq!(s.overall_status, MonitorHealthStatus::Degraded);
632 assert_eq!(s.healthy, 1);
633 assert_eq!(s.degraded, 1);
634 assert_eq!(s.unhealthy, 0);
635 }
636
637 #[test]
638 fn test_stats_empty_monitor() {
639 let m = default_monitor();
640 let s = m.stats();
641 assert_eq!(s.check_count, 0);
642 assert_eq!(s.overall_status, MonitorHealthStatus::Healthy);
643 assert_eq!(s.healthy, 0);
644 assert_eq!(s.degraded, 0);
645 assert_eq!(s.unhealthy, 0);
646 }
647
648 #[test]
651 fn test_severity_ordering() {
652 assert!(MonitorHealthStatus::Healthy.severity() < MonitorHealthStatus::Unknown.severity());
653 assert!(MonitorHealthStatus::Unknown.severity() < MonitorHealthStatus::Degraded.severity());
654 assert!(
655 MonitorHealthStatus::Degraded.severity() < MonitorHealthStatus::Unhealthy.severity()
656 );
657 }
658
659 #[test]
660 fn test_worst_picks_higher_severity() {
661 assert_eq!(
662 MonitorHealthStatus::Healthy.worst(MonitorHealthStatus::Degraded),
663 MonitorHealthStatus::Degraded
664 );
665 assert_eq!(
666 MonitorHealthStatus::Unhealthy.worst(MonitorHealthStatus::Healthy),
667 MonitorHealthStatus::Unhealthy
668 );
669 }
670
671 #[test]
674 fn test_register_overwrites() {
675 let mut m = default_monitor();
676 m.register_check("a");
677 m.update_check("a", MonitorHealthStatus::Unhealthy, "bad")
678 .ok();
679 m.register_check("a"); let c = m.get_check("a").expect("should exist");
681 assert_eq!(c.status, MonitorHealthStatus::Unknown);
682 assert!(c.message.is_empty());
683 }
684
685 #[test]
688 fn test_should_check_interval_one() {
689 let cfg = HealthMonitorConfig {
690 check_interval_ticks: 1,
691 ..HealthMonitorConfig::default()
692 };
693 let mut m = StorageHealthMonitor::new(cfg);
694 assert!(!m.should_check()); m.tick();
696 assert!(m.should_check()); m.run_checks();
698 assert!(!m.should_check()); m.tick();
700 assert!(m.should_check()); }
702
703 #[test]
706 fn test_all_checks_returns_all() {
707 let mut m = default_monitor();
708 m.register_check("x");
709 m.register_check("y");
710 let all = m.all_checks();
711 assert_eq!(all.len(), 2);
712 let names: Vec<&str> = all.iter().map(|c| c.name.as_str()).collect();
713 assert!(names.contains(&"x"));
714 assert!(names.contains(&"y"));
715 }
716}