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 fn create_healthy_status() -> HealthStatus {
135 HealthStatus {
136 status: "healthy".to_string(),
137 timestamp: "2024-01-01T00:00:00Z".to_string(),
138 uptime_seconds: 10,
139 version: "0.1.0".to_string(),
140 }
141 }
142
143 fn create_unhealthy_status(reason: &str) -> HealthStatus {
144 HealthStatus {
145 status: format!("unhealthy: {}", reason),
146 timestamp: "2024-01-01T00:00:00Z".to_string(),
147 uptime_seconds: 10,
148 version: "0.1.0".to_string(),
149 }
150 }
151
152 #[test]
154 fn test_health_status_is_healthy() {
155 let status = create_healthy_status();
156 assert!(status.is_healthy());
157 }
158
159 #[test]
160 fn test_health_status_is_not_healthy() {
161 let status = create_unhealthy_status("database connection failed");
162 assert!(!status.is_healthy());
163 }
164
165 #[test]
166 fn test_health_status_empty_status_not_healthy() {
167 let status = HealthStatus {
168 status: "".to_string(),
169 timestamp: "2024-01-01T00:00:00Z".to_string(),
170 uptime_seconds: 0,
171 version: "0.1.0".to_string(),
172 };
173 assert!(!status.is_healthy());
174 }
175
176 #[test]
177 fn test_health_status_clone() {
178 let status = create_healthy_status();
179 let cloned = status.clone();
180 assert_eq!(status.status, cloned.status);
181 assert_eq!(status.uptime_seconds, cloned.uptime_seconds);
182 assert_eq!(status.version, cloned.version);
183 }
184
185 #[test]
186 fn test_health_status_debug() {
187 let status = create_healthy_status();
188 let debug = format!("{:?}", status);
189 assert!(debug.contains("HealthStatus"));
190 assert!(debug.contains("healthy"));
191 }
192
193 #[test]
194 fn test_health_status_serialize() {
195 let status = create_healthy_status();
196 let json = serde_json::to_string(&status).unwrap();
197 assert!(json.contains("\"status\":\"healthy\""));
198 assert!(json.contains("\"uptime_seconds\":10"));
199 assert!(json.contains("\"version\":\"0.1.0\""));
200 }
201
202 #[test]
203 fn test_health_status_deserialize() {
204 let json = r#"{
205 "status": "healthy",
206 "timestamp": "2025-01-01T12:00:00Z",
207 "uptime_seconds": 3600,
208 "version": "1.0.0"
209 }"#;
210
211 let status: HealthStatus = serde_json::from_str(json).unwrap();
212 assert_eq!(status.status, "healthy");
213 assert_eq!(status.uptime_seconds, 3600);
214 assert_eq!(status.version, "1.0.0");
215 assert!(status.is_healthy());
216 }
217
218 #[test]
219 fn test_health_status_with_long_uptime() {
220 let status = HealthStatus {
221 status: "healthy".to_string(),
222 timestamp: "2024-01-01T00:00:00Z".to_string(),
223 uptime_seconds: 86400 * 365, version: "0.1.0".to_string(),
225 };
226 assert!(status.is_healthy());
227 assert_eq!(status.uptime_seconds, 31_536_000);
228 }
229
230 #[test]
232 fn test_health_check_creation() {
233 let health = HealthCheck::new("localhost", 3000);
234 assert_eq!(health.base_url(), "http://localhost:3000");
235 }
236
237 #[test]
238 fn test_health_check_creation_different_host() {
239 let health = HealthCheck::new("192.168.1.100", 8080);
240 assert_eq!(health.base_url(), "http://192.168.1.100:8080");
241 }
242
243 #[test]
244 fn test_health_check_creation_with_hostname() {
245 let health = HealthCheck::new("api.example.com", 443);
246 assert_eq!(health.base_url(), "http://api.example.com:443");
247 }
248
249 #[test]
250 fn test_health_check_base_url_method() {
251 let health = HealthCheck::new("test-server", 9000);
252 let url = health.base_url();
253 assert_eq!(url, "http://test-server:9000");
254 }
255}