wowlab_sentinel/http/routes/
status.rs1use 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}