use crate::driver::{ExecResponse, Rows};
use crate::stmt::Value;
use std::fmt::Write;
use std::time::{Duration, Instant};
pub const TARGET: &str = "toasty::query";
const MAX_PARAMS: usize = 32;
const MAX_STR_LEN: usize = 128;
const MAX_LIST_ITEMS: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct QueryLogConfig {
pub params: bool,
pub slow_statement_threshold: Option<Duration>,
}
impl Default for QueryLogConfig {
fn default() -> Self {
Self {
params: false,
slow_statement_threshold: Some(Duration::from_secs(1)),
}
}
}
#[derive(Debug)]
pub struct QueryLog<'a> {
config: QueryLogConfig,
system: &'static str,
query_text: Option<&'a str>,
operation: Option<&'a str>,
collection: Option<&'a str>,
params: Option<String>,
rows: Option<u64>,
start: Instant,
}
impl<'a> QueryLog<'a> {
pub fn sql<'v>(
config: &QueryLogConfig,
system: &'static str,
sql: &'a str,
params: impl IntoIterator<Item = &'v Value>,
) -> Self {
Self {
config: *config,
system,
query_text: Some(sql),
operation: None,
collection: None,
params: render_params(config, params),
rows: None,
start: Instant::now(),
}
}
pub fn operation(
config: &QueryLogConfig,
system: &'static str,
operation: &'a str,
collection: Option<&'a str>,
) -> Self {
Self {
config: *config,
system,
query_text: None,
operation: Some(operation),
collection,
params: None,
rows: None,
start: Instant::now(),
}
}
pub fn rows(&mut self, rows: u64) {
self.rows = Some(rows);
}
pub fn finish(self, result: &crate::Result<ExecResponse>) {
let elapsed = self.start.elapsed();
let duration_ms = elapsed.as_secs_f64() * 1e3;
let rows = self.rows.or(match result {
Ok(response) => match &response.values {
Rows::Count(count) => Some(*count),
_ => None,
},
Err(_) => None,
});
let slow = self
.config
.slow_statement_threshold
.is_some_and(|threshold| elapsed >= threshold);
let error = result.as_ref().err().map(tracing::field::display);
let message = match (slow, result) {
(_, Err(_)) => "query failed",
(true, Ok(_)) => "slow query",
(false, Ok(_)) => "query executed",
};
let system = tracing::field::display(self.system);
let statement = self.query_text.map(tracing::field::display);
let operation = self.operation.map(tracing::field::display);
let collection = self.collection.map(tracing::field::display);
let params = self.params.as_deref().map(tracing::field::display);
if slow {
tracing::warn!(
target: "toasty::query",
duration_ms,
rows,
error,
db.system = system,
db.statement = statement,
db.operation = operation,
db.collection = collection,
db.params = params,
"{message}"
);
} else {
tracing::debug!(
target: "toasty::query",
duration_ms,
rows,
error,
db.system = system,
db.statement = statement,
db.operation = operation,
db.collection = collection,
db.params = params,
"{message}"
);
}
}
}
fn render_params<'v>(
config: &QueryLogConfig,
params: impl IntoIterator<Item = &'v Value>,
) -> Option<String> {
let enabled = tracing::event_enabled!(target: "toasty::query", tracing::Level::DEBUG)
|| tracing::event_enabled!(target: "toasty::query", tracing::Level::WARN);
if !config.params || !enabled {
return None;
}
let mut out = String::from("[");
for (i, value) in params.into_iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if i == MAX_PARAMS {
out.push_str("...");
break;
}
render_value(&mut out, value);
}
out.push(']');
Some(out)
}
fn render_value(out: &mut String, value: &Value) {
let _ = match value {
Value::Null => {
out.push_str("NULL");
Ok(())
}
Value::Bool(v) => write!(out, "{v}"),
Value::I8(v) => write!(out, "{v}"),
Value::I16(v) => write!(out, "{v}"),
Value::I32(v) => write!(out, "{v}"),
Value::I64(v) => write!(out, "{v}"),
Value::U8(v) => write!(out, "{v}"),
Value::U16(v) => write!(out, "{v}"),
Value::U32(v) => write!(out, "{v}"),
Value::U64(v) => write!(out, "{v}"),
Value::F32(v) => write!(out, "{v}"),
Value::F64(v) => write!(out, "{v}"),
Value::Uuid(v) => write!(out, "{v}"),
Value::String(s) => {
render_str(out, s);
Ok(())
}
Value::Bytes(b) => write!(out, "<{} bytes>", b.len()),
Value::List(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
if i == MAX_LIST_ITEMS {
out.push_str("...");
break;
}
render_value(out, item);
}
out.push(']');
Ok(())
}
other => write!(out, "{other:?}"),
};
}
fn render_str(out: &mut String, s: &str) {
let total = s.chars().count();
if total <= MAX_STR_LEN {
let _ = write!(out, "{s:?}");
} else {
let prefix: String = s.chars().take(MAX_STR_LEN).collect();
let _ = write!(out, "{prefix:?}...({total} chars)");
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rendered(value: Value) -> String {
let mut out = String::new();
render_value(&mut out, &value);
out
}
#[test]
fn renders_scalars_bare() {
assert_eq!(rendered(Value::Null), "NULL");
assert_eq!(rendered(Value::Bool(true)), "true");
assert_eq!(rendered(Value::I64(-42)), "-42");
assert_eq!(rendered(Value::F64(1.5)), "1.5");
}
#[test]
fn renders_strings_quoted_and_truncated() {
assert_eq!(rendered(Value::String("hi".into())), "\"hi\"");
let long = "x".repeat(500);
let out = rendered(Value::String(long));
assert!(out.starts_with('"'));
assert!(out.ends_with("...(500 chars)"));
assert!(out.len() < 200);
}
#[test]
fn renders_bytes_as_length() {
assert_eq!(rendered(Value::Bytes(vec![0; 16])), "<16 bytes>");
}
#[test]
fn caps_list_items() {
let list = Value::List((0..20).map(Value::I64).collect());
assert_eq!(rendered(list), "[0, 1, 2, 3, 4, 5, 6, 7, ...]");
}
}