1use serde::{Deserialize, Serialize};
7use chrono::{DateTime, Utc};
8
9use crate::server::ServerId;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum HealthStatus {
15 Healthy,
17 Degraded,
19 Unhealthy,
21 Unknown,
23}
24
25impl Default for HealthStatus {
26 fn default() -> Self {
27 Self::Unknown
28 }
29}
30
31impl HealthStatus {
32 pub fn is_operational(&self) -> bool {
34 matches!(self, Self::Healthy | Self::Degraded)
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct HealthCheck {
41 pub server_id: ServerId,
43 pub status: HealthStatus,
45 pub timestamp: DateTime<Utc>,
47 pub player_count: u32,
49 pub capacity: u32,
51 pub uptime_secs: u64,
53 pub tick_rate: f32,
55 pub memory_mb: u32,
57 pub cpu_percent: f32,
59 pub components: Vec<ComponentHealth>,
61 #[serde(default)]
63 pub message: Option<String>,
64}
65
66impl HealthCheck {
67 pub fn healthy(server_id: ServerId, player_count: u32, capacity: u32) -> Self {
69 Self {
70 server_id,
71 status: HealthStatus::Healthy,
72 timestamp: Utc::now(),
73 player_count,
74 capacity,
75 uptime_secs: 0,
76 tick_rate: 60.0,
77 memory_mb: 0,
78 cpu_percent: 0.0,
79 components: Vec::new(),
80 message: None,
81 }
82 }
83
84 pub fn unhealthy(server_id: ServerId, message: String) -> Self {
86 Self {
87 server_id,
88 status: HealthStatus::Unhealthy,
89 timestamp: Utc::now(),
90 player_count: 0,
91 capacity: 0,
92 uptime_secs: 0,
93 tick_rate: 0.0,
94 memory_mb: 0,
95 cpu_percent: 0.0,
96 components: Vec::new(),
97 message: Some(message),
98 }
99 }
100
101 pub fn load_factor(&self) -> f32 {
103 if self.capacity == 0 {
104 0.0
105 } else {
106 self.player_count as f32 / self.capacity as f32
107 }
108 }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ComponentHealth {
114 pub name: String,
116 pub status: HealthStatus,
118 #[serde(default)]
120 pub details: Option<String>,
121 #[serde(default)]
123 pub response_time_ms: Option<u64>,
124}
125
126impl ComponentHealth {
127 pub fn healthy(name: impl Into<String>) -> Self {
129 Self {
130 name: name.into(),
131 status: HealthStatus::Healthy,
132 details: None,
133 response_time_ms: None,
134 }
135 }
136
137 pub fn unhealthy(name: impl Into<String>, details: impl Into<String>) -> Self {
139 Self {
140 name: name.into(),
141 status: HealthStatus::Unhealthy,
142 details: Some(details.into()),
143 response_time_ms: None,
144 }
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct HealthCheckRequest {
151 #[serde(default)]
153 pub include_components: bool,
154 #[serde(default)]
156 pub include_metrics: bool,
157}
158
159impl Default for HealthCheckRequest {
160 fn default() -> Self {
161 Self {
162 include_components: false,
163 include_metrics: false,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ClusterHealth {
171 pub status: HealthStatus,
173 pub healthy_servers: u32,
175 pub degraded_servers: u32,
177 pub unhealthy_servers: u32,
179 pub total_players: u32,
181 pub total_capacity: u32,
183 pub timestamp: DateTime<Utc>,
185}
186
187impl ClusterHealth {
188 pub fn new(checks: &[HealthCheck]) -> Self {
190 let mut healthy = 0u32;
191 let mut degraded = 0u32;
192 let mut unhealthy = 0u32;
193 let mut total_players = 0u32;
194 let mut total_capacity = 0u32;
195
196 for check in checks {
197 match check.status {
198 HealthStatus::Healthy => healthy += 1,
199 HealthStatus::Degraded => degraded += 1,
200 HealthStatus::Unhealthy | HealthStatus::Unknown => unhealthy += 1,
201 }
202 total_players += check.player_count;
203 total_capacity += check.capacity;
204 }
205
206 let status = if unhealthy > 0 && healthy == 0 {
207 HealthStatus::Unhealthy
208 } else if degraded > 0 || unhealthy > 0 {
209 HealthStatus::Degraded
210 } else if healthy > 0 {
211 HealthStatus::Healthy
212 } else {
213 HealthStatus::Unknown
214 };
215
216 Self {
217 status,
218 healthy_servers: healthy,
219 degraded_servers: degraded,
220 unhealthy_servers: unhealthy,
221 total_players,
222 total_capacity,
223 timestamp: Utc::now(),
224 }
225 }
226
227 pub fn load_factor(&self) -> f32 {
229 if self.total_capacity == 0 {
230 0.0
231 } else {
232 self.total_players as f32 / self.total_capacity as f32
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_health_status_operational() {
243 assert!(HealthStatus::Healthy.is_operational());
244 assert!(HealthStatus::Degraded.is_operational());
245 assert!(!HealthStatus::Unhealthy.is_operational());
246 assert!(!HealthStatus::Unknown.is_operational());
247 }
248
249 #[test]
250 fn test_cluster_health_aggregation() {
251 let server_id = ServerId::new();
252 let checks = vec![
253 HealthCheck::healthy(server_id, 50, 100),
254 HealthCheck::healthy(server_id, 30, 100),
255 ];
256 let cluster = ClusterHealth::new(&checks);
257 assert_eq!(cluster.healthy_servers, 2);
258 assert_eq!(cluster.total_players, 80);
259 assert_eq!(cluster.total_capacity, 200);
260 }
261}