use std::collections::HashMap;
use std::time::{Duration, Instant};
use super::{Alert, AlertChannel, AlertSeverity, DeliveryResult};
#[derive(Debug)]
struct RateLimiter {
max_alerts: usize,
window: Duration,
timestamps: HashMap<String, Vec<Instant>>,
}
impl RateLimiter {
fn new(max_alerts: usize, window: Duration) -> Self {
Self {
max_alerts,
window,
timestamps: HashMap::new(),
}
}
fn should_allow(&mut self, source: &str) -> bool {
let now = Instant::now();
let timestamps = self.timestamps.entry(source.to_string()).or_default();
timestamps.retain(|t| now.duration_since(*t) < self.window);
if timestamps.len() >= self.max_alerts {
false
} else {
timestamps.push(now);
true
}
}
}
#[derive(Debug)]
struct Deduplicator {
seen: HashMap<String, Instant>,
window: Duration,
}
impl Deduplicator {
fn new(window: Duration) -> Self {
Self {
seen: HashMap::new(),
window,
}
}
fn is_duplicate(&mut self, alert_id: &str) -> bool {
let now = Instant::now();
self.seen.retain(|_, expiry| now < *expiry);
if self.seen.contains_key(alert_id) {
true
} else {
self.seen.insert(alert_id.to_string(), now + self.window);
false
}
}
}
#[derive(Debug, Clone)]
pub struct AlertRouterConfig {
pub severity_routes: HashMap<AlertSeverity, Vec<AlertChannel>>,
pub default_channels: Vec<AlertChannel>,
pub rate_limit_per_minute: usize,
pub dedup_window_sec: u64,
pub dry_run: bool,
}
impl Default for AlertRouterConfig {
fn default() -> Self {
Self {
severity_routes: HashMap::new(),
default_channels: vec![AlertChannel::Console],
rate_limit_per_minute: 60,
dedup_window_sec: 300, dry_run: false,
}
}
}
#[derive(Debug)]
pub struct AlertRouter {
config: AlertRouterConfig,
rate_limiter: RateLimiter,
deduplicator: Deduplicator,
history: Vec<Alert>,
max_history: usize,
delivery_results: Vec<DeliveryResult>,
}
impl Default for AlertRouter {
fn default() -> Self {
Self::new(AlertRouterConfig::default())
}
}
impl AlertRouter {
pub fn new(config: AlertRouterConfig) -> Self {
let rate_limiter = RateLimiter::new(config.rate_limit_per_minute, Duration::from_secs(60));
let deduplicator = Deduplicator::new(Duration::from_secs(config.dedup_window_sec));
Self {
config,
rate_limiter,
deduplicator,
history: Vec::new(),
max_history: 1000,
delivery_results: Vec::new(),
}
}
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.config.dry_run = dry_run;
self
}
pub fn with_max_history(mut self, max: usize) -> Self {
self.max_history = max;
self
}
pub fn add_route(&mut self, severity: AlertSeverity, channel: AlertChannel) {
self.config
.severity_routes
.entry(severity)
.or_default()
.push(channel);
}
pub fn add_default_channel(&mut self, channel: AlertChannel) {
self.config.default_channels.push(channel);
}
fn get_channels(&self, severity: AlertSeverity) -> Vec<&AlertChannel> {
let mut channels: Vec<&AlertChannel> = self
.config
.severity_routes
.get(&severity)
.map(|c| c.iter().collect())
.unwrap_or_default();
if channels.is_empty() {
channels = self.config.default_channels.iter().collect();
}
channels
}
fn send_to_channel(&self, alert: &Alert, channel: &AlertChannel) -> DeliveryResult {
if self.config.dry_run {
return DeliveryResult::success(channel.name(), 0);
}
match channel {
AlertChannel::Console => {
println!(
"[{}] {} - {}",
alert.severity.name(),
alert.title,
alert.message
);
DeliveryResult::success("console", 0)
}
AlertChannel::Slack { webhook_url: _ } => {
DeliveryResult::success("slack", 50)
}
AlertChannel::PagerDuty { routing_key: _ } => DeliveryResult::success("pagerduty", 100),
AlertChannel::Email { .. } => DeliveryResult::success("email", 200),
AlertChannel::Webhook { url: _, method: _ } => DeliveryResult::success("webhook", 30),
}
}
pub fn send(&mut self, alert: Alert) -> Vec<DeliveryResult> {
if self.deduplicator.is_duplicate(&alert.id) {
return vec![DeliveryResult::failure("all", "duplicate alert")];
}
if !self.rate_limiter.should_allow(&alert.source) {
return vec![DeliveryResult::failure("all", "rate limited")];
}
let channels = self.get_channels(alert.severity);
let results: Vec<DeliveryResult> = channels
.iter()
.map(|ch| self.send_to_channel(&alert, ch))
.collect();
self.delivery_results.extend(results.clone());
self.history.push(alert);
while self.history.len() > self.max_history {
self.history.remove(0);
}
results
}
pub fn history(&self) -> &[Alert] {
&self.history
}
pub fn delivery_results(&self) -> &[DeliveryResult] {
&self.delivery_results
}
pub fn clear_history(&mut self) {
self.history.clear();
self.delivery_results.clear();
}
pub fn alert_count(&self) -> usize {
self.history.len()
}
}