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:
co-authored by
Claude Sonnet 5
parent
2cc8ac8138
commit
c3108677ce
+250
-66
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user