1use crate::error::{Error, Result};
4use reqwest::Client;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7use tokio::time::{interval, timeout};
8use tracing::{debug, trace};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct HealthStatus {
13 pub status: String,
15
16 pub timestamp: String,
18
19 pub uptime_seconds: u64,
21
22 pub version: String,
24}
25
26impl HealthStatus {
27 pub fn is_healthy(&self) -> bool {
29 self.status == "healthy"
30 }
31}
32
33pub struct HealthCheck {
35 client: Client,
36 base_url: String,
37}
38
39impl HealthCheck {
40 pub fn new(host: &str, port: u16) -> Self {
47 Self {
48 client: Client::builder()
49 .timeout(Duration::from_secs(5))
50 .build()
51 .expect("Failed to build HTTP client"),
52 base_url: format!("http://{}:{}", host, port),
53 }
54 }
55
56 pub async fn check(&self) -> Result<HealthStatus> {
58 let url = format!("{}/health", self.base_url);
59 trace!("Checking health at: {}", url);
60
61 let response = self.client.get(&url).send().await?;
62
63 if !response.status().is_success() {
64 return Err(Error::HealthCheckFailed(format!(
65 "HTTP {} - {}",
66 response.status(),
67 response.text().await.unwrap_or_default()
68 )));
69 }
70
71 let status: HealthStatus = response.json().await?;
72 debug!("Health check response: {:?}", status);
73
74 Ok(status)
75 }
76
77 pub async fn wait_until_healthy(
84 &self,
85 timeout_duration: Duration,
86 check_interval: Duration,
87 ) -> Result<HealthStatus> {
88 debug!(
89 "Waiting for server to become healthy (timeout: {:?}, interval: {:?})",
90 timeout_duration, check_interval
91 );
92
93 let check_fut = async {
94 let mut check_timer = interval(check_interval);
95
96 loop {
97 check_timer.tick().await;
98
99 match self.check().await {
100 Ok(status) => {
101 if status.is_healthy() {
102 debug!("Server is healthy!");
103 return Ok(status);
104 }
105 trace!("Server not healthy yet: {}", status.status);
106 }
107 Err(e) => {
108 trace!("Health check failed: {}", e);
109 }
110 }
111 }
112 };
113
114 timeout(timeout_duration, check_fut)
115 .await
116 .map_err(|_| Error::HealthCheckTimeout(timeout_duration.as_secs()))?
117 }
118
119 pub async fn is_ready(&self) -> bool {
121 self.check().await.is_ok()
122 }
123
124 pub fn base_url(&self) -> &str {
126 &self.base_url
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_health_status_is_healthy() {
136 let status = HealthStatus {
137 status: "healthy".to_string(),
138 timestamp: "2024-01-01T00:00:00Z".to_string(),
139 uptime_seconds: 10,
140 version: "0.1.0".to_string(),
141 };
142
143 assert!(status.is_healthy());
144 }
145
146 #[test]
147 fn test_health_status_is_not_healthy() {
148 let status = HealthStatus {
149 status: "unhealthy: database connection failed".to_string(),
150 timestamp: "2024-01-01T00:00:00Z".to_string(),
151 uptime_seconds: 10,
152 version: "0.1.0".to_string(),
153 };
154
155 assert!(!status.is_healthy());
156 }
157
158 #[test]
159 fn test_health_check_creation() {
160 let health = HealthCheck::new("localhost", 3000);
161 assert_eq!(health.base_url(), "http://localhost:3000");
162 }
163}