Rename reserved sequence/timestamp params to ottyl_seq/ottyl_ts; add ottyl_fmt
Ticket 044: `sequence` and `timestamp` collided with ordinary user-declared
param names and didn't signal they're ottyl-owned, so they're renamed
(breaking change, no back-compat alias) to `ottyl_seq`/`ottyl_ts` in the
filter grammar (src/filter_parser.pest, src/filter_parser.rs), alert query
restriction/evaluation (src/alerts.rs), and the filter-bar docs/placeholder
in the webapp.
Also adds a new reserved param `ottyl_fmt` exposing which format a line was
auto-detected as ("json"/"logfmt"/"none") via a new
`crate::logfmt::detect_format`, wired into the same filter grammar and alert
restriction list as `ottyl_seq`/`ottyl_ts`. A mixed-format fixture test in
engine.rs verifies `ottyl_fmt` reflects the actual detected format per line.
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
c84276937a
commit
534dc52e05
+32
-5
@@ -1,6 +1,6 @@
|
||||
//! Alert rules: definitions, trigger evaluation, and ntfy delivery.
|
||||
//!
|
||||
//! Alert queries are restricted to regex/sequence/timestamp/json[field] — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee. json[field] is fine here since, like regex/sequence/timestamp, it only inspects the line's own text/fields, no index lookup.
|
||||
//! Alert queries are restricted to regex/ottyl_seq/ottyl_ts/ottyl_fmt/json[field] — a metric[param] comparison would require a full bucket rescan per line, breaking the proportional-cost guarantee. json[field] is fine here since, like regex/ottyl_seq/ottyl_ts/ottyl_fmt, it only inspects the line's own text/fields, no index lookup.
|
||||
//!
|
||||
//! `Threshold`/`Ratio` windows are maintained via `crate::rate::RateWindow` — the same windowed-rate
|
||||
//! implementation PromQL's `rate()` uses (`crate::engine::evaluate_promql`) — rather than a
|
||||
@@ -196,16 +196,16 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
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 == "sequence" => {
|
||||
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!("'sequence' comparisons require a numeric value, got {raw:?}"))?,
|
||||
.map_err(|_| format!("'ottyl_seq' comparisons require a numeric value, got {raw:?}"))?,
|
||||
};
|
||||
Ok(apply_compare_op((sequence as f64).total_cmp(&target), *op))
|
||||
}
|
||||
FilterExpr::Comparison { metric, param: None, op, value } if metric == "timestamp" => {
|
||||
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())
|
||||
@@ -214,8 +214,15 @@ fn eval_alert_expr(expr: &FilterExpr, sequence: u64, timestamp: Timestamp, text:
|
||||
};
|
||||
Ok(apply_compare_op(timestamp.as_millis().cmp(&target_ms), *op))
|
||||
}
|
||||
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(),
|
||||
};
|
||||
Ok(apply_compare_op(crate::logfmt::detect_format(text).cmp(target.as_str()), *op))
|
||||
}
|
||||
FilterExpr::Comparison { metric, .. } => Err(format!(
|
||||
"alert queries only support regex()/sequence/timestamp (not metric comparisons like \
|
||||
"alert queries only support regex()/ottyl_seq/ottyl_ts/ottyl_fmt (not metric comparisons like \
|
||||
'{metric}[param]'): metric-based alert conditions aren't supported yet"
|
||||
)),
|
||||
FilterExpr::JsonComparison { field, op, value } => {
|
||||
@@ -574,6 +581,26 @@ mod tests {
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ottyl_fmt_alert_query_matches_detected_format() {
|
||||
let rule = immediate_rule("ottyl_fmt==json");
|
||||
let mut states = HashMap::new();
|
||||
let index: BTreeMap<IndexKey, DataValue> = BTreeMap::new();
|
||||
|
||||
let fired = evaluate_alerts(
|
||||
&[rule.clone()],
|
||||
&mut states,
|
||||
&index,
|
||||
0,
|
||||
Timestamp::from_millis(0),
|
||||
r#"{"level":"error"}"#,
|
||||
);
|
||||
assert_eq!(fired.len(), 1);
|
||||
|
||||
let fired = evaluate_alerts(&[rule], &mut states, &index, 1, Timestamp::from_millis(1), "plain text");
|
||||
assert!(fired.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metric_comparison_in_alert_query_is_rejected_not_panicking() {
|
||||
let rule = immediate_rule("some_metric[param]==5");
|
||||
|
||||
Reference in New Issue
Block a user