Trim comments across Rust and webapp source to direct one-liners (ticket 030)
Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose: condensed multi-paragraph doc comments to single lines that state the non-obvious why, deleted comments that only restated adjacent code, and tightened the Docs.tsx help-panel wording. No logic, structure, or code formatting changed — verified via cargo test (143 passed) and tsc -b && vite build. 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
3cef6dc8ae
commit
00c21c8032
+14
-74
@@ -1,21 +1,6 @@
|
||||
//! Alert rules: definitions (ticket 017), trigger evaluation (ticket 018),
|
||||
//! and ntfy delivery (ticket 019).
|
||||
//! Alert rules: definitions, trigger evaluation, and ntfy delivery.
|
||||
//!
|
||||
//! ## Design: why alert queries only support regex/sequence/timestamp
|
||||
//!
|
||||
//! `Immediate`/`Threshold`/`Ratio` conditions reuse FilterBar query strings
|
||||
//! (see `crate::filter_parser`), evaluated against a *single newly-ingested
|
||||
//! line* on `EngineMessage::Ingest` (see `evaluate_alerts`, called from
|
||||
//! `engine::run`). `engine.rs`'s own `compile_filter_query` resolves
|
||||
//! `metric[param]` comparisons by scanning that metric's *entire* bucket
|
||||
//! history up front (`compile_metric_comparison`) — reusing that per alert
|
||||
//! check, per ingested line, would make alert evaluation cost proportional to
|
||||
//! total log volume rather than to new logs ingested, which this ticket
|
||||
//! requires. So alert queries here only support the subset of FilterBar that
|
||||
//! evaluates against a single line's own fields — `regex(...)`, `sequence`,
|
||||
//! `timestamp`, and `and`/`or` — via `eval_alert_expr` below. A `metric[param]`
|
||||
//! comparison in an alert query is rejected with a clear error (logged, the
|
||||
//! rule just never fires) rather than silently misbehaving.
|
||||
//! Alert queries are restricted to regex/sequence/timestamp — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee.
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -34,7 +19,6 @@ use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
||||
use crate::settings::Settings;
|
||||
use crate::timestamp::Timestamp;
|
||||
|
||||
/// A single alert rule definition (ticket 017).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AlertRule {
|
||||
pub id: String,
|
||||
@@ -44,36 +28,23 @@ pub struct AlertRule {
|
||||
pub sink: AlertSink,
|
||||
}
|
||||
|
||||
/// What triggers an alert rule (ticket 018).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "lowercase")]
|
||||
pub enum AlertCondition {
|
||||
/// Fires once per log line matching `query`.
|
||||
Immediate { query: String },
|
||||
/// Fires when `query`'s match count within a rolling `window_ms` crosses
|
||||
/// `threshold` (via `op`).
|
||||
/// Fires when `query`'s match count within a rolling `window_ms` crosses `threshold` (via `op`).
|
||||
Threshold {
|
||||
query: String,
|
||||
op: CompareOp,
|
||||
threshold: f64,
|
||||
window_ms: i64,
|
||||
},
|
||||
/// Fires the first time `field_regex` matches a line and `capture_group`
|
||||
/// captures a value not seen before *for this rule*. Seen values live
|
||||
/// only in memory (`AlertRuntimeState::seen_values`) for the process's
|
||||
/// lifetime — a restart forgets them, so previously-seen values can fire
|
||||
/// again. This is a deliberate simplicity tradeoff (matches the
|
||||
/// codebase's general "no restart persistence for in-memory engine
|
||||
/// state" pattern), not an oversight.
|
||||
/// Seen values live only in memory — a restart forgets them, so previously-seen values can fire again; a deliberate simplicity tradeoff, not an oversight.
|
||||
NewValue {
|
||||
field_regex: String,
|
||||
capture_group: String,
|
||||
},
|
||||
/// Fires when `count(query_a) / count(query_b)` (rolling over
|
||||
/// `window_ms`) crosses `ratio` (via `op`). A zero `count_b` skips
|
||||
/// evaluation entirely for that line (rather than treating the ratio as
|
||||
/// infinite) — there's no meaningful "actual vs threshold" comparison to
|
||||
/// report yet.
|
||||
/// A zero `count_b` skips evaluation for that line rather than treating the ratio as infinite.
|
||||
Ratio {
|
||||
query_a: String,
|
||||
query_b: String,
|
||||
@@ -83,9 +54,7 @@ pub enum AlertCondition {
|
||||
},
|
||||
}
|
||||
|
||||
/// Where a fired alert is delivered (ticket 019). Modeled as an enum (rather
|
||||
/// than `AlertRule` holding a bare topic/server) so more sink kinds can be
|
||||
/// added later without reshaping `AlertRule`.
|
||||
/// 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 {
|
||||
@@ -96,7 +65,6 @@ pub enum AlertSink {
|
||||
},
|
||||
}
|
||||
|
||||
/// Error returned by alert-rule mutations/lookups. Mirrors `CssError`'s shape.
|
||||
#[derive(Debug)]
|
||||
pub enum AlertError {
|
||||
NotFound,
|
||||
@@ -120,9 +88,7 @@ impl From<io::Error> for AlertError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an id for a newly-created alert rule. Same approach as
|
||||
/// `settings::generate_imported_css_id`: hash the name and current time
|
||||
/// rather than pulling in a `uuid` dependency.
|
||||
/// Hashes the name and current time rather than pulling in a `uuid` dependency.
|
||||
fn generate_alert_rule_id(name: &str) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
name.hash(&mut hasher);
|
||||
@@ -135,7 +101,6 @@ fn generate_alert_rule_id(name: &str) -> String {
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
/// Creates a new alert rule, persisting settings to `settings_path`.
|
||||
pub fn create_alert_rule(
|
||||
&mut self,
|
||||
settings_path: &Path,
|
||||
@@ -150,9 +115,6 @@ impl Settings {
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
/// Replaces an existing alert rule's fields (id stays fixed), persisting
|
||||
/// settings to `settings_path`. Returns `AlertError::NotFound` if no rule
|
||||
/// with `id` exists.
|
||||
pub fn update_alert_rule(
|
||||
&mut self,
|
||||
settings_path: &Path,
|
||||
@@ -176,8 +138,6 @@ impl Settings {
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Removes an alert rule by id, persisting settings to `settings_path`.
|
||||
/// Returns `AlertError::NotFound` if no rule with `id` exists.
|
||||
pub fn remove_alert_rule(&mut self, settings_path: &Path, id: &str) -> Result<(), AlertError> {
|
||||
let index = self
|
||||
.alert_rules
|
||||
@@ -190,11 +150,7 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-rule in-memory evaluation state (ticket 018): rolling match-timestamp
|
||||
/// windows for `Threshold`/`Ratio`, and seen-values for `NewValue`. Lives
|
||||
/// only for the engine process's lifetime (see `AlertCondition::NewValue`'s
|
||||
/// doc comment) — not persisted, not backfilled against already-ingested
|
||||
/// logs.
|
||||
/// Per-rule in-memory evaluation state; lives only for the process's lifetime — not persisted, not backfilled against already-ingested logs.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AlertRuntimeState {
|
||||
threshold_matches: VecDeque<i64>,
|
||||
@@ -203,9 +159,7 @@ pub struct AlertRuntimeState {
|
||||
seen_values: HashSet<String>,
|
||||
}
|
||||
|
||||
/// Drops every entry older than `window_ms` relative to `now_ms` from the
|
||||
/// front of a rolling window. `timestamps` is push_back-ordered, so once a
|
||||
/// front entry is within the window every entry behind it is too.
|
||||
/// `timestamps` is push_back-ordered, so once the front entry is within the window, every entry behind it is too.
|
||||
fn prune_window(timestamps: &mut VecDeque<i64>, now_ms: i64, window_ms: i64) {
|
||||
while let Some(&front) = timestamps.front() {
|
||||
if now_ms - front > window_ms {
|
||||
@@ -216,9 +170,7 @@ fn prune_window(timestamps: &mut VecDeque<i64>, now_ms: i64, window_ms: i64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates a FilterBar query string against a single log line's own fields
|
||||
/// only — see this module's doc comment for why `metric[param]` comparisons
|
||||
/// aren't supported here.
|
||||
/// Evaluates a FilterBar query against a single line's own fields only (metric[param] comparisons aren't supported — see module doc).
|
||||
fn eval_alert_query(query: &str, sequence: u64, timestamp: Timestamp, text: &str) -> Result<bool, String> {
|
||||
let expr = filter_parser::parse(query).map_err(|err| err.to_string())?;
|
||||
eval_alert_expr(&expr, sequence, timestamp, text)
|
||||
@@ -261,12 +213,7 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates every enabled rule's condition against one newly-ingested line,
|
||||
/// mutating each rule's runtime state (rolling windows / seen-values) along
|
||||
/// the way. Returns `(rule, message)` for every rule that fired — the caller
|
||||
/// (`engine::run`) hands each off to `deliver_for_rule`. A malformed query
|
||||
/// (bad regex, unsupported metric comparison) is logged to stderr and simply
|
||||
/// never matches, rather than panicking or propagating.
|
||||
/// A malformed query (bad regex, unsupported metric comparison) is logged and simply never matches, rather than panicking.
|
||||
pub fn evaluate_alerts(
|
||||
rules: &[AlertRule],
|
||||
states: &mut HashMap<String, AlertRuntimeState>,
|
||||
@@ -362,9 +309,7 @@ fn evaluate_condition(
|
||||
let count_a = state.ratio_a_matches.len() as f64;
|
||||
let count_b = state.ratio_b_matches.len() as f64;
|
||||
if count_b == 0.0 {
|
||||
// Zero denominator: skip evaluation rather than treat the
|
||||
// ratio as infinite — there's nothing meaningful to compare
|
||||
// against `ratio` yet.
|
||||
// Zero denominator: skip rather than treat the ratio as infinite.
|
||||
return None;
|
||||
}
|
||||
let observed = count_a / count_b;
|
||||
@@ -377,10 +322,7 @@ fn evaluate_condition(
|
||||
}
|
||||
}
|
||||
|
||||
/// Delivers one fired alert to its rule's configured sink. Errors are logged
|
||||
/// to stderr and dropped — no retry queue, matching `remove_css`'s
|
||||
/// best-effort style. Called via `tokio::spawn` from the ingest hot path
|
||||
/// (`engine::run`) so a slow/unreachable sink never blocks ingestion.
|
||||
/// 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 } => {
|
||||
@@ -392,9 +334,7 @@ pub async fn deliver_for_rule(rule: AlertRule, message: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes `message` to `{server}/{topic}` via ntfy's plain-text publish
|
||||
/// API (no auth, raw body). Standalone and `AlertRule`-free so a future
|
||||
/// "send test notification" endpoint can call it directly.
|
||||
/// Standalone and `AlertRule`-free so a "send test notification" endpoint can call it directly.
|
||||
pub async fn deliver_ntfy(server: &str, topic: &str, message: &str) -> Result<(), String> {
|
||||
let url = format!("{}/{}", server.trim_end_matches('/'), topic);
|
||||
let response = reqwest::Client::new()
|
||||
|
||||
Reference in New Issue
Block a user