diff --git a/changelog.d/1855.fix.md b/changelog.d/1855.fix.md new file mode 100644 index 0000000000..78507a8a04 --- /dev/null +++ b/changelog.d/1855.fix.md @@ -0,0 +1,3 @@ +Fixed `datadog_filter` wildcard matching failing on values containing newlines. Wildcard patterns (e.g. `field:*`, `field:*some*`) compiled to regexes where `.` could not match `\n`, so values with a trailing newline (common in Lambda, container stdout, and other line-delimited sources) or embedded newlines never matched. Wildcards now match across newlines. + +authors: Stunned1 diff --git a/src/datadog/filter/regex.rs b/src/datadog/filter/regex.rs index dce76807d9..3f77b267f8 100644 --- a/src/datadog/filter/regex.rs +++ b/src/datadog/filter/regex.rs @@ -2,13 +2,16 @@ use regex::Regex; /// Returns compiled word boundary regex. /// +/// The `(?s)` flag makes `.` match any character including `\n`, so that +/// wildcards (`*`) match across newlines in multi-line values. +/// /// # Panics /// Panics if an invalid wildcard regex is provided. #[allow(clippy::module_name_repetitions)] // Renaming is a breaking change. #[must_use] pub fn word_regex(to_match: &str) -> Regex { Regex::new(&format!( - r"\b{}\b", + r"(?s)\b{}\b", regex::escape(to_match).replace("\\*", ".*") )) .expect("invalid wildcard regex") @@ -16,14 +19,52 @@ pub fn word_regex(to_match: &str) -> Regex { /// Returns compiled wildcard regex. /// +/// The `(?s)` flag makes `.` match any character including `\n`, so that +/// wildcards (`*`) match values with trailing or embedded newlines. +/// /// # Panics /// Panics if an invalid wildcard regex is provided. #[allow(clippy::module_name_repetitions)] // Renaming is a breaking change. #[must_use] pub fn wildcard_regex(to_match: &str) -> Regex { Regex::new(&format!( - "^{}$", + "(?s)^{}$", regex::escape(to_match).replace("\\*", ".*") )) .expect("invalid wildcard regex") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wildcard_matches_trailing_newline() { + assert!(wildcard_regex("*").is_match("hello world\n")); + assert!(wildcard_regex("*some*").is_match("here is some content\n")); + assert!(wildcard_regex("hello*").is_match("hello world\n")); + } + + #[test] + fn wildcard_matches_embedded_newline() { + assert!(wildcard_regex("*").is_match("line1\nline2")); + assert!(wildcard_regex("*line2").is_match("line1\nline2")); + assert!(wildcard_regex("a*b").is_match("a\nb")); + } + + #[test] + fn wildcard_does_not_over_match() { + // No wildcard at the end: the value must end with the literal. + assert!(!wildcard_regex("*some").is_match("here is some\n")); + // Exact patterns still require exact matches. + assert!(wildcard_regex("abc").is_match("abc")); + assert!(!wildcard_regex("abc").is_match("abc\n")); + } + + #[test] + fn word_matches_across_newline() { + assert!(word_regex("foo*bar").is_match("foo\nbar")); + assert!(word_regex("*some*").is_match("has some stuff\n")); + assert!(!word_regex("foo*bar").is_match("foo\nbaz")); + } +} diff --git a/src/stdlib/match_datadog_query.rs b/src/stdlib/match_datadog_query.rs index e527a40440..6713028758 100644 --- a/src/stdlib/match_datadog_query.rs +++ b/src/stdlib/match_datadog_query.rs @@ -1060,6 +1060,42 @@ mod test { tdef: type_def(), } + wildcard_facet_trailing_newline { + args: func_args![value: value!({"a": "hello world\n"}), query: "@a:*"], + want: Ok(true), + tdef: type_def(), + } + + wildcard_contains_facet_trailing_newline { + args: func_args![value: value!({"a": "here is some content\n"}), query: "@a:*some*"], + want: Ok(true), + tdef: type_def(), + } + + wildcard_suffix_facet_trailing_newline { + args: func_args![value: value!({"a": "hello world\n"}), query: "@a:hello*"], + want: Ok(true), + tdef: type_def(), + } + + wildcard_prefix_facet_trailing_newline_no_match { + args: func_args![value: value!({"a": "here is some\n"}), query: "@a:*some"], + want: Ok(false), + tdef: type_def(), + } + + wildcard_facet_multiline { + args: func_args![value: value!({"a": "line1\nline2"}), query: "@a:*line2"], + want: Ok(true), + tdef: type_def(), + } + + wildcard_message_multiline { + args: func_args![value: value!({"message": "foo\nbar"}), query: "foo*bar"], + want: Ok(true), + tdef: type_def(), + } + range_message_unbounded { args: func_args![value: value!({"message": "1"}), query: "[* TO *]"], want: Ok(true),