1use axum::{extract::State, http::StatusCode, response::Json, routing::get};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18use tokio::sync::RwLock;
19use tracing::{debug, error, info, warn};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ServiceStatus {
24 Initializing,
26 Ready,
28 ShuttingDown,
30 Failed,
32}
33
34impl ServiceStatus {
35 pub fn is_ready(&self) -> bool {
37 matches!(self, ServiceStatus::Ready)
38 }
39
40 pub fn is_alive(&self) -> bool {
42 !matches!(self, ServiceStatus::Failed)
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct HealthManager {
49 status: Arc<RwLock<ServiceStatus>>,
51 start_time: Arc<Instant>,
53 init_deadline: Arc<Option<Instant>>,
55 shutdown_signal: Arc<RwLock<Option<tokio::sync::oneshot::Sender<()>>>>,
57}
58
59impl HealthManager {
60 pub fn new() -> Self {
62 Self {
63 status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
64 start_time: Arc::new(Instant::now()),
65 init_deadline: Arc::new(None),
66 shutdown_signal: Arc::new(RwLock::new(None)),
67 }
68 }
69
70 pub fn with_init_timeout(timeout: Duration) -> Self {
72 let deadline = Instant::now() + timeout;
73 Self {
74 status: Arc::new(RwLock::new(ServiceStatus::Initializing)),
75 start_time: Arc::new(Instant::now()),
76 init_deadline: Arc::new(Some(deadline)),
77 shutdown_signal: Arc::new(RwLock::new(None)),
78 }
79 }
80
81 pub async fn set_ready(&self) {
83 let mut status = self.status.write().await;
84 *status = ServiceStatus::Ready;
85 info!("Service marked as ready");
86 }
87
88 pub async fn set_failed(&self, reason: &str) {
90 let mut status = self.status.write().await;
91 *status = ServiceStatus::Failed;
92 error!("Service marked as failed: {}", reason);
93 }
94
95 pub async fn set_shutting_down(&self) {
97 let mut status = self.status.write().await;
98 *status = ServiceStatus::ShuttingDown;
99 info!("Service marked as shutting down");
100 }
101
102 pub async fn get_status(&self) -> ServiceStatus {
104 *self.status.read().await
105 }
106
107 pub fn uptime_seconds(&self) -> u64 {
109 self.start_time.elapsed().as_secs()
110 }
111
112 pub fn is_init_timeout(&self) -> bool {
114 if let Some(deadline) = *self.init_deadline {
115 Instant::now() > deadline
116 } else {
117 false
118 }
119 }
120
121 pub async fn set_shutdown_signal(&self, sender: tokio::sync::oneshot::Sender<()>) {
123 let mut signal = self.shutdown_signal.write().await;
124 *signal = Some(sender);
125 }
126
127 pub async fn trigger_shutdown(&self) {
129 self.set_shutting_down().await;
130 let mut signal = self.shutdown_signal.write().await;
131 if let Some(sender) = signal.take() {
132 let _ = sender.send(());
133 info!("Graceful shutdown signal sent");
134 }
135 }
136}
137
138impl Default for HealthManager {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144#[derive(Debug, Serialize, Deserialize)]
146pub struct HealthResponse {
147 pub status: String,
149 pub timestamp: String,
151 pub uptime_seconds: u64,
153 pub version: String,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub details: Option<HealthDetails>,
158}
159
160#[derive(Debug, Serialize, Deserialize)]
162pub struct HealthDetails {
163 pub initialization: String,
165 #[serde(skip_serializing_if = "Option::is_none")]
167 pub connections: Option<u64>,
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub memory_bytes: Option<u64>,
171}
172
173async fn liveness_probe(
180 State(health): State<Arc<HealthManager>>,
181) -> Result<Json<HealthResponse>, StatusCode> {
182 let status = health.get_status().await;
183 let uptime = health.uptime_seconds();
184
185 if status.is_alive() {
187 let response = HealthResponse {
188 status: "alive".to_string(),
189 timestamp: chrono::Utc::now().to_rfc3339(),
190 uptime_seconds: uptime,
191 version: env!("CARGO_PKG_VERSION").to_string(),
192 details: None,
193 };
194 Ok(Json(response))
195 } else {
196 Err(StatusCode::SERVICE_UNAVAILABLE)
197 }
198}
199
200async fn readiness_probe(
207 State(health): State<Arc<HealthManager>>,
208) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
209 let status = health.get_status().await;
210 let uptime = health.uptime_seconds();
211
212 if status.is_ready() {
214 let response = HealthResponse {
215 status: "ready".to_string(),
216 timestamp: chrono::Utc::now().to_rfc3339(),
217 uptime_seconds: uptime,
218 version: env!("CARGO_PKG_VERSION").to_string(),
219 details: Some(HealthDetails {
220 initialization: "complete".to_string(),
221 connections: None,
222 memory_bytes: None,
223 }),
224 };
225 Ok(Json(response))
226 } else {
227 let details = match status {
228 ServiceStatus::Initializing => {
229 if health.is_init_timeout() {
230 "initialization_timeout".to_string()
231 } else {
232 "initializing".to_string()
233 }
234 }
235 ServiceStatus::ShuttingDown => "shutting_down".to_string(),
236 ServiceStatus::Failed => "failed".to_string(),
237 ServiceStatus::Ready => unreachable!(),
238 };
239
240 let response = HealthResponse {
241 status: "not_ready".to_string(),
242 timestamp: chrono::Utc::now().to_rfc3339(),
243 uptime_seconds: uptime,
244 version: env!("CARGO_PKG_VERSION").to_string(),
245 details: Some(HealthDetails {
246 initialization: details,
247 connections: None,
248 memory_bytes: None,
249 }),
250 };
251
252 Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
253 }
254}
255
256async fn startup_probe(
262 State(health): State<Arc<HealthManager>>,
263) -> Result<Json<HealthResponse>, StatusCode> {
264 let status = health.get_status().await;
265 let uptime = health.uptime_seconds();
266
267 match status {
269 ServiceStatus::Ready => {
270 let response = HealthResponse {
271 status: "startup_complete".to_string(),
272 timestamp: chrono::Utc::now().to_rfc3339(),
273 uptime_seconds: uptime,
274 version: env!("CARGO_PKG_VERSION").to_string(),
275 details: Some(HealthDetails {
276 initialization: "complete".to_string(),
277 connections: None,
278 memory_bytes: None,
279 }),
280 };
281 Ok(Json(response))
282 }
283 ServiceStatus::Initializing => {
284 if health.is_init_timeout() {
285 warn!("Startup probe: initialization timeout exceeded");
286 Err(StatusCode::SERVICE_UNAVAILABLE)
287 } else {
288 debug!("Startup probe: still initializing");
289 Err(StatusCode::SERVICE_UNAVAILABLE)
290 }
291 }
292 ServiceStatus::Failed => Err(StatusCode::SERVICE_UNAVAILABLE),
293 ServiceStatus::ShuttingDown => {
294 let response = HealthResponse {
296 status: "startup_complete".to_string(),
297 timestamp: chrono::Utc::now().to_rfc3339(),
298 uptime_seconds: uptime,
299 version: env!("CARGO_PKG_VERSION").to_string(),
300 details: Some(HealthDetails {
301 initialization: "complete".to_string(),
302 connections: None,
303 memory_bytes: None,
304 }),
305 };
306 Ok(Json(response))
307 }
308 }
309}
310
311async fn health_check(
316 State(health): State<Arc<HealthManager>>,
317) -> Result<Json<HealthResponse>, (StatusCode, Json<HealthResponse>)> {
318 let status = health.get_status().await;
319 let uptime = health.uptime_seconds();
320
321 if status.is_ready() {
322 let response = HealthResponse {
323 status: "healthy".to_string(),
324 timestamp: chrono::Utc::now().to_rfc3339(),
325 uptime_seconds: uptime,
326 version: env!("CARGO_PKG_VERSION").to_string(),
327 details: Some(HealthDetails {
328 initialization: "complete".to_string(),
329 connections: None,
330 memory_bytes: None,
331 }),
332 };
333 Ok(Json(response))
334 } else {
335 let status_str = match status {
336 ServiceStatus::Initializing => "initializing",
337 ServiceStatus::ShuttingDown => "shutting_down",
338 ServiceStatus::Failed => "failed",
339 ServiceStatus::Ready => unreachable!(),
340 };
341
342 let response = HealthResponse {
343 status: status_str.to_string(),
344 timestamp: chrono::Utc::now().to_rfc3339(),
345 uptime_seconds: uptime,
346 version: env!("CARGO_PKG_VERSION").to_string(),
347 details: Some(HealthDetails {
348 initialization: status_str.to_string(),
349 connections: None,
350 memory_bytes: None,
351 }),
352 };
353
354 Err((StatusCode::SERVICE_UNAVAILABLE, Json(response)))
355 }
356}
357
358pub fn health_router(health_manager: Arc<HealthManager>) -> axum::Router {
360 health_router_with_exclusions(health_manager, &[])
361}
362
363pub(super) const HEALTH_PATHS: [&str; 4] = [
365 "/health",
366 "/health/live",
367 "/health/ready",
368 "/health/startup",
369];
370
371pub(super) fn health_router_with_exclusions(
373 health_manager: Arc<HealthManager>,
374 excluded_paths: &[&str],
375) -> axum::Router {
376 use axum::Router;
377 let mut app = Router::new();
378 let handlers = [
379 get(health_check),
380 get(liveness_probe),
381 get(readiness_probe),
382 get(startup_probe),
383 ];
384 for (path, handler) in HEALTH_PATHS.into_iter().zip(handlers) {
385 if !excluded_paths.contains(&path) {
386 app = app.route(path, handler);
387 }
388 }
389 app.with_state(health_manager)
390}
391
392pub fn health_router_with_prefix(health_manager: Arc<HealthManager>, prefix: &str) -> axum::Router {
394 use axum::Router;
395 Router::new()
396 .route(&format!("{}/health", prefix), get(health_check))
397 .route(&format!("{}/health/live", prefix), get(liveness_probe))
398 .route(&format!("{}/health/ready", prefix), get(readiness_probe))
399 .route(&format!("{}/health/startup", prefix), get(startup_probe))
400 .with_state(health_manager)
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use axum::body::Body;
407 use axum::http::Request;
408 use tower::ServiceExt;
409
410 #[test]
413 fn test_service_status() {
414 assert!(ServiceStatus::Ready.is_ready());
415 assert!(!ServiceStatus::Initializing.is_ready());
416 assert!(!ServiceStatus::ShuttingDown.is_ready());
417 assert!(!ServiceStatus::Failed.is_ready());
418
419 assert!(ServiceStatus::Ready.is_alive());
420 assert!(ServiceStatus::Initializing.is_alive());
421 assert!(ServiceStatus::ShuttingDown.is_alive());
422 assert!(!ServiceStatus::Failed.is_alive());
423 }
424
425 #[test]
426 fn test_service_status_eq() {
427 assert_eq!(ServiceStatus::Ready, ServiceStatus::Ready);
428 assert_ne!(ServiceStatus::Ready, ServiceStatus::Failed);
429 assert_eq!(ServiceStatus::Initializing, ServiceStatus::Initializing);
430 }
431
432 #[test]
433 fn test_service_status_clone() {
434 let status = ServiceStatus::Ready;
435 let cloned = status;
436 assert_eq!(status, cloned);
437 }
438
439 #[test]
440 fn test_service_status_debug() {
441 let debug = format!("{:?}", ServiceStatus::Ready);
442 assert!(debug.contains("Ready"));
443 }
444
445 #[tokio::test]
448 async fn test_health_manager_new() {
449 let manager = HealthManager::new();
450 let status = manager.get_status().await;
451 assert_eq!(status, ServiceStatus::Initializing);
452 }
453
454 #[tokio::test]
455 async fn test_health_manager_default() {
456 let manager = HealthManager::default();
457 let status = manager.get_status().await;
458 assert_eq!(status, ServiceStatus::Initializing);
459 }
460
461 #[tokio::test]
462 async fn test_health_manager_with_init_timeout() {
463 let manager = HealthManager::with_init_timeout(Duration::from_secs(30));
464 let status = manager.get_status().await;
465 assert_eq!(status, ServiceStatus::Initializing);
466 assert!(!manager.is_init_timeout());
467 }
468
469 #[tokio::test]
470 async fn test_health_manager_set_ready() {
471 let manager = HealthManager::new();
472 manager.set_ready().await;
473 let status = manager.get_status().await;
474 assert_eq!(status, ServiceStatus::Ready);
475 }
476
477 #[tokio::test]
478 async fn test_health_manager_set_failed() {
479 let manager = HealthManager::new();
480 manager.set_failed("test error").await;
481 let status = manager.get_status().await;
482 assert_eq!(status, ServiceStatus::Failed);
483 }
484
485 #[tokio::test]
486 async fn test_health_manager_set_shutting_down() {
487 let manager = HealthManager::new();
488 manager.set_shutting_down().await;
489 let status = manager.get_status().await;
490 assert_eq!(status, ServiceStatus::ShuttingDown);
491 }
492
493 #[tokio::test]
494 async fn test_health_manager_uptime() {
495 let manager = HealthManager::new();
496 let uptime = manager.uptime_seconds();
497 assert!(uptime < 5);
499 }
500
501 #[tokio::test]
502 async fn test_health_manager_clone() {
503 let manager = HealthManager::new();
504 manager.set_ready().await;
505 let cloned = manager.clone();
506 let status = cloned.get_status().await;
507 assert_eq!(status, ServiceStatus::Ready);
508 }
509
510 #[tokio::test]
511 async fn test_health_manager_trigger_shutdown() {
512 let manager = HealthManager::new();
513 manager.set_ready().await;
514 manager.trigger_shutdown().await;
515 let status = manager.get_status().await;
516 assert_eq!(status, ServiceStatus::ShuttingDown);
517 }
518
519 #[test]
522 fn test_health_response_serialization() {
523 let response = HealthResponse {
524 status: "healthy".to_string(),
525 timestamp: "2024-01-15T10:30:00Z".to_string(),
526 uptime_seconds: 3600,
527 version: "1.0.0".to_string(),
528 details: None,
529 };
530
531 let json = serde_json::to_string(&response).unwrap();
532 assert!(json.contains("healthy"));
533 assert!(json.contains("3600"));
534 assert!(json.contains("1.0.0"));
535 }
536
537 #[test]
538 fn test_health_response_with_details() {
539 let response = HealthResponse {
540 status: "healthy".to_string(),
541 timestamp: "2024-01-15T10:30:00Z".to_string(),
542 uptime_seconds: 3600,
543 version: "1.0.0".to_string(),
544 details: Some(HealthDetails {
545 initialization: "complete".to_string(),
546 connections: Some(10),
547 memory_bytes: Some(1024 * 1024),
548 }),
549 };
550
551 let json = serde_json::to_string(&response).unwrap();
552 assert!(json.contains("complete"));
553 assert!(json.contains("connections"));
554 }
555
556 #[test]
557 fn test_health_response_deserialization() {
558 let json = r#"{
559 "status": "healthy",
560 "timestamp": "2024-01-15T10:30:00Z",
561 "uptime_seconds": 7200,
562 "version": "2.0.0"
563 }"#;
564
565 let response: HealthResponse = serde_json::from_str(json).unwrap();
566 assert_eq!(response.status, "healthy");
567 assert_eq!(response.uptime_seconds, 7200);
568 assert_eq!(response.version, "2.0.0");
569 }
570
571 #[test]
574 fn test_health_details_serialization() {
575 let details = HealthDetails {
576 initialization: "complete".to_string(),
577 connections: Some(5),
578 memory_bytes: Some(2048),
579 };
580
581 let json = serde_json::to_string(&details).unwrap();
582 assert!(json.contains("complete"));
583 assert!(json.contains("5"));
584 assert!(json.contains("2048"));
585 }
586
587 #[test]
588 fn test_health_details_optional_fields() {
589 let details = HealthDetails {
590 initialization: "initializing".to_string(),
591 connections: None,
592 memory_bytes: None,
593 };
594
595 let json = serde_json::to_string(&details).unwrap();
596 assert!(json.contains("initializing"));
597 assert!(!json.contains("connections"));
598 assert!(!json.contains("memory_bytes"));
599 }
600
601 #[tokio::test]
604 async fn test_liveness_probe_alive() {
605 let health = Arc::new(HealthManager::new());
606 health.set_ready().await;
607
608 let app = health_router(health.clone());
609 let response = app
610 .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
611 .await
612 .unwrap();
613
614 assert_eq!(response.status(), StatusCode::OK);
615 }
616
617 #[tokio::test]
618 async fn test_liveness_probe_failed() {
619 let health = Arc::new(HealthManager::new());
620 health.set_failed("test failure").await;
621
622 let app = health_router(health.clone());
623 let response = app
624 .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
625 .await
626 .unwrap();
627
628 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
629 }
630
631 #[tokio::test]
632 async fn test_liveness_probe_initializing() {
633 let health = Arc::new(HealthManager::new());
634 let app = health_router(health.clone());
637 let response = app
638 .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
639 .await
640 .unwrap();
641
642 assert_eq!(response.status(), StatusCode::OK);
643 }
644
645 #[tokio::test]
646 async fn test_liveness_probe_shutting_down() {
647 let health = Arc::new(HealthManager::new());
648 health.set_shutting_down().await;
649
650 let app = health_router(health.clone());
651 let response = app
652 .oneshot(Request::builder().uri("/health/live").body(Body::empty()).unwrap())
653 .await
654 .unwrap();
655
656 assert_eq!(response.status(), StatusCode::OK);
658 }
659
660 #[tokio::test]
661 async fn test_readiness_probe_ready() {
662 let health = Arc::new(HealthManager::new());
663 health.set_ready().await;
664
665 let app = health_router(health.clone());
666 let response = app
667 .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
668 .await
669 .unwrap();
670
671 assert_eq!(response.status(), StatusCode::OK);
672 }
673
674 #[tokio::test]
675 async fn test_readiness_probe_initializing() {
676 let health = Arc::new(HealthManager::new());
677
678 let app = health_router(health.clone());
679 let response = app
680 .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
681 .await
682 .unwrap();
683
684 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
685 }
686
687 #[tokio::test]
688 async fn test_readiness_probe_shutting_down() {
689 let health = Arc::new(HealthManager::new());
690 health.set_shutting_down().await;
691
692 let app = health_router(health.clone());
693 let response = app
694 .oneshot(Request::builder().uri("/health/ready").body(Body::empty()).unwrap())
695 .await
696 .unwrap();
697
698 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
699 }
700
701 #[tokio::test]
702 async fn test_startup_probe_ready() {
703 let health = Arc::new(HealthManager::new());
704 health.set_ready().await;
705
706 let app = health_router(health.clone());
707 let response = app
708 .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
709 .await
710 .unwrap();
711
712 assert_eq!(response.status(), StatusCode::OK);
713 }
714
715 #[tokio::test]
716 async fn test_startup_probe_initializing() {
717 let health = Arc::new(HealthManager::new());
718
719 let app = health_router(health.clone());
720 let response = app
721 .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
722 .await
723 .unwrap();
724
725 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
726 }
727
728 #[tokio::test]
729 async fn test_startup_probe_shutting_down() {
730 let health = Arc::new(HealthManager::new());
731 health.set_ready().await;
732 health.set_shutting_down().await;
733
734 let app = health_router(health.clone());
735 let response = app
736 .oneshot(Request::builder().uri("/health/startup").body(Body::empty()).unwrap())
737 .await
738 .unwrap();
739
740 assert_eq!(response.status(), StatusCode::OK);
742 }
743
744 #[tokio::test]
745 async fn test_health_check_ready() {
746 let health = Arc::new(HealthManager::new());
747 health.set_ready().await;
748
749 let app = health_router(health.clone());
750 let response = app
751 .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
752 .await
753 .unwrap();
754
755 assert_eq!(response.status(), StatusCode::OK);
756 }
757
758 #[tokio::test]
759 async fn test_health_check_not_ready() {
760 let health = Arc::new(HealthManager::new());
761
762 let app = health_router(health.clone());
763 let response = app
764 .oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
765 .await
766 .unwrap();
767
768 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
769 }
770
771 #[test]
774 fn test_health_router_creation() {
775 let health = Arc::new(HealthManager::new());
776 let router = health_router(health);
777 let _ = router;
778 }
779
780 #[test]
781 fn test_health_router_with_prefix() {
782 let health = Arc::new(HealthManager::new());
783 let router = health_router_with_prefix(health, "/api/v1");
784 let _ = router;
785 }
786}