Skip to main content

cbtop/alerting/
router.rs

1//! Alert routing, rate limiting, and deduplication.
2
3use std::collections::HashMap;
4use std::time::{Duration, Instant};
5
6use super::{Alert, AlertChannel, AlertSeverity, DeliveryResult};
7
8/// Rate limiter for alerts
9#[derive(Debug)]
10struct RateLimiter {
11    /// Max alerts per window
12    max_alerts: usize,
13    /// Window duration
14    window: Duration,
15    /// Alert timestamps by source
16    timestamps: HashMap<String, Vec<Instant>>,
17}
18
19impl RateLimiter {
20    fn new(max_alerts: usize, window: Duration) -> Self {
21        Self {
22            max_alerts,
23            window,
24            timestamps: HashMap::new(),
25        }
26    }
27
28    fn should_allow(&mut self, source: &str) -> bool {
29        let now = Instant::now();
30        let timestamps = self.timestamps.entry(source.to_string()).or_default();
31
32        // Remove old timestamps
33        timestamps.retain(|t| now.duration_since(*t) < self.window);
34
35        if timestamps.len() >= self.max_alerts {
36            false
37        } else {
38            timestamps.push(now);
39            true
40        }
41    }
42}
43
44/// Deduplication tracker
45#[derive(Debug)]
46struct Deduplicator {
47    /// Seen alert IDs with expiry
48    seen: HashMap<String, Instant>,
49    /// Dedup window
50    window: Duration,
51}
52
53impl Deduplicator {
54    fn new(window: Duration) -> Self {
55        Self {
56            seen: HashMap::new(),
57            window,
58        }
59    }
60
61    fn is_duplicate(&mut self, alert_id: &str) -> bool {
62        let now = Instant::now();
63
64        // Clean old entries
65        self.seen.retain(|_, expiry| now < *expiry);
66
67        if self.seen.contains_key(alert_id) {
68            true
69        } else {
70            self.seen.insert(alert_id.to_string(), now + self.window);
71            false
72        }
73    }
74}
75
76/// Alert router configuration
77#[derive(Debug, Clone)]
78pub struct AlertRouterConfig {
79    /// Channels by severity
80    pub severity_routes: HashMap<AlertSeverity, Vec<AlertChannel>>,
81    /// Default channels
82    pub default_channels: Vec<AlertChannel>,
83    /// Rate limit per minute
84    pub rate_limit_per_minute: usize,
85    /// Dedup window seconds
86    pub dedup_window_sec: u64,
87    /// Dry run mode
88    pub dry_run: bool,
89}
90
91impl Default for AlertRouterConfig {
92    fn default() -> Self {
93        Self {
94            severity_routes: HashMap::new(),
95            default_channels: vec![AlertChannel::Console],
96            rate_limit_per_minute: 60,
97            dedup_window_sec: 300, // 5 minutes
98            dry_run: false,
99        }
100    }
101}
102
103/// Alert router for multi-channel delivery
104#[derive(Debug)]
105pub struct AlertRouter {
106    /// Configuration
107    config: AlertRouterConfig,
108    /// Rate limiter
109    rate_limiter: RateLimiter,
110    /// Deduplicator
111    deduplicator: Deduplicator,
112    /// Alert history
113    history: Vec<Alert>,
114    /// Max history size
115    max_history: usize,
116    /// Delivery results
117    delivery_results: Vec<DeliveryResult>,
118}
119
120impl Default for AlertRouter {
121    fn default() -> Self {
122        Self::new(AlertRouterConfig::default())
123    }
124}
125
126impl AlertRouter {
127    /// Create new router
128    pub fn new(config: AlertRouterConfig) -> Self {
129        let rate_limiter = RateLimiter::new(config.rate_limit_per_minute, Duration::from_secs(60));
130        let deduplicator = Deduplicator::new(Duration::from_secs(config.dedup_window_sec));
131
132        Self {
133            config,
134            rate_limiter,
135            deduplicator,
136            history: Vec::new(),
137            max_history: 1000,
138            delivery_results: Vec::new(),
139        }
140    }
141
142    /// Enable dry run mode
143    pub fn with_dry_run(mut self, dry_run: bool) -> Self {
144        self.config.dry_run = dry_run;
145        self
146    }
147
148    /// Set max history
149    pub fn with_max_history(mut self, max: usize) -> Self {
150        self.max_history = max;
151        self
152    }
153
154    /// Add channel for severity
155    pub fn add_route(&mut self, severity: AlertSeverity, channel: AlertChannel) {
156        self.config
157            .severity_routes
158            .entry(severity)
159            .or_default()
160            .push(channel);
161    }
162
163    /// Add default channel
164    pub fn add_default_channel(&mut self, channel: AlertChannel) {
165        self.config.default_channels.push(channel);
166    }
167
168    /// Get channels for alert
169    fn get_channels(&self, severity: AlertSeverity) -> Vec<&AlertChannel> {
170        let mut channels: Vec<&AlertChannel> = self
171            .config
172            .severity_routes
173            .get(&severity)
174            .map(|c| c.iter().collect())
175            .unwrap_or_default();
176
177        if channels.is_empty() {
178            channels = self.config.default_channels.iter().collect();
179        }
180
181        channels
182    }
183
184    /// Send alert to channel
185    fn send_to_channel(&self, alert: &Alert, channel: &AlertChannel) -> DeliveryResult {
186        if self.config.dry_run {
187            return DeliveryResult::success(channel.name(), 0);
188        }
189
190        match channel {
191            AlertChannel::Console => {
192                println!(
193                    "[{}] {} - {}",
194                    alert.severity.name(),
195                    alert.title,
196                    alert.message
197                );
198                DeliveryResult::success("console", 0)
199            }
200            AlertChannel::Slack { webhook_url: _ } => {
201                // In a real implementation, this would make an HTTP request
202                // For now, simulate success
203                DeliveryResult::success("slack", 50)
204            }
205            AlertChannel::PagerDuty { routing_key: _ } => DeliveryResult::success("pagerduty", 100),
206            AlertChannel::Email { .. } => DeliveryResult::success("email", 200),
207            AlertChannel::Webhook { url: _, method: _ } => DeliveryResult::success("webhook", 30),
208        }
209    }
210
211    /// Route and send alert
212    pub fn send(&mut self, alert: Alert) -> Vec<DeliveryResult> {
213        // Check deduplication
214        if self.deduplicator.is_duplicate(&alert.id) {
215            return vec![DeliveryResult::failure("all", "duplicate alert")];
216        }
217
218        // Check rate limit
219        if !self.rate_limiter.should_allow(&alert.source) {
220            return vec![DeliveryResult::failure("all", "rate limited")];
221        }
222
223        // Get channels and send
224        let channels = self.get_channels(alert.severity);
225        let results: Vec<DeliveryResult> = channels
226            .iter()
227            .map(|ch| self.send_to_channel(&alert, ch))
228            .collect();
229
230        // Store results
231        self.delivery_results.extend(results.clone());
232
233        // Store in history
234        self.history.push(alert);
235        while self.history.len() > self.max_history {
236            self.history.remove(0);
237        }
238
239        results
240    }
241
242    /// Get alert history
243    pub fn history(&self) -> &[Alert] {
244        &self.history
245    }
246
247    /// Get delivery results
248    pub fn delivery_results(&self) -> &[DeliveryResult] {
249        &self.delivery_results
250    }
251
252    /// Clear history
253    pub fn clear_history(&mut self) {
254        self.history.clear();
255        self.delivery_results.clear();
256    }
257
258    /// Get alert count
259    pub fn alert_count(&self) -> usize {
260        self.history.len()
261    }
262}