Skip to main content

oxihuman_core/
health_check.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Health check result aggregator — collects and summarizes component health.
6
7/// Overall health status.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum HealthStatus {
10    Healthy,
11    Degraded,
12    Unhealthy,
13    Unknown,
14}
15
16/// A single component's health check result.
17#[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/// Aggregated health report across all components.
26#[derive(Clone, Debug)]
27pub struct HealthReport {
28    pub overall: HealthStatus,
29    pub results: Vec<HealthCheckResult>,
30}
31
32/// Configuration for the health check aggregator.
33#[derive(Clone, Debug)]
34pub struct HealthCheckConfig {
35    /// Maximum acceptable latency in ms before marking degraded.
36    pub max_latency_ms: u64,
37    /// Name of this service.
38    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
50/// A health check aggregator that collects component results.
51pub struct HealthAggregator {
52    pub config: HealthCheckConfig,
53    results: Vec<HealthCheckResult>,
54}
55
56/// Creates a new health aggregator.
57pub fn new_aggregator(config: HealthCheckConfig) -> HealthAggregator {
58    HealthAggregator {
59        config,
60        results: Vec::new(),
61    }
62}
63
64/// Adds a health check result for a component.
65pub fn add_result(agg: &mut HealthAggregator, result: HealthCheckResult) {
66    agg.results.push(result);
67}
68
69/// Computes the overall health from all recorded results.
70pub 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
103/// Returns true if all components are healthy.
104pub fn all_healthy(report: &HealthReport) -> bool {
105    report.overall == HealthStatus::Healthy
106}
107
108/// Counts components by status.
109pub fn count_by_status(report: &HealthReport, status: HealthStatus) -> usize {
110    report.results.iter().filter(|r| r.status == status).count()
111}
112
113impl HealthAggregator {
114    /// Creates a new aggregator with default config.
115    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(); /* max_latency = 500ms */
164        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}