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}