use crate::record::Sqllog;
type Predicate = Box<dyn Fn(&Sqllog) -> bool + Send + Sync>;
pub struct Filter {
predicates: Vec<Predicate>,
}
impl Filter {
#[inline]
pub fn matches(&self, record: &Sqllog) -> bool {
self.predicates.iter().all(|pred| pred(record))
}
}
pub struct FilterBuilder {
predicates: Vec<Predicate>,
}
impl FilterBuilder {
pub fn new() -> Self {
Self {
predicates: Vec::new(),
}
}
pub fn build(self) -> Filter {
Filter {
predicates: self.predicates,
}
}
fn add<F>(mut self, pred: F) -> Self
where
F: Fn(&Sqllog) -> bool + Send + Sync + 'static,
{
self.predicates.push(Box::new(pred));
self
}
pub fn ts_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.ts.contains(&pattern))
}
pub fn ts_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.ts == value)
}
pub fn ts_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.ts.starts_with(&prefix))
}
pub fn ts_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.ts.ends_with(&suffix))
}
pub fn tag_is_some(self) -> Self {
self.add(|r| r.tag.is_some())
}
pub fn tag_is_none(self) -> Self {
self.add(|r| r.tag.is_none())
}
pub fn tag_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.tag.as_deref() == Some(&value))
}
pub fn tag_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.tag.as_deref().is_some_and(|t| t.contains(&pattern)))
}
pub fn ep_eq(self, value: u8) -> Self {
self.add(move |r| r.ep == value)
}
pub fn ep_gt(self, value: u8) -> Self {
self.add(move |r| r.ep > value)
}
pub fn ep_lt(self, value: u8) -> Self {
self.add(move |r| r.ep < value)
}
pub fn ep_between(self, min: u8, max: u8) -> Self {
assert!(min <= max, "ep_between: min ({min}) must be <= max ({max})");
self.add(move |r| r.ep >= min && r.ep <= max)
}
pub fn sess_id_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.sess_id.contains(&pattern))
}
pub fn sess_id_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.sess_id == value)
}
pub fn sess_id_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.sess_id.starts_with(&prefix))
}
pub fn sess_id_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.sess_id.ends_with(&suffix))
}
pub fn thrd_id_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.thrd_id.contains(&pattern))
}
pub fn thrd_id_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.thrd_id == value)
}
pub fn thrd_id_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.thrd_id.starts_with(&prefix))
}
pub fn thrd_id_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.thrd_id.ends_with(&suffix))
}
pub fn username_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.username.contains(&pattern))
}
pub fn username_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.username == value)
}
pub fn username_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.username.starts_with(&prefix))
}
pub fn username_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.username.ends_with(&suffix))
}
pub fn trxid_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.trxid.contains(&pattern))
}
pub fn trxid_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.trxid == value)
}
pub fn trxid_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.trxid.starts_with(&prefix))
}
pub fn trxid_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.trxid.ends_with(&suffix))
}
pub fn statement_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.statement.contains(&pattern))
}
pub fn statement_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.statement == value)
}
pub fn statement_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.statement.starts_with(&prefix))
}
pub fn statement_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.statement.ends_with(&suffix))
}
pub fn appname_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.appname.contains(&pattern))
}
pub fn appname_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.appname == value)
}
pub fn appname_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.appname.starts_with(&prefix))
}
pub fn appname_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.appname.ends_with(&suffix))
}
pub fn client_ip_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.client_ip.contains(&pattern))
}
pub fn client_ip_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.client_ip == value)
}
pub fn client_ip_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.client_ip.starts_with(&prefix))
}
pub fn client_ip_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.client_ip.ends_with(&suffix))
}
pub fn sql_contains(self, pattern: impl Into<String>) -> Self {
let pattern = pattern.into();
self.add(move |r| r.sql.contains(&pattern))
}
pub fn sql_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.sql == value)
}
pub fn sql_starts_with(self, prefix: impl Into<String>) -> Self {
let prefix = prefix.into();
self.add(move |r| r.sql.starts_with(&prefix))
}
pub fn sql_ends_with(self, suffix: impl Into<String>) -> Self {
let suffix = suffix.into();
self.add(move |r| r.sql.ends_with(&suffix))
}
pub fn exec_time_gt(self, min_ms: f32) -> Self {
self.add(move |r| r.exectime > min_ms)
}
pub fn exec_time_gte(self, min_ms: f32) -> Self {
self.add(move |r| r.exectime >= min_ms)
}
pub fn exec_time_lt(self, max_ms: f32) -> Self {
self.add(move |r| r.exectime < max_ms)
}
pub fn exec_time_between(self, min_ms: f32, max_ms: f32) -> Self {
assert!(
min_ms <= max_ms,
"exec_time_between: min_ms ({min_ms}) must be <= max_ms ({max_ms})"
);
self.add(move |r| r.exectime >= min_ms && r.exectime <= max_ms)
}
pub fn rowcount_eq(self, value: u32) -> Self {
self.add(move |r| r.rowcount == value)
}
pub fn rowcount_gt(self, value: u32) -> Self {
self.add(move |r| r.rowcount > value)
}
pub fn rowcount_lt(self, value: u32) -> Self {
self.add(move |r| r.rowcount < value)
}
pub fn rowcount_between(self, min: u32, max: u32) -> Self {
assert!(
min <= max,
"rowcount_between: min ({min}) must be <= max ({max})"
);
self.add(move |r| r.rowcount >= min && r.rowcount <= max)
}
pub fn exec_id_eq(self, value: i64) -> Self {
self.add(move |r| r.exec_id == value)
}
pub fn exec_id_gt(self, value: i64) -> Self {
self.add(move |r| r.exec_id > value)
}
pub fn exec_id_lt(self, value: i64) -> Self {
self.add(move |r| r.exec_id < value)
}
pub fn exec_id_between(self, min: i64, max: i64) -> Self {
assert!(
min <= max,
"exec_id_between: min ({min}) must be <= max ({max})"
);
self.add(move |r| r.exec_id >= min && r.exec_id <= max)
}
}
impl Default for FilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::Sqllog;
fn make_record() -> Sqllog {
Sqllog {
ts: "2024-06-01 10:00:00.000".to_string(),
tag: Some("SEL".to_string()),
ep: 2,
sess_id: "0xABC".to_string(),
thrd_id: "123".to_string(),
username: "alice".to_string(),
trxid: "0".to_string(),
statement: "0x1".to_string(),
appname: "myapp".to_string(),
client_ip: "10.0.0.1".to_string(),
sql: "SELECT * FROM users".to_string(),
exectime: 150.0,
rowcount: 10,
exec_id: 999,
}
}
#[test]
fn test_ts_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.ts_contains("2024-06")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ts_contains("2025-01")
.build()
.matches(&record)
);
}
#[test]
fn test_ts_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.ts_eq("2024-06-01 10:00:00.000")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ts_eq("2024-06-01 10:00:00.001")
.build()
.matches(&record)
);
}
#[test]
fn test_ts_starts_with() {
let record = make_record();
assert!(
FilterBuilder::new()
.ts_starts_with("2024")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ts_starts_with("2025")
.build()
.matches(&record)
);
}
#[test]
fn test_ts_ends_with() {
let record = make_record();
assert!(
FilterBuilder::new()
.ts_ends_with(".000")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ts_ends_with(".999")
.build()
.matches(&record)
);
}
#[test]
fn test_tag_is_some() {
let record = make_record();
assert!(FilterBuilder::new().tag_is_some().build().matches(&record));
let mut no_tag = make_record();
no_tag.tag = None;
assert!(!FilterBuilder::new().tag_is_some().build().matches(&no_tag));
}
#[test]
fn test_tag_is_none() {
let record = make_record();
assert!(!FilterBuilder::new().tag_is_none().build().matches(&record));
let mut no_tag = make_record();
no_tag.tag = None;
assert!(FilterBuilder::new().tag_is_none().build().matches(&no_tag));
}
#[test]
fn test_tag_eq() {
let record = make_record();
assert!(FilterBuilder::new().tag_eq("SEL").build().matches(&record));
assert!(!FilterBuilder::new().tag_eq("ORA").build().matches(&record));
}
#[test]
fn test_tag_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.tag_contains("SE")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.tag_contains("ORA")
.build()
.matches(&record)
);
}
#[test]
fn test_ep_eq() {
let record = make_record();
assert!(FilterBuilder::new().ep_eq(2).build().matches(&record));
assert!(!FilterBuilder::new().ep_eq(3).build().matches(&record));
}
#[test]
fn test_ep_gt() {
let record = make_record();
assert!(FilterBuilder::new().ep_gt(1).build().matches(&record));
assert!(!FilterBuilder::new().ep_gt(2).build().matches(&record));
}
#[test]
fn test_ep_lt() {
let record = make_record();
assert!(FilterBuilder::new().ep_lt(3).build().matches(&record));
assert!(!FilterBuilder::new().ep_lt(2).build().matches(&record));
}
#[test]
fn test_ep_between() {
let record = make_record();
assert!(
FilterBuilder::new()
.ep_between(1, 3)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ep_between(3, 5)
.build()
.matches(&record)
);
}
#[test]
fn test_sess_id_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.sess_id_contains("ABC")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sess_id_contains("XYZ")
.build()
.matches(&record)
);
}
#[test]
fn test_thrd_id_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.thrd_id_contains("12")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.thrd_id_contains("99")
.build()
.matches(&record)
);
}
#[test]
fn test_username_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.username_contains("ali")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.username_contains("bob")
.build()
.matches(&record)
);
}
#[test]
fn test_trxid_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.trxid_contains("0")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.trxid_contains("99")
.build()
.matches(&record)
);
}
#[test]
fn test_statement_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.statement_contains("0x1")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.statement_contains("0x2")
.build()
.matches(&record)
);
}
#[test]
fn test_appname_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.appname_contains("app")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.appname_contains("xyz")
.build()
.matches(&record)
);
}
#[test]
fn test_client_ip_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.client_ip_contains("10.0")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.client_ip_contains("192.168")
.build()
.matches(&record)
);
}
#[test]
fn test_sql_contains() {
let record = make_record();
assert!(
FilterBuilder::new()
.sql_contains("SELECT")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sql_contains("INSERT")
.build()
.matches(&record)
);
}
#[test]
fn test_sql_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.sql_eq("SELECT * FROM users")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sql_eq("SELECT 1")
.build()
.matches(&record)
);
}
#[test]
fn test_sql_starts_with() {
let record = make_record();
assert!(
FilterBuilder::new()
.sql_starts_with("SELECT")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sql_starts_with("UPDATE")
.build()
.matches(&record)
);
}
#[test]
fn test_sql_ends_with() {
let record = make_record();
assert!(
FilterBuilder::new()
.sql_ends_with("users")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sql_ends_with("orders")
.build()
.matches(&record)
);
}
#[test]
fn test_exec_time_gt() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_time_gt(100.0)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_time_gt(200.0)
.build()
.matches(&record)
);
}
#[test]
fn test_exec_time_lt() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_time_lt(200.0)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_time_lt(100.0)
.build()
.matches(&record)
);
}
#[test]
fn test_exec_time_between() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_time_between(100.0, 200.0)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_time_between(200.0, 300.0)
.build()
.matches(&record)
);
}
#[test]
fn test_rowcount_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.rowcount_eq(10)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.rowcount_eq(99)
.build()
.matches(&record)
);
}
#[test]
fn test_rowcount_gt() {
let record = make_record();
assert!(FilterBuilder::new().rowcount_gt(5).build().matches(&record));
assert!(
!FilterBuilder::new()
.rowcount_gt(10)
.build()
.matches(&record)
);
}
#[test]
fn test_rowcount_lt() {
let record = make_record();
assert!(
FilterBuilder::new()
.rowcount_lt(20)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.rowcount_lt(10)
.build()
.matches(&record)
);
}
#[test]
fn test_rowcount_between() {
let record = make_record();
assert!(
FilterBuilder::new()
.rowcount_between(5, 15)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.rowcount_between(20, 50)
.build()
.matches(&record)
);
}
#[test]
fn test_exec_id_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_id_eq(999)
.build()
.matches(&record)
);
assert!(!FilterBuilder::new().exec_id_eq(0).build().matches(&record));
}
#[test]
fn test_exec_id_gt() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_id_gt(500)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_id_gt(999)
.build()
.matches(&record)
);
}
#[test]
fn test_exec_id_lt() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_id_lt(1000)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_id_lt(999)
.build()
.matches(&record)
);
}
#[test]
fn test_exec_id_between() {
let record = make_record();
assert!(
FilterBuilder::new()
.exec_id_between(500, 1000)
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.exec_id_between(1000, 2000)
.build()
.matches(&record)
);
}
#[test]
fn test_empty_filter_matches_all() {
let record = make_record();
assert!(FilterBuilder::new().build().matches(&record));
}
#[test]
fn test_multiple_predicates_all_must_pass() {
let record = make_record();
let filter = FilterBuilder::new()
.ts_contains("2024")
.exec_time_gt(100.0)
.sql_contains("SELECT")
.build();
assert!(filter.matches(&record));
let strict = FilterBuilder::new()
.ts_contains("2024")
.exec_time_gt(200.0)
.build();
assert!(!strict.matches(&record));
}
#[test]
fn test_and_semantics_short_circuit() {
let record = make_record();
let filter = FilterBuilder::new()
.ts_contains("2025")
.exec_time_gt(100.0)
.sql_contains("SELECT")
.build();
assert!(!filter.matches(&record));
}
#[test]
fn test_default_is_same_as_new() {
let record = make_record();
assert!(FilterBuilder::default().build().matches(&record));
}
}