use crate::health::HealthCheckerTrait;
use crate::models::{HealthCheckType, HealthResult, Service};
use async_trait::async_trait;
use reqwest::Client;
use std::time::{Duration, Instant};
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;
}
}
HealthResult::unhealthy(
"No health endpoint found".to_string(),
Some("Configure a health check endpoint in .darpan.yml".to_string()),
)
}
}
}
fn supports(&self, service: &Service) -> bool {
matches!(service.health_check.check_type, HealthCheckType::Http { .. })
|| matches!(service.service_type, crate::models::ServiceType::HttpServer)
}
}