1use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum AlertSeverity {
8 Info,
10 Warning,
12 Critical,
14}
15
16impl AlertSeverity {
17 pub fn name(&self) -> &'static str {
19 match self {
20 Self::Info => "INFO",
21 Self::Warning => "WARNING",
22 Self::Critical => "CRITICAL",
23 }
24 }
25
26 pub fn color(&self) -> &'static str {
28 match self {
29 Self::Info => "#36a64f", Self::Warning => "#ffcc00", Self::Critical => "#ff0000", }
33 }
34
35 pub fn parse(s: &str) -> Option<Self> {
37 match s.to_uppercase().as_str() {
38 "INFO" => Some(Self::Info),
39 "WARNING" | "WARN" => Some(Self::Warning),
40 "CRITICAL" | "CRIT" => Some(Self::Critical),
41 _ => None,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum AlertChannel {
49 Slack { webhook_url: String },
51 PagerDuty { routing_key: String },
53 Email {
55 smtp_host: String,
56 to: String,
57 from: String,
58 },
59 Webhook { url: String, method: String },
61 Console,
63}
64
65impl AlertChannel {
66 pub fn name(&self) -> &'static str {
68 match self {
69 Self::Slack { .. } => "slack",
70 Self::PagerDuty { .. } => "pagerduty",
71 Self::Email { .. } => "email",
72 Self::Webhook { .. } => "webhook",
73 Self::Console => "console",
74 }
75 }
76
77 pub fn slack(webhook_url: &str) -> Self {
79 Self::Slack {
80 webhook_url: webhook_url.to_string(),
81 }
82 }
83
84 pub fn pagerduty(routing_key: &str) -> Self {
86 Self::PagerDuty {
87 routing_key: routing_key.to_string(),
88 }
89 }
90
91 pub fn webhook(url: &str) -> Self {
93 Self::Webhook {
94 url: url.to_string(),
95 method: "POST".to_string(),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct Alert {
103 pub id: String,
105 pub title: String,
107 pub message: String,
109 pub severity: AlertSeverity,
111 pub source: String,
113 pub value: Option<f64>,
115 pub threshold: Option<f64>,
117 pub timestamp: u64,
119 pub metadata: HashMap<String, String>,
121}
122
123impl Alert {
124 pub fn new(title: &str, message: &str, severity: AlertSeverity) -> Self {
126 let timestamp = std::time::SystemTime::now()
127 .duration_since(std::time::UNIX_EPOCH)
128 .map(|d| d.as_millis() as u64)
129 .unwrap_or(0);
130
131 Self {
132 id: format!("{}_{}", title.replace(' ', "_").to_lowercase(), timestamp),
133 title: title.to_string(),
134 message: message.to_string(),
135 severity,
136 source: String::new(),
137 value: None,
138 threshold: None,
139 timestamp,
140 metadata: HashMap::new(),
141 }
142 }
143
144 pub fn with_source(mut self, source: &str) -> Self {
146 self.source = source.to_string();
147 self
148 }
149
150 pub fn with_value(mut self, value: f64) -> Self {
152 self.value = Some(value);
153 self
154 }
155
156 pub fn with_threshold(mut self, threshold: f64) -> Self {
158 self.threshold = Some(threshold);
159 self
160 }
161
162 pub fn with_metadata(mut self, key: &str, value: &str) -> Self {
164 self.metadata.insert(key.to_string(), value.to_string());
165 self
166 }
167
168 pub fn to_slack_json(&self) -> String {
170 let value_str = self.value.map(|v| format!("{:.2}", v)).unwrap_or_default();
171 let threshold_str = self
172 .threshold
173 .map(|t| format!("{:.2}", t))
174 .unwrap_or_default();
175
176 format!(
177 r#"{{"attachments":[{{"color":"{}","title":"{}","text":"{}","fields":[{{"title":"Severity","value":"{}","short":true}},{{"title":"Source","value":"{}","short":true}},{{"title":"Value","value":"{}","short":true}},{{"title":"Threshold","value":"{}","short":true}}],"ts":{}}}]}}"#,
178 self.severity.color(),
179 self.title,
180 self.message,
181 self.severity.name(),
182 self.source,
183 value_str,
184 threshold_str,
185 self.timestamp / 1000
186 )
187 }
188
189 pub fn to_pagerduty_json(&self, routing_key: &str) -> String {
191 let action = match self.severity {
192 AlertSeverity::Critical | AlertSeverity::Warning | AlertSeverity::Info => "trigger",
193 };
194
195 format!(
196 r#"{{"routing_key":"{}","event_action":"{}","dedup_key":"{}","payload":{{"summary":"{}","source":"{}","severity":"{}","timestamp":"{}"}}}}"#,
197 routing_key,
198 action,
199 self.id,
200 self.title,
201 self.source,
202 self.severity.name().to_lowercase(),
203 self.timestamp
204 )
205 }
206
207 pub fn to_json(&self) -> String {
209 format!(
210 r#"{{"id":"{}","title":"{}","message":"{}","severity":"{}","source":"{}","value":{},"threshold":{},"timestamp":{}}}"#,
211 self.id,
212 self.title,
213 self.message,
214 self.severity.name(),
215 self.source,
216 self.value
217 .map(|v| format!("{}", v))
218 .unwrap_or("null".to_string()),
219 self.threshold
220 .map(|t| format!("{}", t))
221 .unwrap_or("null".to_string()),
222 self.timestamp
223 )
224 }
225}
226
227#[derive(Debug, Clone)]
229pub struct DeliveryResult {
230 pub channel: String,
232 pub success: bool,
234 pub error: Option<String>,
236 pub duration_ms: u64,
238}
239
240impl DeliveryResult {
241 pub fn success(channel: &str, duration_ms: u64) -> Self {
243 Self {
244 channel: channel.to_string(),
245 success: true,
246 error: None,
247 duration_ms,
248 }
249 }
250
251 pub fn failure(channel: &str, error: &str) -> Self {
253 Self {
254 channel: channel.to_string(),
255 success: false,
256 error: Some(error.to_string()),
257 duration_ms: 0,
258 }
259 }
260}