1use axum::{
23 http::StatusCode,
24 response::{IntoResponse, Response},
25 Json,
26};
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::time::SystemTime;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum HealthStatus {
35 Healthy,
37 Degraded,
39 Unhealthy,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ComponentHealth {
46 pub status: HealthStatus,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub message: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub response_time_ms: Option<u64>,
54}
55
56impl ComponentHealth {
57 #[must_use]
59 pub const fn healthy() -> Self {
60 Self {
61 status: HealthStatus::Healthy,
62 message: None,
63 response_time_ms: None,
64 }
65 }
66
67 #[must_use]
69 pub fn healthy_with_message(message: impl Into<String>) -> Self {
70 Self {
71 status: HealthStatus::Healthy,
72 message: Some(message.into()),
73 response_time_ms: None,
74 }
75 }
76
77 #[must_use]
79 pub fn degraded(message: impl Into<String>) -> Self {
80 Self {
81 status: HealthStatus::Degraded,
82 message: Some(message.into()),
83 response_time_ms: None,
84 }
85 }
86
87 #[must_use]
89 pub fn unhealthy(message: impl Into<String>) -> Self {
90 Self {
91 status: HealthStatus::Unhealthy,
92 message: Some(message.into()),
93 response_time_ms: None,
94 }
95 }
96
97 #[must_use]
99 pub const fn with_response_time(mut self, ms: u64) -> Self {
100 self.response_time_ms = Some(ms);
101 self
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct HealthCheckResponse {
108 pub status: HealthStatus,
110 pub version: String,
112 pub timestamp: u64,
114 pub components: HashMap<String, ComponentHealth>,
116}
117
118impl HealthCheckResponse {
119 #[must_use]
121 pub fn new(version: impl Into<String>) -> Self {
122 Self {
123 status: HealthStatus::Healthy,
124 version: version.into(),
125 timestamp: SystemTime::now()
126 .duration_since(SystemTime::UNIX_EPOCH)
127 .map_or(0, |d| d.as_secs()),
128 components: HashMap::new(),
129 }
130 }
131
132 pub fn add_component(&mut self, name: impl Into<String>, health: ComponentHealth) {
134 self.components.insert(name.into(), health);
135 self.recalculate_status();
136 }
137
138 fn recalculate_status(&mut self) {
140 if self.components.values().any(|c| c.status == HealthStatus::Unhealthy) {
141 self.status = HealthStatus::Unhealthy;
142 } else if self.components.values().any(|c| c.status == HealthStatus::Degraded) {
143 self.status = HealthStatus::Degraded;
144 } else {
145 self.status = HealthStatus::Healthy;
146 }
147 }
148
149 #[must_use]
151 pub const fn status_code(&self) -> StatusCode {
152 match self.status {
153 HealthStatus::Healthy | HealthStatus::Degraded => StatusCode::OK, HealthStatus::Unhealthy => StatusCode::SERVICE_UNAVAILABLE,
155 }
156 }
157}
158
159impl IntoResponse for HealthCheckResponse {
160 fn into_response(self) -> Response {
161 let status = self.status_code();
162 (status, Json(self)).into_response()
163 }
164}
165
166#[allow(clippy::unused_async)]
181pub async fn liveness() -> impl IntoResponse {
182 (StatusCode::OK, "OK")
183}
184
185#[allow(clippy::unused_async)]
201pub async fn readiness() -> impl IntoResponse {
202 let mut response = HealthCheckResponse::new(env!("CARGO_PKG_VERSION"));
203 response.add_component("application", ComponentHealth::healthy());
204 response
205}
206
207#[allow(clippy::unused_async)]
223pub async fn health_check() -> impl IntoResponse {
224 let mut response = HealthCheckResponse::new(env!("CARGO_PKG_VERSION"));
225 response.add_component("application", ComponentHealth::healthy());
226 response
227}
228
229#[allow(clippy::unused_async)]
243pub async fn health_check_with_state<S: Send + Sync>(_state: &S) -> HealthCheckResponse {
244 let mut response = HealthCheckResponse::new(env!("CARGO_PKG_VERSION"));
245 response.add_component("application", ComponentHealth::healthy());
246
247 response
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_component_health_healthy() {
259 let health = ComponentHealth::healthy();
260 assert_eq!(health.status, HealthStatus::Healthy);
261 assert!(health.message.is_none());
262 assert!(health.response_time_ms.is_none());
263 }
264
265 #[test]
266 fn test_component_health_with_message() {
267 let health = ComponentHealth::healthy_with_message("All good");
268 assert_eq!(health.status, HealthStatus::Healthy);
269 assert_eq!(health.message, Some("All good".to_string()));
270 }
271
272 #[test]
273 fn test_component_health_degraded() {
274 let health = ComponentHealth::degraded("High latency");
275 assert_eq!(health.status, HealthStatus::Degraded);
276 assert_eq!(health.message, Some("High latency".to_string()));
277 }
278
279 #[test]
280 fn test_component_health_unhealthy() {
281 let health = ComponentHealth::unhealthy("Connection failed");
282 assert_eq!(health.status, HealthStatus::Unhealthy);
283 assert_eq!(health.message, Some("Connection failed".to_string()));
284 }
285
286 #[test]
287 fn test_component_health_with_response_time() {
288 let health = ComponentHealth::healthy().with_response_time(150);
289 assert_eq!(health.response_time_ms, Some(150));
290 }
291
292 #[test]
293 fn test_health_check_response_new() {
294 let response = HealthCheckResponse::new("1.0.0");
295 assert_eq!(response.status, HealthStatus::Healthy);
296 assert_eq!(response.version, "1.0.0");
297 assert!(response.components.is_empty());
298 }
299
300 #[test]
301 fn test_health_check_response_add_component() {
302 let mut response = HealthCheckResponse::new("1.0.0");
303 response.add_component("database", ComponentHealth::healthy());
304 assert_eq!(response.components.len(), 1);
305 assert_eq!(response.status, HealthStatus::Healthy);
306 }
307
308 #[test]
309 fn test_health_check_response_degraded_status() {
310 let mut response = HealthCheckResponse::new("1.0.0");
311 response.add_component("app", ComponentHealth::healthy());
312 response.add_component("cache", ComponentHealth::degraded("High latency"));
313 assert_eq!(response.status, HealthStatus::Degraded);
314 assert_eq!(response.status_code(), StatusCode::OK);
315 }
316
317 #[test]
318 fn test_health_check_response_unhealthy_status() {
319 let mut response = HealthCheckResponse::new("1.0.0");
320 response.add_component("app", ComponentHealth::healthy());
321 response.add_component("database", ComponentHealth::unhealthy("Connection failed"));
322 assert_eq!(response.status, HealthStatus::Unhealthy);
323 assert_eq!(response.status_code(), StatusCode::SERVICE_UNAVAILABLE);
324 }
325
326 #[tokio::test]
327 async fn test_liveness_handler() {
328 let response = liveness().await.into_response();
329 assert_eq!(response.status(), StatusCode::OK);
330 }
331
332 #[tokio::test]
333 async fn test_readiness_handler() {
334 let response = readiness().await.into_response();
335 assert_eq!(response.status(), StatusCode::OK);
336 }
337
338 #[tokio::test]
339 async fn test_health_check_handler() {
340 let response = health_check().await.into_response();
341 assert_eq!(response.status(), StatusCode::OK);
342 }
343}