Skip to main content

scouter_types/alert/
alerts.rs

1use crate::spc::SpcAlertEntry;
2use crate::util::PyHelperFuncs;
3use crate::{custom::ComparisonMetricAlert, psi::PsiFeatureAlert};
4use chrono::{DateTime, Utc};
5use pyo3::prelude::*;
6use serde::{Deserialize, Serialize};
7use std::fmt::{Display, Formatter};
8
9#[pyclass(eq)]
10#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
11pub enum AlertThreshold {
12    #[default]
13    Below,
14    Above,
15    Outside,
16}
17
18impl Display for AlertThreshold {
19    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
20        write!(f, "{self:?}")
21    }
22}
23
24#[pymethods]
25impl AlertThreshold {
26    #[staticmethod]
27    pub fn from_value(value: &str) -> Option<Self> {
28        match value.to_lowercase().as_str() {
29            "below" => Some(AlertThreshold::Below),
30            "above" => Some(AlertThreshold::Above),
31            "outside" => Some(AlertThreshold::Outside),
32            _ => None,
33        }
34    }
35
36    pub fn __str__(&self) -> String {
37        match self {
38            AlertThreshold::Below => "Below".to_string(),
39            AlertThreshold::Above => "Above".to_string(),
40            AlertThreshold::Outside => "Outside".to_string(),
41        }
42    }
43}
44
45#[pyclass]
46#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
47pub struct AlertCondition {
48    /// The reference value to compare against
49    #[pyo3(get, set)]
50    pub baseline_value: f64,
51
52    #[pyo3(get, set)]
53    pub alert_threshold: AlertThreshold,
54
55    /// Optional delta value that modifies the baseline to create the alert boundary.
56    /// The interpretation depends on alert_threshold:
57    /// - Above: alert if value > (baseline + delta)
58    /// - Below: alert if value < (baseline - delta)
59    /// - Outside: alert if value is outside [baseline - delta, baseline + delta]
60    #[pyo3(get, set)]
61    pub delta: Option<f64>,
62}
63
64#[pymethods]
65impl AlertCondition {
66    #[new]
67    #[pyo3(signature = (baseline_value, alert_threshold, delta=None))]
68    pub fn new(baseline_value: f64, alert_threshold: AlertThreshold, delta: Option<f64>) -> Self {
69        Self {
70            baseline_value,
71            alert_threshold,
72            delta,
73        }
74    }
75
76    /// Returns the upper bound for the alert condition
77    pub fn upper_bound(&self) -> f64 {
78        match self.delta {
79            Some(d) => self.baseline_value + d,
80            None => self.baseline_value,
81        }
82    }
83
84    /// Returns the lower bound for the alert condition
85    pub fn lower_bound(&self) -> f64 {
86        match self.delta {
87            Some(d) => self.baseline_value - d,
88            None => self.baseline_value,
89        }
90    }
91
92    /// Checks if a value should trigger an alert
93    pub fn should_alert(&self, value: f64) -> bool {
94        match (&self.alert_threshold, self.delta) {
95            (AlertThreshold::Above, Some(d)) => value > (self.baseline_value + d),
96            (AlertThreshold::Above, None) => value > self.baseline_value,
97            (AlertThreshold::Below, Some(d)) => value < (self.baseline_value - d),
98            (AlertThreshold::Below, None) => value < self.baseline_value,
99            (AlertThreshold::Outside, Some(d)) => {
100                value < (self.baseline_value - d) || value > (self.baseline_value + d)
101            }
102            (AlertThreshold::Outside, None) => value != self.baseline_value,
103        }
104    }
105
106    pub fn __str__(&self) -> String {
107        PyHelperFuncs::__str__(self)
108    }
109}
110
111#[derive(Serialize, Deserialize, Debug, Default, Clone)]
112pub enum AlertMap {
113    Custom(ComparisonMetricAlert),
114    Psi(PsiFeatureAlert),
115    Spc(SpcAlertEntry),
116    GenAI(ComparisonMetricAlert),
117
118    #[default]
119    None,
120}
121
122impl AlertMap {
123    pub fn is_none(&self) -> bool {
124        matches!(self, AlertMap::None)
125    }
126
127    pub fn entity_name(&self) -> &str {
128        match self {
129            AlertMap::Custom(alert) => &alert.metric_name,
130            AlertMap::Psi(alert) => &alert.feature,
131            AlertMap::Spc(alert) => &alert.feature,
132            AlertMap::GenAI(alert) => &alert.metric_name,
133            AlertMap::None => "none",
134        }
135    }
136}
137
138#[pyclass]
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Alert {
141    #[pyo3(get)]
142    pub created_at: DateTime<Utc>,
143
144    #[pyo3(get)]
145    pub entity_name: String,
146
147    pub alert: AlertMap,
148
149    #[pyo3(get)]
150    pub id: i32,
151
152    #[pyo3(get)]
153    pub active: bool,
154}
155
156#[cfg(feature = "server")]
157impl<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> for Alert {
158    fn from_row(row: &'r sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
159        use sqlx::Row;
160
161        let alert_value: serde_json::Value = row.try_get("alert")?;
162        let alert: AlertMap = serde_json::from_value(alert_value).unwrap_or_default();
163
164        Ok(Alert {
165            created_at: row.try_get("created_at")?,
166            alert,
167            entity_name: row.try_get("entity_name")?,
168            id: row.try_get("id")?,
169            active: row.try_get("active")?,
170        })
171    }
172}
173
174#[pymethods]
175impl Alert {
176    pub fn __str__(&self) -> String {
177        // serialize the struct to a string
178        PyHelperFuncs::__str__(self)
179    }
180}
181
182#[derive(Serialize, Deserialize, Debug, Clone)]
183pub struct Alerts {
184    pub alerts: Vec<Alert>,
185}