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::v1::{AttributeValue, SpanKind, TraceChunk};
12use crate::span::vec_map::VecMap;
13use crate::span::{self, trace_utils::get_root_span_index, trace_utils_v1, TraceData};
14
15trait TagFilter {
16 fn matches_tag_value(&self, value: &str) -> bool;
18 fn key(&self) -> &str;
20}
21
22#[derive(Debug)]
23struct TagLiteralFilter {
24 key: String,
25 value: Option<String>,
26}
27
28#[derive(Debug)]
29struct TagRegexFilter {
30 key: String,
31 value: Option<Regex>,
32}
33
34#[derive(Debug, Default)]
39pub struct TraceFilterer {
40 reject: Vec<TagLiteralFilter>,
41 reject_regex: Vec<TagRegexFilter>,
42
43 require: Vec<TagLiteralFilter>,
44 require_regex: Vec<TagRegexFilter>,
45
46 ignore_resources: Vec<Regex>,
47}
48
49pub trait Span<'a> {
51 fn resource_normalized(&'a self) -> &'a str;
53 fn get_meta(&'a self, key: &str) -> Option<&'a str>;
55}
56
57impl TagFilter for TagLiteralFilter {
58 fn matches_tag_value(&self, value: &str) -> bool {
59 match &self.value {
60 None => true, Some(required_value) => value == required_value,
62 }
63 }
64
65 fn key(&self) -> &str {
66 &self.key
67 }
68}
69
70impl TagFilter for TagRegexFilter {
71 fn matches_tag_value(&self, value: &str) -> bool {
72 match &self.value {
73 None => true, Some(pattern) => pattern.is_match(value),
75 }
76 }
77
78 fn key(&self) -> &str {
79 &self.key
80 }
81}
82
83impl<'a, T: TraceData> Span<'a> for span::v04::Span<T> {
84 fn resource_normalized(&'a self) -> &'a str {
85 let span_resource = self.resource.borrow();
87 if span_resource.is_empty() {
88 let span_name = self.name.borrow();
89 debug!(
90 ?span_name,
91 "Trace filter: filtering on name because resource is empty"
92 );
93 span_name
94 } else {
95 span_resource
96 }
97 }
98
99 fn get_meta(&'a self, key: &str) -> Option<&'a str> {
100 self.meta.get(key).map(|v| v.borrow())
101 }
102}
103
104impl<'a, T: TraceData> Span<'a> for span::v1::Span<T> {
105 fn resource_normalized(&'a self) -> &'a str {
106 let span_resource = self.resource.borrow();
108 if span_resource.is_empty() {
109 let span_name = self.name.borrow();
110 debug!(
111 ?span_name,
112 "Trace filter: filtering on name because resource is empty"
113 );
114 span_name
115 } else {
116 span_resource
117 }
118 }
119
120 fn get_meta(&'a self, key: &str) -> Option<&'a str> {
121 match self.attributes.get(key) {
122 Some(AttributeValue::String(s)) => Some(s.borrow()),
123 _ => match key {
127 "env" if !self.env.borrow().is_empty() => Some(self.env.borrow()),
128 "version" if !self.version.borrow().is_empty() => Some(self.version.borrow()),
129 "component" if !self.component.borrow().is_empty() => Some(self.component.borrow()),
130 "span.kind" if self.span_kind != SpanKind::Internal => {
131 Some(self.span_kind.as_meta_str())
132 }
133 _ => None,
134 },
135 }
136 }
137}
138
139struct ChunkSpanView<'a, T: TraceData> {
145 span: &'a span::v1::Span<T>,
146 chunk_attributes: &'a VecMap<T::Text, AttributeValue<T>>,
147}
148
149impl<'a, T: TraceData> Span<'a> for ChunkSpanView<'a, T> {
150 fn resource_normalized(&'a self) -> &'a str {
151 self.span.resource_normalized()
152 }
153
154 fn get_meta(&'a self, key: &str) -> Option<&'a str> {
155 self.span
156 .get_meta(key)
157 .or_else(|| match self.chunk_attributes.get(key) {
158 Some(AttributeValue::String(s)) => Some(s.borrow()),
159 _ => None,
160 })
161 }
162}
163
164impl TraceFilterer {
165 fn compile_literal_filters(filters: &[String]) -> Vec<TagLiteralFilter> {
166 let mut tag_regex_filters = Vec::new();
167 for filter in filters {
168 let (key, value) = match filter.split_once(":") {
169 Some((key, value)) if !value.trim().is_empty() => {
170 (key.trim(), Some(value.trim().to_owned()))
171 }
172 _ => (filter.trim(), None),
173 };
174 if key.is_empty() {
175 error!(
176 ?filter,
177 "Invalid tag filter with empty key value, skipping it"
178 );
179 continue;
180 }
181
182 tag_regex_filters.push(TagLiteralFilter {
183 key: key.to_owned(),
184 value,
185 });
186 }
187
188 tag_regex_filters
189 }
190
191 fn compile_regex_filters(filters: &[String]) -> Vec<TagRegexFilter> {
192 let mut tag_regex_filters = Vec::new();
193 for filter in filters {
194 let (key, value) = match filter.split_once(":") {
195 Some((key, value)) if !value.trim().is_empty() => (key.trim(), Some(value.trim())),
196 _ => (filter.trim(), None),
197 };
198 if key.is_empty() {
199 error!(
200 ?filter,
201 "Invalid tag filter with empty key value, skipping it"
202 );
203 continue;
204 }
205
206 let value = match value {
207 Some(value) => match Regex::new(value) {
208 Ok(regex) => Some(regex),
209 Err(err) => {
210 error!(
211 ?filter,
212 ?err,
213 "Invalid regex pattern in tag filter's value, skipping it"
214 );
215 continue;
216 }
217 },
218 None => None,
219 };
220
221 tag_regex_filters.push(TagRegexFilter {
222 key: key.to_owned(),
223 value,
224 });
225 }
226
227 tag_regex_filters
228 }
229
230 fn compile_resource_filters(ignore_resources: &[String]) -> Vec<Regex> {
231 ignore_resources
232 .iter()
233 .filter_map(|regex| {
234 Regex::new(regex)
235 .inspect_err(|err| {
236 error!(
237 ?regex,
238 ?err,
239 "Invalid regex pattern in ignore resources filter, skipping it"
240 )
241 })
242 .ok()
243 })
244 .collect()
245 }
246
247 pub fn new(
251 filter_tags_require: &[String],
252 filter_tags_reject: &[String],
253 filter_tags_regex_require: &[String],
254 filter_tags_regex_reject: &[String],
255 ignore_resources: &[String],
256 ) -> Self {
257 let require_regex = Self::compile_regex_filters(filter_tags_regex_require);
258 let reject_regex = Self::compile_regex_filters(filter_tags_regex_reject);
259 let require = Self::compile_literal_filters(filter_tags_require);
260 let reject = Self::compile_literal_filters(filter_tags_reject);
261 let ignore_resources = Self::compile_resource_filters(ignore_resources);
262
263 Self {
264 reject,
265 require,
266 reject_regex,
267 require_regex,
268 ignore_resources,
269 }
270 }
271 pub fn with_empty_conf() -> Self {
273 Self::default()
274 }
275
276 pub fn filter_traces(&self, traces: &mut Vec<Vec<span::v04::Span<impl TraceData>>>) -> usize {
278 let traces_count_before = traces.len();
279 traces.retain(|trace| {
280 let Ok(root_span_index) = get_root_span_index(trace) else {
281 return true;
282 };
283 let should_drop = self.should_drop(&trace[root_span_index]);
284 if should_drop {
285 debug!("Trace rejected as it fails to meet tag requirements. root: %v");
286 }
287 !should_drop
288 });
289 let traces_count_after = traces.len();
290
291 traces_count_before - traces_count_after
292 }
293
294 pub fn filter_traces_v1<T: TraceData>(&self, traces: &mut Vec<TraceChunk<T>>) -> usize {
297 let traces_count_before = traces.len();
298 traces.retain(|chunk| {
299 let Ok(root_span_index) = trace_utils_v1::get_root_span_index(&chunk.spans) else {
300 return true;
301 };
302 let root_span_view = ChunkSpanView {
303 span: &chunk.spans[root_span_index],
304 chunk_attributes: &chunk.attributes,
305 };
306 let should_drop = self.should_drop(&root_span_view);
307 if should_drop {
308 debug!("Trace rejected as it fails to meet tag requirements. root: %v");
309 }
310 !should_drop
311 });
312 let traces_count_after = traces.len();
313
314 traces_count_before - traces_count_after
315 }
316
317 pub fn should_drop<'a>(&self, root_span: &'a impl Span<'a>) -> bool {
330 if !self.ignore_resources.is_empty() {
331 let span_resource = root_span.resource_normalized();
332
333 if self
334 .ignore_resources
335 .iter()
336 .any(|resource_pattern| resource_pattern.is_match(span_resource))
337 {
338 return true;
339 }
340 }
341
342 if self
343 .reject
344 .iter()
345 .any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
346 {
347 return true;
348 }
349
350 if self
351 .reject_regex
352 .iter()
353 .any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
354 {
355 return true;
356 }
357
358 if !self
359 .require
360 .iter()
361 .all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
362 {
363 return true;
364 }
365
366 if !self
367 .require_regex
368 .iter()
369 .all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
370 {
371 return true;
372 }
373
374 false
375 }
376
377 fn check_tag_filter_with_normalization<'a>(
378 filter: &impl TagFilter,
379 root_span: &'a impl Span<'a>,
380 ) -> bool {
381 let Some(value) = root_span.get_meta(filter.key()) else {
382 return false;
383 };
384 match filter.key() {
385 "env" => {
386 let normalized_value = normalize_utils::normalize_tag_cloned(value);
387 filter.matches_tag_value(&normalized_value)
388 }
389 "http.status_code" => {
390 if !normalizer::is_valid_http_status_code(value) {
391 debug!(?value,"trace filter on http.status_code ignored because root span's `http.status_code` is invalid");
392 return false;
393 }
394 filter.matches_tag_value(value)
395 }
396 _ => filter.matches_tag_value(value),
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::TraceFilterer;
404 use crate::span::v04::{SpanBytes, VecMap};
405 use crate::span::v1::{
406 AttributeValue as AttributeValueV1, SpanBytes as SpanBytesV1, TraceChunk,
407 };
408 fn span_with(resource: &'static str, meta: &[(&'static str, &'static str)]) -> SpanBytes {
411 SpanBytes {
412 service: "svc".into(),
413 name: "op".into(),
414 resource: resource.into(),
415 span_id: 1,
416 trace_id: 1,
417 parent_id: 0,
418 meta: meta
419 .iter()
420 .map(|(k, v)| ((*k).into(), (*v).into()))
421 .collect::<VecMap<_, _>>(),
422 ..Default::default()
423 }
424 }
425
426 fn one_trace(s: SpanBytes) -> Vec<Vec<SpanBytes>> {
427 vec![vec![s]]
428 }
429
430 fn v1_chunk_with(
431 resource: &'static str,
432 meta: &[(&'static str, &'static str)],
433 ) -> TraceChunk<crate::span::BytesData> {
434 TraceChunk {
435 spans: vec![SpanBytesV1 {
436 service: "svc".into(),
437 name: "op".into(),
438 resource: resource.into(),
439 span_id: 1,
440 parent_id: 0,
441 attributes: meta
442 .iter()
443 .map(|(k, v)| ((*k).into(), AttributeValueV1::String((*v).into())))
444 .collect(),
445 ..Default::default()
446 }],
447 ..Default::default()
448 }
449 }
450
451 fn v1_chunk_with_chunk_attributes(
452 resource: &'static str,
453 chunk_attributes: &[(&'static str, &'static str)],
454 ) -> TraceChunk<crate::span::BytesData> {
455 TraceChunk {
456 spans: vec![SpanBytesV1 {
457 service: "svc".into(),
458 name: "op".into(),
459 resource: resource.into(),
460 span_id: 1,
461 parent_id: 0,
462 ..Default::default()
463 }],
464 attributes: chunk_attributes
465 .iter()
466 .map(|(k, v)| ((*k).into(), AttributeValueV1::String((*v).into())))
467 .collect(),
468 ..Default::default()
469 }
470 }
471
472 fn map_to_owned(values: &[&str]) -> Vec<String> {
473 values.iter().map(|&s| s.to_owned()).collect()
474 }
475
476 fn require_str(tags: &[&str]) -> TraceFilterer {
477 TraceFilterer::new(&map_to_owned(tags), &[], &[], &[], &[])
478 }
479
480 fn reject_str(tags: &[&str]) -> TraceFilterer {
481 TraceFilterer::new(&[], &map_to_owned(tags), &[], &[], &[])
482 }
483
484 fn require_regex(tags: &[&str]) -> TraceFilterer {
485 TraceFilterer::new(&[], &[], &map_to_owned(tags), &[], &[])
486 }
487
488 fn reject_regex(tags: &[&str]) -> TraceFilterer {
489 TraceFilterer::new(&[], &[], &[], &map_to_owned(tags), &[])
490 }
491
492 fn ignore_resources(patterns: &[&str]) -> TraceFilterer {
493 TraceFilterer::new(&[], &[], &[], &[], &map_to_owned(patterns))
494 }
495
496 #[test]
499 fn reject_string_exact_match_drops() {
500 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
501 reject_str(&["env:prod"]).filter_traces(&mut traces);
502 assert!(traces.is_empty());
503 }
504
505 #[test]
506 fn reject_string_wrong_value_keeps() {
507 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
508 reject_str(&["env:prod"]).filter_traces(&mut traces);
509 assert_eq!(traces.len(), 1);
510 }
511
512 #[test]
513 fn reject_string_missing_tag_keeps() {
514 let mut traces = one_trace(span_with("r", &[]));
515 reject_str(&["env:prod"]).filter_traces(&mut traces);
516 assert_eq!(traces.len(), 1);
517 }
518
519 #[test]
520 fn reject_string_key_only_matches_any_value() {
521 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
523 reject_str(&["env"]).filter_traces(&mut traces);
524 assert!(traces.is_empty());
525 }
526
527 #[test]
530 fn reject_regex_value_match_drops() {
531 let mut traces = one_trace(span_with("r", &[("env", "production")]));
532 reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
533 assert!(traces.is_empty());
534 }
535
536 #[cfg_attr(miri, ignore)] #[test]
538 fn reject_regex_value_no_match_keeps() {
539 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
540 reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
541 assert_eq!(traces.len(), 1);
542 }
543
544 #[test]
548 fn reject_key_regex_key_and_value_match_drops() {
549 let mut traces = one_trace(span_with("r", &[("error", "timeout")]));
550 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
551 assert!(!traces.is_empty());
553 }
554
555 #[test]
556 fn reject_key_regex_wrong_value_keeps() {
557 let mut traces = one_trace(span_with("r", &[("error", "network")]));
558 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
559 assert_eq!(traces.len(), 1);
560 }
561
562 #[test]
563 fn reject_key_regex_missing_key_keeps() {
564 let mut traces = one_trace(span_with("r", &[]));
565 reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
566 assert_eq!(traces.len(), 1);
567 }
568
569 #[test]
572 fn require_string_present_and_matching_keeps() {
573 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
574 require_str(&["env:prod"]).filter_traces(&mut traces);
575 assert_eq!(traces.len(), 1);
576 }
577
578 #[test]
579 fn require_string_missing_tag_drops() {
580 let mut traces = one_trace(span_with("r", &[]));
581 require_str(&["env:prod"]).filter_traces(&mut traces);
582 assert!(traces.is_empty());
583 }
584
585 #[test]
586 fn require_string_wrong_value_drops() {
587 let mut traces = one_trace(span_with("r", &[("env", "staging")]));
588 require_str(&["env:prod"]).filter_traces(&mut traces);
589 assert!(traces.is_empty());
590 }
591
592 #[cfg_attr(miri, ignore)] #[test]
596 fn require_regex_value_match_keeps() {
597 let mut traces = one_trace(span_with("r", &[("env", "production")]));
598 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
599 assert_eq!(traces.len(), 1);
600 }
601
602 #[test]
603 fn require_regex_missing_drops() {
604 let mut traces = one_trace(span_with("r", &[]));
605 require_regex(&["env:prod.*"]).filter_traces(&mut traces);
606 assert!(traces.is_empty());
607 }
608
609 #[test]
613 fn require_key_regex_key_exists_keeps() {
614 let mut traces = one_trace(span_with("r", &[("error", "any")]));
615 require_regex(&["err.*"]).filter_traces(&mut traces);
616 assert!(traces.is_empty());
618 }
619
620 #[test]
621 fn require_key_regex_missing_key_drops() {
622 let mut traces = one_trace(span_with("r", &[]));
623 require_regex(&["err.*"]).filter_traces(&mut traces);
624 assert!(traces.is_empty());
625 }
626
627 #[test]
630 fn ignore_resources_match_drops() {
631 let mut traces = one_trace(span_with("GET /health", &[]));
632 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
633 assert!(traces.is_empty());
634 }
635
636 #[test]
637 fn ignore_resources_no_match_keeps() {
638 let mut traces = one_trace(span_with("POST /data", &[]));
639 ignore_resources(&["GET /health"]).filter_traces(&mut traces);
640 assert_eq!(traces.len(), 1);
641 }
642
643 #[test]
644 fn ignore_resources_empty_resource_falls_back_to_name() {
645 let mut traces = one_trace(span_with("", &[]));
648 ignore_resources(&["op"]).filter_traces(&mut traces);
649 assert!(traces.is_empty());
650 }
651
652 #[test]
655 fn env_normalization_reject_matches_after_lowercase() {
656 let mut traces = one_trace(span_with("r", &[("env", "PROD")]));
658 reject_str(&["env:prod"]).filter_traces(&mut traces);
659 assert!(
660 traces.is_empty(),
661 "env value should be normalized before matching"
662 );
663 }
664
665 #[test]
666 fn env_normalization_require_matches_normalized_value() {
667 let mut traces = one_trace(span_with("r", &[("env", "Prod Env")]));
669 require_str(&["env:prod_env"]).filter_traces(&mut traces);
670 assert_eq!(
671 traces.len(),
672 1,
673 "normalized env should satisfy the require filter"
674 );
675 }
676
677 #[test]
680 fn http_status_code_invalid_value_skips_reject_filter() {
681 let mut traces = one_trace(span_with("r", &[("http.status_code", "abc")]));
684 reject_str(&["http.status_code:abc"]).filter_traces(&mut traces);
685 assert_eq!(
686 traces.len(),
687 1,
688 "invalid status code should not trigger the filter"
689 );
690 }
691
692 #[test]
693 fn http_status_code_valid_value_triggers_reject_filter() {
694 let mut traces = one_trace(span_with("r", &[("http.status_code", "500")]));
695 reject_str(&["http.status_code:500"]).filter_traces(&mut traces);
696 assert!(traces.is_empty());
697 }
698
699 #[test]
702 fn multiple_traces_partial_rejection() {
703 let f = reject_str(&["env:prod"]);
704 let mut traces = vec![
705 vec![span_with("r", &[("env", "prod")])], vec![span_with("r", &[("env", "staging")])], ];
708 f.filter_traces(&mut traces);
709 assert_eq!(traces.len(), 1);
710 }
711
712 #[test]
713 fn no_filters_keeps_all_traces() {
714 let f = TraceFilterer::new(&[], &[], &[], &[], &[]);
715 let mut traces = vec![
716 vec![span_with("r1", &[])],
717 vec![span_with("r2", &[("env", "prod")])],
718 ];
719 f.filter_traces(&mut traces);
720 assert_eq!(traces.len(), 2);
721 }
722
723 #[test]
724 fn invalid_regex_in_filter_is_skipped_gracefully() {
725 let f = reject_regex(&["env:[invalid"]);
727 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
728 f.filter_traces(&mut traces);
729 assert_eq!(traces.len(), 1);
730 }
731
732 #[test]
735 fn literal_reject_spaces_around_colon_drops() {
736 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
738 reject_str(&[" env : prod "]).filter_traces(&mut traces);
739 assert!(traces.is_empty());
740 }
741
742 #[test]
743 fn literal_require_spaces_around_colon_keeps() {
744 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
745 require_str(&[" env : prod "]).filter_traces(&mut traces);
746 assert_eq!(traces.len(), 1);
747 }
748
749 #[test]
750 fn literal_reject_key_only_with_spaces_drops_any_value() {
751 let mut traces = one_trace(span_with("r", &[("env", "anything")]));
753 reject_str(&[" env "]).filter_traces(&mut traces);
754 assert!(traces.is_empty());
755 }
756
757 #[test]
758 fn literal_reject_empty_key_is_skipped_keeps() {
759 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
761 reject_str(&[":prod"]).filter_traces(&mut traces);
762 assert_eq!(traces.len(), 1);
763 }
764
765 #[test]
766 fn literal_require_empty_key_is_skipped_keeps() {
767 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
769 require_str(&[":prod"]).filter_traces(&mut traces);
770 assert_eq!(traces.len(), 1);
771 }
772
773 #[test]
774 fn regex_reject_spaces_around_colon_drops() {
775 let mut traces = one_trace(span_with("r", &[("env", "production")]));
777 reject_regex(&[" env : prod.* "]).filter_traces(&mut traces);
778 assert!(traces.is_empty());
779 }
780
781 #[cfg_attr(miri, ignore)] #[test]
783 fn regex_require_spaces_around_colon_keeps() {
784 let mut traces = one_trace(span_with("r", &[("env", "production")]));
785 require_regex(&[" env : prod.* "]).filter_traces(&mut traces);
786 assert_eq!(traces.len(), 1);
787 }
788
789 #[test]
790 fn regex_reject_empty_key_is_skipped_keeps() {
791 let mut traces = one_trace(span_with("r", &[("env", "prod")]));
793 reject_regex(&[":prod.*"]).filter_traces(&mut traces);
794 assert_eq!(traces.len(), 1);
795 }
796
797 #[test]
800 fn v1_reject_string_exact_match_drops() {
801 let mut traces = vec![v1_chunk_with("r", &[("env", "prod")])];
802 reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
803 assert!(traces.is_empty());
804 }
805
806 #[test]
807 fn v1_reject_string_wrong_value_keeps() {
808 let mut traces = vec![v1_chunk_with("r", &[("env", "staging")])];
809 reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
810 assert_eq!(traces.len(), 1);
811 }
812
813 #[test]
814 fn v1_ignore_resources_match_drops() {
815 let mut traces = vec![v1_chunk_with("GET /health", &[])];
816 ignore_resources(&["GET /health"]).filter_traces_v1(&mut traces);
817 assert!(traces.is_empty());
818 }
819
820 #[test]
821 fn v1_reject_matches_chunk_level_attribute() {
822 let mut traces = vec![v1_chunk_with_chunk_attributes("r", &[("env", "prod")])];
825 reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
826 assert!(traces.is_empty());
827 }
828
829 #[test]
830 fn v1_span_level_attribute_takes_precedence_over_chunk() {
831 let mut chunk = v1_chunk_with("r", &[("env", "staging")]);
833 chunk.attributes = [("env".into(), AttributeValueV1::String("prod".into()))]
834 .into_iter()
835 .collect();
836 let mut traces = vec![chunk];
837 reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
838 assert_eq!(traces.len(), 1, "span-level value should take precedence");
839 }
840
841 #[test]
842 fn v1_reject_matches_dedicated_promoted_fields() {
843 let mut traces = vec![TraceChunk {
846 spans: vec![SpanBytesV1 {
847 service: "svc".into(),
848 name: "op".into(),
849 resource: "r".into(),
850 span_id: 1,
851 parent_id: 0,
852 env: "prod".into(),
853 version: "1.2.3".into(),
854 component: "http".into(),
855 span_kind: crate::span::v1::SpanKind::Client,
856 ..Default::default()
857 }],
858 ..Default::default()
859 }];
860
861 reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
862 assert!(
863 traces.is_empty(),
864 "dedicated env field should be visible to the filter"
865 );
866 }
867}