autoendpoint/routes/
health.rs

1//! Health and Dockerflow routes
2use 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
25/// get the local memory usage in percentage of limit (presumes running under kubernetes)
26pub fn memory_usage_percentage(memory_path: &str) -> Option<f64> {
27    // If we can read (and there is a limit)
28    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        // get the current memory usage snapshot
32        && 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        // Stars have aligned, and we can return a value.
36        return Some((mem_current as f64 / mem_limit as f64) * 100.0);
37    }
38
39    None
40}
41
42/// Handle the `/health` and `/__heartbeat__` routes
43pub 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 we can display memory usage, do so.
72    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                        // Hint the key values
93                        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                // Record that Redis is down.
107                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
120/// Convert the result of a DB health check to JSON
121fn 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
140/// Handle the `/status` route
141pub 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
148/// Handle the `/__lbheartbeat__` route
149pub async fn lb_heartbeat_route() -> HttpResponse {
150    // Used by the load balancers, just return OK.
151    HttpResponse::Ok().finish()
152}
153
154/// Handle the `/__version__` route
155pub async fn version_route() -> HttpResponse {
156    // Return the contents of the version.json file created by circleci
157    // and stored in the docker root
158    HttpResponse::Ok()
159        .content_type("application/json")
160        .body(include_str!("../../../version.json"))
161}
162
163/// Handle the `/v1/err` route
164pub 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}