Move the ntfy alert sink to Settings, shared by all alert rules
Alert rules no longer carry their own ntfy topic/server; Settings now holds one process-wide ntfy sink (with its own "send test notification"), and the Alerts panel is left to just define rule conditions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ae4e550c1f
commit
18c2d72d22
+11
-33
@@ -25,7 +25,6 @@ pub struct AlertRule {
|
||||
pub name: String,
|
||||
pub enabled: bool,
|
||||
pub condition: AlertCondition,
|
||||
pub sink: AlertSink,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -54,17 +53,6 @@ pub enum AlertCondition {
|
||||
},
|
||||
}
|
||||
|
||||
/// Modeled as an enum (rather than bare fields on `AlertRule`) so more sink kinds can be added later without reshaping it.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum AlertSink {
|
||||
Ntfy {
|
||||
topic: String,
|
||||
/// Defaults to `https://ntfy.sh` when absent.
|
||||
server: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AlertError {
|
||||
NotFound,
|
||||
@@ -107,9 +95,8 @@ impl Settings {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
condition: AlertCondition,
|
||||
sink: AlertSink,
|
||||
) -> Result<AlertRule, AlertError> {
|
||||
let rule = AlertRule { id: generate_alert_rule_id(&name), name, enabled, condition, sink };
|
||||
let rule = AlertRule { id: generate_alert_rule_id(&name), name, enabled, condition };
|
||||
self.alert_rules.push(rule.clone());
|
||||
self.save(settings_path)?;
|
||||
Ok(rule)
|
||||
@@ -122,7 +109,6 @@ impl Settings {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
condition: AlertCondition,
|
||||
sink: AlertSink,
|
||||
) -> Result<AlertRule, AlertError> {
|
||||
let rule = self
|
||||
.alert_rules
|
||||
@@ -132,7 +118,6 @@ impl Settings {
|
||||
rule.name = name;
|
||||
rule.enabled = enabled;
|
||||
rule.condition = condition;
|
||||
rule.sink = sink;
|
||||
let updated = rule.clone();
|
||||
self.save(settings_path)?;
|
||||
Ok(updated)
|
||||
@@ -322,15 +307,16 @@ fn evaluate_condition(
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors are only logged (no retry queue); spawned off the ingest hot path so a slow/unreachable sink never blocks ingestion.
|
||||
pub async fn deliver_for_rule(rule: AlertRule, message: String) {
|
||||
match rule.sink {
|
||||
AlertSink::Ntfy { topic, server } => {
|
||||
let server = server.unwrap_or_else(|| "https://ntfy.sh".to_string());
|
||||
if let Err(err) = deliver_ntfy(&server, &topic, &message).await {
|
||||
eprintln!("warning: failed to deliver alert '{}' via ntfy: {err}", rule.name);
|
||||
}
|
||||
}
|
||||
/// Delivers to the single, process-wide ntfy sink configured in Settings. Errors are only logged
|
||||
/// (no retry queue); spawned off the ingest hot path so a slow/unreachable sink never blocks ingestion.
|
||||
/// A silently no-op'd delivery (no topic configured) is expected — a rule can be defined before the sink is.
|
||||
pub async fn deliver_for_rule(rule_name: String, message: String, topic: Option<String>, server: Option<String>) {
|
||||
let Some(topic) = topic else {
|
||||
return;
|
||||
};
|
||||
let server = server.unwrap_or_else(|| "https://ntfy.sh".to_string());
|
||||
if let Err(err) = deliver_ntfy(&server, &topic, &message).await {
|
||||
eprintln!("warning: failed to deliver alert '{rule_name}' via ntfy: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +350,6 @@ mod tests {
|
||||
name: "test rule".to_string(),
|
||||
enabled: true,
|
||||
condition: AlertCondition::Immediate { query: query.to_string() },
|
||||
sink: AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +365,6 @@ mod tests {
|
||||
"spike".to_string(),
|
||||
true,
|
||||
AlertCondition::Immediate { query: r#"regex("error")"#.to_string() },
|
||||
AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(settings.alert_rules.len(), 1);
|
||||
@@ -395,7 +379,6 @@ mod tests {
|
||||
"renamed".to_string(),
|
||||
false,
|
||||
AlertCondition::Immediate { query: r#"regex("warn")"#.to_string() },
|
||||
AlertSink::Ntfy { topic: "other".to_string(), server: Some("https://example.com".to_string()) },
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(updated.id, created.id);
|
||||
@@ -420,7 +403,6 @@ mod tests {
|
||||
"x".to_string(),
|
||||
true,
|
||||
AlertCondition::Immediate { query: r#"regex("x")"#.to_string() },
|
||||
AlertSink::Ntfy { topic: "x".to_string(), server: None },
|
||||
);
|
||||
assert!(matches!(result, Err(AlertError::NotFound)));
|
||||
|
||||
@@ -463,7 +445,6 @@ mod tests {
|
||||
threshold: 3.0,
|
||||
window_ms: 10_000,
|
||||
},
|
||||
sink: AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
for (seq, ms) in [(0u64, 0i64), (1, 1000)] {
|
||||
@@ -489,7 +470,6 @@ mod tests {
|
||||
threshold: 2.0,
|
||||
window_ms: 1_000,
|
||||
},
|
||||
sink: AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
assert!(evaluate_alerts(&[rule.clone()], &mut states, 0, Timestamp::from_millis(0), "error").is_empty());
|
||||
@@ -507,7 +487,6 @@ mod tests {
|
||||
field_regex: r"agent=(?P<agent>\S+)".to_string(),
|
||||
capture_group: "agent".to_string(),
|
||||
},
|
||||
sink: AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
let fired = evaluate_alerts(&[rule.clone()], &mut states, 0, Timestamp::from_millis(0), "agent=curl");
|
||||
@@ -531,7 +510,6 @@ mod tests {
|
||||
ratio: 0.5,
|
||||
window_ms: 10_000,
|
||||
},
|
||||
sink: AlertSink::Ntfy { topic: "alerts".to_string(), server: None },
|
||||
};
|
||||
let mut states = HashMap::new();
|
||||
// Only an "error" so far; no "request" yet, so denominator is 0 -> skip.
|
||||
|
||||
Reference in New Issue
Block a user