metrics_exporter_plotly/
pattern.rs

1use regex::Regex;
2use std::collections::HashMap;
3
4#[derive(Debug)]
5pub struct PatternGroup {
6    patterns: Vec<(Regex, PlotKind)>,
7}
8
9impl Default for PatternGroup {
10    fn default() -> Self {
11        Self::new()
12    }
13}
14
15impl PatternGroup {
16    pub fn new() -> Self {
17        Self { patterns: vec![] }
18    }
19
20    /// Add a pattern to match for plotting
21    ///
22    /// Takes in a regex pattern with a named capture group which will be used to correlate
23    /// metrics.
24    ///
25    /// # Example
26    ///
27    /// The following will match metrics of the form `*_success` and `*_error`, and group them
28    /// together into plots next to each other.
29    ///
30    /// ```rust,no_run
31    /// PatternGroup::new()
32    ///     .pattern(r"(?<transaction>.*)_success", PlotKind::Rate)
33    ///     .pattern(r"(?<transaction>.*)_error", PlotKind::Rate)
34    /// ```
35    ///
36    /// For instance, `foo_success` and `foo_error` will be grouped, and `bar_success` and
37    /// `bar_error` will be grouped together.
38    ///
39    /// # Panics
40    /// Panics on an invalid Regex
41    pub fn pattern(mut self, regex: &str, kind: PlotKind) -> Self {
42        let regex = Regex::new(regex).unwrap();
43        self.patterns.push((regex, kind));
44        self
45    }
46
47    // TODO: Handle multiple captures
48    pub(crate) fn apply(&self, metrics: &[&str]) -> Vec<Vec<(String, PlotKind)>> {
49        let mut m: HashMap<_, Vec<(String, PlotKind)>> = HashMap::new();
50
51        for metric in metrics {
52            for (re, plot_kind) in &self.patterns {
53                let Some(caps) = re.captures(metric) else {
54                    continue;
55                };
56
57                let cap = caps
58                    .iter()
59                    .skip(1)
60                    .map(|c| c.unwrap().as_str())
61                    .take(1)
62                    .collect::<Vec<_>>();
63                let [cap] = cap[..] else { continue };
64
65                if let Some(v) = m.get_mut(cap) {
66                    v.push((metric.to_string(), *plot_kind));
67                } else {
68                    m.insert(cap, vec![(metric.to_string(), *plot_kind)]);
69                }
70            }
71        }
72
73        m.drain().map(|(_, group)| group).collect()
74    }
75}
76
77#[derive(Debug, Copy, Clone, PartialEq)]
78pub enum PlotKind {
79    Line,
80    Rate,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_finds_patterns() {
89        let group = PatternGroup::new()
90            .pattern(r"(?<scenario>.*)_success", PlotKind::Rate)
91            .pattern(r"(?<scenario>.*)_error", PlotKind::Line);
92
93        let metrics = vec![
94            "foo_success",
95            "bar_success",
96            "foo_error",
97            "bar_error",
98            "baz_drror_",
99        ];
100
101        let groups = group.apply(&metrics);
102
103        assert!(groups.contains(&vec![
104            ("foo_success".to_string(), PlotKind::Rate),
105            ("foo_error".to_string(), PlotKind::Line)
106        ]));
107
108        assert!(groups.contains(&vec![
109            ("bar_success".to_string(), PlotKind::Rate),
110            ("bar_error".to_string(), PlotKind::Line)
111        ]));
112    }
113}