feat(app): implement state in order to not flood of notification during run

This commit is contained in:
2025-08-12 09:40:55 +02:00
parent c1177c5ea5
commit fc31a953aa
8 changed files with 226 additions and 15 deletions

148
Cargo.lock generated
View File

@@ -17,6 +17,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.20"
@@ -310,6 +319,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
@@ -322,6 +342,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -330,6 +356,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@@ -367,6 +394,12 @@ version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "h2"
version = "0.4.12"
@@ -789,6 +822,7 @@ dependencies = [
"discord_client",
"dotenvy",
"mockall",
"rstest",
"system_monitor",
]
@@ -944,6 +978,15 @@ dependencies = [
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -968,6 +1011,41 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "reqwest"
version = "0.12.22"
@@ -1024,12 +1102,50 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rstest"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
dependencies = [
"futures-timer",
"futures-util",
"rstest_macros",
]
[[package]]
name = "rstest_macros"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.0.8"
@@ -1120,6 +1236,12 @@ dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "serde"
version = "1.0.219"
@@ -1362,6 +1484,23 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.5.2"
@@ -1878,6 +2017,15 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"

View File

@@ -11,4 +11,5 @@ clap = { version = "4.5", features = ["derive"] }
[dev-dependencies]
mockall = "0.13.1"
rstest = "0.26.1"

View File

@@ -53,4 +53,8 @@ impl ForFormattingMessage for AlertFormatter {
result
}
fn format_resolution(&self, metric_name: &str) -> String {
format!("{} has been resolved", metric_name)
}
}

View File

@@ -10,7 +10,7 @@ impl CronAlertsActor {
Self { alert_monitor, period }
}
pub fn start(&self) {
pub fn start(&mut self) {
loop {
if let Err(e) = self.alert_monitor.alert_on_threshold_violation() {
eprintln!("Error occurred: {}", e);

View File

@@ -12,7 +12,7 @@ impl ForMonitoringAlertsAdapter {
}
impl ForMonitoringAlerts for ForMonitoringAlertsAdapter {
fn alert_on_threshold_violation(&self) -> Result<(), Box<dyn std::error::Error>> {
fn alert_on_threshold_violation(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.app.alert_on_threshold_violation()
}
}

View File

@@ -18,6 +18,7 @@ pub trait ForMonitoringSystem {
pub trait ForFormattingMessage {
fn format_violation(&self, violation: Box<dyn ForGettingViolationData>) -> String;
fn format_summary(&self, metrics: &HashMap<String, f32>) -> String;
fn format_resolution(&self, metric_name: &str) -> String;
}
#[cfg_attr(test, automock)]

View File

@@ -1,3 +1,3 @@
pub trait ForMonitoringAlerts {
fn alert_on_threshold_violation(&self) -> Result<(), Box<dyn std::error::Error>>;
fn alert_on_threshold_violation(&mut self) -> Result<(), Box<dyn std::error::Error>>;
}

View File

@@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::app::ports::driven::{
ForFormattingMessage, ForMonitoringSystem, ForSendingNotification,
};
@@ -6,6 +8,7 @@ pub struct AlertService {
notifier: Box<dyn ForSendingNotification>,
monitor: Box<dyn ForMonitoringSystem>,
formatter: Box<dyn ForFormattingMessage>,
map_alerts_criticity: HashMap<String, bool>,
}
pub fn new(
@@ -17,22 +20,72 @@ pub fn new(
notifier,
monitor,
formatter,
map_alerts_criticity: HashMap::new(),
}
}
impl AlertService {
/// Check for threshold violations and send alerts
pub fn alert_on_threshold_violation(&self) -> Result<(), Box<dyn std::error::Error>> {
/// Check for threshold violations and send alerts on level changes (escalation, improvement, resolution)
pub fn alert_on_threshold_violation(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let violations = self.monitor.check_thresholds();
for violation in violations {
let message = self.formatter.format_violation(violation);
self.notifier.send_notification(&message)?;
}
let current_levels = self.violations_to_map(&violations);
self.process_resolved_violations(&current_levels);
self.process_current_violations(violations)?;
self.update_map_alerts_criticity(current_levels);
Ok(())
}
}
fn violations_to_map(
&self,
violations: &[Box<dyn crate::app::ports::driven::ForGettingViolationData>],
) -> HashMap<String, bool> {
violations
.iter()
.map(|v| (v.get_metric_name(), v.is_critical()))
.collect()
}
fn process_resolved_violations(&mut self, current_levels: &HashMap<String, bool>) {
for (metric_name, &_previous_level) in &self.map_alerts_criticity {
if !current_levels.contains_key(metric_name) {
let message = self.formatter.format_resolution(metric_name);
self.notifier.send_notification(&message).unwrap();
}
}
}
fn process_current_violations(
&mut self,
violations: Vec<Box<dyn crate::app::ports::driven::ForGettingViolationData>>,
) -> Result<(), Box<dyn std::error::Error>> {
for violation in violations {
let metric_name = violation.get_metric_name();
let current_level = violation.is_critical();
let should_alert = match self.map_alerts_criticity.get(&metric_name) {
None => true, // First time seeing this metric
Some(&previous_level) => current_level != previous_level, // Any level change
};
if should_alert {
let message = self.formatter.format_violation(violation);
self.notifier.send_notification(&message)?;
}
// Update the level for this metric
self.map_alerts_criticity.insert(metric_name, current_level);
}
Ok(())
}
fn update_map_alerts_criticity(&mut self, current_levels: HashMap<String, bool>) {
self.map_alerts_criticity = current_levels;
}
}
#[cfg(test)]
mod tests {
@@ -54,7 +107,7 @@ mod tests {
formatter.expect_format_violation().returning(|_| "".to_string());
formatter.expect_format_summary().returning(|_| "".to_string());
let service = new(
let mut service = new(
Box::new(notifier),
Box::new(monitor),
Box::new(formatter),
@@ -69,15 +122,19 @@ mod tests {
let mut monitor = MockForMonitoringSystem::new();
let mut formatter = MockForFormattingMessage::new();
monitor.expect_check_thresholds().returning(|| {
vec![Box::new(crate::app::ports::driven::MockForGettingViolationData::new())]
let mut violation = crate::app::ports::driven::MockForGettingViolationData::new();
violation.expect_get_metric_name().return_const("cpu".to_string());
violation.expect_is_critical().return_const(true);
vec![Box::new(violation)]
});
formatter.expect_format_violation().returning(|_| "Test violation".to_string());
notifier.expect_send_notification()
.returning(|_| Err("Network error".into()));
let service = new(
let mut service = new(
Box::new(notifier),
Box::new(monitor),
Box::new(formatter),