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 fn issue(&mut self, component: impl Into<String>, message: impl Into<String>) {
73 self.issues.push(HealthIssue {
74 component: component.into(),
75 message: message.into(),
76 });
77 self.state = HealthState::Degraded;
78 }
79}
80
81pub fn storage_file_health(path: &Path) -> HealthReport {
82 if !path.exists() {
83 return HealthReport::degraded("database file does not exist");
84 }
85 let mut report = HealthReport::healthy();
86 let meta = match std::fs::metadata(path) {
87 Ok(meta) => meta,
88 Err(err) => {
89 return HealthReport::unhealthy(format!("unable to stat database file: {err}"));
90 }
91 };
92
93 report = report.with_diagnostic("path", path.display().to_string());
94 report = report.with_diagnostic("size_bytes", meta.len().to_string());
95 report
96}
97
98pub trait HealthProvider {
99 fn health(&self) -> HealthReport;
100}