use std::fmt::Write;
use std::sync::Arc;
use std::time::Duration;
use surrealdb_types::ToSql;
use web_time::Instant;
use crate::catalog::{Permission, Permissions};
use crate::ctx::FrozenContext;
use crate::expr::visit::{Visit, Visitor};
#[derive(Clone)]
pub(crate) struct SlowLog(Arc<Inner>);
struct Inner {
duration: Duration,
param_allow: Vec<String>,
param_deny: Vec<String>,
}
pub(crate) struct ParamVisitor<'a> {
params: String,
slow_log: &'a SlowLog,
ctx: &'a FrozenContext,
}
impl Visitor for ParamVisitor<'_> {
type Error = ();
fn visit_permissions(&mut self, _: &Permissions) -> Result<(), Self::Error> {
Ok(())
}
fn visit_permission(&mut self, _: &Permission) -> Result<(), Self::Error> {
Ok(())
}
fn visit_param(&mut self, param: &crate::expr::Param) -> Result<(), Self::Error> {
if !self.slow_log.is_param_allowed(param) {
return Ok(());
}
if let Some(value) = self.ctx.value(param)
&& !value.is_none()
&& !value.is_null()
{
if !self.params.is_empty() {
self.params.push_str(", ");
}
let value = value.to_sql().split_whitespace().collect::<Vec<_>>().join(" ");
write!(&mut self.params, "{}={}", param.to_sql(), value)
.expect("Writing into a string cannot fail");
}
Ok(())
}
}
pub(crate) trait SlowLogVisit: for<'a> Visit<ParamVisitor<'a>> {}
impl<V: for<'a> Visit<ParamVisitor<'a>>> SlowLogVisit for V {}
impl SlowLog {
pub(super) fn new(
duration: Duration,
param_allow: Vec<String>,
param_deny: Vec<String>,
) -> Self {
Self(Arc::new(Inner {
duration,
param_allow,
param_deny,
}))
}
#[inline]
pub(crate) fn is_param_allowed(&self, name: &str) -> bool {
if !self.0.param_deny.is_empty() && self.0.param_deny.iter().any(|s| s == name) {
return false;
}
if self.0.param_allow.is_empty() {
return true;
}
self.0.param_allow.iter().any(|s| s == name)
}
pub(crate) fn check_log<S: SlowLogVisit + ToSql>(
&self,
ctx: &FrozenContext,
start: &Instant,
stm: &S,
) {
let elapsed = start.elapsed();
if elapsed < self.0.duration {
return;
}
let params = self.extract_params(ctx, stm);
let stm = stm.to_sql().split_whitespace().collect::<Vec<_>>().join(" ");
warn!("Slow query detected - time: {elapsed:#?} - query: {stm} - params: [ {params} ]");
}
fn extract_params<S: SlowLogVisit + ToSql>(&self, ctx: &FrozenContext, stm: &S) -> String {
let mut visitor = ParamVisitor {
params: String::new(),
slow_log: self,
ctx,
};
let _ = stm.visit(&mut visitor);
visitor.params
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
fn slowlog(allow: &[&str], deny: &[&str]) -> SlowLog {
SlowLog::new(
Duration::from_millis(1),
allow.iter().map(|s| (*s).to_string()).collect(),
deny.iter().map(|s| (*s).to_string()).collect(),
)
}
#[test]
fn defaults_allow_all() {
let s = slowlog(&[], &[]);
assert!(s.is_param_allowed("a"));
assert!(s.is_param_allowed("any"));
}
#[test]
fn allow_list_filters() {
let s = slowlog(&["a", "b"], &[]);
assert!(s.is_param_allowed("a"));
assert!(s.is_param_allowed("b"));
assert!(!s.is_param_allowed("c"));
}
#[test]
fn deny_list_only_excludes() {
let s = slowlog(&[], &["secret", "token"]);
assert!(!s.is_param_allowed("secret"));
assert!(!s.is_param_allowed("token"));
assert!(s.is_param_allowed("other"));
}
#[test]
fn deny_precedence_over_allow() {
let s = slowlog(&["foo", "bar"], &["bar"]);
assert!(!s.is_param_allowed("bar"));
assert!(s.is_param_allowed("foo"));
}
#[test]
fn allow_list_empty_means_all_except_denied() {
let s = slowlog(&[], &["nope"]);
assert!(s.is_param_allowed("ok"));
assert!(!s.is_param_allowed("nope"));
}
}