1use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
14pub enum AlertLevel {
15 Info,
17 Warning,
19 Critical,
21}
22
23impl std::fmt::Display for AlertLevel {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 AlertLevel::Info => write!(f, "INFO"),
27 AlertLevel::Warning => write!(f, "WARNING"),
28 AlertLevel::Critical => write!(f, "CRITICAL"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Eq)]
35#[serde(tag = "type")]
36pub enum AlertCondition {
37 MissedSchedule {
39 expected_at: DateTime<Utc>,
41 detected_at: DateTime<Utc>,
43 },
44 ConsecutiveFailures {
46 count: u32,
48 threshold: u32,
50 },
51 HighFailureRate {
53 rate: String, threshold: String,
57 },
58 SlowExecution {
60 duration_ms: u64,
62 threshold_ms: u64,
64 },
65 TaskStuck {
67 idle_duration_seconds: i64,
69 expected_interval_seconds: u64,
71 },
72 TaskUnhealthy {
74 issues: Vec<String>,
76 },
77}
78
79impl std::fmt::Display for AlertCondition {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 AlertCondition::MissedSchedule {
83 expected_at,
84 detected_at,
85 } => {
86 let delay = detected_at
87 .signed_duration_since(*expected_at)
88 .num_seconds();
89 write!(
90 f,
91 "Missed schedule ({}s late, expected at {})",
92 delay,
93 expected_at.format("%Y-%m-%d %H:%M:%S UTC")
94 )
95 }
96 AlertCondition::ConsecutiveFailures { count, threshold } => {
97 write!(
98 f,
99 "Consecutive failures ({} failures, threshold: {})",
100 count, threshold
101 )
102 }
103 AlertCondition::HighFailureRate { rate, threshold } => {
104 write!(
105 f,
106 "High failure rate (rate: {}, threshold: {})",
107 rate, threshold
108 )
109 }
110 AlertCondition::SlowExecution {
111 duration_ms,
112 threshold_ms,
113 } => {
114 write!(
115 f,
116 "Slow execution ({}ms, threshold: {}ms)",
117 duration_ms, threshold_ms
118 )
119 }
120 AlertCondition::TaskStuck {
121 idle_duration_seconds,
122 expected_interval_seconds,
123 } => {
124 write!(
125 f,
126 "Task stuck (idle: {}s, expected interval: {}s)",
127 idle_duration_seconds, expected_interval_seconds
128 )
129 }
130 AlertCondition::TaskUnhealthy { issues } => {
131 write!(f, "Task unhealthy: {}", issues.join(", "))
132 }
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct Alert {
140 pub timestamp: DateTime<Utc>,
142 pub task_name: String,
144 pub level: AlertLevel,
146 pub condition: AlertCondition,
148 pub message: String,
150 #[serde(default)]
152 pub metadata: HashMap<String, String>,
153}
154
155impl Alert {
156 pub fn new(
158 task_name: String,
159 level: AlertLevel,
160 condition: AlertCondition,
161 message: String,
162 ) -> Self {
163 Self {
164 timestamp: Utc::now(),
165 task_name,
166 level,
167 condition,
168 message,
169 metadata: HashMap::new(),
170 }
171 }
172
173 pub fn with_metadata(mut self, key: String, value: String) -> Self {
175 self.metadata.insert(key, value);
176 self
177 }
178
179 pub fn is_critical(&self) -> bool {
181 self.level == AlertLevel::Critical
182 }
183
184 pub fn is_warning(&self) -> bool {
186 self.level == AlertLevel::Warning
187 }
188
189 pub fn is_info(&self) -> bool {
191 self.level == AlertLevel::Info
192 }
193
194 fn dedup_key(&self) -> String {
196 format!("{}::{:?}", self.task_name, self.condition)
197 }
198}
199
200impl std::fmt::Display for Alert {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 write!(
203 f,
204 "[{}] {} - {} - {}",
205 self.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
206 self.level,
207 self.task_name,
208 self.message
209 )
210 }
211}
212
213pub type AlertCallback = Arc<dyn Fn(&Alert) + Send + Sync>;
217
218#[derive(Clone, Serialize, Deserialize)]
220pub struct AlertManager {
221 alerts: Vec<Alert>,
223 max_history: usize,
225 dedup_window_seconds: i64,
227 last_alert_time: HashMap<String, DateTime<Utc>>,
229 #[serde(skip)]
231 callbacks: Vec<AlertCallback>,
232}
233
234impl AlertManager {
235 pub fn new(max_history: usize, dedup_window_seconds: i64) -> Self {
241 Self {
242 alerts: Vec::new(),
243 max_history,
244 dedup_window_seconds,
245 last_alert_time: HashMap::new(),
246 callbacks: Vec::new(),
247 }
248 }
249
250 pub fn add_callback(&mut self, callback: AlertCallback) {
252 self.callbacks.push(callback);
253 }
254
255 pub fn record_alert(&mut self, alert: Alert) -> bool {
264 let dedup_key = alert.dedup_key();
265 let now = Utc::now();
266
267 if let Some(last_time) = self.last_alert_time.get(&dedup_key) {
269 let elapsed = now.signed_duration_since(*last_time).num_seconds();
270 if elapsed < self.dedup_window_seconds {
271 return false;
273 }
274 }
275
276 self.last_alert_time.insert(dedup_key, now);
278
279 for callback in &self.callbacks {
281 callback(&alert);
282 }
283
284 self.alerts.push(alert);
286
287 if self.alerts.len() > self.max_history {
289 self.alerts.drain(0..self.alerts.len() - self.max_history);
290 }
291
292 self.last_alert_time.retain(|_, last_time| {
294 now.signed_duration_since(*last_time).num_seconds() < self.dedup_window_seconds * 2
295 });
296
297 true
298 }
299
300 pub fn get_alerts(&self) -> &[Alert] {
302 &self.alerts
303 }
304
305 pub fn get_critical_alerts(&self) -> Vec<&Alert> {
307 self.alerts.iter().filter(|a| a.is_critical()).collect()
308 }
309
310 pub fn get_warning_alerts(&self) -> Vec<&Alert> {
312 self.alerts.iter().filter(|a| a.is_warning()).collect()
313 }
314
315 pub fn get_task_alerts(&self, task_name: &str) -> Vec<&Alert> {
317 self.alerts
318 .iter()
319 .filter(|a| a.task_name == task_name)
320 .collect()
321 }
322
323 pub fn get_recent_alerts(&self, seconds: i64) -> Vec<&Alert> {
325 let cutoff = Utc::now() - Duration::seconds(seconds);
326 self.alerts
327 .iter()
328 .filter(|a| a.timestamp > cutoff)
329 .collect()
330 }
331
332 pub fn clear(&mut self) {
334 self.alerts.clear();
335 self.last_alert_time.clear();
336 }
337
338 pub fn clear_task_alerts(&mut self, task_name: &str) {
340 self.alerts.retain(|a| a.task_name != task_name);
341 self.last_alert_time
342 .retain(|k, _| !k.starts_with(&format!("{}::", task_name)));
343 }
344
345 pub fn alert_count(&self) -> usize {
347 self.alerts.len()
348 }
349
350 pub fn critical_alert_count(&self) -> usize {
352 self.alerts.iter().filter(|a| a.is_critical()).count()
353 }
354
355 pub fn warning_alert_count(&self) -> usize {
357 self.alerts.iter().filter(|a| a.is_warning()).count()
358 }
359}
360
361impl Default for AlertManager {
362 fn default() -> Self {
363 Self::new(1000, 300) }
365}
366
367impl std::fmt::Debug for AlertManager {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 f.debug_struct("AlertManager")
370 .field("alerts_count", &self.alerts.len())
371 .field("max_history", &self.max_history)
372 .field("dedup_window_seconds", &self.dedup_window_seconds)
373 .field("callbacks_count", &self.callbacks.len())
374 .finish()
375 }
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct AlertConfig {
381 #[serde(default = "default_true")]
383 pub enabled: bool,
384 #[serde(default = "default_consecutive_failures_threshold")]
386 pub consecutive_failures_threshold: u32,
387 #[serde(default = "default_failure_rate_threshold")]
389 pub failure_rate_threshold: f64,
390 pub slow_execution_threshold_ms: Option<u64>,
392 #[serde(default = "default_true")]
394 pub alert_on_missed_schedule: bool,
395 #[serde(default = "default_true")]
397 pub alert_on_stuck: bool,
398}
399
400#[allow(dead_code)]
401fn default_true() -> bool {
402 true
403}
404
405#[allow(dead_code)]
406fn default_consecutive_failures_threshold() -> u32 {
407 3
408}
409
410#[allow(dead_code)]
411fn default_failure_rate_threshold() -> f64 {
412 0.5
413}
414
415impl Default for AlertConfig {
416 fn default() -> Self {
417 Self {
418 enabled: true,
419 consecutive_failures_threshold: 3,
420 failure_rate_threshold: 0.5,
421 slow_execution_threshold_ms: None,
422 alert_on_missed_schedule: true,
423 alert_on_stuck: true,
424 }
425 }
426}
427
428impl AlertConfig {
429 pub fn new() -> Self {
431 Self::default()
432 }
433
434 pub fn disabled() -> Self {
436 Self {
437 enabled: false,
438 ..Default::default()
439 }
440 }
441
442 pub fn with_consecutive_failures_threshold(mut self, threshold: u32) -> Self {
444 self.consecutive_failures_threshold = threshold;
445 self
446 }
447
448 pub fn with_failure_rate_threshold(mut self, threshold: f64) -> Self {
450 self.failure_rate_threshold = threshold;
451 self
452 }
453
454 pub fn with_slow_execution_threshold_ms(mut self, threshold_ms: u64) -> Self {
456 self.slow_execution_threshold_ms = Some(threshold_ms);
457 self
458 }
459
460 pub fn without_missed_schedule_alerts(mut self) -> Self {
462 self.alert_on_missed_schedule = false;
463 self
464 }
465
466 pub fn without_stuck_alerts(mut self) -> Self {
468 self.alert_on_stuck = false;
469 self
470 }
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct WebhookConfig {
480 pub url: String,
482 #[serde(default)]
484 pub headers: HashMap<String, String>,
485 #[serde(default = "default_webhook_timeout")]
487 pub timeout_seconds: u64,
488 #[serde(default)]
490 pub alert_levels: Vec<AlertLevel>,
491}
492
493#[allow(dead_code)]
494fn default_webhook_timeout() -> u64 {
495 30
496}
497
498impl WebhookConfig {
499 pub fn new(url: impl Into<String>) -> Self {
511 Self {
512 url: url.into(),
513 headers: HashMap::new(),
514 timeout_seconds: default_webhook_timeout(),
515 alert_levels: Vec::new(),
516 }
517 }
518
519 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
521 self.headers.insert(key.into(), value.into());
522 self
523 }
524
525 pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
527 self.timeout_seconds = timeout_seconds;
528 self
529 }
530
531 pub fn with_alert_levels(mut self, levels: Vec<AlertLevel>) -> Self {
533 self.alert_levels = levels;
534 self
535 }
536
537 pub fn should_send(&self, alert: &Alert) -> bool {
539 if self.alert_levels.is_empty() {
540 return true;
541 }
542 self.alert_levels.contains(&alert.level)
543 }
544
545 pub fn create_payload(&self, alert: &Alert) -> serde_json::Value {
547 serde_json::json!({
548 "timestamp": alert.timestamp.to_rfc3339(),
549 "task_name": alert.task_name,
550 "level": format!("{:?}", alert.level),
551 "condition": format!("{:?}", alert.condition),
552 "message": alert.message,
553 "metadata": alert.metadata,
554 })
555 }
556}