use axum::{extract::State, http::StatusCode, response::Json, routing::get};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::{debug, error, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceStatus {
Initializing,
Ready,
ShuttingDown,
Failed,
}
impl ServiceStatus {
pub fn is_ready(&self) -> bool {
matches!(self, ServiceStatus::Ready)
}
pub fn is_alive(&self) -> bool {
!matches!(self, ServiceStatus::Failed)
}
}
#[derive(Debug, Clone)]
pub struct HealthManager {
status: Arc<RwLock<ServiceStatus>>,
start_time: Arc<Instant>,
init_deadline: Arc<Option<Instant>>,
shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
}
impl HealthManager {
pub fn new() -> Self {
Self {
status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
start_time: Arc::new(Instant::now()),
init_deadline: Arc::new(None),
shutdown_signal: Arc::new(RwLock::new(None)),
}
}
pub fn with_init_timeout(timeout: Duration) -> Self {
let deadline = Instant::now() + timeout;
Self {
status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
start_time: Arc::new(Instant::now()),
init_deadline: Arc::new(Some(deadline)),
shutdown_signal: Arc::new(RwLock::new(None)),
}
}
pub async fn set_ready(&self) {
let mut status = self.status.write().await;
*status = ServiceStatus::Ready;
info!("Service marked as ready");
}
pub async fn set_failed(&self, reason: &str) {
let mut status = self.status.write().await;
*status = ServiceStatus::Failed;
error!("Service marked as failed: {}", reason);
}
pub async fn set_shutting_down(&self) {
let mut status = self.status.write().await;
*status = ServiceStatus::ShuttingDown;
info!("Service marked as shutting down");
}
pub async fn get_status(&self) -> ServiceStatus {
*self.status.read().await
}
pub fn uptime_seconds(&self) -> u64 {
self.start_time.elapsed().as_secs()
}
pub fn is_init_timeout(&self) -> bool {
if let Some(deadline) = *self.init_deadline {
Instant::now() > deadline
} else {
false
}
}
pub async fn set_shutdown_signal(&self, sender: tokio::sync::oneshot::Sender<()>) {
let mut signal = self.shutdown_signal.write().await;
*signal = Some(sender);
}
pub async fn trigger_shutdown(&self) {
self.set_shutting_down().await;
let mut signal = self.shutdown_signal.write().await;
if let Some(sender) = signal.take() {
let _ = sender.send(());
info!("Graceful shutdown signal sent");
}
}
}
impl Default for HealthManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub timestamp: String,
pub uptime_seconds: u64,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<HealthDetails>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct HealthDetails {
pub initialization: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub connections: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_bytes: Option<u64>,
}
async fn liveness_probe(
State(health): State<Arc<HealthManager>>,
) -> Result<Json<HealthResponse>, StatusCode> {
let status = health.get_status().await;
let uptime = health.uptime_seconds();
if status.is_alive() {
let response = HealthResponse {
status: "alive".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: None,
};
Ok(Json(response))
} else {
Err(StatusCode::SERVICE_UNAVAILABLE)
}
}
async fn readiness_probe(
State(health): State<Arc<HealthManager>>,
) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
let status = health.get_status().await;
let uptime = health.uptime_seconds();
if status.is_ready() {
let response = HealthResponse {
status: "ready".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: "complete".to_string(),
connections: None,
memory_bytes: None,
}),
};
Ok(Json(response))
} else {
let details = match status {
ServiceStatus::Initializing => {
if health.is_init_timeout() {
"initialization_timeout".to_string()
} else {
"initializing".to_string()
}
}
ServiceStatus::ShuttingDown => "shutting_down".to_string(),
ServiceStatus::Failed => "failed".to_string(),
ServiceStatus::Ready => unreachable!(),
};
let response = HealthResponse {
status: "not_ready".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: details,
connections: None,
memory_bytes: None,
}),
};
Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
}
}
async fn startup_probe(
State(health): State<Arc<HealthManager>>,
) -> Result<Json<HealthResponse>, StatusCode> {
let status = health.get_status().await;
let uptime = health.uptime_seconds();
match status {
ServiceStatus::Ready => {
let response = HealthResponse {
status: "startup_complete".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: "complete".to_string(),
connections: None,
memory_bytes: None,
}),
};
Ok(Json(response))
}
ServiceStatus::Initializing => {
if health.is_init_timeout() {
warn!("Startup probe: initialization timeout exceeded");
Err(StatusCode::SERVICE_UNAVAILABLE)
} else {
debug!("Startup probe: still initializing");
Err(StatusCode::SERVICE_UNAVAILABLE)
}
}
ServiceStatus::Failed => Err(StatusCode::SERVICE_UNAVAILABLE),
ServiceStatus::ShuttingDown => {
let response = HealthResponse {
status: "startup_complete".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: "complete".to_string(),
connections: None,
memory_bytes: None,
}),
};
Ok(Json(response))
}
}
}
async fn health_check(
State(health): State<Arc<HealthManager>>,
) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
let status = health.get_status().await;
let uptime = health.uptime_seconds();
if status.is_ready() {
let response = HealthResponse {
status: "healthy".to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: "complete".to_string(),
connections: None,
memory_bytes: None,
}),
};
Ok(Json(response))
} else {
let status_str = match status {
ServiceStatus::Initializing => "initializing",
ServiceStatus::ShuttingDown => "shutting_down",
ServiceStatus::Failed => "failed",
ServiceStatus::Ready => unreachable!(),
};
let response = HealthResponse {
status: status_str.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
uptime_seconds: uptime,
version: env!("CARGO_PKG_VERSION").to_string(),
details: Some(HealthDetails {
initialization: status_str.to_string(),
connections: None,
memory_bytes: None,
}),
};
Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
}
}
pub fn health_router(health_manager: Arc<HealthManager>) -> axum::Router {
use axum::Router;
Router::new()
.route("/health", get(health_check))
.route("/health/live", get(liveness_probe))
.route("/health/ready", get(readiness_probe))
.route("/health/startup", get(startup_probe))
.with_state(health_manager)
}
pub fn health_router_with_prefix(health_manager: Arc<HealthManager>, prefix: &str) -> axum::Router {
use axum::Router;
Router::new()
.route(&format!("{}/health", prefix), get(health_check))
.route(&format!("{}/health/live", prefix), get(liveness_probe))
.route(&format!("{}/health/ready", prefix), get(readiness_probe))
.route(&format!("{}/health/startup", prefix), get(startup_probe))
.with_state(health_manager)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use tower::ServiceExt;
#[test]
fn test_service_status() {
assert!(ServiceStatus::Ready.is_ready());
assert!(!ServiceStatus::Initializing.is_ready());
assert!(!ServiceStatus::ShuttingDown.is_ready());
assert!(!ServiceStatus::Failed.is_ready());
assert!(ServiceStatus::Ready.is_alive());
assert!(ServiceStatus::Initializing.is_alive());
assert!(ServiceStatus::ShuttingDown.is_alive());
assert!(!ServiceStatus::Failed.is_alive());
}
#[test]
fn test_service_status_eq() {
assert_eq!(ServiceStatus::Ready, ServiceStatus::Ready);
assert_ne!(ServiceStatus::Ready, ServiceStatus::Failed);
assert_eq!(ServiceStatus::Initializing, ServiceStatus::Initializing);
}
#[test]
fn test_service_status_clone() {
let status = ServiceStatus::Ready;
let cloned = status;
assert_eq!(status, cloned);
}
#[test]
fn test_service_status_debug() {
let debug = format!("{:?}", ServiceStatus::Ready);
assert!(debug.contains("Ready"));
}
#[tokio::test]
async fn test_health_manager_new() {
let manager = HealthManager::new();
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::Initializing);
}
#[tokio::test]
async fn test_health_manager_default() {
let manager = HealthManager::default();
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::Initializing);
}
#[tokio::test]
async fn test_health_manager_with_init_timeout() {
let manager = HealthManager::with_init_timeout(Duration::from_secs(30));
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::Initializing);
assert!(!manager.is_init_timeout());
}
#[tokio::test]
async fn test_health_manager_set_ready() {
let manager = HealthManager::new();
manager.set_ready().await;
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::Ready);
}
#[tokio::test]
async fn test_health_manager_set_failed() {
let manager = HealthManager::new();
manager.set_failed("test error").await;
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::Failed);
}
#[tokio::test]
async fn test_health_manager_set_shutting_down() {
let manager = HealthManager::new();
manager.set_shutting_down().await;
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::ShuttingDown);
}
#[tokio::test]
async fn test_health_manager_uptime() {
let manager = HealthManager::new();
let uptime = manager.uptime_seconds();
assert!(uptime < 5);
}
#[tokio::test]
async fn test_health_manager_clone() {
let manager = HealthManager::new();
manager.set_ready().await;
let cloned = manager.clone();
let status = cloned.get_status().await;
assert_eq!(status, ServiceStatus::Ready);
}
#[tokio::test]
async fn test_health_manager_trigger_shutdown() {
let manager = HealthManager::new();
manager.set_ready().await;
manager.trigger_shutdown().await;
let status = manager.get_status().await;
assert_eq!(status, ServiceStatus::ShuttingDown);
}
#[test]
fn test_health_response_serialization() {
let response = HealthResponse {
status: "healthy".to_string(),
timestamp: "2024-01-15T10:30:00Z".to_string(),
uptime_seconds: 3600,
version: "1.0.0".to_string(),
details: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("healthy"));
assert!(json.contains("3600"));
assert!(json.contains("1.0.0"));
}
#[test]
fn test_health_response_with_details() {
let response = HealthResponse {
status: "healthy".to_string(),
timestamp: "2024-01-15T10:30:00Z".to_string(),
uptime_seconds: 3600,
version: "1.0.0".to_string(),
details: Some(HealthDetails {
initialization: "complete".to_string(),
connections: Some(10),
memory_bytes: Some(1024 * 1024),
}),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("complete"));
assert!(json.contains("connections"));
}
#[test]
fn test_health_response_deserialization() {
let json = r#"{
"status": "healthy",
"timestamp": "2024-01-15T10:30:00Z",
"uptime_seconds": 7200,
"version": "2.0.0"
}"#;
let response: HealthResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.status, "healthy");
assert_eq!(response.uptime_seconds, 7200);
assert_eq!(response.version, "2.0.0");
}
#[test]
fn test_health_details_serialization() {
let details = HealthDetails {
initialization: "complete".to_string(),
connections: Some(5),
memory_bytes: Some(2048),
};
let json = serde_json::to_string(&details).unwrap();
assert!(json.contains("complete"));
assert!(json.contains("5"));
assert!(json.contains("2048"));
}
#[test]
fn test_health_details_optional_fields() {
let details = HealthDetails {
initialization: "initializing".to_string(),
connections: None,
memory_bytes: None,
};
let json = serde_json::to_string(&details).unwrap();
assert!(json.contains("initializing"));
assert!(!json.contains("connections"));
assert!(!json.contains("memory_bytes"));
}
#[tokio::test]
async fn test_liveness_probe_alive() {
let health = Arc::new(HealthManager::new());
health.set_ready().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_liveness_probe_failed() {
let health = Arc::new(HealthManager::new());
health.set_failed("test failure").await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn test_liveness_probe_initializing() {
let health = Arc::new(HealthManager::new());
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_liveness_probe_shutting_down() {
let health = Arc::new(HealthManager::new());
health.set_shutting_down().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_readiness_probe_ready() {
let health = Arc::new(HealthManager::new());
health.set_ready().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_readiness_probe_initializing() {
let health = Arc::new(HealthManager::new());
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn test_readiness_probe_shutting_down() {
let health = Arc::new(HealthManager::new());
health.set_shutting_down().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn test_startup_probe_ready() {
let health = Arc::new(HealthManager::new());
health.set_ready().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_startup_probe_initializing() {
let health = Arc::new(HealthManager::new());
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn test_startup_probe_shutting_down() {
let health = Arc::new(HealthManager::new());
health.set_ready().await;
health.set_shutting_down().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_health_check_ready() {
let health = Arc::new(HealthManager::new());
health.set_ready().await;
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_health_check_not_ready() {
let health = Arc::new(HealthManager::new());
let app = health_router(health.clone());
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[test]
fn test_health_router_creation() {
let health = Arc::new(HealthManager::new());
let router = health_router(health);
let _ = router;
}
#[test]
fn test_health_router_with_prefix() {
let health = Arc::new(HealthManager::new());
let router = health_router_with_prefix(health, "/api/v1");
let _ = router;
}
}