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 #[cfg_attr(miri, ignore)] #[test]
466 fn require_regex_value_match_keeps() {
467 let mut traces = one_trace(span_with("r", &[("env", "production")]));
468 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
469 assert_eq!(traces.len(), 1);
470 }
471
472 #[test]
473 fn require_regex_missing_drops() {
474 let mut traces = one_trace(span_with("r", &[]));
475 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
476 assert!(traces.is_empty());
477 }
478
479 #[test]
483 fn require_key_regex_key_exists_keeps() {
484 let mut traces = one_trace(span_with("r", &[("error", "any")]));
485 require_regex(&["err.*"]).filter_traces(&mut traces);
486 assert!(traces.is_empty());
488 }
489
490 #[test]
491 fn require_key_regex_missing_key_drops() {
492 let mut traces = one_trace(span_with("r", &[]));
493 require_regex(&["err.*"]).filter_traces(&mut traces);
494 assert!(traces.is_empty());
495 }
496
497 #[test]
500 fn ignore_resources_match_drops() {
501 let mut traces = one_trace(span_with("GET /health", &[]));
502 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
503 assert!(traces.is_empty());
504 }
505
506 #[test]
507 fn ignore_resources_no_match_keeps() {
508 let mut traces = one_trace(span_with("POST /data", &[]));
509 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
510 assert_eq!(traces.len(), 1);
511 }
512
513 #[test]
514 fn ignore_resources_empty_resource_falls_back_to_name() {
515 let mut traces = one_trace(span_with("", &[]));
518 ignore_resources(&["op"]).filter_traces(&mut traces);
519 assert!(traces.is_empty());
520 }
521
522 #[test]
525 fn env_normalization_reject_matches_after_lowercase() {
526 let mut traces = one_trace(span_with("r", &[("env", "PROD")]));
528 reject_str(&["env:prod"]).filter_traces(&mut traces);
529 assert!(
530 traces.is_empty(),
531 "env value should be normalized before matching"
532 );
533 }
534
535 #[test]
536 fn env_normalization_require_matches_normalized_value() {
537 let mut traces = one_trace(span_with("r", &[("env", "Prod Env")]));
539 require_str(&["env:prod_env"]).filter_traces(&mut traces);
540 assert_eq!(
541 traces.len(),
542 1,
543 "normalized env should satisfy the require filter"
544 );
545 }
546
547 #[test]
550 fn http_status_code_invalid_value_skips_reject_filter() {
551 let mut traces = one_trace(span_with("r", &[("http.status_code", "abc")]));
554 reject_str(&["http.status_code:abc"]).filter_traces(&mut traces);
555 assert_eq!(
556 traces.len(),
557 1,
558 "invalid status code should not trigger the filter"
559 );
560 }
561
562 #[test]
563 fn http_status_code_valid_value_triggers_reject_filter() {
564 let mut traces = one_trace(span_with("r", &[("http.status_code", "500")]));
565 reject_str(&["http.status_code:500"]).filter_traces(&mut traces);
566 assert!(traces.is_empty());
567 }
568
569 #[test]
572 fn multiple_traces_partial_rejection() {
573 let f = reject_str(&["env:prod"]);
574 let mut traces = vec![
575 vec![span_with("r", &[("env", "prod")])], vec![span_with("r", &[("env", "staging")])], ];
578 f.filter_traces(&mut traces);
579 assert_eq!(traces.len(), 1);
580 }
581
582 #[test]
583 fn no_filters_keeps_all_traces() {
584 let f = TraceFilterer::new(&[], &[], &[], &[], &[]);
585 let mut traces = vec![
586 vec![span_with("r1", &[])],
587 vec![span_with("r2", &[("env", "prod")])],
588 ];
589 f.filter_traces(&mut traces);
590 assert_eq!(traces.len(), 2);
591 }
592
593 #[test]
594 fn invalid_regex_in_filter_is_skipped_gracefully() {
595 let f = reject_regex(&["env:[invalid"]);
597 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
598 f.filter_traces(&mut traces);
599 assert_eq!(traces.len(), 1);
600 }
601
602 #[test]
605 fn literal_reject_spaces_around_colon_drops() {
606 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
608 reject_str(&[" env : prod "]).filter_traces(&mut traces);
609 assert!(traces.is_empty());
610 }
611
612 #[test]
613 fn literal_require_spaces_around_colon_keeps() {
614 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
615 require_str(&[" env : prod "]).filter_traces(&mut traces);
616 assert_eq!(traces.len(), 1);
617 }
618
619 #[test]
620 fn literal_reject_key_only_with_spaces_drops_any_value() {
621 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
623 reject_str(&[" env "]).filter_traces(&mut traces);
624 assert!(traces.is_empty());
625 }
626
627 #[test]
628 fn literal_reject_empty_key_is_skipped_keeps() {
629 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
631 reject_str(&[":prod"]).filter_traces(&mut traces);
632 assert_eq!(traces.len(), 1);
633 }
634
635 #[test]
636 fn literal_require_empty_key_is_skipped_keeps() {
637 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
639 require_str(&[":prod"]).filter_traces(&mut traces);
640 assert_eq!(traces.len(), 1);
641 }
642
643 #[test]
644 fn regex_reject_spaces_around_colon_drops() {
645 let mut traces = one_trace(span_with("r", &[("env", "production")]));
647 reject_regex(&[" env : prod.* "]).filter_traces(&mut traces);
648 assert!(traces.is_empty());
649 }
650
651 #[cfg_attr(miri, ignore)] #[test]
653 fn regex_require_spaces_around_colon_keeps() {
654 let mut traces = one_trace(span_with("r", &[("env", "production")]));
655 require_regex(&[" env : prod.* "]).filter_traces(&mut traces);
656 assert_eq!(traces.len(), 1);
657 }
658
659 #[test]
660 fn regex_reject_empty_key_is_skipped_keeps() {
661 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
663 reject_regex(&[":prod.*"]).filter_traces(&mut traces);
664 assert_eq!(traces.len(), 1);
665 }
666}