mpl-lang 0.4.0

Axioms Metrics Processing Language
Documentation
use crate::{
    CompileError, ParseError, TypeError,
    query::{Cmp, Filter, TagType, TerminalParamType},
    types::Parameterized,
};

#[test]
fn parse_group_by() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[1h..]
| where path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn parse_group_ts() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn parse_group_rfc() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[2025-03-01T13:00:00Z..+1h]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn parse_group_rate() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[2025-03-01T13:00:00Z..+1h]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn parse_re_escape() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[2025-03-01T13:00:00Z..+1h]
| filter path == #/\.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn parse_logic_0() -> Result<(), Box<dyn std::error::Error>> {
    let s = r#"
dataset:metric
| filter a == "snot"
    "#;
    let res = super::compile(s)?;
    let expected = Filter::Cmp {
        field: "a".into(),
        rhs: Cmp::Eq(Parameterized::Concrete("snot".try_into()?)),
    };
    match res {
        crate::Query::Simple { filters, .. } => {
            assert_eq!(1, filters.len());
            assert_eq!(&expected, filters[0].filter());
        }
        crate::Query::Compute { .. } => panic!("not a simple query"),
    }

    Ok(())
}

#[test]
fn parse_logic_1() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
dataset:metric
| filter a == 7.0 and not b == 8
    ";
    let res = super::compile(s)?;
    let expected = Filter::And(vec![
        Filter::Cmp {
            field: "a".into(),
            rhs: Cmp::Eq(Parameterized::Concrete(7.0.into())),
        },
        Filter::Not(Box::new(Filter::Cmp {
            field: "b".into(),
            rhs: Cmp::Eq(Parameterized::Concrete(8.into())),
        })),
    ]);
    match res {
        crate::Query::Simple { filters, .. } => {
            assert_eq!(1, filters.len());
            assert_eq!(&expected, filters[0].filter());
        }
        crate::Query::Compute { .. } => panic!("not a simple query"),
    }

    Ok(())
}

#[test]
fn parse_logic_2() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
dataset:metric
| filter a == 7 and b == 8 or c == 9 and ( d == 10 or e == 11 )
    ";
    let res = super::compile(s)?;
    let expected = Filter::Or(vec![
        Filter::And(vec![
            Filter::Cmp {
                field: "a".into(),
                rhs: Cmp::Eq(Parameterized::Concrete(7.into())),
            },
            Filter::Cmp {
                field: "b".into(),
                rhs: Cmp::Eq(Parameterized::Concrete(8.into())),
            },
        ]),
        Filter::And(vec![
            Filter::Cmp {
                field: "c".into(),
                rhs: Cmp::Eq(Parameterized::Concrete(9.into())),
            },
            Filter::Or(vec![
                Filter::Cmp {
                    field: "d".into(),
                    rhs: Cmp::Eq(Parameterized::Concrete(10.into())),
                },
                Filter::Cmp {
                    field: "e".into(),
                    rhs: Cmp::Eq(Parameterized::Concrete(11.into())),
                },
            ]),
        ]),
    ]);
    match res {
        crate::Query::Simple { filters, .. } => {
            assert_eq!(1, filters.len());
            assert_eq!(&expected, filters[0].filter());
        }
        crate::Query::Compute { .. } => panic!("not a simple query"),
    }

    Ok(())
}

#[test]
fn parse_params() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
param $dataset: Dataset;
param $resolution: Duration;
param $name: string;
param $age: int;
param $height: float;
param $is_cool: bool;
param $re: Regex;

$dataset:metric
| filter name == $name
| filter age > $age
| filter height > $height
| filter is_cool == $is_cool
| filter matches == $re
| align to $resolution using avg
";
    let res = super::compile(s)?;
    match res {
        crate::Query::Simple { source, .. } => {
            assert!(source.metric_id.dataset.is_param());
        }
        crate::Query::Compute { .. } => panic!("not a simple query"),
    }

    Ok(())
}

#[test]
fn parse_params_multi_define() {
    let s = r"
param $dataset: Dataset;
param $dataset: Duration;

$dataset:metric
";

    match super::compile(s) {
        Err(CompileError::Parse(ParseError::ParamDefinedMultipleTimes { span: _, param })) => {
            assert_eq!("dataset", param);
        }
        res => panic!("Expected param defined multiple times error, got {res:?}"),
    }
}

