Skip to main content

rtb_cli/
health.rs

1//! Health checks — the `doctor` subcommand's plug-in point.
2
3use async_trait::async_trait;
4use linkme::distributed_slice;
5use rtb_app::app::App;
6
7/// A single health-check's verdict.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum HealthStatus {
10    /// Everything's fine. `summary` is shown verbatim.
11    Ok {
12        /// One-line human-readable description.
13        summary: String,
14    },
15    /// Operable but worth knowing about.
16    Warn {
17        /// One-line human-readable description.
18        summary: String,
19    },
20    /// Degraded. `doctor` exits non-zero if any check reports this.
21    Fail {
22        /// One-line human-readable description.
23        summary: String,
24    },
25}
26
27impl HealthStatus {
28    /// Convenience — `Ok` with a static summary.
29    #[must_use]
30    pub fn ok(summary: impl Into<String>) -> Self {
31        Self::Ok { summary: summary.into() }
32    }
33
34    /// Convenience — `Warn` with a static summary.
35    #[must_use]
36    pub fn warn(summary: impl Into<String>) -> Self {
37        Self::Warn { summary: summary.into() }
38    }
39
40    /// Convenience — `Fail` with a static summary.
41    #[must_use]
42    pub fn fail(summary: impl Into<String>) -> Self {
43        Self::Fail { summary: summary.into() }
44    }
45
46    /// `true` iff the status is [`HealthStatus::Fail`].
47    #[must_use]
48    pub const fn is_fail(&self) -> bool {
49        matches!(self, Self::Fail { .. })
50    }
51}
52
53/// A pluggable diagnostic check run by the `doctor` subcommand.
54///
55/// Register implementations via [`HEALTH_CHECKS`] using `linkme`.
56#[async_trait]
57pub trait HealthCheck: Send + Sync + 'static {
58    /// Short identifier shown in `doctor` output.
59    fn name(&self) -> &'static str;
60
61    /// Perform the check against the live `App`.
62    async fn check(&self, app: &App) -> HealthStatus;
63}
64
65/// Link-time registry of health-check factories.
66///
67/// ```no_run
68/// use rtb_app::app::App;
69/// use rtb_app::linkme::distributed_slice;
70/// use rtb_cli::health::{HealthCheck, HealthStatus, HEALTH_CHECKS};
71///
72/// struct MyCheck;
73///
74/// #[async_trait::async_trait]
75/// impl HealthCheck for MyCheck {
76///     fn name(&self) -> &'static str {
77///         "my-check"
78///     }
79///
80///     async fn check(&self, _app: &App) -> HealthStatus {
81///         HealthStatus::ok("everything is fine")
82///     }
83/// }
84///
85/// #[distributed_slice(HEALTH_CHECKS)]
86/// fn register() -> Box<dyn HealthCheck> { Box::new(MyCheck) }
87/// ```
88#[distributed_slice]
89pub static HEALTH_CHECKS: [fn() -> Box<dyn HealthCheck>];
90
91/// Aggregated report from every registered [`HealthCheck`].
92#[derive(Debug, Clone)]
93pub struct HealthReport {
94    /// Per-check verdicts in registration order.
95    pub entries: Vec<(&'static str, HealthStatus)>,
96}
97
98impl HealthReport {
99    /// `true` iff no entry is [`HealthStatus::Fail`].
100    #[must_use]
101    pub fn is_ok(&self) -> bool {
102        self.entries.iter().all(|(_, s)| !s.is_fail())
103    }
104
105    /// Human-readable multi-line rendering.
106    #[must_use]
107    pub fn render(&self) -> String {
108        use std::fmt::Write;
109        let mut out = String::new();
110        for (name, status) in &self.entries {
111            let (label, summary) = match status {
112                HealthStatus::Ok { summary } => ("OK  ", summary),
113                HealthStatus::Warn { summary } => ("WARN", summary),
114                HealthStatus::Fail { summary } => ("FAIL", summary),
115            };
116            let _ = writeln!(out, "  [{label}] {name}: {summary}");
117        }
118        out
119    }
120}
121
122/// Run every registered [`HealthCheck`] against `app` and aggregate
123/// their verdicts.
124pub async fn run_all(app: &App) -> HealthReport {
125    let mut entries = Vec::with_capacity(HEALTH_CHECKS.len());
126    for factory in HEALTH_CHECKS {
127        let check = factory();
128        let status = check.check(app).await;
129        entries.push((check.name(), status));
130    }
131    HealthReport { entries }
132}