1use crate::error::TypeError;
2use crate::AlertMap;
3use crate::{
4 dispatch::AlertDispatchType, AlertDispatchConfig, CommonCrons, DispatchAlertDescription,
5 OpsGenieDispatchConfig, PyHelperFuncs, SlackDispatchConfig, ValidateAlertConfig,
6};
7use core::fmt::Debug;
8use pyo3::prelude::*;
9use pyo3::types::PyString;
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::collections::HashSet;
14use std::fmt::Display;
15use tracing::error;
16
17#[pyclass(eq)]
18#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, std::cmp::Eq, Hash, Default)]
19pub enum AlertZone {
20 Zone1,
21 Zone2,
22 Zone3,
23 Zone4,
24 #[default]
25 NotApplicable,
26}
27
28impl Display for AlertZone {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 AlertZone::Zone1 => write!(f, "Zone 1"),
32 AlertZone::Zone2 => write!(f, "Zone 2"),
33 AlertZone::Zone3 => write!(f, "Zone 3"),
34 AlertZone::Zone4 => write!(f, "Zone 4"),
35 AlertZone::NotApplicable => write!(f, "NA"),
36 }
37 }
38}
39
40#[pyclass]
41#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
42pub struct SpcAlertRule {
43 #[pyo3(get, set)]
44 pub rule: String,
45
46 #[pyo3(get, set)]
47 pub zones_to_monitor: Vec<AlertZone>,
48}
49
50#[pymethods]
51impl SpcAlertRule {
52 #[new]
53 #[pyo3(signature = (rule="8 16 4 8 2 4 1 1", zones_to_monitor=vec![
54 AlertZone::Zone1,
55 AlertZone::Zone2,
56 AlertZone::Zone3,
57 AlertZone::Zone4,
58 ]))]
59 pub fn new(rule: &str, zones_to_monitor: Vec<AlertZone>) -> Self {
60 Self {
61 rule: rule.to_string(),
62 zones_to_monitor,
63 }
64 }
65}
66
67impl Default for SpcAlertRule {
68 fn default() -> SpcAlertRule {
69 Self {
70 rule: "8 16 4 8 2 4 1 1".to_string(),
71 zones_to_monitor: vec![
72 AlertZone::Zone1,
73 AlertZone::Zone2,
74 AlertZone::Zone3,
75 AlertZone::Zone4,
76 ],
77 }
78 }
79}
80
81#[pyclass]
82#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
83pub struct SpcAlertConfig {
84 #[pyo3(get, set)]
85 pub rule: SpcAlertRule,
86
87 #[pyo3(get, set)]
88 pub schedule: String,
89
90 #[pyo3(get, set)]
91 pub features_to_monitor: Vec<String>,
92
93 pub dispatch_config: AlertDispatchConfig,
94}
95
96impl ValidateAlertConfig for SpcAlertConfig {}
97
98#[pymethods]
99impl SpcAlertConfig {
100 #[new]
101 #[pyo3(signature = (rule=SpcAlertRule::default(), schedule=None, features_to_monitor=vec![], dispatch_config=None))]
102 pub fn new(
103 rule: SpcAlertRule,
104 schedule: Option<&Bound<'_, PyAny>>,
105 features_to_monitor: Vec<String>,
106 dispatch_config: Option<&Bound<'_, PyAny>>,
107 ) -> Result<Self, TypeError> {
108 let alert_dispatch_config = match dispatch_config {
109 None => AlertDispatchConfig::default(),
110 Some(config) => {
111 if config.is_instance_of::<SlackDispatchConfig>() {
112 AlertDispatchConfig::Slack(config.extract::<SlackDispatchConfig>()?)
113 } else if config.is_instance_of::<OpsGenieDispatchConfig>() {
114 AlertDispatchConfig::OpsGenie(config.extract::<OpsGenieDispatchConfig>()?)
115 } else {
116 AlertDispatchConfig::default()
117 }
118 }
119 };
120
121 let schedule = match schedule {
124 Some(schedule) => {
125 if schedule.is_instance_of::<PyString>() {
126 schedule.to_string()
127 } else if schedule.is_instance_of::<CommonCrons>() {
128 schedule.extract::<CommonCrons>().unwrap().cron()
129 } else {
130 error!("Invalid schedule type");
131 return Err(TypeError::InvalidScheduleError)?;
132 }
133 }
134 None => CommonCrons::EveryDay.cron(),
135 };
136
137 let schedule = Self::resolve_schedule(&schedule);
138
139 Ok(Self {
140 rule,
141 schedule,
142 features_to_monitor,
143 dispatch_config: alert_dispatch_config,
144 })
145 }
146
147 #[getter]
148 pub fn dispatch_type(&self) -> AlertDispatchType {
149 self.dispatch_config.dispatch_type()
150 }
151
152 #[getter]
153 pub fn dispatch_config<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
154 self.dispatch_config.config(py)
155 }
156}
157
158impl Default for SpcAlertConfig {
159 fn default() -> SpcAlertConfig {
160 Self {
161 rule: SpcAlertRule::default(),
162 dispatch_config: AlertDispatchConfig::default(),
163 schedule: CommonCrons::EveryDay.cron(),
164 features_to_monitor: Vec::new(),
165 }
166 }
167}
168
169#[pyclass(eq)]
170#[derive(Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Default, Clone, Copy)]
171pub enum SpcAlertType {
172 #[default]
173 OutOfBounds,
174 Consecutive,
175 Alternating,
176 AllGood,
177 Trend,
178}
179
180impl Display for SpcAlertType {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 match self {
183 SpcAlertType::OutOfBounds => write!(f, "Out of bounds"),
184 SpcAlertType::Consecutive => write!(f, "Consecutive"),
185 SpcAlertType::Alternating => write!(f, "Alternating"),
186 SpcAlertType::AllGood => write!(f, "All good"),
187 SpcAlertType::Trend => write!(f, "Trend"),
188 }
189 }
190}
191
192#[pyclass]
193#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, Hash, PartialEq)]
194pub struct SpcAlert {
195 #[pyo3(get)]
196 pub kind: SpcAlertType,
197
198 #[pyo3(get)]
199 pub zone: AlertZone,
200}
201
202#[pymethods]
203#[allow(clippy::new_without_default)]
204impl SpcAlert {
205 #[new]
206 pub fn new(kind: SpcAlertType, zone: AlertZone) -> Self {
207 Self { kind, zone }
208 }
209
210 pub fn __str__(&self) -> String {
211 PyHelperFuncs::__str__(self)
213 }
214}
215
216#[derive(Serialize, Deserialize, Debug, Default, Clone)]
217pub struct SpcAlertEntry {
218 pub feature: String,
219 pub kind: SpcAlertType,
220 pub zone: AlertZone,
221}
222
223#[derive(Serialize, Deserialize, Debug, Default, Clone)]
226pub struct SpcFeatureAlert {
227 pub feature: String,
228 pub alerts: Vec<SpcAlert>,
229}
230
231impl SpcFeatureAlert {
232 pub fn new(feature: String, alerts: Vec<SpcAlert>) -> Self {
233 Self { feature, alerts }
234 }
235
236 pub fn to_alert_map(&self) -> Vec<AlertMap> {
237 self.alerts
238 .iter()
239 .map(|alert| {
240 AlertMap::Spc(SpcAlertEntry {
241 feature: self.feature.clone(),
242 kind: alert.kind,
243 zone: alert.zone.clone(),
244 })
245 })
246 .collect()
247 }
248}
249
250#[derive(Debug, Serialize, Deserialize, Clone)]
251pub struct SpcFeatureAlerts {
252 pub features: HashMap<String, SpcFeatureAlert>,
253 pub has_alerts: bool,
254}
255
256impl SpcFeatureAlerts {
257 pub fn new(has_alerts: bool) -> Self {
258 Self {
259 features: HashMap::new(),
260 has_alerts,
261 }
262 }
263
264 pub fn insert_feature_alert(&mut self, feature: &str, alerts: HashSet<SpcAlert>) {
265 let alerts: Vec<SpcAlert> = alerts.into_iter().collect();
267
268 let feature_alert = SpcFeatureAlert::new(feature.to_string(), alerts);
269
270 self.features.insert(feature.to_string(), feature_alert);
271 }
272}
273
274impl DispatchAlertDescription for SpcFeatureAlerts {
275 fn create_alert_description(&self, dispatch_type: AlertDispatchType) -> String {
276 let mut alert_description = String::new();
277
278 for (i, (_, feature_alert)) in self.features.iter().enumerate() {
279 if feature_alert.alerts.is_empty() {
280 continue;
281 }
282 if i == 0 {
283 let header = match dispatch_type {
284 AlertDispatchType::Console => "Features that have drifted: \n",
285 AlertDispatchType::OpsGenie => {
286 "Drift has been detected for the following features:\n"
287 }
288 AlertDispatchType::Slack => {
289 "Drift has been detected for the following features:\n"
290 }
291 };
292 alert_description.push_str(header);
293 }
294
295 let feature_name = match dispatch_type {
296 AlertDispatchType::Console | AlertDispatchType::OpsGenie => {
297 format!("{:indent$}{}: \n", "", &feature_alert.feature, indent = 4)
298 }
299 AlertDispatchType::Slack => format!("{}: \n", &feature_alert.feature),
300 };
301
302 alert_description = format!("{alert_description}{feature_name}");
303 feature_alert.alerts.iter().for_each(|alert| {
304 let alert_details = match dispatch_type {
305 AlertDispatchType::Console | AlertDispatchType::OpsGenie => {
306 let kind = format!("{:indent$}Kind: {}\n", "", &alert.kind, indent = 8);
307 let zone = format!("{:indent$}Zone: {}\n", "", &alert.zone, indent = 8);
308 format!("{kind}{zone}")
309 }
310 AlertDispatchType::Slack => format!(
311 "{:indent$}{} error in {}\n",
312 "",
313 &alert.kind,
314 &alert.zone,
315 indent = 4
316 ),
317 };
318 alert_description = format!("{alert_description}{alert_details}");
319 });
320 }
321 alert_description
322 }
323}
324
325pub struct TaskAlerts {
326 pub alerts: SpcFeatureAlerts,
327}
328
329impl TaskAlerts {
330 pub fn new() -> Self {
331 Self {
332 alerts: SpcFeatureAlerts::new(false),
333 }
334 }
335}
336
337impl Default for TaskAlerts {
338 fn default() -> Self {
339 Self::new()
340 }
341}
342
343#[cfg(test)]
344mod tests {
345
346 use super::*;
347
348 #[test]
349 fn test_types() {
350 let control_alert = SpcAlertRule::default().rule;
352
353 assert_eq!(control_alert, "8 16 4 8 2 4 1 1");
354 assert_eq!(AlertZone::NotApplicable.to_string(), "NA");
355 assert_eq!(AlertZone::Zone1.to_string(), "Zone 1");
356 assert_eq!(AlertZone::Zone2.to_string(), "Zone 2");
357 assert_eq!(AlertZone::Zone3.to_string(), "Zone 3");
358 assert_eq!(AlertZone::Zone4.to_string(), "Zone 4");
359 assert_eq!(SpcAlertType::AllGood.to_string(), "All good");
360 assert_eq!(SpcAlertType::Consecutive.to_string(), "Consecutive");
361 assert_eq!(SpcAlertType::Alternating.to_string(), "Alternating");
362 assert_eq!(SpcAlertType::OutOfBounds.to_string(), "Out of bounds");
363 }
364
365 #[test]
366 fn test_alert_config() {
367 let alert_config = SpcAlertConfig::default();
369 assert_eq!(alert_config.dispatch_config, AlertDispatchConfig::default());
370 assert_eq!(alert_config.dispatch_type(), AlertDispatchType::Console);
371
372 let slack_dispatch_config = SlackDispatchConfig {
373 channel: "test-channel".to_string(),
374 };
375 let alert_config = SpcAlertConfig {
377 dispatch_config: AlertDispatchConfig::Slack(slack_dispatch_config.clone()),
378 ..Default::default()
379 };
380 assert_eq!(
381 alert_config.dispatch_config,
382 AlertDispatchConfig::Slack(slack_dispatch_config)
383 );
384 assert_eq!(alert_config.dispatch_type(), AlertDispatchType::Slack);
385 assert_eq!(
386 match &alert_config.dispatch_config {
387 AlertDispatchConfig::Slack(config) => &config.channel,
388 _ => panic!("Expected Slack dispatch config"),
389 },
390 "test-channel"
391 );
392
393 let opsgenie_dispatch_config = OpsGenieDispatchConfig {
395 team: "test-team".to_string(),
396 priority: "P5".to_string(),
397 };
398
399 let alert_config = SpcAlertConfig {
400 dispatch_config: AlertDispatchConfig::OpsGenie(opsgenie_dispatch_config.clone()),
401 ..Default::default()
402 };
403 assert_eq!(
404 alert_config.dispatch_config,
405 AlertDispatchConfig::OpsGenie(opsgenie_dispatch_config.clone())
406 );
407 assert_eq!(alert_config.dispatch_type(), AlertDispatchType::OpsGenie);
408 assert_eq!(
409 match &alert_config.dispatch_config {
410 AlertDispatchConfig::OpsGenie(config) => &config.team,
411 _ => panic!("Expected OpsGenie dispatch config"),
412 },
413 "test-team"
414 );
415 }
416
417 #[test]
418 fn test_spc_feature_alerts() {
419 let sample_alert = SpcFeatureAlert {
421 feature: "feature1".to_string(),
422 alerts: vec![SpcAlert {
423 kind: SpcAlertType::OutOfBounds,
424 zone: AlertZone::Zone1,
425 }]
426 .into_iter()
427 .collect(),
428 };
430
431 let mut features = HashMap::new();
433 features.insert("feature1".to_string(), sample_alert.clone());
434
435 let alerts = SpcFeatureAlerts {
437 features: features.clone(),
438 has_alerts: true,
439 };
440
441 assert!(alerts.has_alerts);
443
444 assert_eq!(alerts.features["feature1"].feature, sample_alert.feature);
446
447 let _ = alerts.create_alert_description(AlertDispatchType::Console);
449 let _ = alerts.create_alert_description(AlertDispatchType::OpsGenie);
450 let _ = alerts.create_alert_description(AlertDispatchType::Slack);
451 }
452}