Skip to main content

xbp_cli/commands/
monitor.rs

1use crate::logging::{log_error, log_info, log_success, log_warn};
2use anyhow::Result;
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use std::time::{Duration, Instant};
6use tokio::time::sleep;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct MonitorConfig {
10    pub url: String,
11    pub method: String,
12    pub expected_code: u16,
13    pub interval: u64,
14    pub timeout: u64,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct MonitorResult {
19    pub url: String,
20    pub status_code: u16,
21    pub response_time_ms: f64,
22    pub success: bool,
23    pub timestamp: i64,
24    pub error: Option<String>,
25}
26
27impl MonitorConfig {
28    pub async fn from_xbp_config() -> Result<Option<Self>> {
29        let config = match crate::commands::service::load_xbp_config().await {
30            Ok(cfg) => cfg,
31            Err(_) => return Ok(None),
32        };
33
34        if let Some(url) = config.monitor_url {
35            let monitor_config = MonitorConfig {
36                url,
37                method: config.monitor_method.unwrap_or_else(|| "GET".to_string()),
38                expected_code: config.monitor_expected_code.unwrap_or(200),
39                interval: config.monitor_interval.unwrap_or(60),
40                timeout: 30,
41            };
42
43            crate::data::athena::persist_schedule(
44                "monitor_interval",
45                "monitor_url",
46                Some(monitor_config.url.as_str()),
47                &format!("every_{}s", monitor_config.interval),
48                true,
49                serde_json::json!({
50                    "method": monitor_config.method.clone(),
51                    "expected_code": monitor_config.expected_code
52                }),
53            )
54            .await;
55
56            Ok(Some(monitor_config))
57        } else {
58            Ok(None)
59        }
60    }
61}
62
63pub async fn run_monitor_check(config: &MonitorConfig) -> Result<MonitorResult> {
64    let start_time = Instant::now();
65    let client = Client::builder()
66        .timeout(Duration::from_secs(config.timeout))
67        .build()?;
68
69    let method = match config.method.to_uppercase().as_str() {
70        "GET" => reqwest::Method::GET,
71        "POST" => reqwest::Method::POST,
72        "PUT" => reqwest::Method::PUT,
73        "DELETE" => reqwest::Method::DELETE,
74        "HEAD" => reqwest::Method::HEAD,
75        _ => reqwest::Method::GET,
76    };
77
78    let result = match client.request(method, &config.url).send().await {
79        Ok(response) => {
80            let status_code = response.status().as_u16();
81            let duration = start_time.elapsed();
82            let response_time_ms = duration.as_secs_f64() * 1000.0;
83            let success = status_code == config.expected_code;
84
85            MonitorResult {
86                url: config.url.clone(),
87                status_code,
88                response_time_ms,
89                success,
90                timestamp: chrono::Utc::now().timestamp(),
91                error: None,
92            }
93        }
94        Err(e) => {
95            let duration = start_time.elapsed();
96            let response_time_ms = duration.as_secs_f64() * 1000.0;
97
98            MonitorResult {
99                url: config.url.clone(),
100                status_code: 0,
101                response_time_ms,
102                success: false,
103                timestamp: chrono::Utc::now().timestamp(),
104                error: Some(e.to_string()),
105            }
106        }
107    };
108
109    Ok(result)
110}
111
112pub async fn start_monitoring_loop(config: MonitorConfig) -> Result<()> {
113    let _ = log_info(
114        "monitor",
115        &format!("Starting monitoring for {}", config.url),
116        None,
117    )
118    .await;
119    let _ = log_info(
120        "monitor",
121        &format!(
122            "Interval: {}s, Expected: {}",
123            config.interval, config.expected_code
124        ),
125        None,
126    )
127    .await;
128
129    loop {
130        match run_monitor_check(&config).await {
131            Ok(result) => {
132                if result.success {
133                    let _ = log_success(
134                        "monitor",
135                        &format!(
136                            "{} - {} - {:.2}ms",
137                            result.url, result.status_code, result.response_time_ms
138                        ),
139                        None,
140                    )
141                    .await;
142                } else {
143                    let error_msg = result.error.as_deref().unwrap_or("Status code mismatch");
144                    let _ = log_error(
145                        "monitor",
146                        &format!("{} - FAILED", result.url),
147                        Some(&format!(
148                            "Code: {}, Error: {}",
149                            result.status_code, error_msg
150                        )),
151                    )
152                    .await;
153                }
154            }
155            Err(e) => {
156                let _ = log_error("monitor", "Monitor check failed", Some(&e.to_string())).await;
157            }
158        }
159
160        sleep(Duration::from_secs(config.interval)).await;
161    }
162}
163
164pub async fn run_single_check() -> Result<()> {
165    match MonitorConfig::from_xbp_config().await? {
166        Some(config) => {
167            let _ = log_info("monitor", &format!("Checking {}", config.url), None).await;
168
169            let result = run_monitor_check(&config).await?;
170
171            if result.success {
172                let _ = log_success(
173                    "monitor",
174                    &format!(
175                        "✓ {} returned {} in {:.2}ms",
176                        result.url, result.status_code, result.response_time_ms
177                    ),
178                    None,
179                )
180                .await;
181            } else {
182                let error_msg = result.error.as_deref().unwrap_or("Status code mismatch");
183                let _ = log_error(
184                    "monitor",
185                    &format!("✗ {} check failed", result.url),
186                    Some(&format!(
187                        "Expected: {}, Got: {}, Error: {}",
188                        config.expected_code, result.status_code, error_msg
189                    )),
190                )
191                .await;
192            }
193
194            Ok(())
195        }
196        None => {
197            let _ = log_warn(
198                "monitor",
199                "No monitor configuration found in xbp.yaml/xbp.json",
200                None,
201            )
202            .await;
203            let _ = log_info(
204                "monitor",
205                "Add monitor_url, monitor_method, monitor_expected_code to xbp.yaml (or legacy xbp.json)",
206                None,
207            )
208            .await;
209            Ok(())
210        }
211    }
212}
213
214pub async fn start_monitor_daemon() -> Result<()> {
215    match MonitorConfig::from_xbp_config().await? {
216        Some(config) => start_monitoring_loop(config).await,
217        None => {
218            let _ = log_error(
219                "monitor",
220                "No monitor configuration found in xbp.yaml/xbp.json",
221                None,
222            )
223            .await;
224            Err(anyhow::anyhow!("Monitor configuration required"))
225        }
226    }
227}