hyperi_rustlib/health/
registry.rs1use std::sync::{Arc, Mutex, OnceLock};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum HealthStatus {
28 Healthy,
30 Degraded,
33 Unhealthy,
35}
36
37impl HealthStatus {
38 #[must_use]
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Healthy => "healthy",
43 Self::Degraded => "degraded",
44 Self::Unhealthy => "unhealthy",
45 }
46 }
47}
48
49type HealthCheck = Arc<dyn Fn() -> HealthStatus + Send + Sync>;
51
52struct HealthEntry {
54 name: String,
55 check: HealthCheck,
56}
57
58pub struct HealthRegistry {
69 components: Mutex<Vec<HealthEntry>>,
70}
71
72static REGISTRY: OnceLock<HealthRegistry> = OnceLock::new();
74
75impl HealthRegistry {
76 fn new() -> Self {
78 Self {
79 components: Mutex::new(Vec::new()),
80 }
81 }
82
83 fn global() -> &'static Self {
85 REGISTRY.get_or_init(Self::new)
86 }
87
88 pub fn register(
99 name: impl Into<String>,
100 check: impl Fn() -> HealthStatus + Send + Sync + 'static,
101 ) {
102 let registry = Self::global();
103 if let Ok(mut components) = registry.components.lock() {
104 components.push(HealthEntry {
105 name: name.into(),
106 check: Arc::new(check),
107 });
108 }
109 }
110
111 #[must_use]
116 pub fn is_healthy() -> bool {
117 let registry = Self::global();
118 let Ok(components) = registry.components.lock() else {
119 return false;
120 };
121 components
122 .iter()
123 .all(|c| (c.check)() == HealthStatus::Healthy)
124 }
125
126 #[must_use]
134 pub fn is_ready() -> bool {
135 let registry = Self::global();
136 let Ok(components) = registry.components.lock() else {
137 return false;
138 };
139 components
140 .iter()
141 .all(|c| (c.check)() != HealthStatus::Unhealthy)
142 }
143
144 #[must_use]
149 pub fn components() -> Vec<(String, HealthStatus)> {
150 let registry = Self::global();
151 let Ok(components) = registry.components.lock() else {
152 return Vec::new();
153 };
154 components
155 .iter()
156 .map(|c| (c.name.clone(), (c.check)()))
157 .collect()
158 }
159
160 #[cfg(feature = "serde_json")]
164 #[must_use]
165 pub fn to_json() -> serde_json::Value {
166 let components = Self::components();
167 let overall = if Self::is_healthy() {
168 "healthy"
169 } else if Self::is_ready() {
170 "degraded"
171 } else {
172 "unhealthy"
173 };
174
175 serde_json::json!({
176 "status": overall,
177 "components": components.iter().map(|(name, status)| {
178 serde_json::json!({
179 "name": name,
180 "status": status.as_str(),
181 })
182 }).collect::<Vec<_>>()
183 })
184 }
185
186 #[cfg(test)]
188 pub(crate) fn reset() {
189 let registry = Self::global();
190 if let Ok(mut components) = registry.components.lock() {
191 components.clear();
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use std::sync::atomic::{AtomicU8, Ordering};
199
200 use super::*;
201
202 static TEST_LOCK: Mutex<()> = Mutex::new(());
204
205 macro_rules! serial_test {
206 () => {
207 let _guard = TEST_LOCK.lock().unwrap();
208 HealthRegistry::reset();
209 };
210 }
211
212 #[test]
213 fn empty_registry_is_healthy() {
214 serial_test!();
215
216 assert!(HealthRegistry::is_healthy());
217 assert!(HealthRegistry::is_ready());
218 assert!(HealthRegistry::components().is_empty());
219 }
220
221 #[test]
222 fn register_and_check_healthy() {
223 serial_test!();
224
225 HealthRegistry::register("transport", || HealthStatus::Healthy);
226 HealthRegistry::register("database", || HealthStatus::Healthy);
227
228 assert!(HealthRegistry::is_healthy());
229 assert!(HealthRegistry::is_ready());
230
231 let components = HealthRegistry::components();
232 assert_eq!(components.len(), 2);
233 assert_eq!(components[0].0, "transport");
234 assert_eq!(components[0].1, HealthStatus::Healthy);
235 assert_eq!(components[1].0, "database");
236 assert_eq!(components[1].1, HealthStatus::Healthy);
237 }
238
239 #[test]
240 fn unhealthy_component_fails_check() {
241 serial_test!();
242
243 HealthRegistry::register("transport", || HealthStatus::Healthy);
244 HealthRegistry::register("database", || HealthStatus::Unhealthy);
245
246 assert!(!HealthRegistry::is_healthy());
247 assert!(!HealthRegistry::is_ready());
248 }
249
250 #[test]
251 fn degraded_is_ready_but_not_healthy() {
252 serial_test!();
253
254 HealthRegistry::register("transport", || HealthStatus::Healthy);
255 HealthRegistry::register("circuit_breaker", || HealthStatus::Degraded);
256
257 assert!(!HealthRegistry::is_healthy());
258 assert!(HealthRegistry::is_ready());
259 }
260
261 #[test]
262 fn dynamic_health_check_reflects_state_changes() {
263 serial_test!();
264
265 let state = Arc::new(AtomicU8::new(0)); let state_clone = state.clone();
268
269 HealthRegistry::register("dynamic", move || {
270 match state_clone.load(Ordering::Relaxed) {
271 0 => HealthStatus::Healthy,
272 1 => HealthStatus::Degraded,
273 _ => HealthStatus::Unhealthy,
274 }
275 });
276
277 assert!(HealthRegistry::is_healthy());
279 assert!(HealthRegistry::is_ready());
280
281 state.store(1, Ordering::Relaxed);
283 assert!(!HealthRegistry::is_healthy());
284 assert!(HealthRegistry::is_ready());
285
286 state.store(2, Ordering::Relaxed);
288 assert!(!HealthRegistry::is_healthy());
289 assert!(!HealthRegistry::is_ready());
290
291 state.store(0, Ordering::Relaxed);
293 assert!(HealthRegistry::is_healthy());
294 assert!(HealthRegistry::is_ready());
295 }
296
297 #[test]
298 fn health_status_as_str() {
299 assert_eq!(HealthStatus::Healthy.as_str(), "healthy");
300 assert_eq!(HealthStatus::Degraded.as_str(), "degraded");
301 assert_eq!(HealthStatus::Unhealthy.as_str(), "unhealthy");
302 }
303
304 #[test]
305 #[cfg(feature = "serde_json")]
306 fn to_json_includes_all_components() {
307 serial_test!();
308
309 HealthRegistry::register("kafka", || HealthStatus::Healthy);
310 HealthRegistry::register("clickhouse", || HealthStatus::Degraded);
311
312 let json = HealthRegistry::to_json();
313
314 assert_eq!(json["status"], "degraded");
315
316 let components = json["components"].as_array().unwrap();
317 assert_eq!(components.len(), 2);
318
319 assert_eq!(components[0]["name"], "kafka");
320 assert_eq!(components[0]["status"], "healthy");
321
322 assert_eq!(components[1]["name"], "clickhouse");
323 assert_eq!(components[1]["status"], "degraded");
324 }
325
326 #[test]
327 #[cfg(feature = "serde_json")]
328 fn to_json_empty_registry() {
329 serial_test!();
330
331 let json = HealthRegistry::to_json();
332 assert_eq!(json["status"], "healthy");
333 assert!(json["components"].as_array().unwrap().is_empty());
334 }
335
336 #[test]
337 #[cfg(feature = "serde_json")]
338 fn to_json_unhealthy_status() {
339 serial_test!();
340
341 HealthRegistry::register("broken", || HealthStatus::Unhealthy);
342
343 let json = HealthRegistry::to_json();
344 assert_eq!(json["status"], "unhealthy");
345 }
346}