Skip to main content

wowlab_sentinel/http/routes/
status.rs

1use std::sync::Arc;
2
3use axum::extract::State;
4use serde::Serialize;
5use wowlab_common::{sys as common_sys, time::Instant};
6
7use super::super::PrettyJson;
8use crate::{state::ServerState, utils::sys};
9
10const LOAD_AVERAGE_WINDOW_COUNT: usize = 3;
11const ONE_DECIMAL_SCALE: f64 = 10.0;
12
13const fn status_str(healthy: bool) -> &'static str {
14    if healthy { "OK" } else { "DEGRADED" }
15}
16
17pub(crate) async fn handler(State(state): State<Arc<ServerState>>) -> PrettyJson<StatusResponse> {
18    let uptime = state.health.started_at.elapsed().as_secs();
19    let memory_mb = common_sys::read_memory_mb();
20    let (os_mem_total, os_mem_available) = common_sys::read_os_memory_mb();
21    let load = common_sys::read_load_average();
22    let cpu = sys::cpu_usage_percent().await;
23
24    let db_start = Instant::now();
25    let db_healthy = sqlx::query("SELECT 1")
26        .execute(state.dbs.get::<crate::http::HttpDb>())
27        .await
28        .is_ok();
29    let db_ping_ms = u64::try_from(db_start.elapsed().as_millis()).unwrap_or(u64::MAX);
30
31    let mut services = vec![
32        ComponentStatus::new("Scheduler", status_str(state.scheduler_healthy())),
33        ComponentStatus::new("Presence", status_str(state.presence_healthy())),
34        ComponentStatus::new("Cron", status_str(state.cron_healthy())),
35        ComponentStatus::new("Centrifuge", status_str(state.centrifuge_healthy().await)),
36        ComponentStatus::with_ping("Database", status_str(db_healthy), db_ping_ms),
37    ];
38
39    if state.config.mcp_enabled {
40        services.push(ComponentStatus::new("MCP", "OK"));
41    }
42
43    PrettyJson(StatusResponse {
44        data: StatusData {
45            cpu_percent: round_one_decimal(cpu),
46            load,
47            memory_mb: round_one_decimal(memory_mb),
48            os_memory_mb: OsMemoryStatus {
49                available: round_one_decimal(os_mem_available),
50                total: round_one_decimal(os_mem_total),
51            },
52            services,
53            uptime,
54            version: env!("CARGO_PKG_VERSION"),
55        },
56        status: "success",
57    })
58}
59
60#[derive(Debug, Serialize)]
61pub(crate) struct StatusResponse {
62    data: StatusData,
63    status: &'static str,
64}
65
66#[derive(Debug, Serialize)]
67struct StatusData {
68    cpu_percent: f64,
69    load: [f64; LOAD_AVERAGE_WINDOW_COUNT],
70    memory_mb: f64,
71    os_memory_mb: OsMemoryStatus,
72    services: Vec<ComponentStatus>,
73    uptime: u64,
74    version: &'static str,
75}
76
77#[derive(Debug, Serialize)]
78struct OsMemoryStatus {
79    available: f64,
80    total: f64,
81}
82
83#[derive(Debug, Serialize)]
84struct ComponentStatus {
85    name: &'static str,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    ping_ms: Option<u64>,
88    status: &'static str,
89}
90
91impl ComponentStatus {
92    const fn new(name: &'static str, status: &'static str) -> Self {
93        Self {
94            name,
95            ping_ms: None,
96            status,
97        }
98    }
99
100    const fn with_ping(name: &'static str, status: &'static str, ping_ms: u64) -> Self {
101        Self {
102            name,
103            ping_ms: Some(ping_ms),
104            status,
105        }
106    }
107}
108
109fn round_one_decimal(value: f64) -> f64 {
110    (value * ONE_DECIMAL_SCALE).round() / ONE_DECIMAL_SCALE
111}
112
113#[cfg(test)]
114mod tests {
115    use googletest::prelude::*;
116
117    use super::*;
118
119    #[gtest]
120    fn typed_status_shape_matches_existing_json_contract() -> Result<()> {
121        let response = StatusResponse {
122            data: StatusData {
123                cpu_percent: 1.2,
124                load: [3.4, 2.3, 1.2],
125                memory_mb: 5.6,
126                os_memory_mb: OsMemoryStatus {
127                    available: 7.8,
128                    total: 9.0,
129                },
130                services: vec![
131                    ComponentStatus::new("Scheduler", "OK"),
132                    ComponentStatus::with_ping("Database", "DEGRADED", 12),
133                ],
134                uptime: 34,
135                version: "0.1.0",
136            },
137            status: "success",
138        };
139
140        verify_eq!(
141            serde_json::to_value(response).or_fail()?,
142            serde_json::json!({
143                "status": "success",
144                "data": {
145                    "version": "0.1.0",
146                    "uptime": 34,
147                    "load": [3.4, 2.3, 1.2],
148                    "cpu_percent": 1.2,
149                    "memory_mb": 5.6,
150                    "os_memory_mb": { "total": 9.0, "available": 7.8 },
151                    "services": [
152                        { "name": "Scheduler", "status": "OK" },
153                        { "name": "Database", "status": "DEGRADED", "ping_ms": 12 }
154                    ]
155                }
156            })
157        )?;
158
159        Ok(())
160    }
161}