scouter_types/spc/
alert.rs

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