#![cfg(feature = "sqlx")]
use moniof::core::stats::{QueryStatsHandle};
use moniof::core::task_ctx::MONIOF_HANDLE;
use moniof::instrumentation::sql_events::MOFSqlEvents;
use tracing_subscriber::{prelude::*, EnvFilter};
fn capture(f: impl FnOnce()) -> QueryStatsHandle {
capture_with(tracing_subscriber::registry().with(MOFSqlEvents::new()), f)
}
fn capture_with<S>(subscriber: S, f: impl FnOnce()) -> QueryStatsHandle
where
S: tracing::Subscriber + Send + Sync + 'static,
{
let handle = QueryStatsHandle::new();
let read = handle.clone();
tracing::subscriber::with_default(subscriber, || {
MONIOF_HANDLE.sync_scope(handle, f);
});
read
}
macro_rules! sqlx_long_query {
($level:expr, $sql:expr, $secs:expr) => {
tracing::event!(
target: "sqlx::query",
$level,
summary = "SELECT id, name, email …",
db.statement = concat!("\n\n", $sql, "\n"),
rows_affected = 0u64,
rows_returned = 0u64,
elapsed_secs = $secs,
)
};
}
macro_rules! sqlx_short_query {
($level:expr, $summary:expr, $secs:expr) => {
tracing::event!(
target: "sqlx::query",
$level,
summary = $summary,
db.statement = "",
rows_affected = 0u64,
rows_returned = 0u64,
elapsed_secs = $secs,
)
};
}
#[test]
fn long_query_is_counted_under_its_normalized_statement() {
let read = capture(|| {
sqlx_long_query!(
tracing::Level::INFO,
"SELECT id, name, email FROM users WHERE tenant = $1",
0.004f64
);
});
let s = read.0.lock();
assert_eq!(s.total, 1);
assert_eq!(s.per_key["sql/select id, name, email from users where tenant = $1"], 1);
}
#[test]
fn repeated_query_accumulates_its_count() {
let read = capture(|| {
for _ in 0..7 {
sqlx_long_query!(
tracing::Level::INFO,
"SELECT id, name, email FROM users WHERE tenant = $1",
0.004f64
);
}
});
let s = read.0.lock();
assert_eq!(s.total, 7);
assert_eq!(s.per_key.len(), 1, "one logical query must occupy one bucket");
}
#[test]
fn non_sqlx_events_are_ignored() {
let read = capture(|| {
tracing::info!(target: "my_app::handlers", "unrelated log line");
tracing::info!(target: "sqlx::pool", "pool acquired");
});
assert_eq!(read.0.lock().total, 0);
}
#[test]
#[ignore = "known bug: on_event never calls mark_latency; elapsed_secs is dropped"]
fn sql_latency_is_recorded_from_elapsed_secs() {
let read = capture(|| {
sqlx_long_query!(
tracing::Level::INFO,
"SELECT id, name, email FROM users WHERE tenant = $1",
0.004f64
);
sqlx_long_query!(
tracing::Level::INFO,
"SELECT id, name, email FROM users WHERE tenant = $1",
0.006f64
);
});
let s = read.0.lock();
assert_eq!(s.total_db_latency_ms, 10, "4ms + 6ms must accumulate");
assert_eq!(
s.per_key_latency_ms["sql/select id, name, email from users where tenant = $1"],
10
);
}
#[test]
#[ignore = "known bug: short queries all key to `sql/` because db.statement is empty"]
fn short_queries_do_not_collapse_into_one_key() {
let read = capture(|| {
sqlx_short_query!(tracing::Level::INFO, "SELECT id FROM users", 0.001f64);
sqlx_short_query!(tracing::Level::INFO, "SELECT id FROM orders", 0.001f64);
});
let s = read.0.lock();
assert_eq!(s.total, 2);
assert!(
!s.per_key.contains_key("sql/"),
"empty key means the query text was lost; got {:?}",
s.per_key
);
assert_eq!(s.per_key.len(), 2, "two distinct tables => two buckets, got {:?}", s.per_key);
assert_eq!(s.per_key["sql/select id from users"], 1);
assert_eq!(s.per_key["sql/select id from orders"], 1);
}
#[test]
#[ignore = "known bug: initiate() filters sqlx at info, but sqlx logs statements at debug"]
fn default_filter_observes_sqlx_statements_at_their_default_level() {
let filter = EnvFilter::new("moniof=debug,moniof::sql=debug,sqlx=info");
let subscriber = tracing_subscriber::registry()
.with(filter)
.with(MOFSqlEvents::new());
let read = capture_with(subscriber, || {
sqlx_long_query!(
tracing::Level::DEBUG,
"SELECT id, name, email FROM users WHERE tenant = $1",
0.004f64
);
});
assert_eq!(
read.0.lock().total,
1,
"a sqlx statement logged at its default DEBUG level must reach the layer"
);
}