vector_core/metrics/
metric_matcher.rs

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