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
+64
-11
@@ -738,6 +738,9 @@ enum CompiledFilter {
|
||||
Regex(Regex),
|
||||
Sequence { op: CompareOp, target: f64 },
|
||||
Timestamp { op: CompareOp, target_ms: i64 },
|
||||
/// `ottyl_fmt <op> value` — like `Json`, this is line-local and re-detected fresh per line in
|
||||
/// `eval_compiled` (`crate::logfmt::detect_format`) rather than pre-resolved against the index.
|
||||
Fmt { op: CompareOp, target: String },
|
||||
MetricMatch(HashSet<u64>),
|
||||
/// `json[field] <op> value` — unlike `MetricMatch`, this can't be pre-resolved against the
|
||||
/// index (the JSON lives in the log line itself, not a declared metric/param), so it's
|
||||
@@ -767,8 +770,9 @@ fn compile_filter(
|
||||
.map(CompiledFilter::Regex)
|
||||
.map_err(|err| format!("invalid regex in filter: {err}")),
|
||||
FilterExpr::Comparison { metric, param, op, value } => match metric.as_str() {
|
||||
"sequence" => compile_sequence_comparison(*op, value),
|
||||
"timestamp" => compile_timestamp_comparison(*op, value, now),
|
||||
"ottyl_seq" => compile_ottyl_seq_comparison(*op, value),
|
||||
"ottyl_ts" => compile_ottyl_ts_comparison(*op, value, now),
|
||||
"ottyl_fmt" => Ok(compile_ottyl_fmt_comparison(*op, value)),
|
||||
other => compile_metric_comparison(index, other, param.as_deref(), *op, value, now),
|
||||
},
|
||||
FilterExpr::JsonComparison { field, op, value } => Ok(CompiledFilter::Json {
|
||||
@@ -787,19 +791,19 @@ fn compile_filter(
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Raw` value is accepted defensively (parsed as a number) even though the grammar shouldn't produce one for `sequence`.
|
||||
fn compile_sequence_comparison(op: CompareOp, value: &FilterValue) -> Result<CompiledFilter, String> {
|
||||
/// A `Raw` value is accepted defensively (parsed as a number) even though the grammar shouldn't produce one for `ottyl_seq`.
|
||||
fn compile_ottyl_seq_comparison(op: CompareOp, value: &FilterValue) -> Result<CompiledFilter, String> {
|
||||
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:?}")
|
||||
format!("'ottyl_seq' comparisons require a numeric value, got {raw:?}")
|
||||
})?,
|
||||
};
|
||||
Ok(CompiledFilter::Sequence { op, target })
|
||||
}
|
||||
|
||||
/// A `Number` is treated as raw milliseconds; a `Raw` token is resolved via `filter_datetime::parse_datetime` against `now`.
|
||||
fn compile_timestamp_comparison(
|
||||
fn compile_ottyl_ts_comparison(
|
||||
op: CompareOp,
|
||||
value: &FilterValue,
|
||||
now: Timestamp,
|
||||
@@ -813,7 +817,19 @@ fn compile_timestamp_comparison(
|
||||
Ok(CompiledFilter::Timestamp { op, target_ms })
|
||||
}
|
||||
|
||||
/// `metric[param] <op> value` for any metric name other than `sequence`/`timestamp` — see [`CompiledFilter`]'s doc comment.
|
||||
/// A `Number` target is stringified since `ottyl_fmt`'s value ("json"/"logfmt"/"none",
|
||||
/// `crate::logfmt::detect_format`) is always text; any op is accepted since the strings compare
|
||||
/// lexicographically fine for `==`/`!=` (the only ops that make real sense here), same as
|
||||
/// `compare_metric_primitive`'s `String` arm.
|
||||
fn compile_ottyl_fmt_comparison(op: CompareOp, value: &FilterValue) -> CompiledFilter {
|
||||
let target = match value {
|
||||
FilterValue::Number(n) => n.to_string(),
|
||||
FilterValue::Raw(raw) => raw.clone(),
|
||||
};
|
||||
CompiledFilter::Fmt { op, target }
|
||||
}
|
||||
|
||||
/// `metric[param] <op> value` for any metric name other than `ottyl_seq`/`ottyl_ts`/`ottyl_fmt` — see [`CompiledFilter`]'s doc comment.
|
||||
fn compile_metric_comparison(
|
||||
index: &BTreeMap<IndexKey, DataValue>,
|
||||
metric: &str,
|
||||
@@ -985,6 +1001,9 @@ fn eval_compiled(compiled: &CompiledFilter, sequence: u64, timestamp: Timestamp,
|
||||
CompiledFilter::Timestamp { op, target_ms } => {
|
||||
apply_compare_op(timestamp.as_millis().cmp(target_ms), *op)
|
||||
}
|
||||
CompiledFilter::Fmt { op, target } => {
|
||||
apply_compare_op(crate::logfmt::detect_format(text).cmp(target.as_str()), *op)
|
||||
}
|
||||
CompiledFilter::MetricMatch(sequences) => sequences.contains(&sequence),
|
||||
CompiledFilter::Json { field, op, value } => eval_json_comparison(text, field, *op, value),
|
||||
CompiledFilter::And(lhs, rhs) => {
|
||||
@@ -2983,13 +3002,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_query_with_sequence_comparison_filters_by_sequence() {
|
||||
fn logs_query_with_ottyl_seq_comparison_filters_by_sequence() {
|
||||
let mut index = BTreeMap::new();
|
||||
for i in 0..5u64 {
|
||||
ingest_log(&mut index, i, &format!("line {i}"));
|
||||
}
|
||||
|
||||
match handle_query(&index, logs_query(Some("sequence<2")), 0) {
|
||||
match handle_query(&index, logs_query(Some("ottyl_seq<2")), 0) {
|
||||
EngineResponse::Logs(entries) => {
|
||||
let sequences: Vec<u64> = entries.iter().map(|e| e.sequence).collect();
|
||||
assert_eq!(sequences, vec![0, 1]);
|
||||
@@ -2999,7 +3018,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_query_with_timestamp_comparison_filters_by_timestamp() {
|
||||
fn logs_query_with_ottyl_ts_comparison_filters_by_timestamp() {
|
||||
let mut index = BTreeMap::new();
|
||||
for i in 0..5u64 {
|
||||
let timestamp = Timestamp::from_millis(i as i64 * 1000);
|
||||
@@ -3009,7 +3028,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
match handle_query(&index, logs_query(Some("timestamp>2000")), 0) {
|
||||
match handle_query(&index, logs_query(Some("ottyl_ts>2000")), 0) {
|
||||
EngineResponse::Logs(entries) => {
|
||||
let sequences: Vec<u64> = entries.iter().map(|e| e.sequence).collect();
|
||||
assert_eq!(sequences, vec![3, 4]);
|
||||
@@ -3018,6 +3037,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_query_with_ottyl_fmt_comparison_filters_by_detected_format_for_mixed_format_lines() {
|
||||
let mut index = BTreeMap::new();
|
||||
// Seeded fixture mixing all three detectable formats on purpose (ticket 044).
|
||||
ingest_log(&mut index, 0, r#"{"level":"error","code":500}"#);
|
||||
ingest_log(&mut index, 1, r#"ts=1788648105773 msg="entity_moved" x1=-4"#);
|
||||
ingest_log(&mut index, 2, "just some plain prose here");
|
||||
ingest_log(&mut index, 3, r#"{"level":"info","code":200}"#);
|
||||
|
||||
match handle_query(&index, logs_query(Some("ottyl_fmt==json")), 0) {
|
||||
EngineResponse::Logs(entries) => {
|
||||
let sequences: Vec<u64> = entries.iter().map(|e| e.sequence).collect();
|
||||
assert_eq!(sequences, vec![0, 3]);
|
||||
}
|
||||
_ => panic!("expected Logs response"),
|
||||
}
|
||||
|
||||
match handle_query(&index, logs_query(Some("ottyl_fmt==logfmt")), 0) {
|
||||
EngineResponse::Logs(entries) => {
|
||||
let sequences: Vec<u64> = entries.iter().map(|e| e.sequence).collect();
|
||||
assert_eq!(sequences, vec![1]);
|
||||
}
|
||||
_ => panic!("expected Logs response"),
|
||||
}
|
||||
|
||||
match handle_query(&index, logs_query(Some("ottyl_fmt==none")), 0) {
|
||||
EngineResponse::Logs(entries) => {
|
||||
let sequences: Vec<u64> = entries.iter().map(|e| e.sequence).collect();
|
||||
assert_eq!(sequences, vec![2]);
|
||||
}
|
||||
_ => panic!("expected Logs response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logs_query_with_json_field_comparison_filters_by_json_field() {
|
||||
let mut index = BTreeMap::new();
|
||||
|
||||
Reference in New Issue
Block a user