add regex support

This commit is contained in:
Chris Beck
2025-12-07 00:24:39 -07:00
parent ba69f5d592
commit 0b244b8e38
4 changed files with 132 additions and 0 deletions
Generated
+13
View File
@@ -1425,6 +1425,18 @@ dependencies = [
"thiserror",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
@@ -1668,6 +1680,7 @@ dependencies = [
"jsonrpsee",
"prometheus-http-client",
"rand",
"regex",
"serde",
"serde_json",
"thiserror",
+1
View File
@@ -41,6 +41,7 @@ hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto"] }
jsonrpsee = { version = "0.26", features = ["macros", "async-client"] }
plotters = { version = "0.3", default-features = false, features = ["bitmap_backend", "bitmap_encoder", "ttf", "datetime", "area_series", "line_series", "full_palette"] }
rand = "0.9"
regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+1
View File
@@ -25,6 +25,7 @@ http-body = { workspace = true }
http-body-util = { workspace = true }
humantime = { workspace = true }
jsonrpsee = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
+117
View File
@@ -266,6 +266,35 @@ impl Origin {
}
}
/// A compiled regex that can be cloned and deserialized.
#[derive(Clone)]
pub struct CompiledRegex(regex::Regex);
impl CompiledRegex {
/// Check if the regex matches the given text.
pub fn is_match(&self, text: &str) -> bool {
self.0.is_match(text)
}
}
impl std::fmt::Debug for CompiledRegex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "/{}/", self.0.as_str())
}
}
impl<'de> Deserialize<'de> for CompiledRegex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let pattern = String::deserialize(deserializer)?;
regex::Regex::new(&pattern)
.map(CompiledRegex)
.map_err(serde::de::Error::custom)
}
}
/// Filter criteria for matching log messages.
///
/// All non-empty fields must match for the filter to pass.
@@ -274,6 +303,12 @@ pub struct LogFilter {
/// If non-empty, the message must contain this substring.
#[serde(default)]
pub msg_contains: String,
/// If non-empty, the message must equal this value exactly.
#[serde(default)]
pub msg_equals: String,
/// If set, the message must match this regex.
#[serde(default)]
pub msg_regex: Option<CompiledRegex>,
/// If non-empty, the module path must equal this value exactly.
#[serde(default)]
pub module_equals: String,
@@ -291,6 +326,12 @@ impl std::fmt::Debug for LogFilter {
if !self.msg_contains.is_empty() {
s.field("msg_contains", &self.msg_contains);
}
if !self.msg_equals.is_empty() {
s.field("msg_equals", &self.msg_equals);
}
if let Some(regex) = &self.msg_regex {
s.field("msg_regex", regex);
}
if !self.module_equals.is_empty() {
s.field("module_equals", &self.module_equals);
}
@@ -313,6 +354,16 @@ impl LogFilter {
return false;
}
if !self.msg_equals.is_empty() && *log_msg.msg != *self.msg_equals {
return false;
}
if let Some(regex) = &self.msg_regex {
if !regex.is_match(&log_msg.msg) {
return false;
}
}
if !self.module_equals.is_empty() {
match log_msg.module_path.as_deref() {
Some(module) if module == self.module_equals.as_str() => {}
@@ -391,4 +442,70 @@ mod tests {
assert!(origin2.matches_filter("app@host"));
assert!(!origin2.matches_filter("app@other"));
}
fn make_log_msg(msg: &str) -> LogMessage {
LogMessage::builder(Level::ERROR, msg)
.module_path("test::module")
.file("test.rs")
.line("42")
.build()
}
#[test]
fn test_log_filter_msg_contains() {
let filter: LogFilter = serde_json::from_str(r#"{"msg_contains": "error"}"#).unwrap();
assert!(filter.matches(&make_log_msg("an error occurred")));
assert!(filter.matches(&make_log_msg("error")));
assert!(!filter.matches(&make_log_msg("warning message")));
}
#[test]
fn test_log_filter_msg_equals() {
let filter: LogFilter = serde_json::from_str(r#"{"msg_equals": "exact match"}"#).unwrap();
assert!(filter.matches(&make_log_msg("exact match")));
assert!(!filter.matches(&make_log_msg("exact match with extra")));
assert!(!filter.matches(&make_log_msg("not exact match")));
}
#[test]
fn test_log_filter_msg_regex() {
let filter: LogFilter = serde_json::from_str(r#"{"msg_regex": "error \\d+"}"#).unwrap();
assert!(filter.matches(&make_log_msg("error 123")));
assert!(filter.matches(&make_log_msg("an error 456 occurred")));
assert!(!filter.matches(&make_log_msg("error")));
assert!(!filter.matches(&make_log_msg("warning 123")));
}
#[test]
fn test_log_filter_combined() {
// msg_contains AND module_equals must both match
let filter: LogFilter = serde_json::from_str(
r#"{"msg_contains": "error", "module_equals": "test::module"}"#,
)
.unwrap();
assert!(filter.matches(&make_log_msg("an error occurred")));
// Wrong module
let mut msg = make_log_msg("an error occurred");
msg.module_path = Some("other::module".into());
assert!(!filter.matches(&msg));
// Wrong message
let msg2 = make_log_msg("warning message");
assert!(!filter.matches(&msg2));
}
#[test]
fn test_log_filter_empty_matches_all() {
let filter: LogFilter = serde_json::from_str(r#"{}"#).unwrap();
assert!(filter.matches(&make_log_msg("anything")));
assert!(filter.matches(&make_log_msg("")));
}
#[test]
fn test_compiled_regex_debug() {
let filter: LogFilter = serde_json::from_str(r#"{"msg_regex": "test.*pattern"}"#).unwrap();
let debug_str = format!("{:?}", filter);
assert!(debug_str.contains("/test.*pattern/"));
}
}