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_gt(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.ts.as_str() > value.as_str())
}
pub fn ts_gte(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.ts.as_str() >= value.as_str())
}
pub fn ts_lt(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.ts.as_str() < value.as_str())
}
pub fn ts_lte(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.ts.as_str() <= value.as_str())
}
pub fn ts_between(self, start: impl Into<String>, end: impl Into<String>) -> Self {
let start = start.into();
let end = end.into();
assert!(start <= end, "ts_between: start ({start}) must be <= end ({end})");
self.add(move |r| r.ts.as_str() >= start.as_str() && r.ts.as_str() <= end.as_str())
}
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 ep_eq(self, value: u8) -> Self {
self.add(move |r| r.ep == value)
}
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 thrd_id_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.thrd_id == value)
}
pub fn username_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.username == value)
}
pub fn trxid_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.trxid == value)
}
pub fn statement_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.statement == value)
}
pub fn appname_eq(self, value: impl Into<String>) -> Self {
let value = value.into();
self.add(move |r| r.appname == value)
}
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 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 exec_id_eq(self, value: i64) -> Self {
self.add(move |r| r.exec_id == value)
}
}
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_gt() {
let record = make_record();
assert!(FilterBuilder::new().ts_gt("2024-05-31").build().matches(&record));
assert!(!FilterBuilder::new().ts_gt("2024-06-01 10:00:00.000").build().matches(&record));
}
#[test]
fn test_ts_gte() {
let record = make_record();
assert!(FilterBuilder::new().ts_gte("2024-06-01 10:00:00.000").build().matches(&record));
assert!(!FilterBuilder::new().ts_gte("2024-06-01 10:00:00.001").build().matches(&record));
}
#[test]
fn test_ts_lt() {
let record = make_record();
assert!(FilterBuilder::new().ts_lt("2024-06-02").build().matches(&record));
assert!(!FilterBuilder::new().ts_lt("2024-06-01 10:00:00.000").build().matches(&record));
}
#[test]
fn test_ts_lte() {
let record = make_record();
assert!(FilterBuilder::new().ts_lte("2024-06-01 10:00:00.000").build().matches(&record));
assert!(!FilterBuilder::new().ts_lte("2024-06-01 09:59:59.999").build().matches(&record));
}
#[test]
fn test_ts_between() {
let record = make_record();
assert!(
FilterBuilder::new()
.ts_between("2024-06-01 10:00:00.000", "2024-06-01 11:00:00.000")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.ts_between("2024-06-01 10:00:00.001", "2024-06-01 11:00:00.000")
.build()
.matches(&record)
);
}
#[test]
#[should_panic(expected = "ts_between: start")]
fn test_ts_between_panics_on_invalid_range() {
FilterBuilder::new().ts_between("2024-06-02", "2024-06-01").build();
}
#[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_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_sess_id_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.sess_id_eq("0xABC")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.sess_id_eq("0xXYZ")
.build()
.matches(&record)
);
}
#[test]
fn test_thrd_id_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.thrd_id_eq("123")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.thrd_id_eq("999")
.build()
.matches(&record)
);
}
#[test]
fn test_username_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.username_eq("alice")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.username_eq("bob")
.build()
.matches(&record)
);
}
#[test]
fn test_trxid_eq() {
let record = make_record();
assert!(FilterBuilder::new().trxid_eq("0").build().matches(&record));
assert!(
!FilterBuilder::new()
.trxid_eq("99")
.build()
.matches(&record)
);
}
#[test]
fn test_statement_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.statement_eq("0x1")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.statement_eq("0x2")
.build()
.matches(&record)
);
}
#[test]
fn test_appname_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.appname_eq("myapp")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.appname_eq("other")
.build()
.matches(&record)
);
}
#[test]
fn test_client_ip_eq() {
let record = make_record();
assert!(
FilterBuilder::new()
.client_ip_eq("10.0.0.1")
.build()
.matches(&record)
);
assert!(
!FilterBuilder::new()
.client_ip_eq("192.168.0.1")
.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_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_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()
.exec_time_gt(100.0)
.sql_contains("SELECT")
.username_eq("alice")
.build();
assert!(filter.matches(&record));
let strict = FilterBuilder::new()
.username_eq("alice")
.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()
.username_eq("bob")
.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));
}
}