use axum::{
Json, Router,
extract::State,
http::StatusCode,
routing::get,
};
use futures::future::join_all;
use serde::Serialize;
use std::sync::Arc;
use super::{HealthCheck, HealthCheckResult};
#[derive(Debug, Serialize)]
struct CheckEntry {
name: String,
healthy: bool,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
}
#[derive(Debug, Serialize)]
struct ReadinessResponse {
status: &'static str,
checks: Vec<CheckEntry>,
}
#[derive(Debug, Serialize)]
struct LivenessResponse {
status: &'static str,
}
pub struct HealthRouter {
pub(super) checks: Vec<Arc<dyn HealthCheck>>,
}
impl HealthRouter {
pub fn new() -> Self {
Self { checks: Vec::new() }
}
pub fn with_check(mut self, check: Arc<dyn HealthCheck>) -> Self {
self.checks.push(check);
self
}
pub fn into_axum_router(self) -> Router {
let state = Arc::new(self.checks);
Router::new()
.route("/health/live", get(live_handler))
.route(
"/health/ready",
get(ready_handler).with_state(Arc::clone(&state)),
)
}
}
impl Default for HealthRouter {
fn default() -> Self {
Self::new()
}
}
async fn live_handler() -> (StatusCode, Json<LivenessResponse>) {
(StatusCode::OK, Json(LivenessResponse { status: "ok" }))
}
async fn ready_handler(
State(checks): State<Arc<Vec<Arc<dyn HealthCheck>>>>,
) -> (StatusCode, Json<ReadinessResponse>) {
let futures: Vec<_> = checks
.iter()
.map(|c| {
let name = c.name().to_string();
let fut = c.check();
async move { (name, fut.await) }
})
.collect();
let results = join_all(futures).await;
let mut entries = Vec::with_capacity(results.len());
let mut all_healthy = true;
for (name, outcome) in results {
let check_result: HealthCheckResult = match outcome {
Ok(r) => r,
Err(e) => HealthCheckResult::unhealthy(format!("internal error: {e}")),
};
if !check_result.is_healthy() {
all_healthy = false;
}
entries.push(CheckEntry {
name,
healthy: check_result.healthy,
detail: check_result.details,
});
}
let status_code = if all_healthy {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
let body = ReadinessResponse {
status: if all_healthy { "ok" } else { "degraded" },
checks: entries,
};
(status_code, Json(body))
}