use async_trait::async_trait;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
use tracing::{debug, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum HealthStatus {
Healthy,
Starting,
ShuttingDown,
Unhealthy,
Degraded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
pub message: Option<String>,
pub last_check: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthProbeResult {
pub status: ProbeStatus,
pub latency_ms: f64,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProbeStatus {
Healthy,
Degraded,
Unhealthy,
}
impl ProbeStatus {
pub fn to_health_status(self) -> HealthStatus {
match self {
ProbeStatus::Healthy => HealthStatus::Healthy,
ProbeStatus::Degraded => HealthStatus::Degraded,
ProbeStatus::Unhealthy => HealthStatus::Unhealthy,
}
}
pub fn worse(self, other: ProbeStatus) -> ProbeStatus {
match (self, other) {
(ProbeStatus::Unhealthy, _) | (_, ProbeStatus::Unhealthy) => ProbeStatus::Unhealthy,
(ProbeStatus::Degraded, _) | (_, ProbeStatus::Degraded) => ProbeStatus::Degraded,
_ => ProbeStatus::Healthy,
}
}
}
#[async_trait]
pub trait DeepHealthCheck: Send + Sync {
async fn check(&self) -> HealthProbeResult;
}
pub struct StorageProbe {
storage_path: std::path::PathBuf,
}
impl StorageProbe {
pub fn new(storage_path: std::path::PathBuf) -> Self {
Self { storage_path }
}
}
#[async_trait]
impl DeepHealthCheck for StorageProbe {
async fn check(&self) -> HealthProbeResult {
let start = Instant::now();
let test_file = self.storage_path.join(".health_probe_test");
let write_result = tokio::fs::write(&test_file, b"health_probe").await;
if let Err(e) = write_result {
return HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: format!("storage write failed: {e}"),
};
}
let read_result = tokio::fs::read(&test_file).await;
match read_result {
Ok(data) if data == b"health_probe" => {}
Ok(_) => {
let _ = tokio::fs::remove_file(&test_file).await;
return HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: "storage read returned unexpected data".to_string(),
};
}
Err(e) => {
let _ = tokio::fs::remove_file(&test_file).await;
return HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: format!("storage read failed: {e}"),
};
}
}
if let Err(e) = tokio::fs::remove_file(&test_file).await {
return HealthProbeResult {
status: ProbeStatus::Degraded,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: format!("storage cleanup failed (non-critical): {e}"),
};
}
HealthProbeResult {
status: ProbeStatus::Healthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: "storage read/write/delete OK".to_string(),
}
}
}
pub struct WalProbe {
wal_path: std::path::PathBuf,
}
impl WalProbe {
pub fn new(wal_path: std::path::PathBuf) -> Self {
Self { wal_path }
}
}
#[async_trait]
impl DeepHealthCheck for WalProbe {
async fn check(&self) -> HealthProbeResult {
let start = Instant::now();
let test_file = self.wal_path.join(".wal_health_probe");
let result = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.truncate(false)
.open(&test_file)
.await;
match result {
Ok(_file) => {
let _ = tokio::fs::remove_file(&test_file).await;
HealthProbeResult {
status: ProbeStatus::Healthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: "WAL directory is appendable".to_string(),
}
}
Err(e) => HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: format!("WAL append test failed: {e}"),
},
}
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
unsafe extern "C" {
#[link_name = "statvfs"]
fn statvfs_raw(path: *const std::ffi::c_char, buf: *mut u8) -> std::ffi::c_int;
}
pub struct DiskSpaceProbe {
path: std::path::PathBuf,
min_free_bytes: u64,
}
impl DiskSpaceProbe {
pub fn new(path: std::path::PathBuf, min_free_bytes: u64) -> Self {
Self {
path,
min_free_bytes,
}
}
fn available_space(&self) -> Result<u64, String> {
self.available_space_impl()
}
#[cfg(target_os = "macos")]
fn available_space_impl(&self) -> Result<u64, String> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path = CString::new(self.path.as_os_str().as_bytes())
.map_err(|e| format!("invalid path: {e}"))?;
#[repr(C)]
struct Statvfs {
f_bsize: u64,
f_frsize: u64,
f_blocks: u64,
f_bfree: u64,
f_bavail: u64,
_pad: [u64; 11],
}
let mut buf: Statvfs = unsafe { std::mem::zeroed() };
let ret = unsafe { statvfs_raw(c_path.as_ptr(), &mut buf as *mut Statvfs as *mut u8) };
if ret != 0 {
return Err(format!(
"statvfs failed: {}",
std::io::Error::last_os_error()
));
}
let available = buf.f_bavail.saturating_mul(buf.f_frsize);
Ok(available)
}
#[cfg(target_os = "linux")]
fn available_space_impl(&self) -> Result<u64, String> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path = CString::new(self.path.as_os_str().as_bytes())
.map_err(|e| format!("invalid path: {e}"))?;
#[repr(C)]
struct Statvfs {
f_bsize: u64,
f_frsize: u64,
f_blocks: u64,
f_bfree: u64,
f_bavail: u64,
_pad: [u64; 11],
}
let mut buf: Statvfs = unsafe { std::mem::zeroed() };
let ret = unsafe { statvfs_raw(c_path.as_ptr(), &mut buf as *mut Statvfs as *mut u8) };
if ret != 0 {
return Err(format!(
"statvfs failed: {}",
std::io::Error::last_os_error()
));
}
let available = buf.f_bavail.saturating_mul(buf.f_frsize);
Ok(available)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn available_space_impl(&self) -> Result<u64, String> {
if self.path.exists() {
Ok(u64::MAX)
} else {
Err("path does not exist".to_string())
}
}
}
#[async_trait]
impl DeepHealthCheck for DiskSpaceProbe {
async fn check(&self) -> HealthProbeResult {
let start = Instant::now();
match self.available_space() {
Ok(available) => {
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
if available >= self.min_free_bytes {
HealthProbeResult {
status: ProbeStatus::Healthy,
latency_ms,
message: format!(
"disk space OK: {} bytes available (threshold: {})",
available, self.min_free_bytes
),
}
} else if available >= self.min_free_bytes / 4 {
HealthProbeResult {
status: ProbeStatus::Degraded,
latency_ms,
message: format!(
"disk space low: {} bytes available (threshold: {})",
available, self.min_free_bytes
),
}
} else {
HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms,
message: format!(
"disk space critically low: {} bytes available (threshold: {})",
available, self.min_free_bytes
),
}
}
}
Err(e) => HealthProbeResult {
status: ProbeStatus::Unhealthy,
latency_ms: start.elapsed().as_secs_f64() * 1000.0,
message: format!("disk space check failed: {e}"),
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthSnapshot {
pub timestamp: u64,
pub status: HealthStatus,
pub alive: bool,
pub ready: bool,
}
#[derive(Debug)]
pub struct HealthHistory {
buffer: Vec<Option<HealthSnapshot>>,
write_pos: usize,
total_written: usize,
capacity: usize,
}
impl HealthHistory {
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(1); Self {
buffer: (0..capacity).map(|_| None).collect(),
write_pos: 0,
total_written: 0,
capacity,
}
}
pub fn record(&mut self, snapshot: HealthSnapshot) {
self.buffer[self.write_pos] = Some(snapshot);
self.write_pos = (self.write_pos + 1) % self.capacity;
self.total_written += 1;
}
pub fn snapshots(&self) -> Vec<HealthSnapshot> {
let count = self.total_written.min(self.capacity);
let mut result = Vec::with_capacity(count);
if self.total_written < self.capacity {
for s in self.buffer.iter().take(self.write_pos).flatten() {
result.push(s.clone());
}
} else {
for i in 0..self.capacity {
let idx = (self.write_pos + i) % self.capacity;
if let Some(s) = &self.buffer[idx] {
result.push(s.clone());
}
}
}
result
}
pub fn uptime_percent(&self) -> f64 {
let snaps = self.snapshots();
if snaps.is_empty() {
return 100.0;
}
let alive_count = snaps.iter().filter(|s| s.alive).count();
(alive_count as f64 / snaps.len() as f64) * 100.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyHealth {
pub name: String,
pub status: ProbeStatus,
pub latency_ms: f64,
pub last_checked: u64,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LivenessResponse {
pub alive: bool,
pub status: HealthStatus,
pub uptime_seconds: u64,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadinessResponse {
pub ready: bool,
pub status: HealthStatus,
pub components: Vec<ComponentHealth>,
pub dependencies: Vec<DependencyHealth>,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckResponse {
pub status: HealthStatus,
pub version: String,
pub uptime_seconds: u64,
pub components: Vec<ComponentHealth>,
pub dependencies: Vec<DependencyHealth>,
pub probes: HashMap<String, HealthProbeResult>,
pub uptime_percent: f64,
pub timestamp: u64,
}
#[derive(Clone)]
pub struct HealthChecker {
inner: Arc<HealthCheckerInner>,
}
struct HealthCheckerInner {
start_time: AtomicU64,
status: AtomicU64,
storage_healthy: AtomicBool,
network_healthy: AtomicBool,
cluster_enabled: AtomicBool,
cluster_healthy: AtomicBool,
probes: RwLock<HashMap<String, Arc<dyn DeepHealthCheck>>>,
dependency_checkers: RwLock<HashMap<String, Arc<dyn DeepHealthCheck>>>,
dependency_health: RwLock<HashMap<String, DependencyHealth>>,
history: RwLock<HealthHistory>,
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
impl HealthChecker {
pub fn new() -> Self {
Self::with_history_capacity(10)
}
pub fn with_history_capacity(capacity: usize) -> Self {
Self {
inner: Arc::new(HealthCheckerInner {
start_time: AtomicU64::new(now_secs()),
status: AtomicU64::new(HealthStatus::Starting as u64),
storage_healthy: AtomicBool::new(false),
network_healthy: AtomicBool::new(false),
cluster_enabled: AtomicBool::new(false),
cluster_healthy: AtomicBool::new(false),
probes: RwLock::new(HashMap::new()),
dependency_checkers: RwLock::new(HashMap::new()),
dependency_health: RwLock::new(HashMap::new()),
history: RwLock::new(HealthHistory::new(capacity)),
}),
}
}
pub fn set_status(&self, status: HealthStatus) {
self.inner.status.store(status as u64, Ordering::SeqCst);
}
pub fn status(&self) -> HealthStatus {
match self.inner.status.load(Ordering::SeqCst) {
0 => HealthStatus::Healthy,
1 => HealthStatus::Starting,
2 => HealthStatus::ShuttingDown,
3 => HealthStatus::Unhealthy,
4 => HealthStatus::Degraded,
_ => HealthStatus::Unhealthy,
}
}
pub fn set_storage_healthy(&self, healthy: bool) {
self.inner.storage_healthy.store(healthy, Ordering::SeqCst);
}
pub fn set_network_healthy(&self, healthy: bool) {
self.inner.network_healthy.store(healthy, Ordering::SeqCst);
}
pub fn set_cluster_enabled(&self, enabled: bool) {
self.inner.cluster_enabled.store(enabled, Ordering::SeqCst);
}
pub fn set_cluster_healthy(&self, healthy: bool) {
self.inner.cluster_healthy.store(healthy, Ordering::SeqCst);
}
pub fn uptime_seconds(&self) -> u64 {
let now = now_secs();
let start = self.inner.start_time.load(Ordering::SeqCst);
now.saturating_sub(start)
}
pub fn is_alive(&self) -> bool {
matches!(
self.status(),
HealthStatus::Healthy | HealthStatus::Starting | HealthStatus::Degraded
)
}
pub fn is_ready(&self) -> bool {
let status = self.status();
let base_ok = matches!(status, HealthStatus::Healthy | HealthStatus::Degraded);
base_ok
&& self.inner.storage_healthy.load(Ordering::SeqCst)
&& self.inner.network_healthy.load(Ordering::SeqCst)
}
pub fn liveness_response(&self) -> LivenessResponse {
LivenessResponse {
alive: self.is_alive(),
status: self.status(),
uptime_seconds: self.uptime_seconds(),
timestamp: now_secs(),
}
}
pub fn readiness_response(&self) -> ReadinessResponse {
let components = self.build_component_list();
let dependencies: Vec<DependencyHealth> = self
.inner
.dependency_health
.read()
.values()
.cloned()
.collect();
ReadinessResponse {
ready: self.is_ready(),
status: self.status(),
components,
dependencies,
timestamp: now_secs(),
}
}
pub fn register_probe(&self, name: impl Into<String>, probe: Arc<dyn DeepHealthCheck>) {
self.inner.probes.write().insert(name.into(), probe);
}
pub async fn run_probes(&self) -> HashMap<String, HealthProbeResult> {
let probes: Vec<(String, Arc<dyn DeepHealthCheck>)> = {
let guard = self.inner.probes.read();
guard
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(v)))
.collect()
};
let mut results = HashMap::with_capacity(probes.len());
for (name, probe) in probes {
let result = probe.check().await;
results.insert(name, result);
}
results
}
pub fn register_dependency(&self, name: impl Into<String>, checker: Arc<dyn DeepHealthCheck>) {
let name = name.into();
self.inner
.dependency_checkers
.write()
.insert(name.clone(), checker);
self.inner.dependency_health.write().insert(
name.clone(),
DependencyHealth {
name,
status: ProbeStatus::Unhealthy,
latency_ms: 0.0,
last_checked: 0,
message: "not yet checked".to_string(),
},
);
}
pub async fn check_dependencies(&self) -> ProbeStatus {
let checkers: Vec<(String, Arc<dyn DeepHealthCheck>)> = {
let guard = self.inner.dependency_checkers.read();
guard
.iter()
.map(|(k, v)| (k.clone(), Arc::clone(v)))
.collect()
};
let mut worst = ProbeStatus::Healthy;
let now = now_secs();
for (name, checker) in checkers {
let result = checker.check().await;
worst = worst.worse(result.status);
let dep = DependencyHealth {
name: name.clone(),
status: result.status,
latency_ms: result.latency_ms,
last_checked: now,
message: result.message,
};
self.inner.dependency_health.write().insert(name, dep);
}
worst
}
pub fn aggregated_dependency_status(&self) -> ProbeStatus {
let guard = self.inner.dependency_health.read();
guard
.values()
.fold(ProbeStatus::Healthy, |acc, d| acc.worse(d.status))
}
pub fn record_snapshot(&self) {
let snapshot = HealthSnapshot {
timestamp: now_secs(),
status: self.status(),
alive: self.is_alive(),
ready: self.is_ready(),
};
self.inner.history.write().record(snapshot);
}
pub fn health_history(&self) -> Vec<HealthSnapshot> {
self.inner.history.read().snapshots()
}
pub fn uptime_percent(&self) -> f64 {
self.inner.history.read().uptime_percent()
}
pub fn get_health(&self) -> HealthCheckResponse {
let components = self.build_component_list();
let dependencies: Vec<DependencyHealth> = self
.inner
.dependency_health
.read()
.values()
.cloned()
.collect();
let probes = HashMap::new(); let uptime_pct = self.uptime_percent();
HealthCheckResponse {
status: self.status(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: self.uptime_seconds(),
components,
dependencies,
probes,
uptime_percent: uptime_pct,
timestamp: now_secs(),
}
}
pub async fn get_health_deep(&self) -> HealthCheckResponse {
let components = self.build_component_list();
let dependencies: Vec<DependencyHealth> = self
.inner
.dependency_health
.read()
.values()
.cloned()
.collect();
let probes = self.run_probes().await;
let uptime_pct = self.uptime_percent();
HealthCheckResponse {
status: self.status(),
version: env!("CARGO_PKG_VERSION").to_string(),
uptime_seconds: self.uptime_seconds(),
components,
dependencies,
probes,
uptime_percent: uptime_pct,
timestamp: now_secs(),
}
}
pub fn get_health_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.get_health())
}
fn build_component_list(&self) -> Vec<ComponentHealth> {
let now = now_secs();
let storage_status = if self.inner.storage_healthy.load(Ordering::SeqCst) {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
};
let network_status = if self.inner.network_healthy.load(Ordering::SeqCst) {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
};
let cluster_healthy = self.inner.cluster_healthy.load(Ordering::SeqCst);
let cluster_enabled = self.inner.cluster_enabled.load(Ordering::SeqCst);
let cluster_status = if cluster_enabled {
if cluster_healthy {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
}
} else {
HealthStatus::Starting };
let cluster_message = if cluster_enabled {
if cluster_healthy {
"cluster active".to_string()
} else {
"cluster unhealthy".to_string()
}
} else {
"cluster disabled (standalone mode)".to_string()
};
vec![
ComponentHealth {
name: "storage".to_string(),
status: storage_status,
message: None,
last_check: now,
},
ComponentHealth {
name: "network".to_string(),
status: network_status,
message: None,
last_check: now,
},
ComponentHealth {
name: "cluster".to_string(),
status: cluster_status,
message: Some(cluster_message),
last_check: now,
},
]
}
}
impl Default for HealthChecker {
fn default() -> Self {
Self::new()
}
}
pub struct HealthHttpServer {
checker: Arc<HealthChecker>,
bind_addr: SocketAddr,
shutdown: Arc<AtomicBool>,
}
pub struct HealthHttpHandle {
shutdown: Arc<AtomicBool>,
port: u16,
join_handle: tokio::task::JoinHandle<Result<(), std::io::Error>>,
}
impl HealthHttpHandle {
pub fn stop(&self) {
self.shutdown.store(true, Ordering::SeqCst);
}
pub fn port(&self) -> u16 {
self.port
}
pub async fn join(self) -> Result<(), std::io::Error> {
match self.join_handle.await {
Ok(inner) => inner,
Err(e) => Err(std::io::Error::other(e)),
}
}
}
impl HealthHttpServer {
pub fn new(checker: Arc<HealthChecker>, bind_addr: SocketAddr) -> Self {
Self {
checker,
bind_addr,
shutdown: Arc::new(AtomicBool::new(false)),
}
}
pub async fn start(self) -> Result<HealthHttpHandle, std::io::Error> {
let listener = TcpListener::bind(self.bind_addr).await?;
let local_addr = listener.local_addr()?;
let port = local_addr.port();
let shutdown = Arc::clone(&self.shutdown);
let checker = Arc::clone(&self.checker);
let shutdown_flag = Arc::clone(&shutdown);
let join_handle =
tokio::spawn(async move { Self::accept_loop(listener, checker, shutdown_flag).await });
Ok(HealthHttpHandle {
shutdown,
port,
join_handle,
})
}
async fn accept_loop(
listener: TcpListener,
checker: Arc<HealthChecker>,
shutdown: Arc<AtomicBool>,
) -> Result<(), std::io::Error> {
loop {
if shutdown.load(Ordering::SeqCst) {
debug!("health HTTP server shutting down");
break;
}
let accept_result =
tokio::time::timeout(Duration::from_millis(200), listener.accept()).await;
match accept_result {
Ok(Ok((stream, _addr))) => {
let checker = Arc::clone(&checker);
tokio::spawn(async move {
if let Err(e) = Self::handle_connection(stream, &checker).await {
warn!("health HTTP connection error: {e}");
}
});
}
Ok(Err(e)) => {
warn!("health HTTP accept error: {e}");
}
Err(_) => {
}
}
}
Ok(())
}
async fn handle_connection(
mut stream: tokio::net::TcpStream,
checker: &HealthChecker,
) -> Result<(), std::io::Error> {
let mut buf = [0u8; 4096];
let n = stream.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
let request = String::from_utf8_lossy(&buf[..n]);
let (method, path) = Self::parse_request_line(&request);
let (status_code, status_text, body) = match method {
"GET" => Self::route(path, checker),
_ => (
405,
"Method Not Allowed",
r#"{"error":"method not allowed"}"#.to_string(),
),
};
let response = format!(
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
status_code,
status_text,
body.len(),
body
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
fn parse_request_line(request: &str) -> (&str, &str) {
let first_line = request.lines().next().unwrap_or("");
let mut parts = first_line.split_whitespace();
let method = parts.next().unwrap_or("");
let path = parts.next().unwrap_or("");
(method, path)
}
fn route(path: &str, checker: &HealthChecker) -> (u16, &'static str, String) {
match path {
"/health" => Self::handle_health(checker),
"/healthz" => Self::handle_healthz(checker),
"/readyz" => Self::handle_readyz(checker),
"/livez" => Self::handle_livez(checker),
"/metrics" => Self::handle_metrics(checker),
_ => (404, "Not Found", r#"{"error":"not found"}"#.to_string()),
}
}
fn handle_health(checker: &HealthChecker) -> (u16, &'static str, String) {
let health = checker.get_health();
let status_code = match health.status {
HealthStatus::Healthy | HealthStatus::Degraded => 200,
_ => 503,
};
let status_text = if status_code == 200 {
"OK"
} else {
"Service Unavailable"
};
let body = serde_json::to_string(&health)
.unwrap_or_else(|e| format!(r#"{{"error":"serialization failed: {e}"}}"#));
(status_code, status_text, body)
}
fn handle_healthz(checker: &HealthChecker) -> (u16, &'static str, String) {
let alive = checker.is_alive();
let status_code = if alive { 200 } else { 503 };
let status_text = if alive { "OK" } else { "Service Unavailable" };
let body = format!(r#"{{"alive":{alive}}}"#);
(status_code, status_text, body)
}
fn handle_readyz(checker: &HealthChecker) -> (u16, &'static str, String) {
let ready = checker.is_ready();
let status_code = if ready { 200 } else { 503 };
let status_text = if ready { "OK" } else { "Service Unavailable" };
let body = format!(r#"{{"ready":{ready}}}"#);
(status_code, status_text, body)
}
fn handle_livez(checker: &HealthChecker) -> (u16, &'static str, String) {
let resp = checker.liveness_response();
let status_code = if resp.alive { 200 } else { 503 };
let status_text = if resp.alive {
"OK"
} else {
"Service Unavailable"
};
let body = serde_json::to_string(&resp)
.unwrap_or_else(|e| format!(r#"{{"error":"serialization failed: {e}"}}"#));
(status_code, status_text, body)
}
fn handle_metrics(checker: &HealthChecker) -> (u16, &'static str, String) {
let history = checker.health_history();
let uptime_percent = checker.uptime_percent();
let uptime_seconds = checker.uptime_seconds();
#[derive(Serialize)]
struct MetricsResponse {
uptime_seconds: u64,
uptime_percent: f64,
history_count: usize,
history: Vec<HealthSnapshot>,
}
let resp = MetricsResponse {
uptime_seconds,
uptime_percent,
history_count: history.len(),
history,
};
let body = serde_json::to_string(&resp)
.unwrap_or_else(|e| format!(r#"{{"error":"serialization failed: {e}"}}"#));
(200, "OK", body)
}
}
#[cfg(test)]
#[path = "health_tests.rs"]
mod tests;