prometheus_parser/
line.rs

1//! Parse a single line of Prometheus text format.
2
3use std::collections::BTreeMap;
4
5use nom::{
6    branch::alt,
7    bytes::complete::{is_not, tag, take_while, take_while1},
8    character::complete::{char, digit1},
9    combinator::{map, map_res, opt, recognize, value},
10    error::ParseError,
11    multi::fold_many0,
12    number::complete::double,
13    sequence::{delimited, pair, preceded},
14    Parser,
15};
16
17/// We try to catch all nom's `ErrorKind` with our own `ErrorKind`,
18/// to provide a meaningful error message.
19/// Parsers in this module should return this IResult instead of `nom::IResult`.
20type IResult<'a, O> = Result<(&'a str, O), nom::Err<ErrorKind>>;
21
22#[derive(Debug, snafu::Snafu, PartialEq, Eq)]
23pub enum ErrorKind {
24    #[snafu(display("invalid metric type, parsing: `{}`", input))]
25    InvalidMetricKind { input: String },
26    #[snafu(display("expected token {:?}, parsing: `{}`", expected, input))]
27    ExpectedToken {
28        expected: &'static str,
29        input: String,
30    },
31    #[snafu(display("expected blank space or tab, parsing: `{}`", input))]
32    ExpectedSpace { input: String },
33    #[snafu(display("expected token {:?}, parsing: `{}`", expected, input))]
34    ExpectedChar { expected: char, input: String },
35    #[snafu(display("name must start with [a-zA-Z_], parsing: `{}`", input))]
36    ParseNameError { input: String },
37    #[snafu(display("parse float value error, parsing: `{}`", input))]
38    ParseFloatError { input: String },
39    #[snafu(display("parse timestamp error, parsing: `{}`", input))]
40    ParseTimestampError { input: String },
41
42    // Error that we didn't catch
43    #[snafu(display("error kind: {:?}, parsing: `{}`", kind, input))]
44    Nom {
45        input: String,
46        kind: nom::error::ErrorKind,
47    },
48}
49
50impl From<ErrorKind> for nom::Err<ErrorKind> {
51    fn from(error: ErrorKind) -> Self {
52        nom::Err::Error(error)
53    }
54}
55
56impl From<nom::Err<ErrorKind>> for ErrorKind {
57    fn from(error: nom::Err<ErrorKind>) -> Self {
58        match error {
59            // this error only occurs when "streaming" nom is used.
60            nom::Err::Incomplete(_) => unreachable!(),
61            nom::Err::Error(e) | nom::Err::Failure(e) => e,
62        }
63    }
64}
65
66impl nom::error::ParseError<&str> for ErrorKind {
67    fn from_error_kind(input: &str, kind: nom::error::ErrorKind) -> Self {
68        ErrorKind::Nom {
69            input: input.to_owned(),
70            kind,
71        }
72    }
73
74    fn append(_: &str, _: nom::error::ErrorKind, other: Self) -> Self {
75        other
76    }
77}
78
79type NomErrorType<'a> = (&'a str, nom::error::ErrorKind);
80
81type NomError<'a> = nom::Err<NomErrorType<'a>>;
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum MetricKind {
85    Counter,
86    Gauge,
87    Histogram,
88    Summary,
89    Untyped,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Header {
94    pub metric_name: String,
95    pub kind: MetricKind,
96}
97
98#[derive(Debug, Clone, PartialEq)]
99pub struct Metric {
100    pub name: String,
101    pub labels: BTreeMap<String, String>,
102    pub value: f64,
103    pub timestamp: Option<i64>,
104}
105
106impl Metric {
107    /// Parse a single line with format
108    ///
109    /// ``` text
110    /// metric_name [
111    ///   "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}"
112    /// ] value [ timestamp ]
113    /// ```
114    ///
115    /// We don't parse timestamp.
116    fn parse(input: &str) -> IResult<Self> {
117        let input = trim_space(input);
118        let (input, name) = parse_name(input)?;
119        let (input, labels) = Self::parse_labels(input)?;
120        let (input, value) = Self::parse_value(input)?;
121        let (input, timestamp) = Self::parse_timestamp(input)?;
122        Ok((
123            input,
124            Metric {
125                name,
126                labels,
127                value,
128                timestamp,
129            },
130        ))
131    }
132
133    /// Float value, and +Inf, -Int, Nan.
134    pub(crate) fn parse_value(input: &str) -> IResult<f64> {
135        let input = trim_space(input);
136        alt((
137            value(f64::INFINITY, tag("+Inf")),
138            value(f64::NEG_INFINITY, tag("-Inf")),
139            value(f64::NAN, tag("Nan")),
140            // Note see https://github.com/Geal/nom/issues/1384
141            // This shouldn't be necessary if that issue is remedied.
142            value(f64::NAN, tag("NaN")),
143            double,
144        ))
145        .parse(input)
146        .map_err(|_: NomError| {
147            ErrorKind::ParseFloatError {
148                input: input.to_owned(),
149            }
150            .into()
151        })
152    }
153
154    fn parse_timestamp(input: &str) -> IResult<Option<i64>> {
155        let input = trim_space(input);
156        opt(map_res(
157            recognize(pair(opt(char('-')), digit1)),
158            |s: &str| s.parse(),
159        ))
160        .parse(input)
161        .map_err(|_: NomError| {
162            ErrorKind::ParseTimestampError {
163                input: input.to_owned(),
164            }
165            .into()
166        })
167    }
168
169    fn parse_name_value(input: &str) -> IResult<(String, String)> {
170        map(
171            (parse_name, match_char('='), Self::parse_escaped_string),
172            |(name, _, value)| (name, value),
173        )
174        .parse(input)
175    }
176
177    // Return:
178    // - Some((name, value)) => success
179    // - None => list is properly ended with "}"
180    // - Error => errors of parse_name_value
181    fn element_parser(input: &str) -> IResult<Option<(String, String)>> {
182        match Self::parse_name_value(input) {
183            Ok((input, result)) => Ok((input, Some(result))),
184            Err(nom::Err::Error(parse_name_value_error)) => match match_char('}')(input) {
185                Ok((input, _)) => Ok((input, None)),
186                Err(nom::Err::Error(_)) => Err(nom::Err::Error(parse_name_value_error)),
187                Err(failure) => Err(failure),
188            },
189            Err(failure) => Err(failure),
190        }
191    }
192
193    fn parse_labels_inner(mut input: &str) -> IResult<BTreeMap<String, String>> {
194        let sep = match_char(',');
195
196        let mut result = BTreeMap::new();
197        loop {
198            match Self::element_parser(input)? {
199                (inner_input, None) => {
200                    input = inner_input;
201                    break;
202                }
203                (inner_input, Some((name, value))) => {
204                    result.insert(name, value);
205
206                    // try matching ",", if doesn't match then
207                    // check if the list ended with "}".
208                    // If not ended then return error `expected token ','`.
209                    let inner_input = match sep(inner_input) {
210                        Ok((inner_input, _)) => inner_input,
211                        Err(sep_err) => match match_char('}')(inner_input) {
212                            Ok((inner_input, _)) => {
213                                input = inner_input;
214                                break;
215                            }
216                            Err(_) => return Err(sep_err),
217                        },
218                    };
219
220                    input = inner_input;
221                }
222            }
223        }
224        Ok((input, result))
225    }
226
227    /// Parse `{label_name="value",...}`
228    fn parse_labels(input: &str) -> IResult<BTreeMap<String, String>> {
229        let input = trim_space(input);
230
231        match opt(char('{')).parse(input) {
232            Ok((input, None)) => Ok((input, BTreeMap::new())),
233            Ok((input, Some(_))) => Self::parse_labels_inner(input),
234            Err(failure) => Err(failure),
235        }
236    }
237
238    /// Parse `'"' string_content '"'`. `string_content` can contain any unicode characters,
239    /// backslash (`\`), double-quote (`"`), and line feed (`\n`) characters have to be
240    /// escaped as `\\`, `\"`, and `\n`, respectively.
241    fn parse_escaped_string(input: &str) -> IResult<String> {
242        #[derive(Debug)]
243        enum StringFragment<'a> {
244            Literal(&'a str),
245            EscapedChar(char),
246        }
247
248        let parse_string_fragment = alt((
249            map(is_not("\"\\"), StringFragment::Literal),
250            map(
251                preceded(
252                    char('\\'),
253                    alt((
254                        value('\n', char('n')),
255                        value('"', char('"')),
256                        value('\\', char('\\')),
257                    )),
258                ),
259                StringFragment::EscapedChar,
260            ),
261        ));
262
263        let input = trim_space(input);
264
265        let build_string = fold_many0(
266            parse_string_fragment,
267            String::new,
268            |mut result, fragment| {
269                match fragment {
270                    StringFragment::Literal(s) => result.push_str(s),
271                    StringFragment::EscapedChar(c) => result.push(c),
272                }
273                result
274            },
275        );
276
277        fn match_quote(input: &str) -> IResult<char> {
278            char('"')(input).map_err(|_: NomError| {
279                ErrorKind::ExpectedChar {
280                    expected: '"',
281                    input: input.to_owned(),
282                }
283                .into()
284            })
285        }
286
287        delimited(match_quote, build_string, match_quote).parse(input)
288    }
289}
290
291impl Header {
292    fn space1(input: &str) -> IResult<()> {
293        take_while1(|c| c == ' ' || c == '\t')(input)
294            .map_err(|_: NomError| {
295                ErrorKind::ExpectedSpace {
296                    input: input.to_owned(),
297                }
298                .into()
299            })
300            .map(|(input, _)| (input, ()))
301    }
302
303    /// `# TYPE <metric_name> <metric_type>`
304    fn parse(input: &str) -> IResult<Self> {
305        let input = trim_space(input);
306        let (input, _) = char('#')(input).map_err(|_: NomError| ErrorKind::ExpectedChar {
307            expected: '#',
308            input: input.to_owned(),
309        })?;
310        let input = trim_space(input);
311        let (input, _) = tag("TYPE")(input).map_err(|_: NomError| ErrorKind::ExpectedToken {
312            expected: "TYPE",
313            input: input.to_owned(),
314        })?;
315        let (input, _) = Self::space1(input)?;
316        let (input, metric_name) = parse_name(input)?;
317        let (input, _) = Self::space1(input)?;
318        let (input, kind) = alt((
319            value(MetricKind::Counter, tag("counter")),
320            value(MetricKind::Gauge, tag("gauge")),
321            value(MetricKind::Summary, tag("summary")),
322            value(MetricKind::Histogram, tag("histogram")),
323            value(MetricKind::Untyped, tag("untyped")),
324        ))
325        .parse(input)
326        .map_err(|_: NomError| ErrorKind::InvalidMetricKind {
327            input: input.to_owned(),
328        })?;
329        Ok((input, Header { metric_name, kind }))
330    }
331}
332
333/// Each line of Prometheus text format.
334/// We discard empty lines, comments, and timestamps.
335#[derive(Debug, Clone, PartialEq)]
336pub enum Line {
337    Header(Header),
338    Metric(Metric),
339}
340
341impl Line {
342    /// Parse a single line. Return `None` if it is a comment or an empty line.
343    pub(crate) fn parse(input: &str) -> Result<Option<Self>, ErrorKind> {
344        let input = input.trim();
345        if input.is_empty() {
346            return Ok(None);
347        }
348
349        let metric_error = match Metric::parse(input) {
350            Ok((_, metric)) => {
351                return Ok(Some(Line::Metric(metric)));
352            }
353            Err(e) => e.into(),
354        };
355
356        let header_error = match Header::parse(input) {
357            Ok((_, header)) => {
358                return Ok(Some(Line::Header(header)));
359            }
360            Err(e) => e.into(),
361        };
362
363        if let Ok((input, _)) = char::<_, NomErrorType>('#')(input) {
364            if (sp, tag::<_, _, NomErrorType>("TYPE")).parse(input).is_ok() {
365                return Err(header_error);
366            }
367            Ok(None)
368        } else {
369            Err(metric_error)
370        }
371    }
372}
373
374/// Name matches the regex `[a-zA-Z_][a-zA-Z0-9_]*`.
375fn parse_name(input: &str) -> IResult<String> {
376    let input = trim_space(input);
377    let (input, (a, b)) = pair(
378        take_while1(|c: char| c.is_alphabetic() || c == '_'),
379        take_while(|c: char| c.is_alphanumeric() || c == '_' || c == ':'),
380    )
381    .parse(input)
382    .map_err(|_: NomError| ErrorKind::ParseNameError {
383        input: input.to_owned(),
384    })?;
385    Ok((input, a.to_owned() + b))
386}
387
388fn trim_space(input: &str) -> &str {
389    input.trim_start_matches([' ', '\t'])
390}
391
392fn sp<'a, E: ParseError<&'a str>>(i: &'a str) -> nom::IResult<&'a str, &'a str, E> {
393    take_while(|c| c == ' ' || c == '\t')(i)
394}
395
396fn match_char(c: char) -> impl Fn(&str) -> IResult<char> {
397    move |input| {
398        preceded(sp, char(c)).parse(input).map_err(|_: NomError| {
399            ErrorKind::ExpectedChar {
400                expected: c,
401                input: input.to_owned(),
402            }
403            .into()
404        })
405    }
406}
407
408#[cfg(test)]
409mod test {
410    use vector_common::btreemap;
411
412    use super::*;
413
414    #[test]
415    fn test_parse_escaped_string() {
416        fn wrap(s: &str) -> String {
417            format!("  \t \"{s}\"  .")
418        }
419
420        // parser should not consume more that it needed
421        let tail = "  .";
422
423        let input = wrap("");
424        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
425        assert_eq!(left, tail);
426        assert_eq!(r, "");
427
428        let input = wrap(r"a\\ asdf");
429        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
430        assert_eq!(left, tail);
431        assert_eq!(r, "a\\ asdf");
432
433        let input = wrap(r#"\"\""#);
434        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
435        assert_eq!(left, tail);
436        assert_eq!(r, "\"\"");
437
438        let input = wrap(r#"\"\\\n"#);
439        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
440        assert_eq!(left, tail);
441        assert_eq!(r, "\"\\\n");
442
443        let input = wrap(r"\\n");
444        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
445        assert_eq!(left, tail);
446        assert_eq!(r, "\\n");
447
448        let input = wrap(r#"  😂  "#);
449        let (left, r) = Metric::parse_escaped_string(&input).unwrap();
450        assert_eq!(left, tail);
451        assert_eq!(r, "  😂  ");
452    }
453
454    #[test]
455    fn test_parse_name() {
456        fn wrap(s: &str) -> String {
457            format!("  \t {s}  .")
458        }
459        let tail = "  .";
460
461        let input = wrap("abc_def");
462        let (left, r) = parse_name(&input).unwrap();
463        assert_eq!(left, tail);
464        assert_eq!(r, "abc_def");
465
466        let input = wrap("__9A0bc_def__");
467        let (left, r) = parse_name(&input).unwrap();
468        assert_eq!(left, tail);
469        assert_eq!(r, "__9A0bc_def__");
470
471        let input = wrap("99");
472        assert!(parse_name(&input).is_err());
473
474        let input = wrap("consul_serf_events_consul:new_leader");
475        let (left, r) = parse_name(&input).unwrap();
476        assert_eq!(left, tail);
477        assert_eq!(r, "consul_serf_events_consul:new_leader");
478    }
479
480    #[test]
481    fn test_parse_header() {
482        fn wrap(s: &str) -> String {
483            format!("  \t {s}  .")
484        }
485        let tail = "  .";
486
487        let input = wrap("#  TYPE abc_def counter");
488        let (left, r) = Header::parse(&input).unwrap();
489        assert_eq!(left, tail);
490        assert_eq!(
491            r,
492            Header {
493                metric_name: "abc_def".into(),
494                kind: MetricKind::Counter,
495            }
496        );
497
498        // We allow this case
499        let input = wrap("#  TYPE abc_def counteraaaaaaaaaaa");
500        let (_, r) = Header::parse(&input).unwrap();
501        assert_eq!(
502            r,
503            Header {
504                metric_name: "abc_def".into(),
505                kind: MetricKind::Counter,
506            }
507        );
508
509        let input = wrap("#TYPE \t abc_def \t gauge");
510        let (left, r) = Header::parse(&input).unwrap();
511        assert_eq!(left, tail);
512        assert_eq!(
513            r,
514            Header {
515                metric_name: "abc_def".into(),
516                kind: MetricKind::Gauge,
517            }
518        );
519
520        let input = wrap("# TYPE abc_def histogram");
521        let (left, r) = Header::parse(&input).unwrap();
522        assert_eq!(left, tail);
523        assert_eq!(
524            r,
525            Header {
526                metric_name: "abc_def".into(),
527                kind: MetricKind::Histogram,
528            }
529        );
530
531        let input = wrap("# TYPE abc_def summary");
532        let (left, r) = Header::parse(&input).unwrap();
533        assert_eq!(left, tail);
534        assert_eq!(
535            r,
536            Header {
537                metric_name: "abc_def".into(),
538                kind: MetricKind::Summary,
539            }
540        );
541
542        let input = wrap("# TYPE abc_def untyped");
543        let (left, r) = Header::parse(&input).unwrap();
544        assert_eq!(left, tail);
545        assert_eq!(
546            r,
547            Header {
548                metric_name: "abc_def".into(),
549                kind: MetricKind::Untyped,
550            }
551        );
552    }
553
554    #[test]
555    fn test_parse_value() {
556        fn wrap(s: &str) -> String {
557            format!("  \t {s}  .")
558        }
559        let tail = "  .";
560
561        let input = wrap("+Inf");
562        let (left, r) = Metric::parse_value(&input).unwrap();
563        assert_eq!(left, tail);
564        assert!(r.is_infinite() && r.is_sign_positive());
565
566        let input = wrap("-Inf");
567        let (left, r) = Metric::parse_value(&input).unwrap();
568        assert_eq!(left, tail);
569        assert!(r.is_infinite() && r.is_sign_negative());
570
571        let input = wrap("Nan");
572        let (left, r) = Metric::parse_value(&input).unwrap();
573        assert_eq!(left, tail);
574        assert!(r.is_nan());
575
576        let input = wrap(&(u64::MAX).to_string());
577        let (left, r) = Metric::parse_value(&input).unwrap();
578        assert_eq!(left, tail);
579        assert_eq!(r as u64, u64::MAX);
580
581        // This doesn't pass because we are parsing as f64 so we lose precision
582        // once the number is bigger than 2^53 (the mantissa for f64).
583        //
584        // let input = wrap(&(u64::MAX - 1).to_string());
585        // let (left, r) = Metric::parse_value(&input).unwrap();
586        // assert_eq!(left, tail);
587        // assert_eq!(r as u64, u64::MAX - 1);
588
589        // Precision is maintained up to 2^53
590        let input = wrap(&(2_u64.pow(53)).to_string());
591        let (left, r) = Metric::parse_value(&input).unwrap();
592        assert_eq!(left, tail);
593        assert_eq!(r as u64, 2_u64.pow(53));
594
595        let input = wrap(&(2_u64.pow(53) - 1).to_string());
596        let (left, r) = Metric::parse_value(&input).unwrap();
597        assert_eq!(left, tail);
598        assert_eq!(r as u64, 2_u64.pow(53) - 1);
599
600        let input = wrap(&(u32::MAX as u64 + 1).to_string());
601        let (left, r) = Metric::parse_value(&input).unwrap();
602        assert_eq!(left, tail);
603        assert_eq!(r as u64, u32::MAX as u64 + 1);
604
605        let tests = [
606            ("0", 0.0f64),
607            ("0.25", 0.25f64),
608            ("-10.25", -10.25f64),
609            ("-10e-25", -10e-25f64),
610            ("-10e+25", -10e+25f64),
611            ("2020", 2020.0f64),
612            ("1.", 1f64),
613        ];
614        for (text, value) in &tests {
615            let input = wrap(text);
616            let (left, r) = Metric::parse_value(&input).unwrap();
617            assert_eq!(left, tail);
618            assert!((r - *value).abs() < f64::EPSILON);
619        }
620    }
621
622    #[test]
623    fn test_parse_labels() {
624        fn wrap(s: &str) -> String {
625            format!("  \t {s}  .")
626        }
627        let tail = "  .";
628
629        let input = wrap("{}");
630        let (left, r) = Metric::parse_labels(&input).unwrap();
631        assert_eq!(left, tail);
632        assert_eq!(r, BTreeMap::new());
633
634        let input = wrap(r#"{name="value"}"#);
635        let (left, r) = Metric::parse_labels(&input).unwrap();
636        assert_eq!(left, tail);
637        assert_eq!(r, BTreeMap::from([("name".into(), "value".into())]));
638
639        let input = wrap(r#"{name="value",}"#);
640        let (left, r) = Metric::parse_labels(&input).unwrap();
641        assert_eq!(left, tail);
642        assert_eq!(r, BTreeMap::from([("name".into(), "value".into())]));
643
644        let input = wrap(r#"{ name = "" ,b="a=b" , a="},", _c = "\""}"#);
645        let (left, r) = Metric::parse_labels(&input).unwrap();
646        assert_eq!(
647            r,
648            btreemap! {"name" => "", "a" => "},", "b" => "a=b", "_c" => "\""}
649        );
650        assert_eq!(left, tail);
651
652        let input = wrap("100");
653        let (left, r) = Metric::parse_labels(&input).unwrap();
654        assert_eq!(left, "100".to_owned() + tail);
655        assert_eq!(r, BTreeMap::new());
656
657        // We don't allow these values
658
659        let input = wrap(r#"{name="value}"#);
660        let error = Metric::parse_labels(&input).unwrap_err().into();
661        assert!(matches!(
662            error,
663            ErrorKind::ExpectedChar { expected: '"', .. }
664        ));
665
666        let input = wrap(r#"{ a="b" c="d" }"#);
667        let error = Metric::parse_labels(&input).unwrap_err().into();
668        assert!(matches!(
669            error,
670            ErrorKind::ExpectedChar { expected: ',', .. }
671        ));
672
673        let input = wrap(r#"{ a="b" ,, c="d" }"#);
674        let error = Metric::parse_labels(&input).unwrap_err().into();
675        assert!(matches!(error, ErrorKind::ParseNameError { .. }));
676    }
677
678    #[test]
679    fn test_parse_timestamp() {
680        assert_eq!(Metric::parse_timestamp(""), Ok(("", None)));
681        assert_eq!(Metric::parse_timestamp("123"), Ok(("", Some(123))));
682        assert_eq!(Metric::parse_timestamp(" -23"), Ok(("", Some(-23))));
683        // Edge cases
684        assert_eq!(
685            Metric::parse_timestamp("9223372036854775807"),
686            Ok(("", Some(9223372036854775807i64)))
687        );
688        assert_eq!(
689            Metric::parse_timestamp("-9223372036854775808"),
690            Ok(("", Some(-9223372036854775808i64)))
691        );
692        // overflow
693        assert_eq!(
694            Metric::parse_timestamp("9223372036854775809"),
695            Ok(("9223372036854775809", None))
696        );
697        assert_eq!(
698            Metric::parse_timestamp("-9223372036854775809"),
699            Ok(("-9223372036854775809", None))
700        );
701    }
702
703    #[test]
704    fn test_parse_line() {
705        let input = r#"
706            # HELP http_requests_total The total number of HTTP requests.
707            # TYPE http_requests_total counter
708            http_requests_total{method="post",code="200"} 1027 1395066363000
709            http_requests_total{method="post",code="400"}    3 1395066363000
710
711            # Escaping in label values:
712            msdos_file_access_time_seconds{path="C:\\DIR\\FILE.TXT",error="Cannot find file:\n\"FILE.TXT\""} 1.458255915e9
713
714            # Minimalistic line:
715            metric_without_timestamp_and_labels 12.47
716
717            # A weird metric from before the epoch:
718            something_weird{problem="division by zero"} +Inf -3982045
719
720            # A histogram, which has a pretty complex representation in the text format:
721            # HELP http_request_duration_seconds A histogram of the request duration.
722            # TYPE http_request_duration_seconds histogram
723            http_request_duration_seconds_bucket{le="0.05"} 24054
724            http_request_duration_seconds_bucket{le="0.1"} 33444
725            http_request_duration_seconds_bucket{le="0.2"} 100392
726            http_request_duration_seconds_bucket{le="0.5"} 129389
727            http_request_duration_seconds_bucket{le="1"} 133988
728            http_request_duration_seconds_bucket{le="+Inf"} 144320
729            http_request_duration_seconds_sum 53423
730            http_request_duration_seconds_count 144320
731
732            # Finally a summary, which has a complex representation, too:
733            # HELP rpc_duration_seconds A summary of the RPC duration in seconds.
734            # TYPE rpc_duration_seconds summary
735            rpc_duration_seconds{quantile="0.01"} 3102
736            rpc_duration_seconds{quantile="0.05"} 3272
737            rpc_duration_seconds{quantile="0.5"} 4773
738            rpc_duration_seconds{quantile="0.9"} 9001
739            rpc_duration_seconds{quantile="0.99"} 76656
740            rpc_duration_seconds_sum 1.7560473e+07
741            rpc_duration_seconds_count 2693
742            "#;
743        assert!(input.lines().map(Line::parse).all(|r| r.is_ok()));
744    }
745}