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 LabelsMatcher::Exact("component_kind".to_string(), "sink".to_string()),
339 LabelsMatcher::Regex("component_type".to_string(), Regex::new(r"aws_.*").unwrap()),
341 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}