Skip to main content

cbtop/alerting/
types.rs

1//! Alert types: severity, channels, messages, and delivery results.
2
3use std::collections::HashMap;
4
5/// Alert severity levels
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum AlertSeverity {
8    /// Informational alert
9    Info,
10    /// Warning alert
11    Warning,
12    /// Critical alert
13    Critical,
14}
15
16impl AlertSeverity {
17    /// Get severity name
18    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    /// Get severity color (for Slack)
27    pub fn color(&self) -> &'static str {
28        match self {
29            Self::Info => "#36a64f",     // Green
30            Self::Warning => "#ffcc00",  // Yellow
31            Self::Critical => "#ff0000", // Red
32        }
33    }
34
35    /// Parse from string
36    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/// Alert channel types
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum AlertChannel {
49    /// Slack webhook
50    Slack { webhook_url: String },
51    /// PagerDuty Events API
52    PagerDuty { routing_key: String },
53    /// Email via SMTP
54    Email {
55        smtp_host: String,
56        to: String,
57        from: String,
58    },
59    /// Generic HTTP webhook
60    Webhook { url: String, method: String },
61    /// Console output (for testing)
62    Console,
63}
64
65impl AlertChannel {
66    /// Get channel name
67    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    /// Create Slack channel
78    pub fn slack(webhook_url: &str) -> Self {
79        Self::Slack {
80            webhook_url: webhook_url.to_string(),
81        }
82    }
83
84    /// Create PagerDuty channel
85    pub fn pagerduty(routing_key: &str) -> Self {
86        Self::PagerDuty {
87            routing_key: routing_key.to_string(),
88        }
89    }
90
91    /// Create webhook channel
92    pub fn webhook(url: &str) -> Self {
93        Self::Webhook {
94            url: url.to_string(),
95            method: "POST".to_string(),
96        }
97    }
98}
99
100/// Alert message
101#[derive(Debug, Clone)]
102pub struct Alert {
103    /// Alert ID (for deduplication)
104    pub id: String,
105    /// Alert title
106    pub title: String,
107    /// Alert message body
108    pub message: String,
109    /// Severity level
110    pub severity: AlertSeverity,
111    /// Source metric/component
112    pub source: String,
113    /// Metric value that triggered alert
114    pub value: Option<f64>,
115    /// Threshold that was exceeded
116    pub threshold: Option<f64>,
117    /// Creation timestamp (Unix millis)
118    pub timestamp: u64,
119    /// Additional metadata
120    pub metadata: HashMap<String, String>,
121}
122
123impl Alert {
124    /// Create new alert
125    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    /// Set source
145    pub fn with_source(mut self, source: &str) -> Self {
146        self.source = source.to_string();
147        self
148    }
149
150    /// Set value
151    pub fn with_value(mut self, value: f64) -> Self {
152        self.value = Some(value);
153        self
154    }
155
156    /// Set threshold
157    pub fn with_threshold(mut self, threshold: f64) -> Self {
158        self.threshold = Some(threshold);
159        self
160    }
161
162    /// Add metadata
163    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    /// Format as Slack message
169    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    /// Format as PagerDuty event
190    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    /// Format as generic JSON
208    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/// Alert delivery result
228#[derive(Debug, Clone)]
229pub struct DeliveryResult {
230    /// Channel name
231    pub channel: String,
232    /// Was delivery successful
233    pub success: bool,
234    /// Error message if failed
235    pub error: Option<String>,
236    /// Delivery time (millis)
237    pub duration_ms: u64,
238}
239
240impl DeliveryResult {
241    /// Create success result
242    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    /// Create failure result
252    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}