1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::lifecycle::{HealthStatus, ServiceStatus};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HealthReport {
15 pub status: HealthStatus,
16 pub services: Vec<ServiceHealth>,
17 pub timestamp: DateTime<Utc>,
18}
19
20impl Default for HealthReport {
21 fn default() -> Self {
22 Self {
23 status: HealthStatus::Healthy,
24 services: Vec::new(),
25 timestamp: Utc::now(),
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ServiceHealth {
33 pub name: String,
34 pub status: ServiceStatus,
35 #[serde(default)]
36 pub message: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CheckResult {
41 pub name: String,
42 pub status: HealthStatus,
43 pub message: Option<String>,
44}
45
46impl CheckResult {
47 pub fn healthy(name: &str) -> Self {
48 Self {
49 name: name.to_string(),
50 status: HealthStatus::Healthy,
51 message: None,
52 }
53 }
54
55 pub fn unhealthy(name: &str, reason: &str) -> Self {
56 Self {
57 name: name.to_string(),
58 status: HealthStatus::Unhealthy,
59 message: Some(reason.to_string()),
60 }
61 }
62
63 pub fn degraded(name: &str, reason: &str) -> Self {
64 Self {
65 name: name.to_string(),
66 status: HealthStatus::Degraded,
67 message: Some(reason.to_string()),
68 }
69 }
70}
71
72#[async_trait]
73pub trait AsyncHealthCheck: Send + Sync {
74 fn name(&self) -> &str;
75 async fn check(&self) -> CheckResult;
76}
77
78#[async_trait]
82pub trait HealthSource: Send + Sync {
83 async fn liveness(&self) -> HealthStatus;
84 async fn readiness(&self) -> HealthStatus;
85
86 async fn health_report(&self) -> HealthReport {
87 HealthReport {
88 status: self.readiness().await,
89 services: vec![],
90 timestamp: chrono::Utc::now(),
91 }
92 }
93
94 async fn liveness_report(&self) -> HealthReport {
98 HealthReport {
99 status: self.liveness().await,
100 ..Default::default()
101 }
102 }
103
104 async fn readiness_report(&self) -> HealthReport {
108 HealthReport {
109 status: self.readiness().await,
110 ..Default::default()
111 }
112 }
113
114 async fn startup_report(&self) -> HealthReport {
118 HealthReport {
119 status: self.startup().await,
120 ..Default::default()
121 }
122 }
123
124 async fn startup(&self) -> HealthStatus {
126 HealthStatus::Healthy
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[tokio::test]
135 async fn test_health_source_default_startup() {
136 struct MinimalSource;
137 #[async_trait]
138 impl HealthSource for MinimalSource {
139 async fn liveness(&self) -> HealthStatus {
140 HealthStatus::Healthy
141 }
142
143 async fn readiness(&self) -> HealthStatus {
144 HealthStatus::Healthy
145 }
146 }
147 let s = MinimalSource;
148 assert_eq!(s.startup().await, HealthStatus::Healthy);
149 }
150
151 #[tokio::test]
152 async fn test_health_source_custom_startup() {
153 struct BootingSource;
154 #[async_trait]
155 impl HealthSource for BootingSource {
156 async fn liveness(&self) -> HealthStatus {
157 HealthStatus::Healthy
158 }
159
160 async fn readiness(&self) -> HealthStatus {
161 HealthStatus::Healthy
162 }
163
164 async fn startup(&self) -> HealthStatus {
165 HealthStatus::Unhealthy
166 }
167 }
168 let s = BootingSource;
169 assert_eq!(s.startup().await, HealthStatus::Unhealthy);
170 }
171
172 #[test]
173 fn test_health_report_serialization() {
174 let report = HealthReport {
175 status: HealthStatus::Healthy,
176 services: vec![ServiceHealth {
177 name: "prometheus".to_string(),
178 status: ServiceStatus::Started,
179 message: None,
180 }],
181 timestamp: chrono::Utc::now(),
182 };
183
184 let json = serde_json::to_string(&report).unwrap();
185 assert!(json.contains("Healthy"));
186 assert!(json.contains("prometheus"));
187 assert!(json.contains("Started"));
188 assert!(json.contains("timestamp"));
189 }
190
191 #[test]
192 fn test_health_report_default() {
193 let report = HealthReport::default();
194 assert_eq!(report.status, HealthStatus::Healthy);
195 assert!(report.services.is_empty());
196 assert!(report.timestamp <= chrono::Utc::now());
197 }
198
199 #[test]
200 fn test_service_health_serialization_round_trip() {
201 let svc = ServiceHealth {
202 name: "kafka".to_string(),
203 status: ServiceStatus::Stopped,
204 message: None,
205 };
206
207 let json = serde_json::to_string(&svc).unwrap();
208 assert!(json.contains("kafka"));
209 assert!(json.contains("Stopped"));
210
211 let decoded: ServiceHealth = serde_json::from_str(&json).unwrap();
212 assert_eq!(decoded.name, "kafka");
213 assert_eq!(decoded.status, ServiceStatus::Stopped);
214 assert!(decoded.message.is_none());
215 }
216
217 #[tokio::test]
218 async fn test_health_source_liveness_and_readiness() {
219 struct MixedSource;
220 #[async_trait]
221 impl HealthSource for MixedSource {
222 async fn liveness(&self) -> HealthStatus {
223 HealthStatus::Healthy
224 }
225
226 async fn readiness(&self) -> HealthStatus {
227 HealthStatus::Unhealthy
228 }
229 }
230
231 let s = MixedSource;
232 assert_eq!(s.liveness().await, HealthStatus::Healthy);
233 assert_eq!(s.readiness().await, HealthStatus::Unhealthy);
234 }
235
236 #[test]
237 fn test_health_report_serialization_round_trip() {
238 let report = HealthReport {
239 status: HealthStatus::Unhealthy,
240 services: vec![
241 ServiceHealth {
242 name: "db".to_string(),
243 status: ServiceStatus::Started,
244 message: None,
245 },
246 ServiceHealth {
247 name: "queue".to_string(),
248 status: ServiceStatus::Stopped,
249 message: None,
250 },
251 ],
252 timestamp: chrono::Utc::now(),
253 };
254
255 let json = serde_json::to_string(&report).unwrap();
256 let back: HealthReport = serde_json::from_str(&json).unwrap();
257 assert_eq!(back.status, HealthStatus::Unhealthy);
258 assert_eq!(back.services.len(), 2);
259 assert_eq!(back.services[0].name, "db");
260 assert_eq!(back.services[1].status, ServiceStatus::Stopped);
261 }
262
263 #[tokio::test]
264 async fn test_health_source_startup_override_independent_from_readiness() {
265 struct StartupOnly;
266 #[async_trait]
267 impl HealthSource for StartupOnly {
268 async fn liveness(&self) -> HealthStatus {
269 HealthStatus::Healthy
270 }
271
272 async fn readiness(&self) -> HealthStatus {
273 HealthStatus::Unhealthy
274 }
275
276 async fn startup(&self) -> HealthStatus {
277 HealthStatus::Healthy
278 }
279 }
280
281 let source = StartupOnly;
282 assert_eq!(source.liveness().await, HealthStatus::Healthy);
283 assert_eq!(source.readiness().await, HealthStatus::Unhealthy);
284 assert_eq!(source.startup().await, HealthStatus::Healthy);
285 }
286
287 #[test]
288 fn test_check_result_healthy() {
289 let r = CheckResult::healthy("kafka");
290 assert_eq!(r.name, "kafka");
291 assert_eq!(r.status, HealthStatus::Healthy);
292 assert!(r.message.is_none());
293 }
294
295 #[test]
296 fn test_check_result_unhealthy() {
297 let r = CheckResult::unhealthy("redis", "connection refused");
298 assert_eq!(r.name, "redis");
299 assert_eq!(r.status, HealthStatus::Unhealthy);
300 assert_eq!(r.message.as_deref(), Some("connection refused"));
301 }
302
303 #[test]
304 fn test_check_result_degraded() {
305 let r = CheckResult::degraded("sql", "pool exhausted");
306 assert_eq!(r.name, "sql");
307 assert_eq!(r.status, HealthStatus::Degraded);
308 assert_eq!(r.message.as_deref(), Some("pool exhausted"));
309 }
310
311 #[test]
312 fn test_check_result_serialization_round_trip() {
313 let r = CheckResult::unhealthy("opensearch", "timeout");
314 let json = serde_json::to_string(&r).unwrap();
315 let back: CheckResult = serde_json::from_str(&json).unwrap();
316 assert_eq!(back.name, "opensearch");
317 assert_eq!(back.status, HealthStatus::Unhealthy);
318 assert_eq!(back.message.as_deref(), Some("timeout"));
319 }
320
321 #[tokio::test]
322 async fn report_defaults_mirror_bare_status() {
323 struct MinimalSource;
324 #[async_trait]
325 impl HealthSource for MinimalSource {
326 async fn liveness(&self) -> HealthStatus {
327 HealthStatus::Healthy
328 }
329
330 async fn readiness(&self) -> HealthStatus {
331 HealthStatus::Healthy
332 }
333 }
334
335 let s = MinimalSource;
336 for report in [
337 s.liveness_report().await,
338 s.readiness_report().await,
339 s.startup_report().await,
340 ] {
341 assert_eq!(report.status, HealthStatus::Healthy);
342 assert!(report.services.is_empty());
343 }
344 }
345
346 #[tokio::test]
347 async fn report_defaults_carry_unhealthy_status() {
348 struct UnreadySource;
349 #[async_trait]
350 impl HealthSource for UnreadySource {
351 async fn liveness(&self) -> HealthStatus {
352 HealthStatus::Healthy
353 }
354
355 async fn readiness(&self) -> HealthStatus {
356 HealthStatus::Unhealthy
357 }
358 }
359
360 let report = UnreadySource.readiness_report().await;
361 assert_eq!(report.status, HealthStatus::Unhealthy);
362 assert!(report.services.is_empty());
363 }
364}