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}