#[test]
fn parse_params_undefined() {
    let s = "$dataset:metric";

    match super::compile(s) {
        Err(CompileError::Parse(ParseError::UndefinedParam { span: _, param })) => {
            assert_eq!("dataset", param);
        }
        res => panic!("Expected undefined param error, got {res:?}"),
    }
}

#[test]
fn parse_params_mismatched_type() {
    let s = r"
param $dataset: Duration;

$dataset:metric
";

    match super::compile(s) {
        Err(CompileError::Type(TypeError::TypeMismatch {
            use_span,
            declaration_span,
            param_name,
            expected,
            actual,
        })) => {
            assert_eq!("dataset", param_name);
            assert_eq!(&[TerminalParamType::Dataset], expected.as_slice());
            assert_eq!(TerminalParamType::Duration, actual);
            assert_eq!(28, use_span.offset());
            assert_eq!(8, use_span.len());
            assert_eq!(7, declaration_span.offset());
            assert_eq!(8, declaration_span.len());
        }
        res => panic!("Expected mismatched param type error, got {res:?}"),
    }
}

#[test]
fn parse_params_mismatched_type_value() {
    let s = r"
param $value: Dataset;

dataset:metric
| where key == $value
";

    match super::compile(s) {
        Err(CompileError::Type(TypeError::TypeMismatch {
            use_span,
            declaration_span,
            param_name,
            expected,
            actual,
        })) => {
            assert_eq!("value", param_name);
            assert_eq!(
                &[
                    TerminalParamType::Tag(TagType::String),
                    TerminalParamType::Tag(TagType::Int),
                    TerminalParamType::Tag(TagType::Float),
                    TerminalParamType::Tag(TagType::Bool)
                ],
                expected.as_slice()
            );
            assert_eq!(TerminalParamType::Dataset, actual);
            assert_eq!(55, use_span.offset());
            assert_eq!(6, use_span.len());
            assert_eq!(7, declaration_span.offset());
            assert_eq!(6, declaration_span.len());
        }
        res => panic!("Expected mismatched param type error, got {res:?}"),
    }
}

#[test]
fn parse_params_mismatched_type_duration() {
    let s = r"
param $duration: Dataset;

dataset:metric
| align to $duration using avg
";

    match super::compile(s) {
        Err(CompileError::Type(TypeError::TypeMismatch {
            use_span,
            declaration_span,
            param_name,
            expected,
            actual,
        })) => {
            assert_eq!("duration", param_name);
            assert_eq!(&[TerminalParamType::Duration], expected.as_slice());
            assert_eq!(TerminalParamType::Dataset, actual);
            assert_eq!(54, use_span.offset());
            assert_eq!(9, use_span.len());
            assert_eq!(7, declaration_span.offset());
            assert_eq!(9, declaration_span.len());
        }
        res => panic!("Expected mismatched param type error, got {res:?}"),
    }
}

#[test]
fn group_by_two() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
| group by method, path using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn group_by_two_same() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path, code using sum
| group by method, path, code using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn group_by_two_error() {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path using sum
| group by method, path, code using sum
    ";
    assert!(super::compile(s).is_err());
}

#[test]
fn bucket_group_by() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| bucket by method, path, code to 5m using histogram(max)
| group by method, path using sum
    ";
    super::compile(s)?;
    Ok(())
}

#[test]
fn bucket_group_by_error() {
    let s = r"
`dev.metrics`:http_requests_total[1747077736092..]
| filter path == #/.*(elastic\/_bulk|ingest|(?:v1\/(traces|logs|metrics))).*/
| filter code == #/[123]../
| align to 3m using prom::rate
| group by method, path using sum
| bucket by method, path, code to 5m using histogram(max)
    ";
    assert!(super::compile(s).is_err());
}
#[test]
fn group_by_compute() -> Result<(), Box<dyn std::error::Error>> {
    let s = r"
(
  `ds`:m1[1h..]
  | group by method, code using sum,
  `ds`:m2[1h..]
  | group by method, path using sum
)
| compute test using +
    ";
    super::compile(s)?;
    Ok(())
}