use std::time::Duration;
pub(crate) async fn timed<F, T>(threshold: Option<Duration>, sql: &str, future: F) -> T
where
F: std::future::Future<Output = T>,
{
#[cfg(not(feature = "tracing"))]
{
let _ = (threshold, sql);
future.await
}
#[cfg(feature = "tracing")]
{
let started = std::time::Instant::now();
let out = future.await;
warn_if_slow(threshold, started.elapsed(), sql);
out
}
}
#[cfg(feature = "tracing")]
pub(crate) const SQL_HEAD_CHARS: usize = 200;
#[cfg(feature = "tracing")]
fn warn_if_slow(threshold: Option<Duration>, elapsed: Duration, sql: &str) {
let Some(threshold) = threshold else {
return;
};
if elapsed < threshold {
return;
}
tracing::warn!(
elapsed_ms = elapsed.as_millis() as u64,
sql_chars = sql.chars().count(),
sql = head_of(sql).as_ref(),
"慢查询:{elapsed:?} 超过阈值 {threshold:?}"
);
}
#[cfg(feature = "tracing")]
fn head_of(sql: &str) -> std::borrow::Cow<'_, str> {
if sql.len() <= SQL_HEAD_CHARS || sql.chars().count() <= SQL_HEAD_CHARS {
return std::borrow::Cow::Borrowed(sql);
}
std::borrow::Cow::Owned(sql.chars().take(SQL_HEAD_CHARS).collect())
}
#[cfg(all(test, feature = "tracing"))]
mod tests {
use super::*;
use crate::SqlxConfig;
use ecat_data::SqlExecutor;
use std::io;
use std::sync::{Arc, Mutex};
#[derive(Clone, Default)]
struct Capture(Arc<Mutex<Vec<u8>>>);
impl Capture {
fn text(&self) -> String {
String::from_utf8_lossy(&self.0.lock().unwrap()).to_string()
}
}
impl io::Write for Capture {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for Capture {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
fn capture_logs() -> (Capture, tracing::subscriber::DefaultGuard) {
let capture = Capture::default();
let subscriber = tracing_subscriber::fmt()
.with_writer(capture.clone())
.with_ansi(false)
.finish();
(capture, tracing::subscriber::set_default(subscriber))
}
#[test]
fn head_of_keeps_short_sql_verbatim() {
let sql = "SELECT 1";
assert!(matches!(head_of(sql), std::borrow::Cow::Borrowed(_)));
assert_eq!(head_of(sql), sql);
}
#[test]
fn head_of_counts_chars_not_bytes() {
let at_limit = "中".repeat(SQL_HEAD_CHARS);
assert!(at_limit.len() > SQL_HEAD_CHARS, "前提:字节数超限");
assert!(
matches!(head_of(&at_limit), std::borrow::Cow::Borrowed(_)),
"字符数刚好等于上限,不该截断"
);
assert_eq!(head_of(&at_limit), at_limit);
let longer = at_limit.clone() + "再多一个字";
let head = head_of(&longer);
assert_eq!(head.chars().count(), SQL_HEAD_CHARS, "截断按字符数算");
assert_eq!(head, at_limit, "截出来的必须是完整字符,不是切开的半个");
}
#[test]
fn head_of_truncates_long_sql() {
let sql = "SELECT ".to_string() + &"x".repeat(1000);
let head = head_of(&sql);
assert_eq!(head.chars().count(), SQL_HEAD_CHARS);
assert!(sql.starts_with(head.as_ref()));
}
#[tokio::test]
async fn warn_respects_threshold() {
let (capture, _guard) = capture_logs();
let sql = "SELECT 1";
let threshold = Duration::from_millis(50);
warn_if_slow(None, Duration::from_secs(10), sql);
assert_eq!(capture.text(), "", "未配置阈值不该打");
warn_if_slow(Some(threshold), Duration::from_millis(49), sql);
assert_eq!(capture.text(), "", "没到阈值不该打");
warn_if_slow(Some(threshold), threshold, sql);
assert!(capture.text().contains("慢查询"), "等于阈值应当打");
}
#[tokio::test]
async fn slow_query_warns_through_the_client() {
let cfg: SqlxConfig =
serde_json::from_str(r#"{"url": "sqlite::memory:", "slow_query_ms": 1}"#).unwrap();
let client = crate::SqlxClient::from_config(cfg).await.unwrap();
let (capture, _guard) = capture_logs();
client.query(&slow_sql()).await.unwrap();
let log = capture.text();
assert!(log.contains("慢查询"), "没打出慢查询 warn,实际:\n{log}");
assert!(log.contains("sql_chars="), "warn 要带 SQL 总长度:\n{log}");
assert!(
log.contains(&"n".repeat(100)),
"warn 要带截断后的 SQL 原文:\n{log}"
);
assert!(
!log.contains(&"n".repeat(300)),
"SQL 必须截断(300 个 n 是原文里的填充):\n{log}"
);
}
#[tokio::test]
async fn fast_query_stays_quiet() {
let cfg: SqlxConfig =
serde_json::from_str(r#"{"url": "sqlite::memory:", "slow_query_ms": 600000}"#).unwrap();
let client = crate::SqlxClient::from_config(cfg).await.unwrap();
let (capture, _guard) = capture_logs();
client.query("SELECT 1").await.unwrap();
assert_eq!(capture.text(), "", "阈值远高于实际耗时,不该打");
}
fn slow_sql() -> String {
format!(
"WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x < 200000) \
SELECT /* {} */ count(*) FROM c",
"n".repeat(300)
)
}
}