allframe_core/health/
mod.rs1mod server;
33mod types;
34
35use std::{
36 future::Future,
37 pin::Pin,
38 sync::Arc,
39 time::{Duration, Instant},
40};
41
42pub use server::HealthServer;
43pub use types::*;
44
45pub trait Dependency: Send + Sync {
50 fn name(&self) -> &str;
52
53 fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>>;
58
59 fn is_critical(&self) -> bool {
65 true
66 }
67
68 fn timeout(&self) -> Duration {
73 Duration::from_secs(5)
74 }
75}
76
77pub trait HealthCheck: Send + Sync {
79 fn dependencies(&self) -> Vec<Arc<dyn Dependency>>;
81
82 fn check_all(&self) -> Pin<Box<dyn Future<Output = HealthReport> + Send + '_>> {
84 let deps = self.dependencies();
85 Box::pin(async move {
86 let start = Instant::now();
87 let mut reports = Vec::with_capacity(deps.len());
88 let mut has_critical_failure = false;
89 let mut has_degradation = false;
90
91 for dep in deps {
92 let dep_start = Instant::now();
93 let timeout = dep.timeout();
94 let is_critical = dep.is_critical();
95 let name = dep.name().to_string();
96
97 let status = match tokio::time::timeout(timeout, dep.check()).await {
98 Ok(status) => status,
99 Err(_) => DependencyStatus::Unhealthy(format!(
100 "Health check timed out after {:?}",
101 timeout
102 )),
103 };
104
105 let duration = dep_start.elapsed();
106
107 match &status {
108 DependencyStatus::Unhealthy(_) if is_critical => {
109 has_critical_failure = true;
110 }
111 DependencyStatus::Unhealthy(_) | DependencyStatus::Degraded(_) => {
112 has_degradation = true;
113 }
114 _ => {}
115 }
116
117 reports.push(DependencyReport {
118 name,
119 status,
120 duration,
121 critical: is_critical,
122 });
123 }
124
125 let overall_status = if has_critical_failure {
126 OverallStatus::Unhealthy
127 } else if has_degradation {
128 OverallStatus::Degraded
129 } else {
130 OverallStatus::Healthy
131 };
132
133 HealthReport {
134 status: overall_status,
135 dependencies: reports,
136 total_duration: start.elapsed(),
137 timestamp: std::time::SystemTime::now(),
138 }
139 })
140 }
141}
142
143pub struct SimpleHealthCheck {
145 dependencies: Vec<Arc<dyn Dependency>>,
146}
147
148impl SimpleHealthCheck {
149 pub fn new() -> Self {
151 Self {
152 dependencies: Vec::new(),
153 }
154 }
155
156 pub fn add_dependency<D: Dependency + 'static>(mut self, dep: D) -> Self {
158 self.dependencies.push(Arc::new(dep));
159 self
160 }
161
162 pub fn add_arc_dependency(mut self, dep: Arc<dyn Dependency>) -> Self {
164 self.dependencies.push(dep);
165 self
166 }
167}
168
169impl Default for SimpleHealthCheck {
170 fn default() -> Self {
171 Self::new()
172 }
173}
174
175impl HealthCheck for SimpleHealthCheck {
176 fn dependencies(&self) -> Vec<Arc<dyn Dependency>> {
177 self.dependencies.clone()
178 }
179}
180
181pub struct AlwaysHealthy {
183 name: String,
184}
185
186impl AlwaysHealthy {
187 pub fn new(name: impl Into<String>) -> Self {
189 Self { name: name.into() }
190 }
191}
192
193impl Dependency for AlwaysHealthy {
194 fn name(&self) -> &str {
195 &self.name
196 }
197
198 fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>> {
199 Box::pin(async { DependencyStatus::Healthy })
200 }
201}
202
203pub struct AlwaysUnhealthy {
205 name: String,
206 message: String,
207}
208
209impl AlwaysUnhealthy {
210 pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
212 Self {
213 name: name.into(),
214 message: message.into(),
215 }
216 }
217}
218
219impl Dependency for AlwaysUnhealthy {
220 fn name(&self) -> &str {
221 &self.name
222 }
223
224 fn check(&self) -> Pin<Box<dyn Future<Output = DependencyStatus> + Send + '_>> {
225 let msg = self.message.clone();
226 Box::pin(async move { DependencyStatus::Unhealthy(msg) })
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[tokio::test]
235 async fn test_always_healthy() {
236 let dep = AlwaysHealthy::new("test");
237 assert_eq!(dep.name(), "test");
238 assert!(matches!(dep.check().await, DependencyStatus::Healthy));
239 }
240
241 #[tokio::test]
242 async fn test_always_unhealthy() {
243 let dep = AlwaysUnhealthy::new("test", "connection failed");
244 assert_eq!(dep.name(), "test");
245 match dep.check().await {
246 DependencyStatus::Unhealthy(msg) => assert_eq!(msg, "connection failed"),
247 _ => panic!("Expected unhealthy status"),
248 }
249 }
250
251 #[tokio::test]
252 async fn test_simple_health_check_empty() {
253 let checker = SimpleHealthCheck::new();
254 let report = checker.check_all().await;
255 assert_eq!(report.status, OverallStatus::Healthy);
256 assert!(report.dependencies.is_empty());
257 }
258
259 #[tokio::test]
260 async fn test_simple_health_check_all_healthy() {
261 let checker = SimpleHealthCheck::new()
262 .add_dependency(AlwaysHealthy::new("dep1"))
263 .add_dependency(AlwaysHealthy::new("dep2"));
264
265 let report = checker.check_all().await;
266 assert_eq!(report.status, OverallStatus::Healthy);
267 assert_eq!(report.dependencies.len(), 2);
268 }
269
270 #[tokio::test]
271 async fn test_simple_health_check_critical_failure() {
272 let checker = SimpleHealthCheck::new()
273 .add_dependency(AlwaysHealthy::new("healthy"))
274 .add_dependency(AlwaysUnhealthy::new("critical", "down"));
275
276 let report = checker.check_all().await;
277 assert_eq!(report.status, OverallStatus::Unhealthy);
278 }
279
280 #[tokio::test]
281 async fn test_default_timeout() {
282 let dep = AlwaysHealthy::new("test");
283 assert_eq!(dep.timeout(), Duration::from_secs(5));
284 }
285
286 #[tokio::test]
287 async fn test_default_critical() {
288 let dep = AlwaysHealthy::new("test");
289 assert!(dep.is_critical());
290 }
291}