vector_core/metrics/
metric_matcher.rs

1use std::time::Duration;
2
3use metrics::Key;
4use regex::Regex;
5
6use super::recency::KeyMatcher;
7use crate::config::metrics_expiration::{
8    MetricLabelMatcher, MetricLabelMatcherConfig, MetricNameMatcherConfig, PerMetricSetExpiration,
9};
10
11pub(super) struct MetricKeyMatcher {
12    name: Option<MetricNameMatcher>,
13    labels: Option<LabelsMatcher>,
14}
15
16impl KeyMatcher<Key> for MetricKeyMatcher {
17    fn matches(&self, key: &Key) -> bool {
18        let name_match = self.name.as_ref().is_none_or(|m| m.matches(key));
19        let labels_match = self.labels.as_ref().is_none_or(|l| l.matches(key));
20        name_match && labels_match
21    }
22}
23
24impl TryFrom<PerMetricSetExpiration> for MetricKeyMatcher {
25    type Error = super::Error;
26
27    fn try_from(value: PerMetricSetExpiration) -> Result<Self, Self::Error> {
28        Ok(Self {
29            name: value.name.map(TryInto::try_into).transpose()?,
30            labels: value.labels.map(TryInto::try_into).transpose()?,
31        })
32    }
33}
34
35impl TryFrom<PerMetricSetExpiration> for (MetricKeyMatcher, Duration) {
36    type Error = super::Error;
37
38    fn try_from(value: PerMetricSetExpiration) -> Result<Self, Self::Error> {
39        if value.expire_secs <= 0.0 {
40            return Err(super::Error::TimeoutMustBePositive {
41                timeout: value.expire_secs,
42            });
43        }
44        let duration = Duration::from_secs_f64(value.expire_secs);
45        Ok((value.try_into()?, duration))
46    }
47}
48
49enum MetricNameMatcher {
50    Exact(String),
51    Regex(Regex),
52}
53
54impl KeyMatcher<Key> for MetricNameMatcher {
55    fn matches(&self, key: &Key) -> bool {
56        match self {
57            MetricNameMatcher::Exact(name) => key.name() == name,
58            MetricNameMatcher::Regex(regex) => regex.is_match(key.name()),
59        }
60    }
61}
62
63impl TryFrom<MetricNameMatcherConfig> for MetricNameMatcher {
64    type Error = super::Error;
65
66    fn try_from(value: MetricNameMatcherConfig) -> Result<Self, Self::Error> {
67        Ok(match value {
68            MetricNameMatcherConfig::Exact { value } => MetricNameMatcher::Exact(value),
69            MetricNameMatcherConfig::Regex { pattern } => MetricNameMatcher::Regex(
70                Regex::new(&pattern).map_err(|_| super::Error::InvalidRegexPattern { pattern })?,
71            ),
72        })
73    }
74}
75
76enum LabelsMatcher {
77    Any(Vec<LabelsMatcher>),
78    All(Vec<LabelsMatcher>),
79    Exact(String, String),
80    Regex(String, Regex),
81}
82
83impl KeyMatcher<Key> for LabelsMatcher {
84    fn matches(&self, key: &Key) -> bool {
85        match self {
86            LabelsMatcher::Any(vec) => vec.iter().any(|m| m.matches(key)),
87            LabelsMatcher::All(vec) => vec.iter().all(|m| m.matches(key)),
88            LabelsMatcher::Exact(label_key, label_value) => key
89                .labels()
90                .any(|l| l.key() == label_key && l.value() == label_value),
91            LabelsMatcher::Regex(label_key, regex) => key
92                .labels()
93                .any(|l| l.key() == label_key && regex.is_match(l.value())),
94        }
95    }
96}
97
98impl TryFrom<MetricLabelMatcher> for LabelsMatcher {
99    type Error = super::Error;
100
101    fn try_from(value: MetricLabelMatcher) -> Result<Self, Self::Error> {
102        Ok(match value {
103            MetricLabelMatcher::Exact { key, value } => Self::Exact(key, value),
104            MetricLabelMatcher::Regex { key, value_pattern } => Self::Regex(
105                key,
106                Regex::new(&value_pattern).map_err(|_| super::Error::InvalidRegexPattern {
107                    pattern: value_pattern,
108                })?,
109            ),
110        })
111    }
112}
113
114impl TryFrom<MetricLabelMatcherConfig> for LabelsMatcher {
115    type Error = super::Error;
116
117    fn try_from(value: MetricLabelMatcherConfig) -> Result<Self, Self::Error> {
118        Ok(match value {
119            MetricLabelMatcherConfig::Any { matchers } => Self::Any(
120                matchers
121                    .into_iter()
122                    .map(TryInto::<LabelsMatcher>::try_into)
123                    .collect::<Result<Vec<_>, _>>()?,
124            ),
125            MetricLabelMatcherConfig::All { matchers } => Self::All(
126                matchers
127                    .into_iter()
128                    .map(TryInto::<LabelsMatcher>::try_into)
129                    .collect::<Result<Vec<_>, _>>()?,
130            ),
131        })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use metrics::Label;
138    use vrl::prelude::indoc;
139
140    use super::*;
141
142    const EMPTY: MetricKeyMatcher = MetricKeyMatcher {
143        name: None,
144        labels: None,
145    };
146
147    #[test]
148    fn empty_matcher_should_match_all() {
149        assert!(EMPTY.matches(&Key::from_name("test_name")));
150        assert!(EMPTY.matches(&Key::from_parts(
151            "another",
152            [Label::new("test_key", "test_value")].iter()
153        )));
154    }
155
156    #[test]
157    fn name_matcher_should_ignore_labels() {
158        let matcher = MetricKeyMatcher {
159            name: Some(MetricNameMatcher::Exact("test_metric".to_string())),
160            labels: None,
161        };
162
163        assert!(matcher.matches(&Key::from_name("test_metric")));
164        assert!(matcher.matches(&Key::from_parts(
165            "test_metric",
166            [Label::new("test_key", "test_value")].iter()
167        )));
168        assert!(!matcher.matches(&Key::from_name("different_name")));
169        assert!(!matcher.matches(&Key::from_parts(
170            "different_name",
171            [Label::new("test_key", "test_value")].iter()
172        )));
173    }
174
175    #[test]
176    fn exact_name_matcher_should_check_name() {
177        let matcher = MetricKeyMatcher {
178            name: Some(MetricNameMatcher::Exact("test_metric".to_string())),
179            labels: None,
180        };
181
182        assert!(matcher.matches(&Key::from_name("test_metric")));
183        assert!(!matcher.matches(&Key::from_name("different_name")));
184        assert!(!matcher.matches(&Key::from_name("_test_metric")));
185        assert!(!matcher.matches(&Key::from_name("test_metric123")));
186    }
187
188    #[test]
189    fn regex_name_matcher_should_try_matching_the_name() {
190        let matcher = MetricKeyMatcher {
191            name: Some(MetricNameMatcher::Regex(
192                Regex::new(r".*test_?metric.*").unwrap(),
193            )),
194            labels: None,
195        };
196
197        assert!(matcher.matches(&Key::from_name("test_metric")));
198        assert!(!matcher.matches(&Key::from_name("different_name")));
199        assert!(matcher.matches(&Key::from_name("_test_metric")));
200        assert!(matcher.matches(&Key::from_name("test_metric123")));
201        assert!(matcher.matches(&Key::from_name("__testmetric123")));
202    }
203
204    #[test]
205    fn exact_label_matcher_should_look_for_exact_label_match() {
206        let matcher = MetricKeyMatcher {
207            name: None,
208            labels: Some(LabelsMatcher::Exact(
209                "test_key".to_string(),
210                "test_value".to_string(),
211            )),
212        };
213
214        assert!(!matcher.matches(&Key::from_name("test_metric")));
215        assert!(matcher.matches(&Key::from_parts(
216            "test_metric",
217            [Label::new("test_key", "test_value")].iter()
218        )));
219        assert!(!matcher.matches(&Key::from_name("different_name")));
220        assert!(matcher.matches(&Key::from_parts(
221            "different_name",
222            [Label::new("test_key", "test_value")].iter()
223        )));
224    }
225
226    #[test]
227    fn regex_label_matcher_should_look_for_exact_label_match() {
228        let matcher = MetricKeyMatcher {
229            name: None,
230            labels: Some(LabelsMatcher::Regex(
231                "test_key".to_string(),
232                Regex::new(r"metric_val.*").unwrap(),
233            )),
234        };
235
236        assert!(!matcher.matches(&Key::from_name("test_metric")));
237        assert!(matcher.matches(&Key::from_parts(
238            "test_metric",
239            [Label::new("test_key", "metric_value123")].iter()
240        )));
241        assert!(!matcher.matches(&Key::from_parts(
242            "test_metric",
243            [Label::new("test_key", "test_value123")].iter()
244        )));
245        assert!(matcher.matches(&Key::from_parts(
246            "different_name",
247            [Label::new("test_key", "metric_val0")].iter()
248        )));
249    }
250
251    #[test]
252    fn any_label_matcher_should_look_for_at_least_one_match() {
253        let matcher = MetricKeyMatcher {
254            name: None,
255            labels: Some(LabelsMatcher::Any(vec![
256                LabelsMatcher::Regex("test_key".to_string(), Regex::new(r"metric_val.*").unwrap()),
257                LabelsMatcher::Exact("test_key".to_string(), "test_value".to_string()),
258            ])),
259        };
260
261        assert!(!matcher.matches(&Key::from_name("test_metric")));
262        assert!(matcher.matches(&Key::from_parts(
263            "test_metric",
264            [Label::new("test_key", "metric_value123")].iter()
265        )));
266        assert!(matcher.matches(&Key::from_parts(
267            "test_metric",
268            [Label::new("test_key", "test_value")].iter()
269        )));
270        assert!(matcher.matches(&Key::from_parts(
271            "different_name",
272            [Label::new("test_key", "metric_val0")].iter()
273        )));
274        assert!(!matcher.matches(&Key::from_parts(
275            "different_name",
276            [Label::new("test_key", "different_value")].iter()
277        )));
278    }
279
280    #[test]
281    fn all_label_matcher_should_expect_all_matches() {
282        let matcher = MetricKeyMatcher {
283            name: None,
284            labels: Some(LabelsMatcher::All(vec![
285                LabelsMatcher::Regex("key_one".to_string(), Regex::new(r"metric_val.*").unwrap()),
286                LabelsMatcher::Exact("key_two".to_string(), "test_value".to_string()),
287            ])),
288        };
289
290        assert!(!matcher.matches(&Key::from_name("test_metric")));
291        assert!(!matcher.matches(&Key::from_parts(
292            "test_metric",
293            [Label::new("key_one", "metric_value123")].iter()
294        )));
295        assert!(!matcher.matches(&Key::from_parts(
296            "test_metric",
297            [Label::new("key_two", "test_value")].iter()
298        )));
299        assert!(
300            matcher.matches(&Key::from_parts(
301                "different_name",
302                [
303                    Label::new("key_one", "metric_value_1234"),
304                    Label::new("key_two", "test_value")
305                ]
306                .iter()
307            ))
308        );
309    }
310
311    #[test]
312    fn matcher_with_both_name_and_label_should_expect_both_to_match() {
313        let matcher = MetricKeyMatcher {
314            name: Some(MetricNameMatcher::Exact("test_metric".to_string())),
315            labels: Some(LabelsMatcher::Exact(
316                "test_key".to_string(),
317                "test_value".to_string(),
318            )),
319        };
320
321        assert!(!matcher.matches(&Key::from_name("test_metric")));
322        assert!(!matcher.matches(&Key::from_name("different_name")));
323        assert!(!matcher.matches(&Key::from_parts(
324            "different_name",
325            [Label::new("test_key", "test_value")].iter()
326        )));
327        assert!(matcher.matches(&Key::from_parts(
328            "test_metric",
329            [Label::new("test_key", "test_value")].iter()
330        )));
331    }
332
333    #[test]
334    fn complex_matcher_rules() {
335        let matcher = MetricKeyMatcher {
336            name: Some(MetricNameMatcher::Regex(Regex::new(r"custom_.*").unwrap())),
337            labels: Some(LabelsMatcher::All(vec![
338                // Let's match just sink metrics
339                LabelsMatcher::Exact("component_kind".to_string(), "sink".to_string()),
340                // And only AWS components
341                LabelsMatcher::Regex("component_type".to_string(), Regex::new(r"aws_.*").unwrap()),
342                // And some more rules
343                LabelsMatcher::Any(vec![
344                    LabelsMatcher::Exact("region".to_string(), "some_aws_region_name".to_string()),
345                    LabelsMatcher::Regex(
346                        "endpoint".to_string(),
347                        Regex::new(r"test.com.*").unwrap(),
348                    ),
349                ]),
350            ])),
351        };
352
353        assert!(!matcher.matches(&Key::from_name("test_metric")));
354        assert!(!matcher.matches(&Key::from_name("custom_metric_a")));
355        assert!(!matcher.matches(&Key::from_parts(
356            "custom_metric_with_missing_component_type",
357            [Label::new("component_kind", "sink")].iter()
358        )));
359        assert!(
360            !matcher.matches(&Key::from_parts(
361                "custom_metric_with_missing_extra_labels",
362                [
363                    Label::new("component_kind", "sink"),
364                    Label::new("component_type", "aws_cloudwatch_metrics")
365                ]
366                .iter()
367            ))
368        );
369        assert!(
370            !matcher.matches(&Key::from_parts(
371                "custom_metric_with_wrong_region",
372                [
373                    Label::new("component_kind", "sink"),
374                    Label::new("component_type", "aws_cloudwatch_metrics"),
375                    Label::new("region", "some_other_region")
376                ]
377                .iter()
378            ))
379        );
380        assert!(
381            !matcher.matches(&Key::from_parts(
382                "custom_metric_with_wrong_region_and_endpoint",
383                [
384                    Label::new("component_kind", "sink"),
385                    Label::new("component_type", "aws_cloudwatch_metrics"),
386                    Label::new("region", "some_other_region"),
387                    Label::new("endpoint", "wrong_endpoint.com/metrics")
388                ]
389                .iter()
390            ))
391        );
392        assert!(
393            matcher.matches(&Key::from_parts(
394                "custom_metric_with_wrong_endpoint_but_correct_region",
395                [
396                    Label::new("component_kind", "sink"),
397                    Label::new("component_type", "aws_cloudwatch_metrics"),
398                    Label::new("region", "some_aws_region_name"),
399                    Label::new("endpoint", "wrong_endpoint.com/metrics")
400                ]
401                .iter()
402            ))
403        );
404        assert!(
405            matcher.matches(&Key::from_parts(
406                "custom_metric_with_wrong_region_but_correct_endpoint",
407                [
408                    Label::new("component_kind", "sink"),
409                    Label::new("component_type", "aws_cloudwatch_metrics"),
410                    Label::new("region", "some_other_region"),
411                    Label::new("endpoint", "test.com/metrics")
412                ]
413                .iter()
414            ))
415        );
416        assert!(
417            !matcher.matches(&Key::from_parts(
418                "custom_metric_with_wrong_component_kind",
419                [
420                    Label::new("component_kind", "source"),
421                    Label::new("component_type", "aws_cloudwatch_metrics"),
422                    Label::new("region", "some_other_region"),
423                    Label::new("endpoint", "test.com/metrics")
424                ]
425                .iter()
426            ))
427        );
428    }
429
430    #[test]
431    fn parse_simple_config_into_matcher() {
432        let config = serde_yaml::from_str::<PerMetricSetExpiration>(indoc! {r#"
433            name:
434                type: "exact"
435                value: "test_metric"
436            labels:
437                type: "all"
438                matchers:
439                    - type: "exact"
440                      key: "component_kind"
441                      value: "sink"
442                    - type: "regex"
443                      key: "component_type"
444                      value_pattern: "aws_.*"
445            expire_secs: 1.0
446            "#})
447        .unwrap();
448
449        let matcher: MetricKeyMatcher = config.try_into().unwrap();
450
451        if let Some(MetricNameMatcher::Exact(value)) = matcher.name {
452            assert_eq!("test_metric", value);
453        } else {
454            panic!("Expected exact name matcher");
455        }
456
457        let Some(LabelsMatcher::All(all_matchers)) = matcher.labels else {
458            panic!("Expected main label matcher to be an all matcher");
459        };
460
461        assert_eq!(2, all_matchers.len());
462        if let LabelsMatcher::Exact(key, value) = &all_matchers[0] {
463            assert_eq!("component_kind", key);
464            assert_eq!("sink", value);
465        } else {
466            panic!("Expected first label matcher to be an exact matcher");
467        }
468        if let LabelsMatcher::Regex(key, regex) = &all_matchers[1] {
469            assert_eq!("component_type", key);
470            assert_eq!("aws_.*", regex.as_str());
471        } else {
472            panic!("Expected second label matcher to be a regex matcher");
473        }
474    }
475}