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:
Hugh Kaznowski
2026-09-05 17:02:33 +01:00
co-authored by Claude Sonnet 5
parent 3cef6dc8ae
commit 00c21c8032
32 changed files with 347 additions and 1550 deletions
+14 -74
View File
@@ -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()