use std::sync::Arc;
use std::time::{Duration, Instant};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HealthStatus {
Healthy,
Degraded { reason: String },
Unhealthy { reason: String },
}
impl HealthStatus {
pub fn is_healthy(&self) -> bool {
matches!(self, HealthStatus::Healthy)
}
pub fn is_degraded(&self) -> bool {
matches!(self, HealthStatus::Degraded { .. })
}
pub fn is_unhealthy(&self) -> bool {
matches!(self, HealthStatus::Unhealthy { .. })
}
}
impl std::fmt::Display for HealthStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HealthStatus::Healthy => write!(f, "healthy"),
HealthStatus::Degraded { reason } => write!(f, "degraded: {}", reason),
HealthStatus::Unhealthy { reason } => write!(f, "unhealthy: {}", reason),
}
}
}
#[async_trait]
pub trait HealthCheck: Send + Sync {
fn name(&self) -> &str;
async fn check(&self) -> HealthStatus;
fn timeout(&self) -> Duration {
Duration::from_secs(5)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
pub name: String,
pub status: HealthStatus,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthReport {
pub status: HealthStatus,
pub checks: Vec<CheckResult>,
pub timestamp: String,
}
pub struct HealthRegistry {
checks: Vec<Arc<dyn HealthCheck>>,
}
impl HealthRegistry {
pub fn new() -> Self {
Self {
checks: Vec::new(),
}
}
pub fn register(&mut self, check: Arc<dyn HealthCheck>) {
self.checks.push(check);
}
pub async fn check_all(&self) -> HealthReport {
let mut results = Vec::new();
let mut overall_status = HealthStatus::Healthy;
for check in &self.checks {
let start = Instant::now();
let status = run_check_with_timeout(check.as_ref(), check.timeout()).await;
let duration_ms = start.elapsed().as_millis() as u64;
results.push(CheckResult {
name: check.name().to_string(),
status: status.clone(),
duration_ms,
});
match &status {
HealthStatus::Unhealthy { .. } => {
overall_status = status;
break;
}
HealthStatus::Degraded { .. } => {
if overall_status.is_healthy() {
overall_status = status;
}
}
_ => {}
}
}
HealthReport {
status: overall_status,
checks: results,
timestamp: chrono::Utc::now().to_rfc3339(),
}
}
}
impl Default for HealthRegistry {
fn default() -> Self {
Self::new()
}
}
async fn run_check_with_timeout(check: &dyn HealthCheck, timeout: Duration) -> HealthStatus {
match tokio::time::timeout(timeout, check.check()).await {
Ok(status) => status,
Err(_) => HealthStatus::Unhealthy {
reason: format!("check timed out after {}ms", timeout.as_millis()),
},
}
}
pub struct AlwaysHealthy;
#[async_trait]
impl HealthCheck for AlwaysHealthy {
fn name(&self) -> &str {
"always_healthy"
}
async fn check(&self) -> HealthStatus {
HealthStatus::Healthy
}
}
pub struct AlwaysDegraded {
reason: String,
}
impl AlwaysDegraded {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[async_trait]
impl HealthCheck for AlwaysDegraded {
fn name(&self) -> &str {
"always_degraded"
}
async fn check(&self) -> HealthStatus {
HealthStatus::Degraded {
reason: self.reason.clone(),
}
}
}
pub struct AlwaysUnhealthy {
reason: String,
}
impl AlwaysUnhealthy {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[async_trait]
impl HealthCheck for AlwaysUnhealthy {
fn name(&self) -> &str {
"always_unhealthy"
}
async fn check(&self) -> HealthStatus {
HealthStatus::Unhealthy {
reason: self.reason.clone(),
}
}
}
pub struct AlwaysTimeout {
delay: Duration,
}
impl AlwaysTimeout {
pub fn new(delay: Duration) -> Self {
Self { delay }
}
}
#[async_trait]
impl HealthCheck for AlwaysTimeout {
fn name(&self) -> &str {
"always_timeout"
}
async fn check(&self) -> HealthStatus {
tokio::time::sleep(self.delay).await;
HealthStatus::Healthy
}
fn timeout(&self) -> Duration {
Duration::from_millis(10)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_status_is_healthy() {
assert!(HealthStatus::Healthy.is_healthy());
assert!(!HealthStatus::Degraded { reason: "test".to_string() }.is_healthy());
assert!(!HealthStatus::Unhealthy { reason: "test".to_string() }.is_healthy());
}
#[test]
fn test_health_status_is_degraded() {
assert!(!HealthStatus::Healthy.is_degraded());
assert!(HealthStatus::Degraded { reason: "test".to_string() }.is_degraded());
assert!(!HealthStatus::Unhealthy { reason: "test".to_string() }.is_degraded());
}
#[test]
fn test_health_status_is_unhealthy() {
assert!(!HealthStatus::Healthy.is_unhealthy());
assert!(!HealthStatus::Degraded { reason: "test".to_string() }.is_unhealthy());
assert!(HealthStatus::Unhealthy { reason: "test".to_string() }.is_unhealthy());
}
#[test]
fn test_health_status_display() {
assert_eq!(format!("{}", HealthStatus::Healthy), "healthy");
assert_eq!(
format!("{}", HealthStatus::Degraded { reason: "slow".to_string() }),
"degraded: slow"
);
assert_eq!(
format!("{}", HealthStatus::Unhealthy { reason: "down".to_string() }),
"unhealthy: down"
);
}
#[tokio::test]
async fn test_always_healthy() {
let check = AlwaysHealthy;
assert_eq!(check.name(), "always_healthy");
assert_eq!(check.check().await, HealthStatus::Healthy);
}
#[tokio::test]
async fn test_always_degraded() {
let check = AlwaysDegraded::new("slow");
assert_eq!(check.name(), "always_degraded");
let status = check.check().await;
assert!(status.is_degraded());
}
#[tokio::test]
async fn test_always_unhealthy() {
let check = AlwaysUnhealthy::new("down");
assert_eq!(check.name(), "always_unhealthy");
let status = check.check().await;
assert!(status.is_unhealthy());
}
#[tokio::test]
async fn test_always_timeout() {
let check = AlwaysTimeout::new(Duration::from_millis(100));
assert_eq!(check.name(), "always_timeout");
let status = check.check().await;
assert!(status.is_healthy());
}
#[tokio::test]
async fn test_health_registry_empty() {
let registry = HealthRegistry::new();
let report = registry.check_all().await;
assert_eq!(report.status, HealthStatus::Healthy);
assert!(report.checks.is_empty());
}
#[tokio::test]
async fn test_health_registry_healthy() {
let mut registry = HealthRegistry::new();
registry.register(Arc::new(AlwaysHealthy));
let report = registry.check_all().await;
assert_eq!(report.status, HealthStatus::Healthy);
assert_eq!(report.checks.len(), 1);
assert_eq!(report.checks[0].status, HealthStatus::Healthy);
}
#[tokio::test]
async fn test_health_registry_degraded() {
let mut registry = HealthRegistry::new();
registry.register(Arc::new(AlwaysHealthy));
registry.register(Arc::new(AlwaysDegraded::new("slow")));
let report = registry.check_all().await;
assert!(report.status.is_degraded());
assert_eq!(report.checks.len(), 2);
}
#[tokio::test]
async fn test_health_registry_unhealthy() {
let mut registry = HealthRegistry::new();
registry.register(Arc::new(AlwaysHealthy));
registry.register(Arc::new(AlwaysUnhealthy::new("down")));
let report = registry.check_all().await;
assert!(report.status.is_unhealthy());
assert_eq!(report.checks.len(), 2);
}
#[tokio::test]
async fn test_health_registry_timeout() {
let mut registry = HealthRegistry::new();
registry.register(Arc::new(AlwaysTimeout::new(Duration::from_millis(100))));
let report = registry.check_all().await;
assert!(report.status.is_unhealthy());
}
#[test]
fn test_check_result_serialization() {
let result = CheckResult {
name: "test".to_string(),
status: HealthStatus::Healthy,
duration_ms: 10,
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("test"));
assert!(json.contains("healthy"));
}
#[test]
fn test_health_report_serialization() {
let report = HealthReport {
status: HealthStatus::Healthy,
checks: vec![],
timestamp: "2024-01-01T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&report).unwrap();
assert!(json.contains("healthy"));
}
}