openmetrics-parser 0.5.1

An OpenMetrics/Prometheus Text Exposition Format Parser
Documentation
use crate::prometheus::parse_prometheus;

#[test]
fn test_label_sets() {
    use crate::{
        MetricFamily, MetricNumber, PrometheusCounterValue, PrometheusType, PrometheusValue, Sample,
    };

    let family = MetricFamily::new(
        String::from("test_metric"),
        vec![
            String::from("test_label"),
            String::from("test_label_to_remove"),
        ],
        PrometheusType::Counter,
        String::from("HELP!!"),
        String::new(),
    )
    .with_samples(vec![Sample::new(
        vec![String::from("test1"), String::from("test2")],
        None,
        PrometheusValue::Counter(PrometheusCounterValue {
            value: MetricNumber::Int(1),
            exemplar: None,
        }),
    )])
    .unwrap();

    {
        let metric = family.iter_samples().next().unwrap();
        assert_eq!(
            metric
                .get_labelset()
                .unwrap()
                .get_label_value("test_label")
                .unwrap(),
            "test1"
        );
        assert_eq!(
            metric
                .get_labelset()
                .unwrap()
                .get_label_value("test_label_to_remove")
                .unwrap(),
            "test2"
        );
    }

    let family = family.without_label("test_label_to_remove").unwrap();
    {
        let metric = family.iter_samples().next().unwrap();
        assert_eq!(
            metric
                .get_labelset()
                .unwrap()
                .get_label_value("test_label")
                .unwrap(),
            "test1"
        );
        assert!(metric
            .get_labelset()
            .unwrap()
            .get_label_value("test_label_to_remove")
            .is_none());
    }
}

#[test]
fn test_render() {
    let test_str = include_str!("../prometheus/testdata/upstream_example.txt");
    let exposition = parse_prometheus(test_str).unwrap();
    let exposition_str = exposition.to_string();
    assert!(parse_prometheus(&exposition_str).is_ok());

    let test_str = include_str!("../prometheus/testdata/gravelgateway#5.txt");
    let exposition = parse_prometheus(test_str).unwrap();
    let exposition_str = exposition.to_string();
    assert!(parse_prometheus(&exposition_str).is_ok());
}

#[test]
fn test_metric_number_operations() {
    use crate::MetricNumber;

    let a = MetricNumber::Int(1);
    let b = MetricNumber::Int(2);
    let c = MetricNumber::Float(0.5);
    let d = MetricNumber::Float(1.5);

    assert_eq!(a + b, MetricNumber::Int(3));
    assert_eq!(a + c, MetricNumber::Float(1.5));
    assert_eq!(a + d, MetricNumber::Float(2.5));
    assert_eq!(b + c, MetricNumber::Float(2.5));
    assert_eq!(b + d, MetricNumber::Float(3.5));
    assert_eq!(c + d, MetricNumber::Float(2.0));

    assert_eq!(a - b, MetricNumber::Int(-1));
    assert_eq!(a - c, MetricNumber::Float(0.5));
    assert_eq!(a - d, MetricNumber::Float(-0.5));
    assert_eq!(b - c, MetricNumber::Float(1.5));
    assert_eq!(b - d, MetricNumber::Float(0.5));
    assert_eq!(c - d, MetricNumber::Float(-1.0));

    assert_eq!(a * b, MetricNumber::Int(2));
    assert_eq!(a * c, MetricNumber::Float(0.5));
    assert_eq!(a * d, MetricNumber::Float(1.5));
    assert_eq!(b * c, MetricNumber::Float(1.0));
    assert_eq!(b * d, MetricNumber::Float(3.0));
    assert_eq!(c * d, MetricNumber::Float(0.75));

    assert_eq!(a / b, MetricNumber::Int(0));
    assert_eq!(a / c, MetricNumber::Float(2.0));
    assert_eq!(a / d, MetricNumber::Float(2.0 / 3.0));
    assert_eq!(b / c, MetricNumber::Float(4.0));
    assert_eq!(b / d, MetricNumber::Float(4.0 / 3.0));
    assert_eq!(c / d, MetricNumber::Float(1.0 / 3.0));

    {
        let mut a = MetricNumber::Int(1);
        a += b;
        assert_eq!(a, MetricNumber::Int(3));
    }
    {
        let mut a = MetricNumber::Int(1);
        a += c;
        assert_eq!(a, MetricNumber::Float(1.5));
    }
    {
        let mut a = MetricNumber::Int(1);
        a += d;
        assert_eq!(a, MetricNumber::Float(2.5));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a += b;
        assert_eq!(a, MetricNumber::Float(2.5));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a += c;
        assert_eq!(a, MetricNumber::Float(1.0));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a += d;
        assert_eq!(a, MetricNumber::Float(2.0));
    }

    {
        let mut a = MetricNumber::Int(1);
        a -= b;
        assert_eq!(a, MetricNumber::Int(-1));
    }
    {
        let mut a = MetricNumber::Int(1);
        a -= c;
        assert_eq!(a, MetricNumber::Float(0.5));
    }
    {
        let mut a = MetricNumber::Int(1);
        a -= d;
        assert_eq!(a, MetricNumber::Float(-0.5));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a -= b;
        assert_eq!(a, MetricNumber::Float(-1.5));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a -= c;
    }

    {
        let mut a = MetricNumber::Int(1);
        a *= b;
        assert_eq!(a, MetricNumber::Int(2));
    }
    {
        let mut a = MetricNumber::Int(1);
        a *= c;
        assert_eq!(a, MetricNumber::Float(0.5));
    }
    {
        let mut a = MetricNumber::Int(1);
        a *= d;
        assert_eq!(a, MetricNumber::Float(1.5));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a *= b;
        assert_eq!(a, MetricNumber::Float(1.0));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a *= c;
        assert_eq!(a, MetricNumber::Float(0.25));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a *= d;
        assert_eq!(a, MetricNumber::Float(0.75));
    }

    {
        let mut a = MetricNumber::Int(1);
        a /= b;
        assert_eq!(a, MetricNumber::Int(0));
    }
    {
        let mut a = MetricNumber::Int(1);
        a /= c;
        assert_eq!(a, MetricNumber::Float(2.0));
    }
    {
        let mut a = MetricNumber::Int(1);
        a /= d;
        assert_eq!(a, MetricNumber::Float(2.0 / 3.0));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a /= b;
        assert_eq!(a, MetricNumber::Float(0.25));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a /= c;
        assert_eq!(a, MetricNumber::Float(1.0));
    }
    {
        let mut a = MetricNumber::Float(0.5);
        a /= d;
        assert_eq!(a, MetricNumber::Float(1.0 / 3.0));
    }
}

