use crate::health::HealthCheckerTrait;
use crate::models::{HealthCheckType, HealthResult, Service};
use async_trait::async_trait;
use reqwest::Client;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tracing::trace;
pub struct HttpHealthChecker {
client: Client,
}
impl HttpHealthChecker {
pub fn new(timeout: Duration) -> Self {
let client = Client::builder()
.timeout(timeout)
.build()
.unwrap();
Self { client }
}
async fn check_endpoint(&self, url: &str, expected_status: Option<u16>) -> HealthResult {
let start = Instant::now();
match self.client.get(url).send().await {
Ok(response) => {
let response_time = start.elapsed().as_millis() as u64;
let status = response.status().as_u16();
if let Some(expected) = expected_status {
if status == expected {
trace!("HTTP check OK: {} returned {} ({}ms)", url, status, response_time);
HealthResult::healthy(response_time)
} else {
trace!("HTTP check failed: {} returned {} (expected {})", url, status, expected);
HealthResult::unhealthy(
format!("HTTP status {} (expected {})", status, expected),
None,
)
}
} else if (200..300).contains(&status) {
trace!("HTTP check OK: {} returned {} ({}ms)", url, status, response_time);
HealthResult::healthy(response_time)
} else {
trace!("HTTP check warning: {} returned {}", url, status);
HealthResult::degraded(
response_time,
format!("HTTP status {}", status),
)
}
}
Err(e) => {
trace!("HTTP check failed: {} - {}", url, e);
HealthResult::unhealthy(
format!("HTTP request failed: {}", e),
Some(format!("Check if service is running and accessible at {}", url)),
)
}
}
}
}
#[async_trait]
impl HealthCheckerTrait for HttpHealthChecker {
async fn check(&self, service: &Service) -> HealthResult {
match &service.health_check.check_type {
HealthCheckType::Http { path, expected_status } => {
let url = format!("http://{}:{}{}", service.host, service.port, path);
self.check_endpoint(&url, *expected_status).await
}
_ => {
let default_paths = vec!["/health", "/api/health", "/healthz", "/ping"];
for path in default_paths {
let url = format!("http://{}:{}{}", service.host, service.port, path);
let result = self.check_endpoint(&url, None).await;
if matches!(result.status, crate::models::HealthStatus::Healthy) {
return result;
}
}
trace!("No health endpoint found for {}:{}, falling back to port check", service.host, service.port);
let addr = format!("{}:{}", service.host, service.port);
let start = Instant::now();
match tokio::time::timeout(Duration::from_secs(2), TcpStream::connect(&addr)).await {
Ok(Ok(_)) => {
let response_time = start.elapsed().as_millis() as u64;
trace!("Port {} is open and accepting connections ({}ms)", addr, response_time);
HealthResult::healthy(response_time)
}
Ok(Err(e)) => {
trace!("Port {} connection failed: {}", addr, e);
HealthResult::unhealthy(
format!("Port not accessible: {}", e),
Some(format!("Service may not be running on {}", addr)),
)
}
Err(_) => {
trace!("Port {} connection timeout", addr);
HealthResult::unhealthy(
"Connection timeout".to_string(),
Some(format!("Port {} not responding", service.port)),
)
}
}
}
}
}
fn supports(&self, service: &Service) -> bool {
matches!(service.health_check.check_type, HealthCheckType::Http { .. })
|| matches!(service.service_type, crate::models::ServiceType::HttpServer)
}
}