Skip to main content

Horizon_Network_Common/
health.rs

1//! Health check types for monitoring Horizon instances.
2//!
3//! These types are used by Atlas to monitor the health of Horizon servers
4//! and by Maestro to check container status.
5
6use serde::{Deserialize, Serialize};
7use chrono::{DateTime, Utc};
8
9use crate::server::ServerId;
10
11/// Overall health status of a service.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum HealthStatus {
15    /// Service is healthy and operating normally
16    Healthy,
17    /// Service is experiencing issues but still functional
18    Degraded,
19    /// Service is not responding or critically failed
20    Unhealthy,
21    /// Health status is unknown (no recent check)
22    Unknown,
23}
24
25impl Default for HealthStatus {
26    fn default() -> Self {
27        Self::Unknown
28    }
29}
30
31impl HealthStatus {
32    /// Returns true if the service is operational (healthy or degraded).
33    pub fn is_operational(&self) -> bool {
34        matches!(self, Self::Healthy | Self::Degraded)
35    }
36}
37
38/// Detailed health check response from a Horizon server.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct HealthCheck {
41    /// Server identifier
42    pub server_id: ServerId,
43    /// Overall health status
44    pub status: HealthStatus,
45    /// Timestamp of this health check
46    pub timestamp: DateTime<Utc>,
47    /// Current player count
48    pub player_count: u32,
49    /// Maximum player capacity
50    pub capacity: u32,
51    /// Server uptime in seconds
52    pub uptime_secs: u64,
53    /// Average tick rate (ticks per second)
54    pub tick_rate: f32,
55    /// Memory usage in megabytes
56    pub memory_mb: u32,
57    /// CPU usage percentage (0-100)
58    pub cpu_percent: f32,
59    /// Individual component checks
60    pub components: Vec<ComponentHealth>,
61    /// Optional message
62    #[serde(default)]
63    pub message: Option<String>,
64}
65
66impl HealthCheck {
67    /// Creates a healthy status response.
68    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    /// Creates an unhealthy status response.
85    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    /// Calculates load factor (0.0 to 1.0).
102    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/// Health status of an individual component.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct ComponentHealth {
114    /// Component name
115    pub name: String,
116    /// Component health status
117    pub status: HealthStatus,
118    /// Optional details
119    #[serde(default)]
120    pub details: Option<String>,
121    /// Response time in milliseconds (if applicable)
122    #[serde(default)]
123    pub response_time_ms: Option<u64>,
124}
125
126impl ComponentHealth {
127    /// Creates a healthy component status.
128    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    /// Creates an unhealthy component status.
138    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/// Health check request.
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct HealthCheckRequest {
151    /// Whether to include detailed component checks
152    #[serde(default)]
153    pub include_components: bool,
154    /// Whether to include system metrics
155    #[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/// Aggregated health status for all servers (used by Atlas).
169#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct ClusterHealth {
171    /// Overall cluster status
172    pub status: HealthStatus,
173    /// Number of healthy servers
174    pub healthy_servers: u32,
175    /// Number of degraded servers
176    pub degraded_servers: u32,
177    /// Number of unhealthy servers
178    pub unhealthy_servers: u32,
179    /// Total players across all servers
180    pub total_players: u32,
181    /// Total capacity across all servers
182    pub total_capacity: u32,
183    /// Timestamp of this aggregation
184    pub timestamp: DateTime<Utc>,
185}
186
187impl ClusterHealth {
188    /// Creates a new cluster health summary.
189    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    /// Calculates overall load factor.
228    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}