//! FilterBar query language: grammar (see `src/filter_parser.pest`), AST and parser. //! Evaluating the AST against real log data is left to `engine.rs`. #![allow(dead_code)] use pest::Parser; use pest::iterators::Pair; use pest_derive::Parser as PestParser; use serde::{Deserialize, Serialize}; #[derive(PestParser)] #[grammar = "filter_parser.pest"] struct FilterGrammar; /// The parsed AST for a FilterBar query. #[derive(Debug, Clone, PartialEq)] pub enum FilterExpr { /// `regex("...")` — raw-text regex search over log lines. Regex(String), /// `metric[param] value` — a single metric comparison. Comparison { metric: String, param: Option, op: CompareOp, value: FilterValue, }, And(Box, Box), Or(Box, Box), } /// Comparison operators for metric comparisons; also reused by `crate::alerts::AlertCondition` /// for threshold/ratio comparisons (hence `Serialize`/`Deserialize`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CompareOp { Lt, Gt, Le, Ge, Eq, Ne, } /// The right-hand side of a comparison: values that parse as a number become /// [`FilterValue::Number`], everything else is kept as an opaque [`FilterValue::Raw`] string. #[derive(Debug, Clone, PartialEq)] pub enum FilterValue { Number(f64), Raw(String), } /// A located, human-readable parse error. #[derive(Debug, Clone, PartialEq)] pub struct FilterParseError { message: String, } impl FilterParseError { fn new(message: impl Into) -> Self { FilterParseError { message: message.into(), } } } impl std::fmt::Display for FilterParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl std::error::Error for FilterParseError {} impl From> for FilterParseError { fn from(err: pest::error::Error) -> Self { // pest's Display already renders a located message, so use it and add a short prefix. FilterParseError::new(format!("failed to parse filter expression: {err}")) } } /// Parse a FilterBar query string into a [`FilterExpr`] AST. pub fn parse(input: &str) -> Result { let mut pairs = FilterGrammar::parse(Rule::file, input)?; let file = pairs .next() .ok_or_else(|| FilterParseError::new("empty filter expression"))?; let or_expr = file .into_inner() .find(|p| p.as_rule() == Rule::or_expr) .ok_or_else(|| FilterParseError::new("malformed filter expression: missing body"))?; build_or_expr(or_expr) } fn build_or_expr(pair: Pair) -> Result { debug_assert_eq!(pair.as_rule(), Rule::or_expr); // `or_kw` is atomic so it's not silent, showing up as a sibling pair between `and_expr` operands — skip it rather than assuming a fixed alternating structure. let mut and_exprs = pair .into_inner() .filter(|p| p.as_rule() != Rule::or_kw); let first = and_exprs .next() .ok_or_else(|| FilterParseError::new("malformed 'or' expression"))?; let mut expr = build_and_expr(first)?; for next in and_exprs { let rhs = build_and_expr(next)?; expr = FilterExpr::Or(Box::new(expr), Box::new(rhs)); } Ok(expr) } fn build_and_expr(pair: Pair) -> Result { debug_assert_eq!(pair.as_rule(), Rule::and_expr); // Same reasoning as `build_or_expr` for `and_kw`. let mut terms = pair .into_inner() .filter(|p| p.as_rule() != Rule::and_kw); let first = terms .next() .ok_or_else(|| FilterParseError::new("malformed 'and' expression"))?; let mut expr = build_term(first)?; for next in terms { let rhs = build_term(next)?; expr = FilterExpr::And(Box::new(expr), Box::new(rhs)); } Ok(expr) } fn build_term(pair: Pair) -> Result { match pair.as_rule() { Rule::regex_expr => build_regex(pair), Rule::comparison => build_comparison(pair), Rule::or_expr => build_or_expr(pair), other => Err(FilterParseError::new(format!( "unexpected grammar element: {other:?}" ))), } } fn build_regex(pair: Pair) -> Result { let string_literal = pair .into_inner() .find(|p| p.as_rule() == Rule::string_literal) .ok_or_else(|| FilterParseError::new("malformed regex(...) expression"))?; let inner_str = string_literal .into_inner() .find(|p| p.as_rule() == Rule::inner_str) .ok_or_else(|| FilterParseError::new("malformed string literal in regex(...)"))?; Ok(FilterExpr::Regex(unescape(inner_str.as_str()))) } fn build_comparison(pair: Pair) -> Result { let mut inner = pair.into_inner(); let metric_pair = inner .next() .ok_or_else(|| FilterParseError::new("malformed comparison: missing metric name"))?; let metric = metric_pair.as_str().to_string(); let mut next = inner .next() .ok_or_else(|| FilterParseError::new("malformed comparison: missing operator"))?; let param = if next.as_rule() == Rule::param { let p = next.as_str().to_string(); next = inner .next() .ok_or_else(|| FilterParseError::new("malformed comparison: missing operator"))?; Some(p) } else { None }; let op = parse_op(next.as_str())?; let value_pair = inner .next() .ok_or_else(|| FilterParseError::new("malformed comparison: missing value"))?; let value = parse_value(value_pair.as_str()); Ok(FilterExpr::Comparison { metric, param, op, value, }) } fn parse_op(s: &str) -> Result { match s { "<" => Ok(CompareOp::Lt), ">" => Ok(CompareOp::Gt), "<=" => Ok(CompareOp::Le), ">=" => Ok(CompareOp::Ge), "==" => Ok(CompareOp::Eq), "!=" => Ok(CompareOp::Ne), other => Err(FilterParseError::new(format!( "unknown comparison operator: {other}" ))), } } fn parse_value(s: &str) -> FilterValue { match s.parse::() { Ok(n) => FilterValue::Number(n), Err(_) => FilterValue::Raw(s.to_string()), } } /// Unescape a regex string literal's inner contents (`\"` -> `"`, `\\` -> `\`, `\x` -> `x`). fn unescape(s: &str) -> String { let mut out = String::with_capacity(s.len()); let mut chars = s.chars(); while let Some(c) = chars.next() { if c == '\\' { if let Some(next) = chars.next() { out.push(next); } } else { out.push(c); } } out } #[cfg(test)] mod tests { use super::*; #[test] fn parses_bare_regex() { let expr = parse(r#"regex("error")"#).expect("should parse"); assert_eq!(expr, FilterExpr::Regex("error".to_string())); } #[test] fn parses_regex_with_escaped_quotes() { let expr = parse(r#"regex("say \"hi\"")"#).expect("should parse"); assert_eq!(expr, FilterExpr::Regex(r#"say "hi""#.to_string())); } #[test] fn parses_single_metric_comparison() { let expr = parse("metric_name[param]<5").expect("should parse"); assert_eq!( expr, FilterExpr::Comparison { metric: "metric_name".to_string(), param: Some("param".to_string()), op: CompareOp::Lt, value: FilterValue::Number(5.0), } ); } #[test] fn parses_reserved_metric_sequence() { let expr = parse("sequence<5").expect("should parse"); assert_eq!( expr, FilterExpr::Comparison { metric: "sequence".to_string(), param: None, op: CompareOp::Lt, value: FilterValue::Number(5.0), } ); } #[test] fn parses_reserved_metric_timestamp_as_raw_value() { let expr = parse("timestamp>12:00").expect("should parse"); assert_eq!( expr, FilterExpr::Comparison { metric: "timestamp".to_string(), param: None, op: CompareOp::Gt, value: FilterValue::Raw("12:00".to_string()), } ); } #[test] fn parses_and_or_with_and_binding_tighter() { let expr = parse(r#"regex("error") and sequence<5 or timestamp>12:00"#) .expect("should parse"); let expected = FilterExpr::Or( Box::new(FilterExpr::And( Box::new(FilterExpr::Regex("error".to_string())), Box::new(FilterExpr::Comparison { metric: "sequence".to_string(), param: None, op: CompareOp::Lt, value: FilterValue::Number(5.0), }), )), Box::new(FilterExpr::Comparison { metric: "timestamp".to_string(), param: None, op: CompareOp::Gt, value: FilterValue::Raw("12:00".to_string()), }), ); assert_eq!(expr, expected); } #[test] fn parenthesized_expression_overrides_precedence() { let expr = parse(r#"regex("error") and (sequence<5 or timestamp>12:00)"#) .expect("should parse"); let expected = FilterExpr::And( Box::new(FilterExpr::Regex("error".to_string())), Box::new(FilterExpr::Or( Box::new(FilterExpr::Comparison { metric: "sequence".to_string(), param: None, op: CompareOp::Lt, value: FilterValue::Number(5.0), }), Box::new(FilterExpr::Comparison { metric: "timestamp".to_string(), param: None, op: CompareOp::Gt, value: FilterValue::Raw("12:00".to_string()), }), )), ); assert_eq!(expr, expected); } #[test] fn malformed_input_produces_located_error_not_panic() { let result = parse("regex(\"unterminated and sequence<"); assert!(result.is_err()); let err = result.unwrap_err(); let msg = err.to_string(); assert!(!msg.is_empty()); } #[test] fn empty_input_is_an_error_not_a_panic() { assert!(parse("").is_err()); } #[test] fn garbage_input_is_an_error_not_a_panic() { assert!(parse("@@@ not a valid filter @@@").is_err()); } }