1use std::borrow::Borrow as _;
6
7use libdd_common::regex_engine::Regex;
8use libdd_trace_normalization::{normalize_utils, normalizer};
9use tracing::{debug, error};
10
11use crate::span::{self, trace_utils::get_root_span_index, TraceData};
12
13trait TagFilter {
14 fn matches_tag_value(&self, value: &str) -> bool;
16 fn key(&self) -> &str;
18}
19
20#[derive(Debug)]
21struct TagLiteralFilter {
22 key: String,
23 value: Option<String>,
24}
25
26#[derive(Debug)]
27struct TagRegexFilter {
28 key: String,
29 value: Option<Regex>,
30}
31
32#[derive(Debug, Default)]
37pub struct TraceFilterer {
38 reject: Vec<TagLiteralFilter>,
39 reject_regex: Vec<TagRegexFilter>,
40
41 require: Vec<TagLiteralFilter>,
42 require_regex: Vec<TagRegexFilter>,
43
44 ignore_resources: Vec<Regex>,
45}
46
47pub trait Span<'a> {
49 fn resource_normalized(&'a self) -> &'a str;
51 fn get_meta(&'a self, key: &str) -> Option<&'a str>;
53}
54
55impl TagFilter for TagLiteralFilter {
56 fn matches_tag_value(&self, value: &str) -> bool {
57 match &self.value {
58 None => true, Some(required_value) => value == required_value,
60 }
61 }
62
63 fn key(&self) -> &str {
64 &self.key
65 }
66}
67
68impl TagFilter for TagRegexFilter {
69 fn matches_tag_value(&self, value: &str) -> bool {
70 match &self.value {
71 None => true, Some(pattern) => pattern.is_match(value),
73 }
74 }
75
76 fn key(&self) -> &str {
77 &self.key
78 }
79}
80
81impl<'a, T: TraceData> Span<'a> for span::v04::Span<T> {
82 fn resource_normalized(&'a self) -> &'a str {
83 let span_resource = self.resource.borrow();
85 if span_resource.is_empty() {
86 let span_name = self.name.borrow();
87 debug!(
88 ?span_name,
89 "Trace filter: filtering on name because resource is empty"
90 );
91 span_name
92 } else {
93 span_resource
94 }
95 }
96
97 fn get_meta(&'a self, key: &str) -> Option<&'a str> {
98 self.meta.get(key).map(|v| v.borrow())
99 }
100}
101
102impl TraceFilterer {
103 fn compile_literal_filters(filters: &[String]) -> Vec<TagLiteralFilter> {
104 let mut tag_regex_filters = Vec::new();
105 for filter in filters {
106 let (key, value) = match filter.split_once(":") {
107 Some((key, value)) if !value.trim().is_empty() => {
108 (key.trim(), Some(value.trim().to_owned()))
109 }
110 _ => (filter.trim(), None),
111 };
112 if key.is_empty() {
113 error!(
114 ?filter,
115 "Invalid tag filter with empty key value, skipping it"
116 );
117 continue;
118 }
119
120 tag_regex_filters.push(TagLiteralFilter {
121 key: key.to_owned(),
122 value,
123 });
124 }
125
126 tag_regex_filters
127 }
128
129 fn compile_regex_filters(filters: &[String]) -> Vec<TagRegexFilter> {
130 let mut tag_regex_filters = Vec::new();
131 for filter in filters {
132 let (key, value) = match filter.split_once(":") {
133 Some((key, value)) if !value.trim().is_empty() => (key.trim(), Some(value.trim())),
134 _ => (filter.trim(), None),
135 };
136 if key.is_empty() {
137 error!(
138 ?filter,
139 "Invalid tag filter with empty key value, skipping it"
140 );
141 continue;
142 }
143
144 let value = match value {
145 Some(value) => match Regex::new(value) {
146 Ok(regex) => Some(regex),
147 Err(err) => {
148 error!(
149 ?filter,
150 ?err,
151 "Invalid regex pattern in tag filter's value, skipping it"
152 );
153 continue;
154 }
155 },
156 None => None,
157 };
158
159 tag_regex_filters.push(TagRegexFilter {
160 key: key.to_owned(),
161 value,
162 });
163 }
164
165 tag_regex_filters
166 }
167
168 fn compile_resource_filters(ignore_resources: &[String]) -> Vec<Regex> {
169 ignore_resources
170 .iter()
171 .filter_map(|regex| {
172 Regex::new(regex)
173 .inspect_err(|err| {
174 error!(
175 ?regex,
176 ?err,
177 "Invalid regex pattern in ignore resources filter, skipping it"
178 )
179 })
180 .ok()
181 })
182 .collect()
183 }
184
185 pub fn new(
189 filter_tags_require: &[String],
190 filter_tags_reject: &[String],
191 filter_tags_regex_require: &[String],
192 filter_tags_regex_reject: &[String],
193 ignore_resources: &[String],
194 ) -> Self {
195 let require_regex = Self::compile_regex_filters(filter_tags_regex_require);
196 let reject_regex = Self::compile_regex_filters(filter_tags_regex_reject);
197 let require = Self::compile_literal_filters(filter_tags_require);
198 let reject = Self::compile_literal_filters(filter_tags_reject);
199 let ignore_resources = Self::compile_resource_filters(ignore_resources);
200
201 Self {
202 reject,
203 require,
204 reject_regex,
205 require_regex,
206 ignore_resources,
207 }
208 }
209 pub fn with_empty_conf() -> Self {
211 Self::default()
212 }
213
214 pub fn filter_traces(&self, traces: &mut Vec<Vec<span::v04::Span<impl TraceData>>>) -> usize {
216 let traces_count_before = traces.len();
217 traces.retain(|trace| {
218 let Ok(root_span_index) = get_root_span_index(trace) else {
219 return true;
220 };
221 let should_drop = self.should_drop(&trace[root_span_index]);
222 if should_drop {
223 debug!("Trace rejected as it fails to meet tag requirements. root: %v");
224 }
225 !should_drop
226 });
227 let traces_count_after = traces.len();
228
229 traces_count_before - traces_count_after
230 }
231
232 pub fn should_drop<'a>(&self, root_span: &'a impl Span<'a>) -> bool {
245 if !self.ignore_resources.is_empty() {
246 let span_resource = root_span.resource_normalized();
247
248 if self
249 .ignore_resources
250 .iter()
251 .any(|resource_pattern| resource_pattern.is_match(span_resource))
252 {
253 return true;
254 }
255 }
256
257 if self
258 .reject
259 .iter()
260 .any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
261 {
262 return true;
263 }
264
265 if self
266 .reject_regex
267 .iter()
268 .any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
269 {
270 return true;
271 }
272
273 if !self
274 .require
275 .iter()
276 .all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
277 {
278 return true;
279 }
280
281 if !self
282 .require_regex
283 .iter()
284 .all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
285 {
286 return true;
287 }
288
289 false
290 }
291
292 fn check_tag_filter_with_normalization<'a>(
293 filter: &impl TagFilter,
294 root_span: &'a impl Span<'a>,
295 ) -> bool {
296 let Some(value) = root_span.get_meta(filter.key()) else {
297 return false;
298 };
299 match filter.key() {
300 "env" => {
301 let normalized_value = normalize_utils::normalize_tag_cloned(value);
302 filter.matches_tag_value(&normalized_value)
303 }
304 "http.status_code" => {
305 if !normalizer::is_valid_http_status_code(value) {
306 debug!(?value,"trace filter on http.status_code ignored because root span's `http.status_code` is invalid");
307 return false;
308 }
309 filter.matches_tag_value(value)
310 }
311 _ => filter.matches_tag_value(value),
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::TraceFilterer;
319 use crate::span::v04::{SpanBytes, VecMap};
320 fn span_with(resource: &'static str, meta: &[(&'static str, &'static str)]) -> SpanBytes {
323 SpanBytes {
324 service: "svc".into(),
325 name: "op".into(),
326 resource: resource.into(),
327 span_id: 1,
328 trace_id: 1,
329 parent_id: 0,
330 meta: meta
331 .iter()
332 .map(|(k, v)| ((*k).into(), (*v).into()))
333 .collect::<VecMap<_, _>>(),
334 ..Default::default()
335 }
336 }
337
338 fn one_trace(s: SpanBytes) -> Vec<Vec<SpanBytes>> {
339 vec![vec![s]]
340 }
341
342 fn map_to_owned(values: &[&str]) -> Vec<String> {
343 values.iter().map(|&s| s.to_owned()).collect()
344 }
345
346 fn require_str(tags: &[&str]) -> TraceFilterer {
347 TraceFilterer::new(&map_to_owned(tags), &[], &[], &[], &[])
348 }
349
350 fn reject_str(tags: &[&str]) -> TraceFilterer {
351 TraceFilterer::new(&[], &map_to_owned(tags), &[], &[], &[])
352 }
353
354 fn require_regex(tags: &[&str]) -> TraceFilterer {
355 TraceFilterer::new(&[], &[], &map_to_owned(tags), &[], &[])
356 }
357
358 fn reject_regex(tags: &[&str]) -> TraceFilterer {
359 TraceFilterer::new(&[], &[], &[], &map_to_owned(tags), &[])
360 }
361
362 fn ignore_resources(patterns: &[&str]) -> TraceFilterer {
363 TraceFilterer::new(&[], &[], &[], &[], &map_to_owned(patterns))
364 }
365
366 #[test]
369 fn reject_string_exact_match_drops() {
370 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
371 reject_str(&["env:prod"]).filter_traces(&mut traces);
372 assert!(traces.is_empty());
373 }
374
375 #[test]
376 fn reject_string_wrong_value_keeps() {
377 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
378 reject_str(&["env:prod"]).filter_traces(&mut traces);
379 assert_eq!(traces.len(), 1);
380 }
381
382 #[test]
383 fn reject_string_missing_tag_keeps() {
384 let mut traces = one_trace(span_with("r", &[]));
385 reject_str(&["env:prod"]).filter_traces(&mut traces);
386 assert_eq!(traces.len(), 1);
387 }
388
389 #[test]
390 fn reject_string_key_only_matches_any_value() {
391 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
393 reject_str(&["env"]).filter_traces(&mut traces);
394 assert!(traces.is_empty());
395 }
396
397 #[test]
400 fn reject_regex_value_match_drops() {
401 let mut traces = one_trace(span_with("r", &[("env", "production")]));
402 reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
403 assert!(traces.is_empty());
404 }
405
406 #[cfg_attr(miri, ignore)] #[test]
408 fn reject_regex_value_no_match_keeps() {
409 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
410 reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
411 assert_eq!(traces.len(), 1);
412 }
413
414 #[test]
418 fn reject_key_regex_key_and_value_match_drops() {
419 let mut traces = one_trace(span_with("r", &[("error", "timeout")]));
420 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
421 assert!(!traces.is_empty());
423 }
424
425 #[test]
426 fn reject_key_regex_wrong_value_keeps() {
427 let mut traces = one_trace(span_with("r", &[("error", "network")]));
428 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
429 assert_eq!(traces.len(), 1);
430 }
431
432 #[test]
433 fn reject_key_regex_missing_key_keeps() {
434 let mut traces = one_trace(span_with("r", &[]));
435 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
436 assert_eq!(traces.len(), 1);
437 }
438
439 #[test]
442 fn require_string_present_and_matching_keeps() {
443 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
444 require_str(&["env:prod"]).filter_traces(&mut traces);
445 assert_eq!(traces.len(), 1);
446 }
447
448 #[test]
449 fn require_string_missing_tag_drops() {
450 let mut traces = one_trace(span_with("r", &[]));
451 require_str(&["env:prod"]).filter_traces(&mut traces);
452 assert!(traces.is_empty());
453 }
454
455 #[test]
456 fn require_string_wrong_value_drops() {
457 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
458 require_str(&["env:prod"]).filter_traces(&mut traces);
459 assert!(traces.is_empty());
460 }
461
462 #[test]
465 fn require_regex_value_match_keeps() {
466 let mut traces = one_trace(span_with("r", &[("env", "production")]));
467 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
468 assert_eq!(traces.len(), 1);
469 }
470
471 #[test]
472 fn require_regex_missing_drops() {
473 let mut traces = one_trace(span_with("r", &[]));
474 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
475 assert!(traces.is_empty());
476 }
477
478 #[test]
482 fn require_key_regex_key_exists_keeps() {
483 let mut traces = one_trace(span_with("r", &[("error", "any")]));
484 require_regex(&["err.*"]).filter_traces(&mut traces);
485 assert!(traces.is_empty());
487 }
488
489 #[test]
490 fn require_key_regex_missing_key_drops() {
491 let mut traces = one_trace(span_with("r", &[]));
492 require_regex(&["err.*"]).filter_traces(&mut traces);
493 assert!(traces.is_empty());
494 }
495
496 #[test]
499 fn ignore_resources_match_drops() {
500 let mut traces = one_trace(span_with("GET /health", &[]));
501 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
502 assert!(traces.is_empty());
503 }
504
505 #[test]
506 fn ignore_resources_no_match_keeps() {
507 let mut traces = one_trace(span_with("POST /data", &[]));
508 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
509 assert_eq!(traces.len(), 1);
510 }
511
512 #[test]
513 fn ignore_resources_empty_resource_falls_back_to_name() {
514 let mut traces = one_trace(span_with("", &[]));
517 ignore_resources(&["op"]).filter_traces(&mut traces);
518 assert!(traces.is_empty());
519 }
520
521 #[test]
524 fn env_normalization_reject_matches_after_lowercase() {
525 let mut traces = one_trace(span_with("r", &[("env", "PROD")]));
527 reject_str(&["env:prod"]).filter_traces(&mut traces);
528 assert!(
529 traces.is_empty(),
530 "env value should be normalized before matching"
531 );
532 }
533
534 #[test]
535 fn env_normalization_require_matches_normalized_value() {
536 let mut traces = one_trace(span_with("r", &[("env", "Prod Env")]));
538 require_str(&["env:prod_env"]).filter_traces(&mut traces);
539 assert_eq!(
540 traces.len(),
541 1,
542 "normalized env should satisfy the require filter"
543 );
544 }
545
546 #[test]
549 fn http_status_code_invalid_value_skips_reject_filter() {
550 let mut traces = one_trace(span_with("r", &[("http.status_code", "abc")]));
553 reject_str(&["http.status_code:abc"]).filter_traces(&mut traces);
554 assert_eq!(
555 traces.len(),
556 1,
557 "invalid status code should not trigger the filter"
558 );
559 }
560
561 #[test]
562 fn http_status_code_valid_value_triggers_reject_filter() {
563 let mut traces = one_trace(span_with("r", &[("http.status_code", "500")]));
564 reject_str(&["http.status_code:500"]).filter_traces(&mut traces);
565 assert!(traces.is_empty());
566 }
567
568 #[test]
571 fn multiple_traces_partial_rejection() {
572 let f = reject_str(&["env:prod"]);
573 let mut traces = vec![
574 vec![span_with("r", &[("env", "prod")])], vec![span_with("r", &[("env", "staging")])], ];
577 f.filter_traces(&mut traces);
578 assert_eq!(traces.len(), 1);
579 }
580
581 #[test]
582 fn no_filters_keeps_all_traces() {
583 let f = TraceFilterer::new(&[], &[], &[], &[], &[]);
584 let mut traces = vec![
585 vec![span_with("r1", &[])],
586 vec![span_with("r2", &[("env", "prod")])],
587 ];
588 f.filter_traces(&mut traces);
589 assert_eq!(traces.len(), 2);
590 }
591
592 #[test]
593 fn invalid_regex_in_filter_is_skipped_gracefully() {
594 let f = reject_regex(&["env:[invalid"]);
596 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
597 f.filter_traces(&mut traces);
598 assert_eq!(traces.len(), 1);
599 }
600
601 #[test]
604 fn literal_reject_spaces_around_colon_drops() {
605 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
607 reject_str(&[" env : prod "]).filter_traces(&mut traces);
608 assert!(traces.is_empty());
609 }
610
611 #[test]
612 fn literal_require_spaces_around_colon_keeps() {
613 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
614 require_str(&[" env : prod "]).filter_traces(&mut traces);
615 assert_eq!(traces.len(), 1);
616 }
617
618 #[test]
619 fn literal_reject_key_only_with_spaces_drops_any_value() {
620 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
622 reject_str(&[" env "]).filter_traces(&mut traces);
623 assert!(traces.is_empty());
624 }
625
626 #[test]
627 fn literal_reject_empty_key_is_skipped_keeps() {
628 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
630 reject_str(&[":prod"]).filter_traces(&mut traces);
631 assert_eq!(traces.len(), 1);
632 }
633
634 #[test]
635 fn literal_require_empty_key_is_skipped_keeps() {
636 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
638 require_str(&[":prod"]).filter_traces(&mut traces);
639 assert_eq!(traces.len(), 1);
640 }
641
642 #[test]
643 fn regex_reject_spaces_around_colon_drops() {
644 let mut traces = one_trace(span_with("r", &[("env", "production")]));
646 reject_regex(&[" env : prod.* "]).filter_traces(&mut traces);
647 assert!(traces.is_empty());
648 }
649
650 #[cfg_attr(miri, ignore)] #[test]
652 fn regex_require_spaces_around_colon_keeps() {
653 let mut traces = one_trace(span_with("r", &[("env", "production")]));
654 require_regex(&[" env : prod.* "]).filter_traces(&mut traces);
655 assert_eq!(traces.len(), 1);
656 }
657
658 #[test]
659 fn regex_reject_empty_key_is_skipped_keeps() {
660 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
662 reject_regex(&[":prod.*"]).filter_traces(&mut traces);
663 assert_eq!(traces.len(), 1);
664 }
665}