1use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30use std::future::Future;
31use std::pin::Pin;
32use std::sync::Arc;
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36#[serde(rename_all = "lowercase")]
37pub enum HealthStatus {
38 #[serde(rename = "healthy")]
40 Healthy,
41 #[serde(rename = "unhealthy")]
43 Unhealthy { reason: String },
44 #[serde(rename = "degraded")]
46 Degraded { reason: String },
47}
48
49impl HealthStatus {
50 pub fn healthy() -> Self {
52 Self::Healthy
53 }
54
55 pub fn unhealthy(reason: impl Into<String>) -> Self {
57 Self::Unhealthy {
58 reason: reason.into(),
59 }
60 }
61
62 pub fn degraded(reason: impl Into<String>) -> Self {
64 Self::Degraded {
65 reason: reason.into(),
66 }
67 }
68
69 pub fn is_healthy(&self) -> bool {
71 matches!(self, Self::Healthy)
72 }
73
74 pub fn is_unhealthy(&self) -> bool {
76 matches!(self, Self::Unhealthy { .. })
77 }
78
79 pub fn is_degraded(&self) -> bool {
81 matches!(self, Self::Degraded { .. })
82 }
83}
84
85#[derive(Debug, Serialize, Deserialize)]
87pub struct HealthCheckResult {
88 pub status: HealthStatus,
90 pub checks: HashMap<String, HealthStatus>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub version: Option<String>,
95 pub timestamp: String,
97}
98
99pub type HealthCheckFn =
101 Arc<dyn Fn() -> Pin<Box<dyn Future<Output = HealthStatus> + Send>> + Send + Sync>;
102
103#[derive(Clone)]
105pub struct HealthCheck {
106 checks: HashMap<String, HealthCheckFn>,
107 version: Option<String>,
108}
109
110impl HealthCheck {
111 pub async fn execute(&self) -> HealthCheckResult {
113 let mut results = HashMap::new();
114 let mut overall_status = HealthStatus::Healthy;
115
116 for (name, check) in &self.checks {
117 let status = check().await;
118
119 match &status {
121 HealthStatus::Unhealthy { .. } => {
122 overall_status = HealthStatus::unhealthy("one or more checks failed");
123 }
124 HealthStatus::Degraded { .. } => {
125 if overall_status.is_healthy() {
126 overall_status = HealthStatus::degraded("one or more checks degraded");
127 }
128 }
129 _ => {}
130 }
131
132 results.insert(name.clone(), status);
133 }
134
135 let timestamp = std::time::SystemTime::now()
137 .duration_since(std::time::UNIX_EPOCH)
138 .map(|d| {
139 let secs = d.as_secs();
140 let nanos = d.subsec_nanos();
141 format!("{}.{:09}Z", secs, nanos)
142 })
143 .unwrap_or_else(|_| "unknown".to_string());
144
145 HealthCheckResult {
146 status: overall_status,
147 checks: results,
148 version: self.version.clone(),
149 timestamp,
150 }
151 }
152}
153
154pub struct HealthCheckBuilder {
156 checks: HashMap<String, HealthCheckFn>,
157 version: Option<String>,
158}
159
160impl HealthCheckBuilder {
161 pub fn new(include_default: bool) -> Self {
167 let mut checks = HashMap::new();
168
169 if include_default {
170 let check: HealthCheckFn = Arc::new(|| Box::pin(async { HealthStatus::healthy() }));
171 checks.insert("self".to_string(), check);
172 }
173
174 Self {
175 checks,
176 version: None,
177 }
178 }
179
180 pub fn add_check<F, Fut>(mut self, name: impl Into<String>, check: F) -> Self
195 where
196 F: Fn() -> Fut + Send + Sync + 'static,
197 Fut: Future<Output = HealthStatus> + Send + 'static,
198 {
199 let check_fn = Arc::new(move || {
200 Box::pin(check()) as Pin<Box<dyn Future<Output = HealthStatus> + Send>>
201 });
202 self.checks.insert(name.into(), check_fn);
203 self
204 }
205
206 pub fn version(mut self, version: impl Into<String>) -> Self {
208 self.version = Some(version.into());
209 self
210 }
211
212 pub fn build(self) -> HealthCheck {
214 HealthCheck {
215 checks: self.checks,
216 version: self.version,
217 }
218 }
219}
220
221impl Default for HealthCheckBuilder {
222 fn default() -> Self {
223 Self::new(true)
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[tokio::test]
232 async fn health_check_all_healthy() {
233 let health = HealthCheckBuilder::new(false)
234 .add_check("db", || async { HealthStatus::healthy() })
235 .add_check("cache", || async { HealthStatus::healthy() })
236 .version("1.0.0")
237 .build();
238
239 let result = health.execute().await;
240
241 assert!(result.status.is_healthy());
242 assert_eq!(result.checks.len(), 2);
243 assert_eq!(result.version, Some("1.0.0".to_string()));
244 }
245
246 #[tokio::test]
247 async fn health_check_one_unhealthy() {
248 let health = HealthCheckBuilder::new(false)
249 .add_check("db", || async { HealthStatus::healthy() })
250 .add_check("cache", || async {
251 HealthStatus::unhealthy("connection failed")
252 })
253 .build();
254
255 let result = health.execute().await;
256
257 assert!(result.status.is_unhealthy());
258 assert_eq!(result.checks.len(), 2);
259 }
260
261 #[tokio::test]
262 async fn health_check_one_degraded() {
263 let health = HealthCheckBuilder::new(false)
264 .add_check("db", || async { HealthStatus::healthy() })
265 .add_check("cache", || async { HealthStatus::degraded("high latency") })
266 .build();
267
268 let result = health.execute().await;
269
270 assert!(result.status.is_degraded());
271 assert_eq!(result.checks.len(), 2);
272 }
273
274 #[tokio::test]
275 async fn health_check_with_default() {
276 let health = HealthCheckBuilder::new(true).build();
277
278 let result = health.execute().await;
279
280 assert!(result.status.is_healthy());
281 assert_eq!(result.checks.len(), 1);
282 assert!(result.checks.contains_key("self"));
283 }
284}