zombienet_prom_metrics_parser/
lib.rs

1use std::{collections::HashMap, num::ParseFloatError};
2
3use pest::Parser;
4use pest_derive::Parser;
5
6/// An error at parsing level.
7#[derive(thiserror::Error, Debug)]
8pub enum ParserError {
9    #[error("error parsing input")]
10    ParseError(Box<pest::error::Error<Rule>>),
11    #[error("root node should be valid: {0}")]
12    ParseRootNodeError(String),
13    #[error("can't cast metric value as f64: {0}")]
14    CastValueError(#[from] ParseFloatError),
15}
16
17// This include forces recompiling this source file if the grammar file changes.
18// Uncomment it when doing changes to the .pest file
19const _GRAMMAR: &str = include_str!("grammar.pest");
20
21#[derive(Parser)]
22#[grammar = "grammar.pest"]
23pub struct MetricsParser;
24
25pub type MetricMap = HashMap<String, f64>;
26
27pub fn parse(input: &str) -> Result<MetricMap, ParserError> {
28    let mut metric_map: MetricMap = Default::default();
29    let mut pairs = MetricsParser::parse(Rule::statement, input)
30        .map_err(|e| ParserError::ParseError(Box::new(e)))?;
31
32    let root = pairs
33        .next()
34        .ok_or(ParserError::ParseRootNodeError(pairs.as_str().to_string()))?;
35    for token in root.into_inner() {
36        if token.as_rule() == Rule::block {
37            let inner = token.into_inner();
38            for value in inner {
39                match value.as_rule() {
40                    Rule::genericomment | Rule::typexpr | Rule::helpexpr => {
41                        // don't need to collect comments/types/helpers blocks.
42                        continue;
43                    },
44                    Rule::promstmt => {
45                        let mut key: &str = "";
46                        let mut labels: Vec<(&str, &str)> = Vec::new();
47                        let mut val: f64 = 0_f64;
48                        for v in value.clone().into_inner() {
49                            match &v.as_rule() {
50                                Rule::key => {
51                                    key = v.as_span().as_str();
52                                },
53                                Rule::NaN | Rule::posInf | Rule::negInf => {
54                                    // noop (not used in substrate metrics)
55                                },
56                                Rule::number => {
57                                    val = v.as_span().as_str().parse::<f64>()?;
58                                },
59                                Rule::labels => {
60                                    // SAFETY: use unwrap should be safe since we are just
61                                    // walking the parser struct and if are matching a label
62                                    // should have a key/vals
63                                    for p in v.into_inner() {
64                                        let mut inner = p.into_inner();
65                                        let key = inner.next().unwrap().as_span().as_str();
66                                        let value = inner
67                                            .next()
68                                            .unwrap()
69                                            .into_inner()
70                                            .next()
71                                            .unwrap()
72                                            .as_span()
73                                            .as_str();
74
75                                        labels.push((key, value));
76                                    }
77                                },
78                                _ => {
79                                    todo!("not implemented");
80                                },
81                            }
82                        }
83
84                        // we should store to make it compatible with zombienet v1:
85                        // key_without_prefix
86                        // key_without_prefix_and_without_chain
87                        // key_with_prefix_with_chain
88                        // key_with_prefix_and_without_chain
89                        let key_with_out_prefix =
90                            key.split('_').collect::<Vec<&str>>()[1..].join("_");
91                        let (labels_without_chain, labels_with_chain) =
92                            labels.iter().fold((vec![], vec![]), |mut acc, item| {
93                                if item.0.eq("chain") {
94                                    acc.1.push(format!("{}=\"{}\"", item.0, item.1));
95                                } else {
96                                    acc.0.push(format!("{}=\"{}\"", item.0, item.1));
97                                    acc.1.push(format!("{}=\"{}\"", item.0, item.1));
98                                }
99                                acc
100                            });
101
102                        let labels_with_chain_str = if labels_with_chain.is_empty() {
103                            String::from("")
104                        } else {
105                            format!("{{{}}}", labels_with_chain.join(","))
106                        };
107
108                        let labels_without_chain_str = if labels_without_chain.is_empty() {
109                            String::from("")
110                        } else {
111                            format!("{{{}}}", labels_without_chain.join(","))
112                        };
113
114                        metric_map.insert(format!("{key}{labels_without_chain_str}"), val);
115                        metric_map.insert(
116                            format!("{key_with_out_prefix}{labels_without_chain_str}"),
117                            val,
118                        );
119                        metric_map.insert(format!("{key}{labels_with_chain_str}"), val);
120                        metric_map
121                            .insert(format!("{key_with_out_prefix}{labels_with_chain_str}"), val);
122                    },
123                    _ => {},
124                }
125            }
126        }
127    }
128
129    Ok(metric_map)
130}
131
132#[cfg(test)]
133mod tests {
134    use std::fs;
135
136    use super::*;
137
138    #[test]
139    fn parse_metrics_works() {
140        let metrics_raw = fs::read_to_string("./testing/metrics.txt").unwrap();
141        let metrics = parse(&metrics_raw).unwrap();
142
143        // full key
144        assert_eq!(
145            metrics
146                .get("polkadot_node_is_active_validator{chain=\"rococo_local_testnet\"}")
147                .unwrap(),
148            &1_f64
149        );
150        // with prefix and no chain
151        assert_eq!(
152            metrics.get("polkadot_node_is_active_validator").unwrap(),
153            &1_f64
154        );
155        // no prefix with chain
156        assert_eq!(
157            metrics
158                .get("node_is_active_validator{chain=\"rococo_local_testnet\"}")
159                .unwrap(),
160            &1_f64
161        );
162        // no prefix without chain
163        assert_eq!(metrics.get("node_is_active_validator").unwrap(), &1_f64);
164    }
165
166    #[test]
167    fn parse_invalid_metrics_str_should_fail() {
168        let metrics_raw = r"
169        # HELP polkadot_node_is_active_validator Tracks if the validator is in the active set. Updates at session boundary.
170        # TYPE polkadot_node_is_active_validator gauge
171        polkadot_node_is_active_validator{chain=} 1
172        ";
173
174        let metrics = parse(metrics_raw);
175        assert!(metrics.is_err());
176        assert!(matches!(metrics, Err(ParserError::ParseError(_))));
177    }
178}