1use crate::response::ApiResponse;
27use axum::response::{IntoResponse, Response};
28use axum::routing::get;
29use axum::Router;
30use parking_lot::Mutex;
31use serde_json::{json, Value};
32use std::sync::Arc;
33use std::time::{Duration, Instant};
34
35pub const DEFAULT_CHECK_TIMEOUT: Duration = Duration::from_secs(3);
37
38pub trait HealthCheck: Send + Sync {
42 fn name(&self) -> &str;
44
45 fn check(&self) -> Result<(), String>;
47}
48
49#[derive(Clone)]
53pub struct HealthRegistry {
54 checks: Arc<Mutex<Vec<Arc<dyn HealthCheck>>>>,
55 timeout: Duration,
57}
58
59impl Default for HealthRegistry {
60 fn default() -> Self {
61 Self {
62 checks: Arc::new(Mutex::new(Vec::new())),
63 timeout: DEFAULT_CHECK_TIMEOUT,
64 }
65 }
66}
67
68impl HealthRegistry {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn with_timeout(timeout: Duration) -> Self {
76 Self {
77 checks: Arc::new(Mutex::new(Vec::new())),
78 timeout,
79 }
80 }
81
82 pub fn register<C: HealthCheck + 'static>(&self, check: C) -> &Self {
84 self.checks.lock().push(Arc::new(check));
85 self
86 }
87
88 pub fn liveness(&self) -> Response {
90 ApiResponse::success(json!({"status": "ok"}), "ok").into_response()
91 }
92
93 pub async fn readiness(&self) -> Response {
104 let checks = self.checks.lock().clone();
105 let mut results = serde_json::Map::new();
106 let mut all_ok = true;
107
108 for check in &checks {
109 let name = check.name().to_string();
110 let started = Instant::now();
111 let result = self.run_with_timeout(check).await;
112 let elapsed_ms = started.elapsed().as_millis();
113
114 match result {
115 Ok(()) => {
116 results.insert(name, json!({"status": "ok", "elapsed_ms": elapsed_ms}));
117 }
118 Err(err) => {
119 all_ok = false;
120 results.insert(
121 name,
122 json!({"status": "fail", "error": err, "elapsed_ms": elapsed_ms}),
123 );
124 }
125 }
126 }
127
128 let data = json!({
129 "status": if all_ok { "ok" } else { "fail" },
130 "checks": Value::Object(results),
131 });
132
133 if all_ok {
134 ApiResponse::success(data, "ok").into_response()
135 } else {
136 ApiResponse::error_with_data("health check failed", data).into_response()
137 }
138 }
139
140 async fn run_with_timeout(&self, check: &Arc<dyn HealthCheck>) -> Result<(), String> {
145 if self.timeout.is_zero() {
147 let check_clone = Arc::clone(check);
148 return tokio::task::spawn_blocking(move || check_clone.check())
149 .await
150 .unwrap_or_else(|_| Err("check thread panicked".to_string()));
151 }
152
153 let check_clone = Arc::clone(check);
154 let timeout = self.timeout;
155 match tokio::time::timeout(
156 timeout,
157 tokio::task::spawn_blocking(move || check_clone.check()),
158 )
159 .await
160 {
161 Ok(Ok(result)) => result,
162 Ok(Err(join_err)) => Err(format!("check thread panicked: {join_err}")),
163 Err(_) => Err(format!("timeout after {}ms", timeout.as_millis())),
164 }
165 }
166
167 pub fn router_at(&self, prefix: &str) -> Router {
172 let liveness_self = self.clone();
173 let readiness_self = self.clone();
174 let liveness_path = format!("{prefix}/");
175 let readiness_path = format!("{prefix}/ready");
176 Router::new()
177 .route(
178 &liveness_path,
179 get(move || {
180 let this = liveness_self.clone();
181 std::future::ready(this.liveness())
182 }),
183 )
184 .route(
185 &readiness_path,
186 get(move || {
187 let this = readiness_self.clone();
188 async move { this.readiness().await }
189 }),
190 )
191 }
192}
193
194pub fn default_health_router() -> Router {
196 HealthRegistry::new().router_at("/health")
197}
198
199pub struct StaticCheck {
205 name: String,
206 ok: bool,
207}
208
209impl StaticCheck {
210 pub fn ok(name: impl Into<String>) -> Self {
212 Self {
213 name: name.into(),
214 ok: true,
215 }
216 }
217 pub fn fail(name: impl Into<String>) -> Self {
219 Self {
220 name: name.into(),
221 ok: false,
222 }
223 }
224}
225
226impl HealthCheck for StaticCheck {
227 fn name(&self) -> &str {
228 &self.name
229 }
230 fn check(&self) -> Result<(), String> {
231 if self.ok {
232 Ok(())
233 } else {
234 Err("static check failed".to_string())
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use axum::body::Body;
243 use axum::http::{Method, Request, StatusCode};
244 use http_body_util::BodyExt;
245 use tower::ServiceExt;
246
247 async fn fetch_body(resp: Response) -> Value {
248 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
249 serde_json::from_slice(&bytes).unwrap()
250 }
251
252 async fn send_get(router: Router, uri: &str) -> Response {
253 let req = Request::builder()
254 .method(Method::GET)
255 .uri(uri)
256 .body(Body::empty())
257 .unwrap();
258 router.oneshot(req).await.unwrap()
259 }
260
261 #[tokio::test]
266 async fn test_liveness_returns_200() {
267 let registry = HealthRegistry::new();
268 let resp = registry.liveness();
269 assert_eq!(resp.status(), StatusCode::OK);
270 }
271
272 #[tokio::test]
273 async fn test_liveness_response_body() {
274 let registry = HealthRegistry::new();
275 let resp = registry.liveness();
276 let json = fetch_body(resp).await;
277 assert_eq!(json["code"], 1);
278 assert_eq!(json["msg"], "ok");
279 assert_eq!(json["data"]["status"], "ok");
280 }
281
282 #[tokio::test]
283 async fn test_liveness_content_type() {
284 let registry = HealthRegistry::new();
285 let resp = registry.liveness();
286 assert_eq!(
287 resp.headers().get("content-type").unwrap(),
288 "application/json; charset=utf-8"
289 );
290 }
291
292 #[tokio::test]
297 async fn test_readiness_empty_registry_returns_ok() {
298 let registry = HealthRegistry::new();
299 let resp = registry.readiness().await;
300 assert_eq!(resp.status(), StatusCode::OK);
301 let json = fetch_body(resp).await;
302 assert_eq!(json["code"], 1);
303 assert_eq!(json["data"]["status"], "ok");
304 assert!(json["data"]["checks"].is_object());
305 }
306
307 #[tokio::test]
312 async fn test_readiness_all_checks_pass() {
313 let registry = HealthRegistry::new();
314 registry.register(StaticCheck::ok("database"));
315 registry.register(StaticCheck::ok("redis"));
316
317 let resp = registry.readiness().await;
318 assert_eq!(resp.status(), StatusCode::OK);
319 let json = fetch_body(resp).await;
320 assert_eq!(json["code"], 1);
321 assert_eq!(json["data"]["status"], "ok");
322 assert_eq!(json["data"]["checks"]["database"]["status"], "ok");
323 assert_eq!(json["data"]["checks"]["redis"]["status"], "ok");
324 }
325
326 #[tokio::test]
327 async fn test_readiness_one_check_fails() {
328 let registry = HealthRegistry::new();
329 registry.register(StaticCheck::ok("database"));
330 registry.register(StaticCheck::fail("redis"));
331
332 let resp = registry.readiness().await;
333 assert_eq!(resp.status(), StatusCode::OK);
334 let json = fetch_body(resp).await;
335 assert_eq!(json["code"], 0);
336 assert_eq!(json["data"]["status"], "fail");
337 assert_eq!(json["data"]["checks"]["database"]["status"], "ok");
338 assert_eq!(json["data"]["checks"]["redis"]["status"], "fail");
339 assert!(json["data"]["checks"]["redis"]["error"].is_string());
340 }
341
342 #[tokio::test]
343 async fn test_readiness_all_checks_fail() {
344 let registry = HealthRegistry::new();
345 registry.register(StaticCheck::fail("db1"));
346 registry.register(StaticCheck::fail("db2"));
347
348 let resp = registry.readiness().await;
349 let json = fetch_body(resp).await;
350 assert_eq!(json["code"], 0);
351 assert_eq!(json["data"]["status"], "fail");
352 }
353
354 #[tokio::test]
359 async fn test_router_liveness_endpoint() {
360 let router = default_health_router();
361 let resp = send_get(router, "/health/").await;
362 assert_eq!(resp.status(), StatusCode::OK);
363 let json = fetch_body(resp).await;
364 assert_eq!(json["data"]["status"], "ok");
365 }
366
367 #[tokio::test]
368 async fn test_router_readiness_endpoint() {
369 let registry = HealthRegistry::new();
370 registry.register(StaticCheck::ok("db"));
371 let router = registry.router_at("/health");
372
373 let resp = send_get(router, "/health/ready").await;
374 assert_eq!(resp.status(), StatusCode::OK);
375 let json = fetch_body(resp).await;
376 assert_eq!(json["code"], 1);
377 assert_eq!(json["data"]["checks"]["db"]["status"], "ok");
378 }
379
380 #[tokio::test]
381 async fn test_router_unknown_path_returns_404() {
382 let router = default_health_router();
383 let resp = send_get(router, "/health/unknown").await;
384 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
385 }
386
387 struct SlowCheck {
392 name: String,
393 delay: Duration,
394 }
395
396 impl HealthCheck for SlowCheck {
397 fn name(&self) -> &str {
398 &self.name
399 }
400 fn check(&self) -> Result<(), String> {
401 std::thread::sleep(self.delay);
402 Ok(())
403 }
404 }
405
406 #[tokio::test]
407 async fn test_readiness_timeout_handled() {
408 let registry = HealthRegistry::with_timeout(Duration::from_millis(100));
409 registry.register(SlowCheck {
410 name: "slow".to_string(),
411 delay: Duration::from_millis(500),
412 });
413
414 let resp = registry.readiness().await;
415 let json = fetch_body(resp).await;
416 assert_eq!(json["code"], 0);
417 assert_eq!(json["data"]["status"], "fail");
418 assert_eq!(json["data"]["checks"]["slow"]["status"], "fail");
419 assert!(json["data"]["checks"]["slow"]["error"]
420 .as_str()
421 .unwrap()
422 .contains("timeout"));
423 }
424
425 #[tokio::test]
426 async fn test_readiness_fast_check_passes_within_timeout() {
427 let registry = HealthRegistry::with_timeout(Duration::from_secs(3));
428 registry.register(SlowCheck {
429 name: "fast".to_string(),
430 delay: Duration::from_millis(10),
431 });
432
433 let resp = registry.readiness().await;
434 let json = fetch_body(resp).await;
435 assert_eq!(json["code"], 1);
436 assert_eq!(json["data"]["checks"]["fast"]["status"], "ok");
437 }
438
439 #[tokio::test]
440 async fn test_readiness_zero_timeout_no_limit() {
441 let registry = HealthRegistry::with_timeout(Duration::ZERO);
443 registry.register(SlowCheck {
444 name: "fast".to_string(),
445 delay: Duration::from_millis(10),
446 });
447
448 let resp = registry.readiness().await;
449 let json = fetch_body(resp).await;
450 assert_eq!(json["code"], 1);
451 assert_eq!(json["data"]["checks"]["fast"]["status"], "ok");
452 }
453
454 #[test]
459 fn test_registry_default_is_empty() {
460 let registry = HealthRegistry::new();
461 assert_eq!(registry.checks.lock().len(), 0);
462 }
463
464 #[test]
465 fn test_registry_default_timeout_is_3s() {
466 let registry = HealthRegistry::new();
467 assert_eq!(registry.timeout, DEFAULT_CHECK_TIMEOUT);
468 assert_eq!(registry.timeout, Duration::from_secs(3));
469 }
470
471 #[test]
472 fn test_registry_register_increases_count() {
473 let registry = HealthRegistry::new();
474 registry.register(StaticCheck::ok("a"));
475 registry.register(StaticCheck::ok("b"));
476 registry.register(StaticCheck::ok("c"));
477 assert_eq!(registry.checks.lock().len(), 3);
478 }
479
480 #[test]
481 fn test_registry_clone_shares_state() {
482 let registry = HealthRegistry::new();
483 let cloned = registry.clone();
484 cloned.register(StaticCheck::ok("shared"));
485 assert_eq!(registry.checks.lock().len(), 1);
486 }
487
488 #[test]
489 fn test_static_check_ok_passes() {
490 let check = StaticCheck::ok("test");
491 assert!(check.check().is_ok());
492 }
493
494 #[test]
495 fn test_static_check_fail_fails() {
496 let check = StaticCheck::fail("test");
497 assert!(check.check().is_err());
498 }
499
500 #[test]
501 fn test_static_check_name() {
502 let check = StaticCheck::ok("my_check");
503 assert_eq!(check.name(), "my_check");
504 }
505
506 #[tokio::test]
511 async fn test_default_health_router_liveness() {
512 let router = default_health_router();
513 let resp = send_get(router, "/health/").await;
514 assert_eq!(resp.status(), StatusCode::OK);
515 }
516
517 #[tokio::test]
518 async fn test_default_health_router_readiness_empty() {
519 let router = default_health_router();
520 let resp = send_get(router, "/health/ready").await;
521 let json = fetch_body(resp).await;
522 assert_eq!(json["code"], 1);
523 }
524
525 #[tokio::test]
530 async fn test_router_at_custom_prefix() {
531 let registry = HealthRegistry::new();
532 let router = registry.router_at("/status");
533
534 let resp = send_get(router, "/status/").await;
535 assert_eq!(resp.status(), StatusCode::OK);
536
537 let registry2 = HealthRegistry::new();
538 let router2 = registry2.router_at("/status");
539 let resp2 = send_get(router2, "/status/ready").await;
540 assert_eq!(resp2.status(), StatusCode::OK);
541 }
542
543 #[tokio::test]
544 async fn test_router_at_nested_prefix() {
545 let registry = HealthRegistry::new();
546 let router = registry.router_at("/api/v1/health");
547
548 let resp = send_get(router, "/api/v1/health/").await;
549 assert_eq!(resp.status(), StatusCode::OK);
550 }
551}