use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HealthStatus {
pub healthy: bool,
pub checks: Vec<HealthCheck>,
}
impl HealthStatus {
#[must_use]
pub fn new(checks: Vec<HealthCheck>) -> Self {
let healthy = checks.iter().all(|c| c.status == CheckStatus::Healthy);
Self { healthy, checks }
}
#[must_use]
pub fn has_unhealthy(&self) -> bool {
self.checks
.iter()
.any(|c| c.status == CheckStatus::Unhealthy)
}
#[must_use]
pub fn has_degraded(&self) -> bool {
self.checks
.iter()
.any(|c| c.status == CheckStatus::Degraded)
}
}
impl fmt::Display for HealthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.healthy {
writeln!(f, "✅ Database is HEALTHY")?;
} else if self.has_unhealthy() {
writeln!(f, "❌ Database is UNHEALTHY")?;
} else {
writeln!(f, "⚠️ Database is DEGRADED")?;
}
for check in &self.checks {
writeln!(f, " {} {}", check.status_icon(), check)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HealthCheck {
pub name: String,
pub status: CheckStatus,
pub message: Option<String>,
}
impl HealthCheck {
pub fn healthy(name: impl Into<String>) -> Self {
Self {
name: name.into(),
status: CheckStatus::Healthy,
message: None,
}
}
pub fn healthy_with_message(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
status: CheckStatus::Healthy,
message: Some(message.into()),
}
}
pub fn degraded(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
status: CheckStatus::Degraded,
message: Some(message.into()),
}
}
pub fn unhealthy(name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
name: name.into(),
status: CheckStatus::Unhealthy,
message: Some(message.into()),
}
}
const fn status_icon(&self) -> &'static str {
match self.status {
CheckStatus::Healthy => "✅",
CheckStatus::Degraded => "⚠️ ",
CheckStatus::Unhealthy => "❌",
}
}
}
impl fmt::Display for HealthCheck {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.name, self.status)?;
if let Some(ref msg) = self.message {
write!(f, " - {msg}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
Healthy,
Degraded,
Unhealthy,
}
impl fmt::Display for CheckStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Healthy => write!(f, "HEALTHY"),
Self::Degraded => write!(f, "DEGRADED"),
Self::Unhealthy => write!(f, "UNHEALTHY"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_status_all_healthy() {
let checks = vec![
HealthCheck::healthy("disk_space"),
HealthCheck::healthy("compaction_lag"),
HealthCheck::healthy("wal_size"),
];
let status = HealthStatus::new(checks);
assert!(status.healthy);
assert!(!status.has_degraded());
assert!(!status.has_unhealthy());
}
#[test]
fn test_health_status_degraded() {
let checks = vec![
HealthCheck::healthy("disk_space"),
HealthCheck::degraded("compaction_lag", "L0 has 12 SSTables (>10)"),
HealthCheck::healthy("wal_size"),
];
let status = HealthStatus::new(checks);
assert!(!status.healthy);
assert!(status.has_degraded());
assert!(!status.has_unhealthy());
}
#[test]
fn test_health_status_unhealthy() {
let checks = vec![
HealthCheck::healthy("disk_space"),
HealthCheck::unhealthy("compaction_lag", "L0 has 25 SSTables (>20)"),
HealthCheck::healthy("wal_size"),
];
let status = HealthStatus::new(checks);
assert!(!status.healthy);
assert!(!status.has_degraded());
assert!(status.has_unhealthy());
}
#[test]
fn test_health_check_display() {
let check = HealthCheck::degraded("compaction_lag", "L0 has 12 SSTables");
let display = format!("{}", check);
assert!(display.contains("compaction_lag"));
assert!(display.contains("DEGRADED"));
assert!(display.contains("L0 has 12 SSTables"));
}
#[test]
fn test_health_status_display() {
let checks = vec![
HealthCheck::healthy("disk_space"),
HealthCheck::degraded("compaction_lag", "L0 has 12 SSTables"),
];
let status = HealthStatus::new(checks);
let display = format!("{}", status);
assert!(display.contains("DEGRADED"));
assert!(display.contains("disk_space"));
assert!(display.contains("compaction_lag"));
}
}