xbp 0.9.1

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use anyhow::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant};
use tokio::time::sleep;
use crate::logging::{log_info, log_success, log_warn, log_error};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitorConfig {
    pub url: String,
    pub method: String,
    pub expected_code: u16,
    pub interval: u64,
    pub timeout: u64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct MonitorResult {
    pub url: String,
    pub status_code: u16,
    pub response_time_ms: f64,
    pub success: bool,
    pub timestamp: i64,
    pub error: Option<String>,
}

impl MonitorConfig {
    pub async fn from_xbp_config() -> Result<Option<Self>> {
        let config = match crate::commands::service::load_xbp_config().await {
            Ok(cfg) => cfg,
            Err(_) => return Ok(None),
        };
        
        if let Some(url) = config.monitor_url {
            Ok(Some(MonitorConfig {
                url,
                method: config.monitor_method.unwrap_or_else(|| "GET".to_string()),
                expected_code: config.monitor_expected_code.unwrap_or(200),
                interval: config.monitor_interval.unwrap_or(60),
                timeout: 30,
            }))
        } else {
            Ok(None)
        }
    }
}

pub async fn run_monitor_check(config: &MonitorConfig) -> Result<MonitorResult> {
    let start_time = Instant::now();
    let client = Client::builder()
        .timeout(Duration::from_secs(config.timeout))
        .build()?;
    
    let method = match config.method.to_uppercase().as_str() {
        "GET" => reqwest::Method::GET,
        "POST" => reqwest::Method::POST,
        "PUT" => reqwest::Method::PUT,
        "DELETE" => reqwest::Method::DELETE,
        "HEAD" => reqwest::Method::HEAD,
        _ => reqwest::Method::GET,
    };
    
    let result = match client.request(method, &config.url).send().await {
        Ok(response) => {
            let status_code = response.status().as_u16();
            let duration = start_time.elapsed();
            let response_time_ms = duration.as_secs_f64() * 1000.0;
            let success = status_code == config.expected_code;
            
            MonitorResult {
                url: config.url.clone(),
                status_code,
                response_time_ms,
                success,
                timestamp: chrono::Utc::now().timestamp(),
                error: None,
            }
        }
        Err(e) => {
            let duration = start_time.elapsed();
            let response_time_ms = duration.as_secs_f64() * 1000.0;
            
            MonitorResult {
                url: config.url.clone(),
                status_code: 0,
                response_time_ms,
                success: false,
                timestamp: chrono::Utc::now().timestamp(),
                error: Some(e.to_string()),
            }
        }
    };
    
    Ok(result)
}

pub async fn start_monitoring_loop(config: MonitorConfig) -> Result<()> {
    let _ = log_info("monitor", &format!("Starting monitoring for {}", config.url), None).await;
    let _ = log_info("monitor", &format!("Interval: {}s, Expected: {}", config.interval, config.expected_code), None).await;
    
    loop {
        match run_monitor_check(&config).await {
            Ok(result) => {
                if result.success {
                    let _ = log_success(
                        "monitor",
                        &format!("{} - {} - {:.2}ms", result.url, result.status_code, result.response_time_ms),
                        None
                    ).await;
                } else {
                    let error_msg = result.error.as_deref().unwrap_or("Status code mismatch");
                    let _ = log_error(
                        "monitor",
                        &format!("{} - FAILED", result.url),
                        Some(&format!("Code: {}, Error: {}", result.status_code, error_msg))
                    ).await;
                }
            }
            Err(e) => {
                let _ = log_error("monitor", "Monitor check failed", Some(&e.to_string())).await;
            }
        }
        
        sleep(Duration::from_secs(config.interval)).await;
    }
}

pub async fn run_single_check() -> Result<()> {
    match MonitorConfig::from_xbp_config().await? {
        Some(config) => {
            let _ = log_info("monitor", &format!("Checking {}", config.url), None).await;
            
            let result = run_monitor_check(&config).await?;
            
            if result.success {
                let _ = log_success(
                    "monitor",
                    &format!("{} returned {} in {:.2}ms", result.url, result.status_code, result.response_time_ms),
                    None
                ).await;
            } else {
                let error_msg = result.error.as_deref().unwrap_or("Status code mismatch");
                let _ = log_error(
                    "monitor",
                    &format!("{} check failed", result.url),
                    Some(&format!("Expected: {}, Got: {}, Error: {}", config.expected_code, result.status_code, error_msg))
                ).await;
            }
            
            Ok(())
        }
        None => {
            let _ = log_warn("monitor", "No monitor configuration found in xbp.json", None).await;
            let _ = log_info("monitor", "Add monitor_url, monitor_method, monitor_expected_code to xbp.json", None).await;
            Ok(())
        }
    }
}

pub async fn start_monitor_daemon() -> Result<()> {
    match MonitorConfig::from_xbp_config().await? {
        Some(config) => {
            start_monitoring_loop(config).await
        }
        None => {
            let _ = log_error("monitor", "No monitor configuration found in xbp.json", None).await;
            Err(anyhow::anyhow!("Monitor configuration required"))
        }
    }
}