use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use parking_lot::RwLock;
use serde::Serialize;
use tracing::{info, warn};
#[derive(Debug, Clone)]
pub struct QueryLogConfig {
pub slow_query_threshold_ms: u64,
pub log_all_queries: bool,
pub enable_stats: bool,
pub max_slow_query_history: usize,
}
impl Default for QueryLogConfig {
fn default() -> Self {
Self {
slow_query_threshold_ms: 100, log_all_queries: false,
enable_stats: true,
max_slow_query_history: 1000,
}
}
}
impl QueryLogConfig {
pub fn development() -> Self {
Self {
slow_query_threshold_ms: 50,
log_all_queries: true,
enable_stats: true,
max_slow_query_history: 100,
}
}
pub fn production() -> Self {
Self {
slow_query_threshold_ms: 200,
log_all_queries: false,
enable_stats: true,
max_slow_query_history: 500,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct SlowQueryRecord {
pub query_hash: u64,
pub query_preview: String,
pub duration_ms: u64,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub location: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct QueryStats {
pub query_hash: u64,
pub query_preview: String,
pub call_count: u64,
pub total_duration_ms: u64,
pub avg_duration_ms: f64,
pub max_duration_ms: u64,
pub min_duration_ms: u64,
pub slow_count: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct AggregateStats {
pub total_queries: u64,
pub total_slow_queries: u64,
pub total_duration_ms: u64,
pub avg_duration_ms: f64,
pub queries_per_second: f64,
pub uptime_seconds: u64,
}
pub struct QueryLogger {
config: QueryLogConfig,
stats: Arc<RwLock<HashMap<u64, QueryStatsInner>>>,
slow_queries: Arc<RwLock<Vec<SlowQueryRecord>>>,
total_queries: AtomicU64,
total_slow: AtomicU64,
total_duration_ms: AtomicU64,
start_time: Instant,
}
#[derive(Debug, Clone)]
struct QueryStatsInner {
query_preview: String,
call_count: u64,
total_duration_ms: u64,
max_duration_ms: u64,
min_duration_ms: u64,
slow_count: u64,
}
impl QueryLogger {
pub fn new(config: QueryLogConfig) -> Self {
Self {
config,
stats: Arc::new(RwLock::new(HashMap::new())),
slow_queries: Arc::new(RwLock::new(Vec::new())),
total_queries: AtomicU64::new(0),
total_slow: AtomicU64::new(0),
total_duration_ms: AtomicU64::new(0),
start_time: Instant::now(),
}
}
pub fn with_default_config() -> Self {
Self::new(QueryLogConfig::default())
}
pub fn log_query(&self, query: &str, duration: Duration, location: Option<&str>) {
let duration_ms = duration.as_millis() as u64;
let query_hash = hash_query(query);
let query_preview = truncate_query(query, 200);
self.total_queries.fetch_add(1, Ordering::Relaxed);
self.total_duration_ms
.fetch_add(duration_ms, Ordering::Relaxed);
let is_slow = duration_ms >= self.config.slow_query_threshold_ms;
if is_slow {
self.total_slow.fetch_add(1, Ordering::Relaxed);
warn!(
query = %query_preview,
duration_ms = duration_ms,
location = ?location,
"Slow query detected"
);
let mut slow_queries = self.slow_queries.write();
slow_queries.push(SlowQueryRecord {
query_hash,
query_preview: query_preview.clone(),
duration_ms,
timestamp: chrono::Utc::now(),
location: location.map(|s| s.to_string()),
});
if slow_queries.len() > self.config.max_slow_query_history {
slow_queries.remove(0);
}
} else if self.config.log_all_queries {
info!(
query = %query_preview,
duration_ms = duration_ms,
"Query executed"
);
}
if self.config.enable_stats {
let mut stats = self.stats.write();
let entry = stats.entry(query_hash).or_insert_with(|| QueryStatsInner {
query_preview,
call_count: 0,
total_duration_ms: 0,
max_duration_ms: 0,
min_duration_ms: u64::MAX,
slow_count: 0,
});
entry.call_count += 1;
entry.total_duration_ms += duration_ms;
entry.max_duration_ms = entry.max_duration_ms.max(duration_ms);
entry.min_duration_ms = entry.min_duration_ms.min(duration_ms);
if is_slow {
entry.slow_count += 1;
}
}
}
pub fn get_stats(&self) -> Vec<QueryStats> {
let stats = self.stats.read();
stats
.iter()
.map(|(hash, inner)| QueryStats {
query_hash: *hash,
query_preview: inner.query_preview.clone(),
call_count: inner.call_count,
total_duration_ms: inner.total_duration_ms,
avg_duration_ms: inner.total_duration_ms as f64 / inner.call_count.max(1) as f64,
max_duration_ms: inner.max_duration_ms,
min_duration_ms: if inner.min_duration_ms == u64::MAX {
0
} else {
inner.min_duration_ms
},
slow_count: inner.slow_count,
})
.collect()
}
pub fn get_slowest_queries(&self, n: usize) -> Vec<QueryStats> {
let mut stats = self.get_stats();
stats.sort_by(|a, b| b.avg_duration_ms.partial_cmp(&a.avg_duration_ms).unwrap());
stats.truncate(n);
stats
}
pub fn get_most_frequent_queries(&self, n: usize) -> Vec<QueryStats> {
let mut stats = self.get_stats();
stats.sort_by(|a, b| b.call_count.cmp(&a.call_count));
stats.truncate(n);
stats
}
pub fn get_slow_query_history(&self, limit: usize) -> Vec<SlowQueryRecord> {
let slow_queries = self.slow_queries.read();
slow_queries.iter().rev().take(limit).cloned().collect()
}
pub fn get_aggregate_stats(&self) -> AggregateStats {
let total_queries = self.total_queries.load(Ordering::Relaxed);
let total_slow = self.total_slow.load(Ordering::Relaxed);
let total_duration = self.total_duration_ms.load(Ordering::Relaxed);
let uptime = self.start_time.elapsed().as_secs();
AggregateStats {
total_queries,
total_slow_queries: total_slow,
total_duration_ms: total_duration,
avg_duration_ms: total_duration as f64 / total_queries.max(1) as f64,
queries_per_second: total_queries as f64 / uptime.max(1) as f64,
uptime_seconds: uptime,
}
}
pub fn reset_stats(&self) {
self.stats.write().clear();
self.slow_queries.write().clear();
self.total_queries.store(0, Ordering::Relaxed);
self.total_slow.store(0, Ordering::Relaxed);
self.total_duration_ms.store(0, Ordering::Relaxed);
}
pub fn is_slow(&self, duration: Duration) -> bool {
duration.as_millis() as u64 >= self.config.slow_query_threshold_ms
}
}
fn hash_query(query: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
let normalized: String = query.split_whitespace().collect::<Vec<_>>().join(" ");
normalized.hash(&mut hasher);
hasher.finish()
}
fn truncate_query(query: &str, max_len: usize) -> String {
let normalized: String = query.split_whitespace().collect::<Vec<_>>().join(" ");
if normalized.len() <= max_len {
normalized
} else {
format!("{}...", &normalized[..max_len])
}
}
pub struct QueryTimer<'a> {
logger: &'a QueryLogger,
query: String,
location: Option<String>,
start: Instant,
}
impl<'a> QueryTimer<'a> {
pub fn new(logger: &'a QueryLogger, query: impl Into<String>) -> Self {
Self {
logger,
query: query.into(),
location: None,
start: Instant::now(),
}
}
pub fn with_location(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
pub fn finish(self) {
}
}
impl Drop for QueryTimer<'_> {
fn drop(&mut self) {
let duration = self.start.elapsed();
self.logger
.log_query(&self.query, duration, self.location.as_deref());
}
}
#[inline]
pub fn time_query(logger: &QueryLogger, query: impl Into<String>) -> QueryTimer<'_> {
QueryTimer::new(logger, query)
}