Add alert rules with FilterBar-based triggers and ntfy delivery (tickets 017-019)
Adds AlertRule/AlertCondition/AlertSink models with CRUD persisted on Settings (mirroring CssEntry's shape), per-line trigger evaluation hooked into the engine's ingest path (Immediate/Threshold/NewValue/Ratio conditions), and best-effort ntfy delivery spawned off the ingest hot path. Design notes: - Alert queries reuse FilterBar syntax but only support the regex()/sequence/timestamp subset (no metric[param] comparisons), since resolving those would require a full bucket rescan per alert per ingested line rather than cost proportional to new logs. - Ratio's zero-denominator case skips evaluation for that line rather than treating the ratio as infinite. - NewValue's seen-value set is in-memory only and does not survive a restart, matching the engine's other non-persisted runtime state. engine::run now takes a SettingsHandle (moved from api.rs to settings.rs so engine.rs can depend on it without a circular import) to read the current alert rules on each ingest. 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
758feca780
commit
dd3320812c
+28
-1
@@ -11,11 +11,18 @@ use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::alerts::AlertRule;
|
||||
|
||||
/// Shared, process-wide settings handle. Cheap to clone; lock is only held
|
||||
/// briefly since settings changes are rare (unlike the engine's hot path).
|
||||
pub type SettingsHandle = Arc<Mutex<Settings>>;
|
||||
|
||||
/// User's preferred colour scheme for the webapp.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -63,7 +70,10 @@ pub struct Profiles {
|
||||
}
|
||||
|
||||
/// The single, process-wide settings instance.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
///
|
||||
/// Only `PartialEq` (not `Eq`): `alert_rules` carries `f64` fields
|
||||
/// (`AlertCondition::Threshold`/`Ratio`), which aren't `Eq`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub color_scheme: ColorScheme,
|
||||
pub css_entries: Vec<CssEntry>,
|
||||
@@ -72,6 +82,10 @@ pub struct Settings {
|
||||
/// means unlimited — matches today's unbounded retention.
|
||||
#[serde(default)]
|
||||
pub max_logs: Option<usize>,
|
||||
/// Alert rule definitions (ticket 017). `#[serde(default)]` so existing
|
||||
/// settings.json files without this field still load.
|
||||
#[serde(default)]
|
||||
pub alert_rules: Vec<AlertRule>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -85,6 +99,7 @@ impl Default for Settings {
|
||||
profiles_dir: "profiles".to_string(),
|
||||
},
|
||||
max_logs: None,
|
||||
alert_rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +370,18 @@ mod tests {
|
||||
});
|
||||
settings.profiles.names.push("default".to_string());
|
||||
settings.profiles.active_profile = Some("default".to_string());
|
||||
settings.alert_rules.push(crate::alerts::AlertRule {
|
||||
id: "rule1".to_string(),
|
||||
name: "error spike".to_string(),
|
||||
enabled: true,
|
||||
condition: crate::alerts::AlertCondition::Immediate {
|
||||
query: r#"regex("error")"#.to_string(),
|
||||
},
|
||||
sink: crate::alerts::AlertSink::Ntfy {
|
||||
topic: "alerts".to_string(),
|
||||
server: None,
|
||||
},
|
||||
});
|
||||
|
||||
settings.save(&path).unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user