greentic_runner_host/http/
health.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2
3use anyhow::Error;
4use axum::Json;
5use axum::extract::State;
6use axum::response::IntoResponse;
7use time::OffsetDateTime;
8use time::format_description::well_known::Rfc3339;
9
10use crate::runner::ServerState;
11
12#[derive(Default)]
13pub struct HealthState {
14    telemetry_ready: AtomicBool,
15    secrets_ready: AtomicBool,
16    meta: parking_lot::Mutex<HealthMeta>,
17}
18
19#[derive(Default, Clone)]
20struct HealthMeta {
21    last_reload: Option<OffsetDateTime>,
22    last_error: Option<String>,
23}
24
25impl HealthState {
26    pub fn new() -> Self {
27        Self {
28            telemetry_ready: AtomicBool::new(false),
29            secrets_ready: AtomicBool::new(false),
30            meta: parking_lot::Mutex::new(HealthMeta::default()),
31        }
32    }
33
34    pub fn mark_telemetry_ready(&self) {
35        self.telemetry_ready.store(true, Ordering::SeqCst);
36    }
37
38    pub fn mark_secrets_ready(&self) {
39        self.secrets_ready.store(true, Ordering::SeqCst);
40    }
41
42    /// Mark all readiness checks as healthy.
43    pub fn set_ready(&self) {
44        self.mark_telemetry_ready();
45        self.mark_secrets_ready();
46    }
47
48    pub fn record_reload_success(&self) {
49        let mut meta = self.meta.lock();
50        meta.last_reload = Some(OffsetDateTime::now_utc());
51        meta.last_error = None;
52    }
53
54    pub fn record_reload_error(&self, err: &Error) {
55        let mut meta = self.meta.lock();
56        meta.last_error = Some(err.to_string());
57    }
58
59    pub fn snapshot(&self) -> HealthSnapshot {
60        let meta = self.meta.lock().clone();
61        HealthSnapshot {
62            telemetry_ready: self.telemetry_ready.load(Ordering::SeqCst),
63            secrets_ready: self.secrets_ready.load(Ordering::SeqCst),
64            last_reload: meta.last_reload,
65            last_error: meta.last_error,
66        }
67    }
68}
69
70pub struct HealthSnapshot {
71    pub telemetry_ready: bool,
72    pub secrets_ready: bool,
73    pub last_reload: Option<OffsetDateTime>,
74    pub last_error: Option<String>,
75}
76
77pub async fn handler(State(state): State<ServerState>) -> impl IntoResponse {
78    let snapshot = state.health.snapshot();
79    let packs = state.active.len();
80    let status = if snapshot.telemetry_ready && snapshot.secrets_ready && packs > 0 {
81        "ok"
82    } else {
83        "degraded"
84    };
85    let last_reload = snapshot.last_reload.and_then(|ts| ts.format(&Rfc3339).ok());
86    Json(serde_json::json!({
87        "status": status,
88        "telemetry_ready": snapshot.telemetry_ready,
89        "secrets_ready": snapshot.secrets_ready,
90        "active_packs": packs,
91        "last_reload": last_reload,
92        "last_error": snapshot.last_error,
93    }))
94}