autoendpoint/routes/
health.rs1use std::collections::HashMap;
3use std::fs::read_to_string;
4use std::thread;
5
6use actix_web::{
7 HttpResponse,
8 web::{Data, Json},
9};
10use reqwest::StatusCode;
11use serde_json::json;
12
13use crate::error::{ApiErrorKind, ApiResult};
14use crate::server::AppState;
15use autopush_common::db::error::DbResult;
16#[cfg(feature = "reliable_report")]
17use autopush_common::errors::ApcError;
18#[cfg(feature = "reliable_report")]
19use autopush_common::metric_name::MetricName;
20#[cfg(feature = "reliable_report")]
21use autopush_common::metrics::StatsdClientExt;
22#[cfg(feature = "reliable_report")]
23use autopush_common::util::b64_encode_url;
24
25pub fn memory_usage_percentage(memory_path: &str) -> Option<f64> {
27 if let Ok(mem_limit_str) = read_to_string(format!("{}/{}", memory_path, "memory.max"))
29 && mem_limit_str.trim() != "max"
30 && let Ok(mem_limit) = mem_limit_str.trim().parse::<u64>()
31 && let Ok(mem_current_str) = read_to_string(format!("{}/{}", memory_path, "memory.current"))
33 && let Ok(mem_current) = mem_current_str.trim().parse::<u64>()
34 {
35 return Some((mem_current as f64 / mem_limit as f64) * 100.0);
37 }
38
39 None
40}
41
42pub async fn health_route(state: Data<AppState>) -> Json<serde_json::Value> {
44 let router_health = interpret_table_health(state.db.router_table_exists().await);
45 let message_health = interpret_table_health(state.db.message_table_exists().await);
46 let mut routers: HashMap<&str, bool> = HashMap::new();
47 routers.insert("apns", state.apns_router.active());
48 routers.insert("fcm", state.fcm_router.active());
49
50 let mut health = json!({
51 "status": if state
52 .db
53 .health_check()
54 .await
55 .map_err(|e| {
56 error!("Autoendpoint health error: {:?}", e);
57 e
58 })
59 .is_ok() {
60 "OK"
61 } else {
62 "ERROR"
63 },
64 "version": env!("CARGO_PKG_VERSION"),
65 "router_table": router_health,
66 "message_table": message_health,
67 "routers": routers,
68 "request_count":state.in_process_subscription_updates.load(std::sync::atomic::Ordering::Relaxed),
69 });
70
71 if let Some(path) = &state.settings.kubernetes_memory_path
73 && let Some(mem_usage) = memory_usage_percentage(path)
74 {
75 health["memory_usage_percentage"] = json!(mem_usage);
76 }
77
78 #[cfg(feature = "reliable_report")]
79 {
80 let reliability_health: Result<String, ApcError> = state
81 .reliability
82 .health_check()
83 .await
84 .map(|_| {
85 let keys: Vec<String> = state
86 .settings
87 .tracking_keys()
88 .unwrap_or_default()
89 .iter()
90 .filter(|k| !k.is_empty())
91 .map(|k|
92 if k.len() > 8 {
94 b64_encode_url(k)[..8].to_string()
95 } else {
96 "".to_owned()
97 })
98 .collect();
99 if keys.is_empty() {
100 Ok("NO_TRACKING_KEYS".to_owned())
101 } else {
102 Ok(format!("OK: {}", keys.join(",")))
103 }
104 })
105 .unwrap_or_else(|e| {
106 state
108 .metrics
109 .incr_with_tags(MetricName::ReliabilityErrorRedisUnavailable)
110 .with_tag("application", "autoendpoint")
111 .send();
112 error!("🔍🟥 Reliability reporting down: {:?}", e);
113 Ok("STORE_ERROR".to_owned())
114 });
115 health["reliability"] = json!(reliability_health);
116 }
117 Json(health)
118}
119
120fn interpret_table_health(health: DbResult<bool>) -> serde_json::Value {
122 match health {
123 Ok(true) => json!({
124 "status": "OK"
125 }),
126 Ok(false) => json!({
127 "status": "NOT OK",
128 "cause": "Nonexistent table"
129 }),
130 Err(e) => {
131 error!("Autoendpoint health error: {:?}", e);
132 json!({
133 "status": "NOT OK",
134 "cause": e.to_string()
135 })
136 }
137 }
138}
139
140pub async fn status_route() -> ApiResult<Json<serde_json::Value>> {
142 Ok(Json(json!({
143 "status": "OK",
144 "version": env!("CARGO_PKG_VERSION"),
145 })))
146}
147
148pub async fn lb_heartbeat_route() -> HttpResponse {
150 HttpResponse::Ok().finish()
152}
153
154pub async fn version_route() -> HttpResponse {
156 HttpResponse::Ok()
159 .content_type("application/json")
160 .body(include_str!("../../../version.json"))
161}
162
163pub async fn log_check() -> ApiResult<String> {
165 error!(
166 "Test Critical Message";
167 "status_code" => StatusCode::IM_A_TEAPOT.as_u16(),
168 "errno" => 999,
169 );
170
171 thread::spawn(|| {
172 panic!("LogCheck");
173 });
174
175 Err(ApiErrorKind::LogCheck.into())
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[actix_rt::test]
183 async fn test_health_route() {
184 let mut mock_db = autopush_common::db::mock::MockDbClient::new();
185 mock_db.expect_router_table_exists().returning(|| Ok(true));
186 mock_db.expect_message_table_exists().returning(|| Ok(true));
187 mock_db.expect_health_check().returning(|| Ok(true));
188
189 let state: AppState = AppState::test_default(mock_db).await;
190 let response = health_route(Data::new(state)).await;
191 assert_eq!(
192 response["reliability"].get("Ok"),
193 Some(&serde_json::Value::String("NO_TRACKING_KEYS".to_owned()))
194 );
195 assert_eq!(response["status"], "OK");
196 }
197}