Adopt rustfmt and clean up clippy warnings

Formats the whole backend to rustfmt defaults and fixes all 23
outstanding clippy warnings (collapsible ifs, clone-to-slice-ref,
type-complexity, single-arm match, unnecessary is_none check), then
wires cargo fmt --check and -D warnings into make lint so both stay
enforced going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
Hugh Kaznowski
2026-09-07 12:21:38 +01:00
co-authored by Claude Sonnet 5
parent 2cc8ac8138
commit c3108677ce
28 changed files with 4721 additions and 2851 deletions
+250 -66
View File
@@ -9,12 +9,12 @@ use std::time::{SystemTime, UNIX_EPOCH};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::engine::{self, eval_json_comparison};
use crate::config::Settings;
use crate::engine::filter_datetime;
use crate::engine::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
use crate::engine::rate::RateWindow;
use crate::config::Settings;
use crate::engine::timestamp::Timestamp;
use crate::engine::{self, eval_json_comparison};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlertRule {
@@ -27,7 +27,9 @@ pub struct AlertRule {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum AlertCondition {
Immediate { query: String },
Immediate {
query: String,
},
Threshold {
query: String,
op: CompareOp,
@@ -89,7 +91,12 @@ impl Settings {
enabled: bool,
condition: AlertCondition,
) -> Result<AlertRule, AlertError> {
let rule = AlertRule { id: generate_alert_rule_id(&name), name, enabled, condition };
let rule = AlertRule {
id: generate_alert_rule_id(&name),
name,
enabled,
condition,
};
self.alert_rules.push(rule.clone());
self.save(settings_path)?;
Ok(rule)
@@ -147,36 +154,56 @@ fn backfill_window(
window_ms: i64,
) -> RateWindow {
let since_ms = now.as_millis() - window_ms;
let matching_timestamps =
snapshot.logs_since(before_sequence, since_ms, now.as_millis()).filter_map(
|(sequence, timestamp, text)| {
(eval_alert_query(query, sequence, timestamp, text) == Ok(true)).then_some(timestamp)
},
);
let matching_timestamps = snapshot
.logs_since(before_sequence, since_ms, now.as_millis())
.filter_map(|(sequence, timestamp, text)| {
(eval_alert_query(query, sequence, timestamp, text) == Ok(true)).then_some(timestamp)
});
RateWindow::backfill(matching_timestamps, now, window_ms)
}
fn eval_alert_query(query: &str, sequence: u64, timestamp: Timestamp, text: &str) -> Result<bool, String> {
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)
}
fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text: &str) -> Result<bool, String> {
fn eval_alert_expr(
expr: &FilterExpr,
sequence: u64,
timestamp: Timestamp,
text: &str,
) -> Result<bool, String> {
match expr {
FilterExpr::Regex(pattern) => {
let re = Regex::new(pattern).map_err(|err| format!("invalid regex in alert query: {err}"))?;
let re = Regex::new(pattern)
.map_err(|err| format!("invalid regex in alert query: {err}"))?;
Ok(re.is_match(text))
}
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_seq" => {
FilterExpr::Comparison {
metric,
param: None,
op,
value,
} if metric == "ottyl_seq" => {
let target = match value {
FilterValue::Number(n) => *n,
FilterValue::Raw(raw) => raw
.parse::<f64>()
.map_err(|_| format!("'ottyl_seq' comparisons require a numeric value, got {raw:?}"))?,
FilterValue::Raw(raw) => raw.parse::<f64>().map_err(|_| {
format!("'ottyl_seq' comparisons require a numeric value, got {raw:?}")
})?,
};
Ok(op.matches((sequence as f64).total_cmp(&target)))
}
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_ts" => {
FilterExpr::Comparison {
metric,
param: None,
op,
value,
} if metric == "ottyl_ts" => {
let target_ms = match value {
FilterValue::Number(n) => *n as i64,
FilterValue::Raw(raw) => filter_datetime::parse_datetime(raw, Timestamp::now())
@@ -185,7 +212,12 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
};
Ok(op.matches(timestamp.as_millis().cmp(&target_ms)))
}
FilterExpr::Comparison { metric, param: None, op, value } if metric == "ottyl_fmt" => {
FilterExpr::Comparison {
metric,
param: None,
op,
value,
} if metric == "ottyl_fmt" => {
let target = match value {
FilterValue::Number(n) => n.to_string(),
FilterValue::Raw(raw) => raw.clone(),
@@ -199,12 +231,10 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
FilterExpr::JsonComparison { field, op, value } => {
Ok(eval_json_comparison(text, field, *op, value))
}
FilterExpr::And(lhs, rhs) => {
Ok(eval_alert_expr(lhs, sequence, timestamp, text)? && eval_alert_expr(rhs, sequence, timestamp, text)?)
}
FilterExpr::Or(lhs, rhs) => {
Ok(eval_alert_expr(lhs, sequence, timestamp, text)? || eval_alert_expr(rhs, sequence, timestamp, text)?)
}
FilterExpr::And(lhs, rhs) => Ok(eval_alert_expr(lhs, sequence, timestamp, text)?
&& eval_alert_expr(rhs, sequence, timestamp, text)?),
FilterExpr::Or(lhs, rhs) => Ok(eval_alert_expr(lhs, sequence, timestamp, text)?
|| eval_alert_expr(rhs, sequence, timestamp, text)?),
}
}
@@ -222,9 +252,15 @@ pub fn evaluate_alerts(
continue;
}
let state = states.entry(rule.id.clone()).or_default();
if let Some(message) =
evaluate_condition(&rule.condition, state, snapshot, sequence, timestamp, text, &rule.name)
{
if let Some(message) = evaluate_condition(
&rule.condition,
state,
snapshot,
sequence,
timestamp,
text,
&rule.name,
) {
fired.push((rule.clone(), message));
}
}
@@ -241,17 +277,25 @@ fn evaluate_condition(
rule_name: &str,
) -> Option<String> {
match condition {
AlertCondition::Immediate { query } => match eval_alert_query(query, sequence, timestamp, text) {
Ok(true) => Some(format!("{rule_name}: matched line: {text}")),
Ok(false) => None,
Err(err) => {
eprintln!("warning: alert '{rule_name}' query error: {err}");
None
AlertCondition::Immediate { query } => {
match eval_alert_query(query, sequence, timestamp, text) {
Ok(true) => Some(format!("{rule_name}: matched line: {text}")),
Ok(false) => None,
Err(err) => {
eprintln!("warning: alert '{rule_name}' query error: {err}");
None
}
}
},
AlertCondition::Threshold { query, op, threshold, window_ms } => {
}
AlertCondition::Threshold {
query,
op,
threshold,
window_ms,
} => {
if !state.threshold_backfilled {
state.threshold_window = backfill_window(snapshot, query, sequence, timestamp, *window_ms);
state.threshold_window =
backfill_window(snapshot, query, sequence, timestamp, *window_ms);
state.threshold_backfilled = true;
}
match eval_alert_query(query, sequence, timestamp, text) {
@@ -269,7 +313,10 @@ fn evaluate_condition(
}
}
}
AlertCondition::NewValue { field_regex, capture_group } => {
AlertCondition::NewValue {
field_regex,
capture_group,
} => {
let re = match Regex::new(field_regex) {
Ok(re) => re,
Err(err) => {
@@ -284,7 +331,13 @@ fn evaluate_condition(
.insert(value.clone())
.then(|| format!("{rule_name}: new value seen: {value} (line: {text})"))
}
AlertCondition::Ratio { query_a, query_b, op, ratio, window_ms } => {
AlertCondition::Ratio {
query_a,
query_b,
op,
ratio,
window_ms,
} => {
let matched_a = eval_alert_query(query_a, sequence, timestamp, text);
let matched_b = eval_alert_query(query_b, sequence, timestamp, text);
let (matched_a, matched_b) = match (matched_a, matched_b) {
@@ -298,11 +351,13 @@ fn evaluate_condition(
return None;
}
if !state.ratio_a_backfilled {
state.ratio_a_window = backfill_window(snapshot, query_a, sequence, timestamp, *window_ms);
state.ratio_a_window =
backfill_window(snapshot, query_a, sequence, timestamp, *window_ms);
state.ratio_a_backfilled = true;
}
if !state.ratio_b_backfilled {
state.ratio_b_window = backfill_window(snapshot, query_b, sequence, timestamp, *window_ms);
state.ratio_b_window =
backfill_window(snapshot, query_b, sequence, timestamp, *window_ms);
state.ratio_b_backfilled = true;
}
if matched_a {
@@ -326,7 +381,12 @@ fn evaluate_condition(
}
}
pub async fn deliver_for_rule(rule_name: String, message: String, topic: Option<String>, server: Option<String>) {
pub async fn deliver_for_rule(
rule_name: String,
message: String,
topic: Option<String>,
server: Option<String>,
) {
let Some(topic) = topic else {
return;
};
@@ -364,7 +424,9 @@ mod tests {
id: "r1".to_string(),
name: "test rule".to_string(),
enabled: true,
condition: AlertCondition::Immediate { query: query.to_string() },
condition: AlertCondition::Immediate {
query: query.to_string(),
},
}
}
@@ -379,7 +441,9 @@ mod tests {
&settings_path,
"spike".to_string(),
true,
AlertCondition::Immediate { query: r#"regex("error")"#.to_string() },
AlertCondition::Immediate {
query: r#"regex("error")"#.to_string(),
},
)
.unwrap();
assert_eq!(settings.alert_rules.len(), 1);
@@ -393,14 +457,18 @@ mod tests {
&created.id,
"renamed".to_string(),
false,
AlertCondition::Immediate { query: r#"regex("warn")"#.to_string() },
AlertCondition::Immediate {
query: r#"regex("warn")"#.to_string(),
},
)
.unwrap();
assert_eq!(updated.id, created.id);
assert_eq!(updated.name, "renamed");
assert!(!updated.enabled);
settings.remove_alert_rule(&settings_path, &created.id).unwrap();
settings
.remove_alert_rule(&settings_path, &created.id)
.unwrap();
assert!(settings.alert_rules.is_empty());
let reloaded = Settings::load(&settings_path);
assert!(reloaded.alert_rules.is_empty());
@@ -417,7 +485,9 @@ mod tests {
"does-not-exist",
"x".to_string(),
true,
AlertCondition::Immediate { query: r#"regex("x")"#.to_string() },
AlertCondition::Immediate {
query: r#"regex("x")"#.to_string(),
},
);
assert!(matches!(result, Err(AlertError::NotFound)));
@@ -466,12 +536,36 @@ mod tests {
let mut states = HashMap::new();
let index = engine::EngineSnapshot::default();
for (seq, ms) in [(0u64, 0i64), (1, 1000)] {
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, seq, Timestamp::from_millis(ms), "error");
assert!(fired.is_empty(), "should not fire before threshold is crossed");
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
seq,
Timestamp::from_millis(ms),
"error",
);
assert!(
fired.is_empty(),
"should not fire before threshold is crossed"
);
}
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 2, Timestamp::from_millis(2000), "error");
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
2,
Timestamp::from_millis(2000),
"error",
);
assert_eq!(fired.len(), 1);
let fired = evaluate_alerts(&[rule], &mut states, &index, 3, Timestamp::from_millis(2100), "fine");
let fired = evaluate_alerts(
&[rule],
&mut states,
&index,
3,
Timestamp::from_millis(2100),
"fine",
);
assert!(fired.is_empty());
}
@@ -490,8 +584,28 @@ mod tests {
};
let mut states = HashMap::new();
let index = engine::EngineSnapshot::default();
assert!(evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "error").is_empty());
assert!(evaluate_alerts(&[rule], &mut states, &index, 1, Timestamp::from_millis(5_000), "error").is_empty());
assert!(
evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
0,
Timestamp::from_millis(0),
"error"
)
.is_empty()
);
assert!(
evaluate_alerts(
&[rule],
&mut states,
&index,
1,
Timestamp::from_millis(5_000),
"error"
)
.is_empty()
);
}
#[test]
@@ -507,11 +621,32 @@ mod tests {
};
let mut states = HashMap::new();
let index = engine::EngineSnapshot::default();
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "agent=curl");
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
0,
Timestamp::from_millis(0),
"agent=curl",
);
assert_eq!(fired.len(), 1);
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 1, Timestamp::from_millis(1), "agent=curl");
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
1,
Timestamp::from_millis(1),
"agent=curl",
);
assert!(fired.is_empty());
let fired = evaluate_alerts(&[rule], &mut states, &index, 2, Timestamp::from_millis(2), "agent=wget");
let fired = evaluate_alerts(
&[rule],
&mut states,
&index,
2,
Timestamp::from_millis(2),
"agent=wget",
);
assert_eq!(fired.len(), 1);
}
@@ -531,12 +666,43 @@ mod tests {
};
let mut states = HashMap::new();
let index = engine::EngineSnapshot::default();
assert!(evaluate_alerts(&[rule.clone()], &mut states, &index, 0, Timestamp::from_millis(0), "error").is_empty());
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 1, Timestamp::from_millis(1), "request");
assert!(
evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
0,
Timestamp::from_millis(0),
"error"
)
.is_empty()
);
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
1,
Timestamp::from_millis(1),
"request",
);
assert_eq!(fired.len(), 1);
let fired = evaluate_alerts(&[rule.clone()], &mut states, &index, 2, Timestamp::from_millis(2), "request");
let fired = evaluate_alerts(
std::slice::from_ref(&rule),
&mut states,
&index,
2,
Timestamp::from_millis(2),
"request",
);
assert_eq!(fired.len(), 1);
let fired = evaluate_alerts(&[rule], &mut states, &index, 3, Timestamp::from_millis(3), "request");
let fired = evaluate_alerts(
&[rule],
&mut states,
&index,
3,
Timestamp::from_millis(3),
"request",
);
assert!(fired.is_empty());
}
@@ -547,7 +713,7 @@ mod tests {
let index = engine::EngineSnapshot::default();
let fired = evaluate_alerts(
&[rule.clone()],
std::slice::from_ref(&rule),
&mut states,
&index,
0,
@@ -556,7 +722,14 @@ mod tests {
);
assert_eq!(fired.len(), 1);
let fired = evaluate_alerts(&[rule], &mut states, &index, 1, Timestamp::from_millis(1), "plain text");
let fired = evaluate_alerts(
&[rule],
&mut states,
&index,
1,
Timestamp::from_millis(1),
"plain text",
);
assert!(fired.is_empty());
}
@@ -565,15 +738,22 @@ mod tests {
let rule = immediate_rule("some_metric[param]==5");
let mut states = HashMap::new();
let index = engine::EngineSnapshot::default();
let fired = evaluate_alerts(&[rule], &mut states, &index, 0, Timestamp::from_millis(0), "anything");
let fired = evaluate_alerts(
&[rule],
&mut states,
&index,
0,
Timestamp::from_millis(0),
"anything",
);
assert!(fired.is_empty());
}
#[tokio::test]
async fn deliver_ntfy_posts_message_to_server_topic_path() {
use axum::Router;
use axum::extract::State;
use axum::routing::post;
use axum::Router;
use std::sync::{Arc, Mutex};
use tokio::net::TcpListener;
@@ -590,7 +770,9 @@ mod tests {
}
let captured = Captured::default();
let app = Router::new().route("/{topic}", post(capture)).with_state(captured.clone());
let app = Router::new()
.route("/{topic}", post(capture))
.with_state(captured.clone());
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
@@ -598,7 +780,9 @@ mod tests {
});
let server = format!("http://{addr}");
deliver_ntfy(&server, "alerts", "hello world").await.unwrap();
deliver_ntfy(&server, "alerts", "hello world")
.await
.unwrap();
let (topic, body) = captured.0.lock().unwrap().clone().unwrap();
assert_eq!(topic, "alerts");