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 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
20fn 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
87fn 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}
95pub fn try_read_sample(line: &str) -> Option<SampleData> {
126 read_sample_line(line).ok().map(|(_, metric)| metric)
127}
128
129pub 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 }
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}