Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/1855.fix.md
Original file line number Diff line number Diff line change
@@ -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
45 changes: 43 additions & 2 deletions src/datadog/filter/regex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,69 @@ 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")
}

/// 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"));
}
}
36 changes: 36 additions & 0 deletions src/stdlib/match_datadog_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down