1use std::collections::BTreeMap;
4use std::path::Path;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum HealthState {
9 Healthy,
10 Degraded,
11 Unhealthy,
12}
13
14#[derive(Debug, Clone)]
15pub struct HealthIssue {
16 pub component: String,
17 pub message: String,
18}
19
20#[derive(Debug, Clone)]
21pub struct HealthReport {
22 pub state: HealthState,
23 pub issues: Vec<HealthIssue>,
24 pub diagnostics: BTreeMap<String, String>,
25 pub checked_at_unix_ms: u128,
26}
27
28impl HealthReport {
29 pub fn new(state: HealthState) -> Self {
30 Self {
31 state,
32 issues: Vec::new(),
33 diagnostics: BTreeMap::new(),
34 checked_at_unix_ms: SystemTime::now()
35 .duration_since(UNIX_EPOCH)
36 .unwrap_or_default()
37 .as_millis(),
38 }
39 }
40
41 pub fn healthy() -> Self {
42 Self::new(HealthState::Healthy)
43 }
44
45 pub fn degraded(message: impl Into<String>) -> Self {
46 let mut report = Self::new(HealthState::Degraded);
47 report.issues.push(HealthIssue {
48 component: "engine".into(),
49 message: message.into(),
50 });
51 report
52 }
53
54 pub fn unhealthy(message: impl Into<String>) -> Self {
55 let mut report = Self::new(HealthState::Unhealthy);
56 report.issues.push(HealthIssue {
57 component: "engine".into(),
58 message: message.into(),
59 });
60 report
61 }
62
63 pub fn with_diagnostic(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
64 self.diagnostics.insert(key.into(), value.into());
65 self
66 }
67
68 pub fn is_healthy(&self) -> bool {
69 matches!(self.state, HealthState::Healthy)
70 }
71
72 pub(crate) fn allows_serving_traffic(&self) -> bool {
73 match self.state {
74 HealthState::Healthy => true,
75 HealthState::Unhealthy => false,
76 HealthState::Degraded => self
77 .issues
78 .iter()
79 .all(|issue| matches!(issue.component.as_str(), "native_registry")),
80 }
81 }
82
83 pub fn issue(&mut self, component: impl Into<String>, message: impl Into<String>) {
84 self.issues.push(HealthIssue {
85 component: component.into(),
86 message: message.into(),
87 });
88 self.state = HealthState::Degraded;
89 }
90}
91
92pub fn storage_file_health(path: &Path) -> HealthReport {
93 if !path.exists() {
94 return HealthReport::degraded("database file does not exist");
95 }
96 let mut report = HealthReport::healthy();
97 let meta = match std::fs::metadata(path) {
98 Ok(meta) => meta,
99 Err(err) => {
100 return HealthReport::unhealthy(format!("unable to stat database file: {err}"));
101 }
102 };
103
104 report = report.with_diagnostic("path", path.display().to_string());
105 report = report.with_diagnostic("size_bytes", meta.len().to_string());
106 report
107}
108
109pub trait HealthProvider {
110 fn health(&self) -> HealthReport;
111}