xbp 10.7.0

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

#[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"))
        }
    }
}