use std::borrow::Borrow as _;
use libdd_common::regex_engine::Regex;
use libdd_trace_normalization::{normalize_utils, normalizer};
use tracing::{debug, error};
use crate::span::v1::{AttributeValue, SpanKind, TraceChunk};
use crate::span::vec_map::VecMap;
use crate::span::{self, trace_utils::get_root_span_index, trace_utils_v1, TraceData};
trait TagFilter {
fn matches_tag_value(&self, value: &str) -> bool;
fn key(&self) -> &str;
}
#[derive(Debug)]
struct TagLiteralFilter {
key: String,
value: Option<String>,
}
#[derive(Debug)]
struct TagRegexFilter {
key: String,
value: Option<Regex>,
}
#[derive(Debug, Default)]
pub struct TraceFilterer {
reject: Vec<TagLiteralFilter>,
reject_regex: Vec<TagRegexFilter>,
require: Vec<TagLiteralFilter>,
require_regex: Vec<TagRegexFilter>,
ignore_resources: Vec<Regex>,
}
pub trait Span<'a> {
fn resource_normalized(&'a self) -> &'a str;
fn get_meta(&'a self, key: &str) -> Option<&'a str>;
}
impl TagFilter for TagLiteralFilter {
fn matches_tag_value(&self, value: &str) -> bool {
match &self.value {
None => true, Some(required_value) => value == required_value,
}
}
fn key(&self) -> &str {
&self.key
}
}
impl TagFilter for TagRegexFilter {
fn matches_tag_value(&self, value: &str) -> bool {
match &self.value {
None => true, Some(pattern) => pattern.is_match(value),
}
}
fn key(&self) -> &str {
&self.key
}
}
impl<'a, T: TraceData> Span<'a> for span::v04::Span<T> {
fn resource_normalized(&'a self) -> &'a str {
let span_resource = self.resource.borrow();
if span_resource.is_empty() {
let span_name = self.name.borrow();
debug!(
?span_name,
"Trace filter: filtering on name because resource is empty"
);
span_name
} else {
span_resource
}
}
fn get_meta(&'a self, key: &str) -> Option<&'a str> {
self.meta.get(key).map(|v| v.borrow())
}
}
impl<'a, T: TraceData> Span<'a> for span::v1::Span<T> {
fn resource_normalized(&'a self) -> &'a str {
let span_resource = self.resource.borrow();
if span_resource.is_empty() {
let span_name = self.name.borrow();
debug!(
?span_name,
"Trace filter: filtering on name because resource is empty"
);
span_name
} else {
span_resource
}
}
fn get_meta(&'a self, key: &str) -> Option<&'a str> {
match self.attributes.get(key) {
Some(AttributeValue::String(s)) => Some(s.borrow()),
_ => match key {
"env" if !self.env.borrow().is_empty() => Some(self.env.borrow()),
"version" if !self.version.borrow().is_empty() => Some(self.version.borrow()),
"component" if !self.component.borrow().is_empty() => Some(self.component.borrow()),
"span.kind" if self.span_kind != SpanKind::Internal => {
Some(self.span_kind.as_meta_str())
}
_ => None,
},
}
}
}
struct ChunkSpanView<'a, T: TraceData> {
span: &'a span::v1::Span<T>,
chunk_attributes: &'a VecMap<T::Text, AttributeValue<T>>,
}
impl<'a, T: TraceData> Span<'a> for ChunkSpanView<'a, T> {
fn resource_normalized(&'a self) -> &'a str {
self.span.resource_normalized()
}
fn get_meta(&'a self, key: &str) -> Option<&'a str> {
self.span
.get_meta(key)
.or_else(|| match self.chunk_attributes.get(key) {
Some(AttributeValue::String(s)) => Some(s.borrow()),
_ => None,
})
}
}
impl TraceFilterer {
fn compile_literal_filters(filters: &[String]) -> Vec<TagLiteralFilter> {
let mut tag_regex_filters = Vec::new();
for filter in filters {
let (key, value) = match filter.split_once(":") {
Some((key, value)) if !value.trim().is_empty() => {
(key.trim(), Some(value.trim().to_owned()))
}
_ => (filter.trim(), None),
};
if key.is_empty() {
error!(
?filter,
"Invalid tag filter with empty key value, skipping it"
);
continue;
}
tag_regex_filters.push(TagLiteralFilter {
key: key.to_owned(),
value,
});
}
tag_regex_filters
}
fn compile_regex_filters(filters: &[String]) -> Vec<TagRegexFilter> {
let mut tag_regex_filters = Vec::new();
for filter in filters {
let (key, value) = match filter.split_once(":") {
Some((key, value)) if !value.trim().is_empty() => (key.trim(), Some(value.trim())),
_ => (filter.trim(), None),
};
if key.is_empty() {
error!(
?filter,
"Invalid tag filter with empty key value, skipping it"
);
continue;
}
let value = match value {
Some(value) => match Regex::new(value) {
Ok(regex) => Some(regex),
Err(err) => {
error!(
?filter,
?err,
"Invalid regex pattern in tag filter's value, skipping it"
);
continue;
}
},
None => None,
};
tag_regex_filters.push(TagRegexFilter {
key: key.to_owned(),
value,
});
}
tag_regex_filters
}
fn compile_resource_filters(ignore_resources: &[String]) -> Vec<Regex> {
ignore_resources
.iter()
.filter_map(|regex| {
Regex::new(regex)
.inspect_err(|err| {
error!(
?regex,
?err,
"Invalid regex pattern in ignore resources filter, skipping it"
)
})
.ok()
})
.collect()
}
pub fn new(
filter_tags_require: &[String],
filter_tags_reject: &[String],
filter_tags_regex_require: &[String],
filter_tags_regex_reject: &[String],
ignore_resources: &[String],
) -> Self {
let require_regex = Self::compile_regex_filters(filter_tags_regex_require);
let reject_regex = Self::compile_regex_filters(filter_tags_regex_reject);
let require = Self::compile_literal_filters(filter_tags_require);
let reject = Self::compile_literal_filters(filter_tags_reject);
let ignore_resources = Self::compile_resource_filters(ignore_resources);
Self {
reject,
require,
reject_regex,
require_regex,
ignore_resources,
}
}
pub fn with_empty_conf() -> Self {
Self::default()
}
pub fn filter_traces(&self, traces: &mut Vec<Vec<span::v04::Span<impl TraceData>>>) -> usize {
let traces_count_before = traces.len();
traces.retain(|trace| {
let Ok(root_span_index) = get_root_span_index(trace) else {
return true;
};
let should_drop = self.should_drop(&trace[root_span_index]);
if should_drop {
debug!("Trace rejected as it fails to meet tag requirements. root: %v");
}
!should_drop
});
let traces_count_after = traces.len();
traces_count_before - traces_count_after
}
pub fn filter_traces_v1<T: TraceData>(&self, traces: &mut Vec<TraceChunk<T>>) -> usize {
let traces_count_before = traces.len();
traces.retain(|chunk| {
let Ok(root_span_index) = trace_utils_v1::get_root_span_index(&chunk.spans) else {
return true;
};
let root_span_view = ChunkSpanView {
span: &chunk.spans[root_span_index],
chunk_attributes: &chunk.attributes,
};
let should_drop = self.should_drop(&root_span_view);
if should_drop {
debug!("Trace rejected as it fails to meet tag requirements. root: %v");
}
!should_drop
});
let traces_count_after = traces.len();
traces_count_before - traces_count_after
}
pub fn should_drop<'a>(&self, root_span: &'a impl Span<'a>) -> bool {
if !self.ignore_resources.is_empty() {
let span_resource = root_span.resource_normalized();
if self
.ignore_resources
.iter()
.any(|resource_pattern| resource_pattern.is_match(span_resource))
{
return true;
}
}
if self
.reject
.iter()
.any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
{
return true;
}
if self
.reject_regex
.iter()
.any(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
{
return true;
}
if !self
.require
.iter()
.all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
{
return true;
}
if !self
.require_regex
.iter()
.all(|filter| Self::check_tag_filter_with_normalization(filter, root_span))
{
return true;
}
false
}
fn check_tag_filter_with_normalization<'a>(
filter: &impl TagFilter,
root_span: &'a impl Span<'a>,
) -> bool {
let Some(value) = root_span.get_meta(filter.key()) else {
return false;
};
match filter.key() {
"env" => {
let normalized_value = normalize_utils::normalize_tag_cloned(value);
filter.matches_tag_value(&normalized_value)
}
"http.status_code" => {
if !normalizer::is_valid_http_status_code(value) {
debug!(?value,"trace filter on http.status_code ignored because root span's `http.status_code` is invalid");
return false;
}
filter.matches_tag_value(value)
}
_ => filter.matches_tag_value(value),
}
}
}
#[cfg(test)]
mod tests {
use super::TraceFilterer;
use crate::span::v04::{SpanBytes, VecMap};
use crate::span::v1::{
AttributeValue as AttributeValueV1, SpanBytes as SpanBytesV1, TraceChunk,
};
fn span_with(resource: &'static str, meta: &[(&'static str, &'static str)]) -> SpanBytes {
SpanBytes {
service: "svc".into(),
name: "op".into(),
resource: resource.into(),
span_id: 1,
trace_id: 1,
parent_id: 0,
meta: meta
.iter()
.map(|(k, v)| ((*k).into(), (*v).into()))
.collect::<VecMap<_, _>>(),
..Default::default()
}
}
fn one_trace(s: SpanBytes) -> Vec<Vec<SpanBytes>> {
vec![vec![s]]
}
fn v1_chunk_with(
resource: &'static str,
meta: &[(&'static str, &'static str)],
) -> TraceChunk<crate::span::BytesData> {
TraceChunk {
spans: vec![SpanBytesV1 {
service: "svc".into(),
name: "op".into(),
resource: resource.into(),
span_id: 1,
parent_id: 0,
attributes: meta
.iter()
.map(|(k, v)| ((*k).into(), AttributeValueV1::String((*v).into())))
.collect(),
..Default::default()
}],
..Default::default()
}
}
fn v1_chunk_with_chunk_attributes(
resource: &'static str,
chunk_attributes: &[(&'static str, &'static str)],
) -> TraceChunk<crate::span::BytesData> {
TraceChunk {
spans: vec![SpanBytesV1 {
service: "svc".into(),
name: "op".into(),
resource: resource.into(),
span_id: 1,
parent_id: 0,
..Default::default()
}],
attributes: chunk_attributes
.iter()
.map(|(k, v)| ((*k).into(), AttributeValueV1::String((*v).into())))
.collect(),
..Default::default()
}
}
fn map_to_owned(values: &[&str]) -> Vec<String> {
values.iter().map(|&s| s.to_owned()).collect()
}
fn require_str(tags: &[&str]) -> TraceFilterer {
TraceFilterer::new(&map_to_owned(tags), &[], &[], &[], &[])
}
fn reject_str(tags: &[&str]) -> TraceFilterer {
TraceFilterer::new(&[], &map_to_owned(tags), &[], &[], &[])
}
fn require_regex(tags: &[&str]) -> TraceFilterer {
TraceFilterer::new(&[], &[], &map_to_owned(tags), &[], &[])
}
fn reject_regex(tags: &[&str]) -> TraceFilterer {
TraceFilterer::new(&[], &[], &[], &map_to_owned(tags), &[])
}
fn ignore_resources(patterns: &[&str]) -> TraceFilterer {
TraceFilterer::new(&[], &[], &[], &[], &map_to_owned(patterns))
}
#[test]
fn reject_string_exact_match_drops() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
reject_str(&["env:prod"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn reject_string_wrong_value_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "staging")]));
reject_str(&["env:prod"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn reject_string_missing_tag_keeps() {
let mut traces = one_trace(span_with("r", &[]));
reject_str(&["env:prod"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn reject_string_key_only_matches_any_value() {
let mut traces = one_trace(span_with("r", &[("env", "anything")]));
reject_str(&["env"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn reject_regex_value_match_drops() {
let mut traces = one_trace(span_with("r", &[("env", "production")]));
reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[cfg_attr(miri, ignore)] #[test]
fn reject_regex_value_no_match_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "staging")]));
reject_regex(&["env:prod.*"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn reject_key_regex_key_and_value_match_drops() {
let mut traces = one_trace(span_with("r", &[("error", "timeout")]));
reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
assert!(!traces.is_empty());
}
#[test]
fn reject_key_regex_wrong_value_keeps() {
let mut traces = one_trace(span_with("r", &[("error", "network")]));
reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn reject_key_regex_missing_key_keeps() {
let mut traces = one_trace(span_with("r", &[]));
reject_regex(&["err.*:timeout"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn require_string_present_and_matching_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
require_str(&["env:prod"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn require_string_missing_tag_drops() {
let mut traces = one_trace(span_with("r", &[]));
require_str(&["env:prod"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn require_string_wrong_value_drops() {
let mut traces = one_trace(span_with("r", &[("env", "staging")]));
require_str(&["env:prod"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[cfg_attr(miri, ignore)] #[test]
fn require_regex_value_match_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "production")]));
require_regex(&["env:prod.*"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn require_regex_missing_drops() {
let mut traces = one_trace(span_with("r", &[]));
require_regex(&["env:prod.*"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn require_key_regex_key_exists_keeps() {
let mut traces = one_trace(span_with("r", &[("error", "any")]));
require_regex(&["err.*"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn require_key_regex_missing_key_drops() {
let mut traces = one_trace(span_with("r", &[]));
require_regex(&["err.*"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn ignore_resources_match_drops() {
let mut traces = one_trace(span_with("GET /health", &[]));
ignore_resources(&["GET /health"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn ignore_resources_no_match_keeps() {
let mut traces = one_trace(span_with("POST /data", &[]));
ignore_resources(&["GET /health"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn ignore_resources_empty_resource_falls_back_to_name() {
let mut traces = one_trace(span_with("", &[]));
ignore_resources(&["op"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn env_normalization_reject_matches_after_lowercase() {
let mut traces = one_trace(span_with("r", &[("env", "PROD")]));
reject_str(&["env:prod"]).filter_traces(&mut traces);
assert!(
traces.is_empty(),
"env value should be normalized before matching"
);
}
#[test]
fn env_normalization_require_matches_normalized_value() {
let mut traces = one_trace(span_with("r", &[("env", "Prod Env")]));
require_str(&["env:prod_env"]).filter_traces(&mut traces);
assert_eq!(
traces.len(),
1,
"normalized env should satisfy the require filter"
);
}
#[test]
fn http_status_code_invalid_value_skips_reject_filter() {
let mut traces = one_trace(span_with("r", &[("http.status_code", "abc")]));
reject_str(&["http.status_code:abc"]).filter_traces(&mut traces);
assert_eq!(
traces.len(),
1,
"invalid status code should not trigger the filter"
);
}
#[test]
fn http_status_code_valid_value_triggers_reject_filter() {
let mut traces = one_trace(span_with("r", &[("http.status_code", "500")]));
reject_str(&["http.status_code:500"]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn multiple_traces_partial_rejection() {
let f = reject_str(&["env:prod"]);
let mut traces = vec![
vec![span_with("r", &[("env", "prod")])], vec![span_with("r", &[("env", "staging")])], ];
f.filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn no_filters_keeps_all_traces() {
let f = TraceFilterer::new(&[], &[], &[], &[], &[]);
let mut traces = vec![
vec![span_with("r1", &[])],
vec![span_with("r2", &[("env", "prod")])],
];
f.filter_traces(&mut traces);
assert_eq!(traces.len(), 2);
}
#[test]
fn invalid_regex_in_filter_is_skipped_gracefully() {
let f = reject_regex(&["env:[invalid"]);
let mut traces = one_trace(span_with("r", &[("env", "anything")]));
f.filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn literal_reject_spaces_around_colon_drops() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
reject_str(&[" env : prod "]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn literal_require_spaces_around_colon_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
require_str(&[" env : prod "]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn literal_reject_key_only_with_spaces_drops_any_value() {
let mut traces = one_trace(span_with("r", &[("env", "anything")]));
reject_str(&[" env "]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn literal_reject_empty_key_is_skipped_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
reject_str(&[":prod"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn literal_require_empty_key_is_skipped_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
require_str(&[":prod"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn regex_reject_spaces_around_colon_drops() {
let mut traces = one_trace(span_with("r", &[("env", "production")]));
reject_regex(&[" env : prod.* "]).filter_traces(&mut traces);
assert!(traces.is_empty());
}
#[cfg_attr(miri, ignore)] #[test]
fn regex_require_spaces_around_colon_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "production")]));
require_regex(&[" env : prod.* "]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn regex_reject_empty_key_is_skipped_keeps() {
let mut traces = one_trace(span_with("r", &[("env", "prod")]));
reject_regex(&[":prod.*"]).filter_traces(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn v1_reject_string_exact_match_drops() {
let mut traces = vec![v1_chunk_with("r", &[("env", "prod")])];
reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn v1_reject_string_wrong_value_keeps() {
let mut traces = vec![v1_chunk_with("r", &[("env", "staging")])];
reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
assert_eq!(traces.len(), 1);
}
#[test]
fn v1_ignore_resources_match_drops() {
let mut traces = vec![v1_chunk_with("GET /health", &[])];
ignore_resources(&["GET /health"]).filter_traces_v1(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn v1_reject_matches_chunk_level_attribute() {
let mut traces = vec![v1_chunk_with_chunk_attributes("r", &[("env", "prod")])];
reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
assert!(traces.is_empty());
}
#[test]
fn v1_span_level_attribute_takes_precedence_over_chunk() {
let mut chunk = v1_chunk_with("r", &[("env", "staging")]);
chunk.attributes = [("env".into(), AttributeValueV1::String("prod".into()))]
.into_iter()
.collect();
let mut traces = vec![chunk];
reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
assert_eq!(traces.len(), 1, "span-level value should take precedence");
}
#[test]
fn v1_reject_matches_dedicated_promoted_fields() {
let mut traces = vec![TraceChunk {
spans: vec![SpanBytesV1 {
service: "svc".into(),
name: "op".into(),
resource: "r".into(),
span_id: 1,
parent_id: 0,
env: "prod".into(),
version: "1.2.3".into(),
component: "http".into(),
span_kind: crate::span::v1::SpanKind::Client,
..Default::default()
}],
..Default::default()
}];
reject_str(&["env:prod"]).filter_traces_v1(&mut traces);
assert!(
traces.is_empty(),
"dedicated env field should be visible to the filter"
);
}
}