prometheus_wire/parser/
line_parser.rs

1use nom::{
2    branch::alt,
3    bytes::complete::{escaped, tag, take_while},
4    character::complete::{none_of, not_line_ending, one_of, space0},
5    combinator::{map, opt},
6    error::VerboseError,
7    multi::separated_list0,
8    number::complete::double as read_double,
9    sequence::{delimited, preceded, separated_pair, terminated, tuple},
10    // Err::{Error as NomError, Failure as NomFailure, Incomplete as NomIncomplete},
11    IResult,
12};
13
14use crate::parser::comment::Comment;
15use crate::parser::label::LabelList;
16use crate::parser::metric_data::SampleData;
17
18type NomRes<I, O> = IResult<I, O, VerboseError<I>>;
19
20// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
21fn is_metric_char(s: char) -> bool {
22    s.is_alphanumeric() || s == '_' || s == ':' || s == '.'
23}
24
25fn read_quoted_string(input: &str) -> NomRes<&str, String> {
26    let normal = none_of("\\\"");
27    let escapable = one_of("\"\\'n");
28    let escape_non_empty = escaped(normal, '\\', escapable);
29    let reduce_special_chars = |s: &str| s.replace("\\\\", "\\");
30    delimited(
31        tag("\""),
32        map(alt((escape_non_empty, tag(""))), reduce_special_chars),
33        tag("\""),
34    )(input)
35}
36
37fn read_variable_name(input: &str) -> NomRes<&str, &str> {
38    preceded(space0, take_while(is_metric_char))(input)
39}
40
41fn read_label(input: &str) -> NomRes<&str, LabelList> {
42    opt(delimited(
43        preceded(space0, tag("{")),
44        separated_list0(
45            preceded(space0, terminated(tag(","), space0)),
46            separated_pair(
47                read_variable_name,
48                preceded(space0, terminated(tag("="), space0)),
49                read_quoted_string,
50            ),
51        ),
52        preceded(space0, tag("}")),
53    ))(input)
54    .map(|(out, label)| (out, label.unwrap_or_default().into()))
55}
56
57fn read_value(input: &str) -> NomRes<&str, f64> {
58    preceded(
59        space0,
60        alt((
61            map(tag("+Inf"), |_| f64::INFINITY),
62            map(tag("-Inf"), |_| f64::NEG_INFINITY),
63            read_double,
64        )),
65    )(input)
66}
67
68fn read_timestamp(input: &str) -> NomRes<&str, Option<i64>> {
69    let read_timestamp_as_i64 = map(read_double, |f: f64| f as i64);
70    opt(preceded(space0, read_timestamp_as_i64))(input)
71}
72
73fn read_comment_line(input: &str) -> NomRes<&str, Comment> {
74    let comment_identifier = tuple((tag("#"), space0));
75    let known_comment_types = alt((tag("HELP"), tag("TYPE")));
76
77    tuple((
78        preceded(comment_identifier, known_comment_types),
79        preceded(space0, read_variable_name),
80        preceded(space0, not_line_ending),
81    ))(input)
82    .map(|(out, (c_type, metric, desc))| {
83        (out, Comment::new(metric.into(), c_type.into(), desc.into()))
84    })
85}
86
87// https://prometheus.io/docs/instrumenting/exposition_formats/#comments-help-text-and-type-information
88fn read_sample_line(input: &str) -> NomRes<&str, SampleData> {
89    tuple((read_variable_name, read_label, read_value, read_timestamp))(input).map(
90        |(out, (name, label, value, timestamp))| {
91            (out, SampleData::new(name.into(), label, value, timestamp))
92        },
93    )
94}
95/// Tries to parse a `&str` line as a sample and returns [`SampleData`]
96/// containg the metric name, labels and value if it succeeds.
97/// # Examples:
98/// ```
99/// use prometheus_wire::parser::{SampleData, LabelList, try_read_sample};
100/// use std::collections::HashMap;
101///
102/// let line = r#"http_requests_total{method="post",code="200"} 1.5e3 1395066363000"#;
103/// let opt_metric = try_read_sample(line);
104///
105/// let mut map = HashMap::new();
106/// map.insert(String::from("method"), String::from("post"));
107/// map.insert(String::from("code"), String::from("200"));
108///
109/// assert_eq!(
110///     opt_metric,
111///     Some(SampleData::new(
112///         String::from("http_requests_total"),
113///         LabelList::from_map(map),
114///         1500.0,
115///         Some(1395066363000))
116///     ));
117///
118/// let metric = opt_metric.unwrap();
119///
120/// assert_eq!(metric.labels.get_string("method"), Some(&String::from("post")));
121/// assert_eq!(metric.labels.get_number("code"), Some(200.0));
122///
123/// assert_eq!(try_read_sample("# test"), None);
124/// ```
125pub fn try_read_sample(line: &str) -> Option<SampleData> {
126    read_sample_line(line).ok().map(|(_, metric)| metric)
127}
128
129/// Tries to parse a `&str` line as a comment and returns [`Comment`] if it succeeds.
130///
131/// # Examples:
132/// ```
133/// use prometheus_wire::parser::{Comment, CommentType, try_read_comment};
134/// assert_eq!(
135///     try_read_comment("# HELP test1 this is a test"),
136///     Some(Comment::new(String::from("test1"), CommentType::HELP, String::from("this is a test")))
137/// );
138///
139/// assert_eq!(try_read_comment("metric 12345"), None);
140/// ```
141pub fn try_read_comment(line: &str) -> Option<Comment> {
142    read_comment_line(line).ok().map(|(_, comment)| comment)
143}
144
145#[cfg(test)]
146mod tests {
147    use crate::parser::comment::CommentType;
148    use crate::parser::line_parser::*;
149    use std::collections::HashMap;
150
151    #[test]
152    fn test_read_variable_name() {
153        assert_eq!(read_variable_name("alfa_123").unwrap(), ("", "alfa_123"));
154        assert_eq!(read_variable_name(" beta:456 ").unwrap(), (" ", "beta:456"));
155        assert_eq!(read_variable_name(" gama.789{").unwrap(), ("{", "gama.789"));
156    }
157
158    #[test]
159    fn test_read_quoted_string() {
160        read_quoted_string("").unwrap_err();
161        assert_eq!(read_quoted_string("\"\"").unwrap(), ("", "".into()));
162        assert_eq!(
163            read_quoted_string("\" alfa_123 \"").unwrap(),
164            ("", " alfa_123 ".into())
165        );
166        assert_eq!(
167            read_quoted_string("\"new\\nline\"").unwrap(),
168            ("", "new\\nline".into())
169        );
170        assert_eq!(
171            read_quoted_string("\" C:\\\\test\\\\ \"").unwrap(),
172            ("", " C:\\test\\ ".into())
173        );
174        assert_eq!(
175            read_quoted_string("\"beta:\\\"456\\\"\"").unwrap(),
176            ("", "beta:\\\"456\\\"".into())
177        );
178    }
179
180    #[test]
181    fn test_read_label() {
182        assert_eq!(read_label("").unwrap(), ("", LabelList::new()));
183        assert_eq!(read_label("{}").unwrap(), ("", LabelList::new()));
184        assert_eq!(read_label(" ").unwrap(), (" ", LabelList::new()));
185        assert_eq!(read_label(" {} ").unwrap(), (" ", LabelList::new()));
186
187        let mut h1 = HashMap::new();
188        h1.insert("alfa".into(), "1".into());
189
190        assert_eq!(
191            read_label("{alfa=\"1\"}").unwrap(),
192            ("", LabelList::from_map(h1.clone()))
193        );
194        assert_eq!(
195            read_label("{ alfa = \"1\" }").unwrap(),
196            ("", LabelList::from_map(h1.clone()))
197        );
198
199        let mut h2 = HashMap::new();
200        h2.insert("a_b:1".into(), "test\\\"1\\\"".into());
201        h2.insert("543_a.76".into(), "C:\\test\\".into());
202
203        let s = " { a_b:1 = \"test\\\"1\\\"\" , 543_a.76=\"C:\\\\test\\\\\"}";
204
205        assert_eq!(
206            read_label(s).unwrap(),
207            ("", LabelList::from_map(h2.clone()))
208        );
209
210        let s_no_spaces = s.replace(" ", "");
211        assert_eq!(
212            read_label(s_no_spaces.as_str()).unwrap(),
213            ("", LabelList::from_map(h2.clone()))
214        );
215
216        // doesn't work (yet) and should be tested again if some case is found
217        // assert_eq!(read_label("{ alfa = \"1\", }").unwrap(), ("", Label::fromMap(h1)));
218    }
219
220    #[test]
221    fn test_read_value() {
222        read_value("").unwrap_err();
223        read_value(" ").unwrap_err();
224        assert_eq!(read_value(" +154.0").unwrap(), ("", 154.0));
225        assert_eq!(read_value("-1500.0 ").unwrap(), (" ", -1500.0));
226        assert_eq!(read_value("1.5e-03 5").unwrap(), (" 5", 0.0015));
227        assert_eq!(read_value("+Inf ").unwrap(), (" ", f64::INFINITY));
228        assert_eq!(read_value("-1.7560473e+07").unwrap(), ("", -17560473.0));
229        assert_eq!(
230            read_value(" -Inf  1234").unwrap(),
231            ("  1234", f64::NEG_INFINITY)
232        );
233    }
234
235    #[test]
236    fn test_read_timestamp() {
237        assert_eq!(read_timestamp("").unwrap(), ("", None));
238        assert_eq!(read_timestamp(" 1").unwrap(), ("", Some(1)));
239        assert_eq!(read_timestamp("    ").unwrap(), ("    ", None));
240        assert_eq!(read_timestamp("123456789").unwrap(), ("", Some(123456789)));
241        assert_eq!(
242            read_timestamp("-987654321 5").unwrap(),
243            (" 5", Some(-987654321))
244        );
245    }
246
247    #[test]
248    fn test_read_comment_line() {
249        read_comment_line("# alfa").unwrap_err();
250        assert_eq!(
251            read_comment_line("# HELP").unwrap(),
252            ("", Comment::new("".into(), CommentType::HELP, "".into()))
253        );
254        assert_eq!(
255            read_comment_line("# HELP node_cpu_seconds_total Seconds the CPUs spent in each mode.")
256                .unwrap(),
257            (
258                "",
259                Comment::new(
260                    "node_cpu_seconds_total".into(),
261                    CommentType::HELP,
262                    "Seconds the CPUs spent in each mode.".into()
263                )
264            )
265        );
266        assert_eq!(
267            read_comment_line("#    TYPE     node_cpu_seconds_total counter").unwrap(),
268            (
269                "",
270                Comment::new(
271                    "node_cpu_seconds_total".into(),
272                    CommentType::TYPE,
273                    "counter".into()
274                )
275            )
276        );
277        assert_eq!(
278            read_comment_line("#    HELP     alfa").unwrap(),
279            (
280                "",
281                Comment::new("alfa".into(), CommentType::HELP, "".into())
282            )
283        );
284    }
285
286    #[test]
287    fn test_read_metric_line() {
288        let s = "something_weird{problem=\"division by zero\"} +Inf -3982045";
289
290        let mut h1 = HashMap::new();
291        h1.insert("problem".into(), "division by zero".into());
292        let l = LabelList::from_map(h1);
293
294        assert_eq!(
295            read_sample_line(s).unwrap(),
296            (
297                "",
298                SampleData::new(
299                    String::from("something_weird"),
300                    l,
301                    f64::INFINITY,
302                    Some(-3982045)
303                )
304            )
305        );
306
307        let s = "msdos_file_access_time_seconds{path=\"C:\\\\DIR\\\\FILE.TXT\",error=\"Cannot find file:\\n\\\"FILE.TXT\\\"\"} 1.458255915e9";
308
309        let mut h1 = HashMap::new();
310        h1.insert("path".into(), "C:\\DIR\\FILE.TXT".into());
311        h1.insert(
312            "error".into(),
313            "Cannot find file:\\n\\\"FILE.TXT\\\"".into(),
314        );
315        let l = LabelList::from_map(h1);
316        assert_eq!(
317            read_sample_line(s).unwrap(),
318            (
319                "",
320                SampleData::new(
321                    String::from("msdos_file_access_time_seconds"),
322                    l,
323                    1458255915.0,
324                    None
325                )
326            )
327        );
328    }
329}