1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::sync::Arc;
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}
36
37pub type HealthChecker = Arc<dyn Fn() -> HealthReport + Send + Sync>;
38
39pub trait HealthSource: Send + Sync {
43 fn liveness(&self) -> HealthStatus;
44 fn readiness(&self) -> HealthStatus;
45
46 fn startup(&self) -> HealthStatus {
48 HealthStatus::Healthy
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn test_health_source_default_startup() {
58 struct MinimalSource;
59 impl HealthSource for MinimalSource {
60 fn liveness(&self) -> HealthStatus {
61 HealthStatus::Healthy
62 }
63
64 fn readiness(&self) -> HealthStatus {
65 HealthStatus::Healthy
66 }
67 }
68 let s = MinimalSource;
69 assert_eq!(s.startup(), HealthStatus::Healthy);
70 }
71
72 #[test]
73 fn test_health_source_custom_startup() {
74 struct BootingSource;
75 impl HealthSource for BootingSource {
76 fn liveness(&self) -> HealthStatus {
77 HealthStatus::Healthy
78 }
79
80 fn readiness(&self) -> HealthStatus {
81 HealthStatus::Healthy
82 }
83
84 fn startup(&self) -> HealthStatus {
85 HealthStatus::Unhealthy
86 }
87 }
88 let s = BootingSource;
89 assert_eq!(s.startup(), HealthStatus::Unhealthy);
90 }
91
92 #[test]
93 fn test_health_report_serialization() {
94 let report = HealthReport {
95 status: HealthStatus::Healthy,
96 services: vec![ServiceHealth {
97 name: "prometheus".to_string(),
98 status: ServiceStatus::Started,
99 }],
100 timestamp: chrono::Utc::now(),
101 };
102
103 let json = serde_json::to_string(&report).unwrap();
104 assert!(json.contains("Healthy"));
105 assert!(json.contains("prometheus"));
106 assert!(json.contains("Started"));
107 assert!(json.contains("timestamp"));
108 }
109
110 #[test]
111 fn test_health_report_default() {
112 let report = HealthReport::default();
113 assert_eq!(report.status, HealthStatus::Healthy);
114 assert!(report.services.is_empty());
115 assert!(report.timestamp <= chrono::Utc::now());
116 }
117
118 #[test]
119 fn test_service_health_serialization_round_trip() {
120 let svc = ServiceHealth {
121 name: "kafka".to_string(),
122 status: ServiceStatus::Stopped,
123 };
124
125 let json = serde_json::to_string(&svc).unwrap();
126 assert!(json.contains("kafka"));
127 assert!(json.contains("Stopped"));
128
129 let decoded: ServiceHealth = serde_json::from_str(&json).unwrap();
130 assert_eq!(decoded.name, "kafka");
131 assert_eq!(decoded.status, ServiceStatus::Stopped);
132 }
133
134 #[test]
135 fn test_health_checker_type_alias_invocation() {
136 let checker: HealthChecker = Arc::new(|| HealthReport {
137 status: HealthStatus::Unhealthy,
138 services: vec![ServiceHealth {
139 name: "db".to_string(),
140 status: ServiceStatus::Started,
141 }],
142 timestamp: chrono::Utc::now(),
143 });
144
145 let report = checker();
146 assert_eq!(report.status, HealthStatus::Unhealthy);
147 assert_eq!(report.services.len(), 1);
148 assert_eq!(report.services[0].name, "db");
149 }
150
151 #[test]
152 fn test_health_source_liveness_and_readiness() {
153 struct MixedSource;
154 impl HealthSource for MixedSource {
155 fn liveness(&self) -> HealthStatus {
156 HealthStatus::Healthy
157 }
158
159 fn readiness(&self) -> HealthStatus {
160 HealthStatus::Unhealthy
161 }
162 }
163
164 let s = MixedSource;
165 assert_eq!(s.liveness(), HealthStatus::Healthy);
166 assert_eq!(s.readiness(), HealthStatus::Unhealthy);
167 }
168
169 #[test]
170 fn test_health_report_serialization_round_trip() {
171 let report = HealthReport {
172 status: HealthStatus::Unhealthy,
173 services: vec![
174 ServiceHealth {
175 name: "db".to_string(),
176 status: ServiceStatus::Started,
177 },
178 ServiceHealth {
179 name: "queue".to_string(),
180 status: ServiceStatus::Stopped,
181 },
182 ],
183 timestamp: chrono::Utc::now(),
184 };
185
186 let json = serde_json::to_string(&report).unwrap();
187 let back: HealthReport = serde_json::from_str(&json).unwrap();
188 assert_eq!(back.status, HealthStatus::Unhealthy);
189 assert_eq!(back.services.len(), 2);
190 assert_eq!(back.services[0].name, "db");
191 assert_eq!(back.services[1].status, ServiceStatus::Stopped);
192 }
193
194 #[test]
195 fn test_health_checker_can_be_cloned_and_reused() {
196 let checker: HealthChecker = Arc::new(HealthReport::default);
197 let checker2 = Arc::clone(&checker);
198 let r1 = checker();
199 let r2 = checker2();
200 assert_eq!(r1.status, HealthStatus::Healthy);
201 assert_eq!(r2.status, HealthStatus::Healthy);
202 }
203
204 #[test]
205 fn test_health_source_startup_override_independent_from_readiness() {
206 struct StartupOnly;
207 impl HealthSource for StartupOnly {
208 fn liveness(&self) -> HealthStatus {
209 HealthStatus::Healthy
210 }
211
212 fn readiness(&self) -> HealthStatus {
213 HealthStatus::Unhealthy
214 }
215
216 fn startup(&self) -> HealthStatus {
217 HealthStatus::Healthy
218 }
219 }
220
221 let source = StartupOnly;
222 assert_eq!(source.liveness(), HealthStatus::Healthy);
223 assert_eq!(source.readiness(), HealthStatus::Unhealthy);
224 assert_eq!(source.startup(), HealthStatus::Healthy);
225 }
226}