1#![allow(dead_code)]
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum HealthStatus {
10 Healthy,
11 Degraded,
12 Unhealthy,
13 Unknown,
14}
15
16#[derive(Clone, Debug)]
18pub struct HealthCheckResult {
19 pub component: String,
20 pub status: HealthStatus,
21 pub message: Option<String>,
22 pub latency_ms: u64,
23}
24
25#[derive(Clone, Debug)]
27pub struct HealthReport {
28 pub overall: HealthStatus,
29 pub results: Vec<HealthCheckResult>,
30}
31
32#[derive(Clone, Debug)]
34pub struct HealthCheckConfig {
35 pub max_latency_ms: u64,
37 pub service_name: String,
39}
40
41impl Default for HealthCheckConfig {
42 fn default() -> Self {
43 Self {
44 max_latency_ms: 500,
45 service_name: "default".into(),
46 }
47 }
48}
49
50pub struct HealthAggregator {
52 pub config: HealthCheckConfig,
53 results: Vec<HealthCheckResult>,
54}
55
56pub fn new_aggregator(config: HealthCheckConfig) -> HealthAggregator {
58 HealthAggregator {
59 config,
60 results: Vec::new(),
61 }
62}
63
64pub fn add_result(agg: &mut HealthAggregator, result: HealthCheckResult) {
66 agg.results.push(result);
67}
68
69pub fn aggregate_health(agg: &HealthAggregator) -> HealthReport {
71 let overall = compute_overall(&agg.results, agg.config.max_latency_ms);
72 HealthReport {
73 overall,
74 results: agg.results.clone(),
75 }
76}
77
78fn compute_overall(results: &[HealthCheckResult], max_latency_ms: u64) -> HealthStatus {
79 if results.is_empty() {
80 return HealthStatus::Unknown;
81 }
82 let mut worst = HealthStatus::Healthy;
83 for r in results {
84 let effective = if r.latency_ms > max_latency_ms {
85 HealthStatus::Degraded
86 } else {
87 r.status
88 };
89 worst = worse_of(worst, effective);
90 }
91 worst
92}
93
94fn worse_of(a: HealthStatus, b: HealthStatus) -> HealthStatus {
95 match (a, b) {
96 (HealthStatus::Unhealthy, _) | (_, HealthStatus::Unhealthy) => HealthStatus::Unhealthy,
97 (HealthStatus::Degraded, _) | (_, HealthStatus::Degraded) => HealthStatus::Degraded,
98 (HealthStatus::Unknown, _) | (_, HealthStatus::Unknown) => HealthStatus::Unknown,
99 _ => HealthStatus::Healthy,
100 }
101}
102
103pub fn all_healthy(report: &HealthReport) -> bool {
105 report.overall == HealthStatus::Healthy
106}
107
108pub fn count_by_status(report: &HealthReport, status: HealthStatus) -> usize {
110 report.results.iter().filter(|r| r.status == status).count()
111}
112
113impl HealthAggregator {
114 pub fn new(config: HealthCheckConfig) -> Self {
116 new_aggregator(config)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn make_agg() -> HealthAggregator {
125 new_aggregator(HealthCheckConfig::default())
126 }
127
128 fn result(component: &str, status: HealthStatus, latency: u64) -> HealthCheckResult {
129 HealthCheckResult {
130 component: component.into(),
131 status,
132 message: None,
133 latency_ms: latency,
134 }
135 }
136
137 #[test]
138 fn test_empty_aggregator_reports_unknown() {
139 let agg = make_agg();
140 let report = aggregate_health(&agg);
141 assert_eq!(report.overall, HealthStatus::Unknown);
142 }
143
144 #[test]
145 fn test_single_healthy_component() {
146 let mut agg = make_agg();
147 add_result(&mut agg, result("db", HealthStatus::Healthy, 10));
148 let report = aggregate_health(&agg);
149 assert_eq!(report.overall, HealthStatus::Healthy);
150 }
151
152 #[test]
153 fn test_one_unhealthy_makes_overall_unhealthy() {
154 let mut agg = make_agg();
155 add_result(&mut agg, result("db", HealthStatus::Healthy, 10));
156 add_result(&mut agg, result("cache", HealthStatus::Unhealthy, 10));
157 let report = aggregate_health(&agg);
158 assert_eq!(report.overall, HealthStatus::Unhealthy);
159 }
160
161 #[test]
162 fn test_high_latency_causes_degraded() {
163 let mut agg = make_agg(); add_result(&mut agg, result("api", HealthStatus::Healthy, 600));
165 let report = aggregate_health(&agg);
166 assert_eq!(report.overall, HealthStatus::Degraded);
167 }
168
169 #[test]
170 fn test_all_healthy_returns_true() {
171 let mut agg = make_agg();
172 add_result(&mut agg, result("a", HealthStatus::Healthy, 1));
173 add_result(&mut agg, result("b", HealthStatus::Healthy, 2));
174 let report = aggregate_health(&agg);
175 assert!(all_healthy(&report));
176 }
177
178 #[test]
179 fn test_count_by_status_works() {
180 let mut agg = make_agg();
181 add_result(&mut agg, result("a", HealthStatus::Healthy, 1));
182 add_result(&mut agg, result("b", HealthStatus::Unhealthy, 1));
183 add_result(&mut agg, result("c", HealthStatus::Healthy, 1));
184 let report = aggregate_health(&agg);
185 assert_eq!(count_by_status(&report, HealthStatus::Healthy), 2);
186 assert_eq!(count_by_status(&report, HealthStatus::Unhealthy), 1);
187 }
188
189 #[test]
190 fn test_degraded_overridden_by_unhealthy() {
191 let mut agg = make_agg();
192 add_result(&mut agg, result("a", HealthStatus::Degraded, 1));
193 add_result(&mut agg, result("b", HealthStatus::Unhealthy, 1));
194 let report = aggregate_health(&agg);
195 assert_eq!(report.overall, HealthStatus::Unhealthy);
196 }
197
198 #[test]
199 fn test_report_contains_all_results() {
200 let mut agg = make_agg();
201 add_result(&mut agg, result("x", HealthStatus::Healthy, 5));
202 add_result(&mut agg, result("y", HealthStatus::Healthy, 5));
203 let report = aggregate_health(&agg);
204 assert_eq!(report.results.len(), 2);
205 }
206}