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 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 PyHelperFuncs::__str__(self)
210 }
211}
212
213#[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 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 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 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 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 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 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 };
407
408 let mut features = HashMap::new();
410 features.insert("feature1".to_string(), sample_alert.clone());
411
412 let alerts = SpcFeatureAlerts {
414 features: features.clone(),
415 has_alerts: true,
416 };
417
418 assert!(alerts.has_alerts);
420
421 assert_eq!(alerts.features["feature1"].feature, sample_alert.feature);
423
424 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}