Skip to main content

prometheus_exposition_format_rs/
samples.rs

1use crate::common::token_parser;
2#[cfg(test)]
3use assert_approx_eq::assert_approx_eq;
4use nom::branch::alt;
5use nom::bytes::complete::{is_not, tag};
6use nom::character::complete::{char, line_ending, none_of, space0, space1};
7use nom::combinator::{map, map_opt, map_res, opt, value};
8#[cfg(test)]
9use nom::error::ErrorKind;
10use nom::multi::{fold_many0, separated_list};
11use nom::sequence::{delimited, preceded, separated_pair, terminated, tuple};
12#[cfg(test)]
13use nom::Err::Error;
14use nom::IResult;
15use std::collections::HashMap;
16
17#[derive(Debug, PartialEq)]
18pub struct SampleEntry<'a> {
19    pub name: &'a str,
20    pub labels: HashMap<&'a str, String>,
21    pub value: f64,
22    pub timestamp_ms: Option<i64>,
23}
24
25fn timestamp_parser(i: &str) -> IResult<&str, i64> {
26    map_opt(is_not("\n "), |x: &str| x.parse::<i64>().ok())(i)
27}
28
29/// Parse a floating point value similar to [Go's strconv.ParseFloat](https://golang.org/pkg/strconv/#ParseFloat)
30/// It's all explained in the [Prometheus exposition format doc](https://prometheus.io/docs/instrumenting/exposition_formats/#comments-help-text-and-type-information)
31fn value_parser(i: &str) -> IResult<&str, f64> {
32    alt((
33        value(std::f64::NAN, tag("NaN")),
34        value(std::f64::INFINITY, tag("+Inf")),
35        value(std::f64::NEG_INFINITY, tag("-Inf")),
36        map_res(is_not("\n "), |x: &str| x.parse::<f64>()),
37    ))(i)
38}
39
40fn tag_value_parser(i: &str) -> IResult<&str, String> {
41    delimited(
42        char('\"'),
43        fold_many0(
44            alt((
45                preceded(
46                    char('\\'),
47                    alt((
48                        value('\n', char('n')),
49                        value('\"', char('\"')),
50                        value('\\', char('\\')),
51                    )),
52                ),
53                none_of("\n\"\\"),
54            )),
55            String::new(),
56            |mut acc, item| {
57                acc.push(item);
58                acc
59            },
60        ),
61        char('\"'),
62    )(i)
63}
64
65fn labels_parser(i: &str) -> IResult<&str, HashMap<&str, String>> {
66    let list_parser = terminated(
67        separated_list(
68            char(','),
69            separated_pair(token_parser, char('='), tag_value_parser),
70        ),
71        opt(char(',')),
72    );
73    let list_parser = map(
74        list_parser,
75        |l: Vec<(&str, String)>| -> HashMap<&str, String> { l.into_iter().collect() },
76    );
77
78    map(
79        opt(delimited(
80            preceded(space0, char('{')),
81            list_parser,
82            char('}'),
83        )),
84        |v| v.unwrap_or(HashMap::new()),
85    )(i)
86}
87
88/// Parse a metric sample according to the [exposition format](https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example).
89///
90/// # Arguments
91///
92/// `i` - A input string to parse
93///
94pub fn parse_sample(i: &str) -> IResult<&str, SampleEntry> {
95    let (input, (name, labels, value, timestamp_ms)) = terminated(
96        tuple((
97            token_parser,
98            labels_parser,
99            preceded(space1, value_parser),
100            opt(preceded(space1, timestamp_parser)),
101        )),
102        line_ending,
103    )(i)?;
104
105    Ok((
106        input,
107        SampleEntry {
108            name,
109            labels,
110            value,
111            timestamp_ms,
112        },
113    ))
114}
115
116#[test]
117fn test_timestamp_parser() {
118    assert_eq!(timestamp_parser(""), Err(Error(("", ErrorKind::IsNot))));
119    assert_eq!(
120        timestamp_parser("foobar"),
121        Err(Error(("foobar", ErrorKind::MapOpt)))
122    );
123    assert_eq!(timestamp_parser("1234"), Ok(("", 1234)));
124    assert_eq!(timestamp_parser("1234 foo"), Ok((" foo", 1234)));
125    assert_eq!(timestamp_parser("-1234 foo"), Ok((" foo", -1234)));
126}
127
128#[test]
129fn test_tag_value_parser() {
130    // Empty string
131    assert_eq!(tag_value_parser("\"\""), Ok(("", "".to_string())));
132    // Simple string
133    assert_eq!(tag_value_parser("\"abc\""), Ok(("", "abc".to_string())));
134    // Doesn't consume trailing
135    assert_eq!(tag_value_parser("\"abc\"aa"), Ok(("aa", "abc".to_string())));
136    // Unescapes escaped "
137    assert_eq!(tag_value_parser("\"\\\"\""), Ok(("", "\"".to_string())));
138    // Unescapes escaped line break
139    assert_eq!(tag_value_parser("\"\\n\""), Ok(("", "\n".to_string())));
140    // Unescapes escaped \
141    assert_eq!(tag_value_parser("\"\\\\\""), Ok(("", "\\".to_string())));
142    // Fails with unescaped line break
143    assert_eq!(
144        tag_value_parser("\"\n\""),
145        Err(Error(("\n\"", ErrorKind::Char)))
146    );
147    // Complex value from the doc
148    assert_eq!(
149        tag_value_parser("\"C:\\\\DIR\\\\FILE.TXT\""),
150        Ok(("", "C:\\DIR\\FILE.TXT".to_string()))
151    );
152    // Complex value from the doc
153    assert_eq!(
154        tag_value_parser("\"Cannot find file:\\n\\\"FILE.TXT\\\"\""),
155        Ok(("", "Cannot find file:\n\"FILE.TXT\"".to_string()))
156    );
157}
158
159#[cfg(test)]
160fn vec_to_hashmap<'a>(vec: Vec<(&'a str, &'a str)>) -> HashMap<&'a str, String> {
161    vec.into_iter().map(|(a, b)| (a, b.to_string())).collect()
162}
163
164#[test]
165fn test_labels_parser() {
166    let assert_labels = |s, vec: Vec<(&str, &str)>| {
167        assert_eq!(labels_parser(s), Ok(("", vec_to_hashmap(vec))));
168    };
169    // Empty space doesn't consume
170    assert_eq!(labels_parser(" "), Ok((" ", HashMap::new())));
171
172    // Empty labels with prefixed space
173    assert_eq!(labels_parser(" {}"), Ok(("", HashMap::new())));
174    // Empty labels
175    assert_eq!(labels_parser("{}"), Ok(("", HashMap::new())));
176    // Empty string
177    assert_eq!(labels_parser(""), Ok(("", HashMap::new())));
178    // Prefixed
179    assert_eq!(labels_parser("d{}"), Ok(("d{}", HashMap::new())));
180    // No quotes on label
181    assert_eq!(labels_parser("{he=e}"), Ok(("{he=e}", HashMap::new())));
182    // A simple label
183    assert_labels("{hello=\"how are you?\"}", vec![("hello", "how are you?")]);
184    // Multiple labels
185    assert_labels("{a=\"b\",c=\"d\"}", vec![("a", "b"), ("c", "d")]);
186    // When there's a trailing comma
187    assert_labels("{a=\"b\",c=\"d\",}", vec![("a", "b"), ("c", "d")]);
188}
189
190#[test]
191fn test_value_parser() {
192    assert_eq!(value_parser("1027"), Ok(("", 1027f64)));
193    assert_eq!(value_parser("1027 ee"), Ok((" ee", 1027f64)));
194    assert_eq!(value_parser("1027\nee"), Ok(("\nee", 1027f64)));
195    assert_eq!(value_parser("ee"), Err(Error(("ee", ErrorKind::MapRes))));
196    assert_eq!(value_parser("+Inf"), Ok(("", std::f64::INFINITY)));
197    assert_eq!(value_parser("-Inf"), Ok(("", std::f64::NEG_INFINITY)));
198    assert!(value_parser("NaN").unwrap().1.is_nan());
199    assert_approx_eq!(value_parser("2.00").unwrap().1, 2f64);
200    assert_approx_eq!(value_parser("1e-3").unwrap().1, 0.001);
201    assert_approx_eq!(value_parser("123.3412312312").unwrap().1, 123.3412312312);
202    assert_approx_eq!(value_parser("1.458255915e9").unwrap().1, 1.458255915e9);
203}
204
205#[cfg(test)]
206fn assert_sample(
207    res: SampleEntry,
208    name: &str,
209    labels: Vec<(&str, &str)>,
210    value: f64,
211    timestamp: Option<i64>,
212) {
213    assert_eq!(res.name, name, "sample name is different {:?}", res);
214    assert_eq!(
215        res.labels,
216        vec_to_hashmap(labels),
217        "labels are different {:?}",
218        res
219    );
220
221    // Ensure we have similar floats considering the extremes and a good epsilon
222    assert!(
223        res.value.is_sign_positive() == value.is_sign_positive()
224            && res.value.is_infinite() == value.is_infinite()
225            && res.value.is_nan() == res.value.is_nan(),
226        "float non similar actual:{} expected:{}",
227        res.value,
228        value
229    );
230    if value.is_finite() {
231        assert_approx_eq!(res.value, value);
232    }
233    assert_eq!(res.timestamp_ms, timestamp, "Timestamps differ {:?}", res);
234}
235
236#[cfg(test)]
237fn assert_sample_parser(
238    s: &str,
239    left: &str,
240    name: &str,
241    labels: Vec<(&str, &str)>,
242    value: f64,
243    timestamp: Option<i64>,
244) {
245    let res = parse_sample(s);
246    assert!(res.is_ok(), "input: {} res: {:?}", s, res);
247    let res = res.unwrap();
248    assert_eq!(res.0, left, "Not the same left string");
249    assert_sample(res.1, name, labels, value, timestamp);
250}
251
252#[test]
253fn test_parse_sample_parser() {
254    // Examples from the doc https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example
255    assert_sample_parser(
256        "http_requests_total{method=\"post\",code=\"200\"} 1027 1395066363000\n",
257        "",
258        "http_requests_total",
259        vec![("method", "post"), ("code", "200")],
260        1027f64,
261        Option::Some(1395066363000i64),
262    );
263    assert_sample_parser(
264        "http_requests_total{method=\"post\",code=\"400\"}    3 1395066363000\n",
265        "",
266        "http_requests_total",
267        vec![("method", "post"), ("code", "400")],
268        3f64,
269        Option::Some(1395066363000i64),
270    );
271    assert_sample_parser("msdos_file_access_time_seconds{path=\"C:\\\\DIR\\\\FILE.TXT\",error=\"Cannot find file:\\n\\\"FILE.TXT\\\"\"} 1.458255915e9\n", "",
272                         "msdos_file_access_time_seconds", vec![("path", "C:\\DIR\\FILE.TXT"), ("error", "Cannot find file:\n\"FILE.TXT\"")], 1.458255915e9, None);
273    assert_sample_parser(
274        "metric_without_timestamp_and_labels 12.47\n",
275        "",
276        "metric_without_timestamp_and_labels",
277        vec![],
278        12.47,
279        None,
280    );
281    assert_sample_parser(
282        "something_weird{problem=\"division by zero\"} +Inf -3982045\n",
283        "",
284        "something_weird",
285        vec![("problem", "division by zero")],
286        std::f64::INFINITY,
287        Some(-3982045),
288    );
289    assert_sample_parser(
290        "http_request_duration_seconds_bucket{le=\"0.05\"} 24054\n",
291        "",
292        "http_request_duration_seconds_bucket",
293        vec![("le", "0.05")],
294        24054f64,
295        None,
296    );
297    assert_sample_parser(
298        "http_request_duration_seconds_bucket{le=\"0.1\"} 33444\n",
299        "",
300        "http_request_duration_seconds_bucket",
301        vec![("le", "0.1")],
302        33444f64,
303        None,
304    );
305    assert_sample_parser(
306        "http_request_duration_seconds_bucket{le=\"0.2\"} 100392\n",
307        "",
308        "http_request_duration_seconds_bucket",
309        vec![("le", "0.2")],
310        100392f64,
311        None,
312    );
313    assert_sample_parser(
314        "http_request_duration_seconds_bucket{le=\"0.5\"} 129389\n",
315        "",
316        "http_request_duration_seconds_bucket",
317        vec![("le", "0.5")],
318        129389f64,
319        None,
320    );
321    assert_sample_parser(
322        "http_request_duration_seconds_bucket{le=\"1\"} 133988\n",
323        "",
324        "http_request_duration_seconds_bucket",
325        vec![("le", "1")],
326        133988f64,
327        None,
328    );
329    assert_sample_parser(
330        "http_request_duration_seconds_bucket{le=\"+Inf\"} 144320\n",
331        "",
332        "http_request_duration_seconds_bucket",
333        vec![("le", "+Inf")],
334        144320f64,
335        None,
336    );
337    assert_sample_parser(
338        "http_request_duration_seconds_sum 53423\n",
339        "",
340        "http_request_duration_seconds_sum",
341        vec![],
342        53423f64,
343        None,
344    );
345    assert_sample_parser(
346        "http_request_duration_seconds_count 144320\n",
347        "",
348        "http_request_duration_seconds_count",
349        vec![],
350        144320f64,
351        None,
352    );
353    assert_sample_parser(
354        "rpc_duration_seconds{quantile=\"0.01\"} 3102\n",
355        "",
356        "rpc_duration_seconds",
357        vec![("quantile", "0.01")],
358        3102f64,
359        None,
360    );
361    assert_sample_parser(
362        "rpc_duration_seconds{quantile=\"0.05\"} 3272\n",
363        "",
364        "rpc_duration_seconds",
365        vec![("quantile", "0.05")],
366        3272f64,
367        None,
368    );
369    assert_sample_parser(
370        "rpc_duration_seconds{quantile=\"0.5\"} 4773\n",
371        "",
372        "rpc_duration_seconds",
373        vec![("quantile", "0.5")],
374        4773f64,
375        None,
376    );
377    assert_sample_parser(
378        "rpc_duration_seconds{quantile=\"0.9\"} 9001\n",
379        "",
380        "rpc_duration_seconds",
381        vec![("quantile", "0.9")],
382        9001f64,
383        None,
384    );
385    assert_sample_parser(
386        "rpc_duration_seconds{quantile=\"0.99\"} 76656\n",
387        "",
388        "rpc_duration_seconds",
389        vec![("quantile", "0.99")],
390        76656f64,
391        None,
392    );
393    assert_sample_parser(
394        "rpc_duration_seconds_sum 1.7560473e+07\n",
395        "",
396        "rpc_duration_seconds_sum",
397        vec![],
398        1.7560473e+07,
399        None,
400    );
401    assert_sample_parser(
402        "rpc_duration_seconds_count 2693\n",
403        "",
404        "rpc_duration_seconds_count",
405        vec![],
406        2693f64,
407        None,
408    );
409
410    // With trailing characters
411    assert_sample_parser(
412        "rpc_duration_seconds_count 2693\nfoo",
413        "foo",
414        "rpc_duration_seconds_count",
415        vec![],
416        2693f64,
417        None,
418    );
419
420    // With space before labels
421    assert_sample_parser(
422        "test {a=\"b\"} 0\n",
423        "",
424        "test",
425        vec![("a", "b")],
426        0f64,
427        None,
428    );
429
430    // Fails when there's just a metric name
431    assert_eq!(
432        parse_sample("metric_without_timestamp_and_labels\n"),
433        Err(Error(("\n", ErrorKind::Space)))
434    );
435    // Fails when no space
436    assert_eq!(
437        parse_sample("metric_without_timestamp_and_labels1234\n"),
438        Err(Error(("\n", ErrorKind::Space)))
439    );
440    // Fails when no line break
441    assert_eq!(
442        parse_sample("metric_without_timestamp_and_labels 1234"),
443        Err(Error(("", ErrorKind::CrLf)))
444    );
445}