darpan 0.2.4

Linux developer service monitoring utility with auto-detection, real-time health checks, and interactive TUI for databases, APIs, Docker containers, and more
Documentation
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
            }
            _ => {
                // Try default health endpoints
                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;
                    }
                }
                
                // Fallback: If no health endpoint found, check if port is open and accepting connections
                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)
    }
}