use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use crate::server::ServerId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthStatus {
Healthy,
Degraded,
Unhealthy,
Unknown,
}
impl Default for HealthStatus {
fn default() -> Self {
Self::Unknown
}
}
impl HealthStatus {
pub fn is_operational(&self) -> bool {
matches!(self, Self::Healthy | Self::Degraded)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheck {
pub server_id: ServerId,
pub status: HealthStatus,
pub timestamp: DateTime<Utc>,
pub player_count: u32,
pub capacity: u32,
pub uptime_secs: u64,
pub tick_rate: f32,
pub memory_mb: u32,
pub cpu_percent: f32,
pub components: Vec<ComponentHealth>,
#[serde(default)]
pub message: Option<String>,
}
impl HealthCheck {
pub fn healthy(server_id: ServerId, player_count: u32, capacity: u32) -> Self {
Self {
server_id,
status: HealthStatus::Healthy,
timestamp: Utc::now(),
player_count,
capacity,
uptime_secs: 0,
tick_rate: 60.0,
memory_mb: 0,
cpu_percent: 0.0,
components: Vec::new(),
message: None,
}
}
pub fn unhealthy(server_id: ServerId, message: String) -> Self {
Self {
server_id,
status: HealthStatus::Unhealthy,
timestamp: Utc::now(),
player_count: 0,
capacity: 0,
uptime_secs: 0,
tick_rate: 0.0,
memory_mb: 0,
cpu_percent: 0.0,
components: Vec::new(),
message: Some(message),
}
}
pub fn load_factor(&self) -> f32 {
if self.capacity == 0 {
0.0
} else {
self.player_count as f32 / self.capacity as f32
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
#[serde(default)]
pub details: Option<String>,
#[serde(default)]
pub response_time_ms: Option<u64>,
}
impl ComponentHealth {
pub fn healthy(name: impl Into<String>) -> Self {
Self {
name: name.into(),
status: HealthStatus::Healthy,
details: None,
response_time_ms: None,
}
}
pub fn unhealthy(name: impl Into<String>, details: impl Into<String>) -> Self {
Self {
name: name.into(),
status: HealthStatus::Unhealthy,
details: Some(details.into()),
response_time_ms: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthCheckRequest {
#[serde(default)]
pub include_components: bool,
#[serde(default)]
pub include_metrics: bool,
}
impl Default for HealthCheckRequest {
fn default() -> Self {
Self {
include_components: false,
include_metrics: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClusterHealth {
pub status: HealthStatus,
pub healthy_servers: u32,
pub degraded_servers: u32,
pub unhealthy_servers: u32,
pub total_players: u32,
pub total_capacity: u32,
pub timestamp: DateTime<Utc>,
}
impl ClusterHealth {
pub fn new(checks: &[HealthCheck]) -> Self {
let mut healthy = 0u32;
let mut degraded = 0u32;
let mut unhealthy = 0u32;
let mut total_players = 0u32;
let mut total_capacity = 0u32;
for check in checks {
match check.status {
HealthStatus::Healthy => healthy += 1,
HealthStatus::Degraded => degraded += 1,
HealthStatus::Unhealthy | HealthStatus::Unknown => unhealthy += 1,
}
total_players += check.player_count;
total_capacity += check.capacity;
}
let status = if unhealthy > 0 && healthy == 0 {
HealthStatus::Unhealthy
} else if degraded > 0 || unhealthy > 0 {
HealthStatus::Degraded
} else if healthy > 0 {
HealthStatus::Healthy
} else {
HealthStatus::Unknown
};
Self {
status,
healthy_servers: healthy,
degraded_servers: degraded,
unhealthy_servers: unhealthy,
total_players,
total_capacity,
timestamp: Utc::now(),
}
}
pub fn load_factor(&self) -> f32 {
if self.total_capacity == 0 {
0.0
} else {
self.total_players as f32 / self.total_capacity as f32
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_status_operational() {
assert!(HealthStatus::Healthy.is_operational());
assert!(HealthStatus::Degraded.is_operational());
assert!(!HealthStatus::Unhealthy.is_operational());
assert!(!HealthStatus::Unknown.is_operational());
}
#[test]
fn test_cluster_health_aggregation() {
let server_id = ServerId::new();
let checks = vec![
HealthCheck::healthy(server_id, 50, 100),
HealthCheck::healthy(server_id, 30, 100),
];
let cluster = ClusterHealth::new(&checks);
assert_eq!(cluster.healthy_servers, 2);
assert_eq!(cluster.total_players, 80);
assert_eq!(cluster.total_capacity, 200);
}
}