#[test]
fn test_exemplar_render() {
    use crate::Exemplar;
    use std::collections::HashMap;

    let labels: HashMap<String, String> = [("trace_id", "abc"), ("span_id", "def")]
        .iter()
        .map(|(k, v)| (k.to_string(), v.to_string()))
        .collect();

    assert_eq!(
        Exemplar::new(labels, 0.5, Some(123.)).to_string(),
        " # {span_id=\"def\",trace_id=\"abc\"} 0.5 123"
    );
    assert_eq!(Exemplar::new(HashMap::new(), 1., None).to_string(), " # {} 1");

    let test_str = "# TYPE foo_total counter\nfoo_total 1 # {trace_id=\"abc\"} 0.5\n";
    let rendered = parse_prometheus(test_str).unwrap().to_string();
    assert_eq!(rendered, test_str);
}

#[test]
fn test_render_keeps_timestamps() {
    let test_str = r#"# TYPE h histogram
h_bucket{le="1"} 0 5
h_bucket{le="+Inf"} 1 5
h_sum 1 5
h_count 1 5
# TYPE s summary
s{quantile="0.5"} 1 5
s_sum 1 5
s_count 1 5
"#;
    let rendered = parse_prometheus(test_str).unwrap().to_string();
    for line in rendered.lines().filter(|l| !l.is_empty() && !l.starts_with('#')) {
        assert!(line.ends_with(" 5"), "missing timestamp on `{}`", line);
    }
}

#[test]
fn test_metric_number_operations_dont_panic() {
    use crate::MetricNumber;

    let max = MetricNumber::Int(i64::MAX);
    let one = MetricNumber::Int(1);
    let zero = MetricNumber::Int(0);

    assert_eq!(max + one, MetricNumber::Float(i64::MAX as f64 + 1.));
    assert_eq!(MetricNumber::Int(i64::MIN) - one, MetricNumber::Float(i64::MIN as f64 - 1.));
    assert_eq!(max * MetricNumber::Int(2), MetricNumber::Float(i64::MAX as f64 * 2.));
    assert_eq!(one / zero, MetricNumber::Float(f64::INFINITY));
    assert!(matches!(zero / zero, MetricNumber::Float(f) if f.is_nan()));
    assert_eq!(MetricNumber::Int(i64::MIN) / MetricNumber::Int(-1), MetricNumber::Float(-(i64::MIN as f64)));

    let mut a = max;
    a += one;
    assert_eq!(a, MetricNumber::Float(i64::MAX as f64 + 1.));

    let mut a = one;
    a /= zero;
    assert_eq!(a, MetricNumber::Float(f64::INFINITY));
}

#[test]
fn test_with_labels() {
    use crate::{MetricFamily, MetricNumber, PrometheusType, PrometheusValue, Sample};

    let gauge = |values: &[&str]| {
        Sample::new(
            values.iter().map(|v| v.to_string()).collect(),
            None,
            PrometheusValue::Gauge(MetricNumber::Int(1)),
        )
    };

    // Label names deliberately unsorted
    let family = MetricFamily::new(
        String::from("test_metric"),
        vec![String::from("zone"), String::from("app")],
        PrometheusType::Gauge,
        String::new(),
        String::new(),
    )
    .with_samples(vec![gauge(&["a", "web"]), gauge(&["b", "web"])])
    .unwrap();

    // Overwrites the existing label rather than inserting a second `app`
    let relabelled = family.with_labels(vec![("app", "api"), ("env", "prod")]).unwrap();
    let mut names = relabelled.get_label_names().to_vec();
    names.sort();
    assert_eq!(names, &["app", "env", "zone"]);
    for sample in relabelled.iter_samples() {
        let labelset = sample.get_labelset().unwrap();
        assert_eq!(labelset.get_label_value("app"), Some("api"));
        assert_eq!(labelset.get_label_value("env"), Some("prod"));
    }

    // Collapsing `zone` would leave two samples with the same labels
    assert!(family.with_labels(vec![("zone", "c")]).is_err());

    // New labels go in sorted position when the family's labels are sorted
    let sorted = MetricFamily::new(
        String::from("test_metric"),
        vec![String::from("app"), String::from("zone")],
        PrometheusType::Gauge,
        String::new(),
        String::new(),
    )
    .with_samples(vec![gauge(&["web", "a"])])
    .unwrap();
    let relabelled = sorted.with_labels(vec![("env", "prod")]).unwrap();
    assert_eq!(relabelled.get_label_names(), &["app", "env", "zone"]);
}