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/// ```ignore
68/// use rtb_cli::health::{HealthCheck, HEALTH_CHECKS};
69/// use rtb_app::linkme::distributed_slice;
70///
71/// #[distributed_slice(HEALTH_CHECKS)]
72/// fn register() -> Box<dyn HealthCheck> { Box::new(MyCheck) }
73/// ```
74#[distributed_slice]
75pub static HEALTH_CHECKS: [fn() -> Box<dyn HealthCheck>];
76
77/// Aggregated report from every registered [`HealthCheck`].
78#[derive(Debug, Clone)]
79pub struct HealthReport {
80    /// Per-check verdicts in registration order.
81    pub entries: Vec<(&'static str, HealthStatus)>,
82}
83
84impl HealthReport {
85    /// `true` iff no entry is [`HealthStatus::Fail`].
86    #[must_use]
87    pub fn is_ok(&self) -> bool {
88        self.entries.iter().all(|(_, s)| !s.is_fail())
89    }
90
91    /// Human-readable multi-line rendering.
92    #[must_use]
93    pub fn render(&self) -> String {
94        use std::fmt::Write;
95        let mut out = String::new();
96        for (name, status) in &self.entries {
97            let (label, summary) = match status {
98                HealthStatus::Ok { summary } => ("OK  ", summary),
99                HealthStatus::Warn { summary } => ("WARN", summary),
100                HealthStatus::Fail { summary } => ("FAIL", summary),
101            };
102            let _ = writeln!(out, "  [{label}] {name}: {summary}");
103        }
104        out
105    }
106}
107
108/// Run every registered [`HealthCheck`] against `app` and aggregate
109/// their verdicts.
110pub async fn run_all(app: &App) -> HealthReport {
111    let mut entries = Vec::with_capacity(HEALTH_CHECKS.len());
112    for factory in HEALTH_CHECKS {
113        let check = factory();
114        let status = check.check(app).await;
115        entries.push((check.name(), status));
116    }
117    HealthReport { entries }
118}