use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use dashmap::DashMap;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct QueryMetrics {
pub table: String,
pub operation: &'static str,
pub rows_scanned: usize,
pub rows_returned: usize,
pub index_used: Option<String>,
pub duration: Duration,
}
impl QueryMetrics {
#[must_use]
pub fn new(table: impl Into<String>, operation: &'static str) -> Self {
Self {
table: table.into(),
operation,
rows_scanned: 0,
rows_returned: 0,
index_used: None,
duration: Duration::ZERO,
}
}
#[must_use]
pub const fn with_rows_scanned(mut self, count: usize) -> Self {
self.rows_scanned = count;
self
}
#[must_use]
pub const fn with_rows_returned(mut self, count: usize) -> Self {
self.rows_returned = count;
self
}
#[must_use]
pub fn with_index(mut self, index: impl Into<String>) -> Self {
self.index_used = Some(index.into());
self
}
#[must_use]
pub const fn with_duration(mut self, duration: Duration) -> Self {
self.duration = duration;
self
}
}
#[derive(Debug, Clone)]
pub struct IndexMissReport {
pub table: String,
pub column: String,
pub miss_count: u64,
pub hit_count: u64,
}
#[derive(Debug, Default)]
pub struct IndexTracker {
hits: DashMap<(String, String), AtomicU64>,
misses: DashMap<(String, String), AtomicU64>,
}
impl IndexTracker {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_hit(&self, table: &str, column: &str) {
let key = (table.to_string(), column.to_string());
self.hits
.entry(key)
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
pub fn record_miss(&self, table: &str, column: &str) {
let key = (table.to_string(), column.to_string());
self.misses
.entry(key)
.or_insert_with(|| AtomicU64::new(0))
.fetch_add(1, Ordering::Relaxed);
}
#[must_use]
pub fn report_misses(&self) -> Vec<IndexMissReport> {
self.misses
.iter()
.map(|entry| {
let (table, column) = entry.key();
let miss_count = entry.value().load(Ordering::Relaxed);
let hit_count = self
.hits
.get(&(table.clone(), column.clone()))
.map_or(0, |v| v.load(Ordering::Relaxed));
IndexMissReport {
table: table.clone(),
column: column.clone(),
miss_count,
hit_count,
}
})
.filter(|r| r.miss_count > 0)
.collect()
}
#[must_use]
pub fn total_hits(&self) -> u64 {
self.hits
.iter()
.map(|entry| entry.value().load(Ordering::Relaxed))
.sum()
}
#[must_use]
pub fn total_misses(&self) -> u64 {
self.misses
.iter()
.map(|entry| entry.value().load(Ordering::Relaxed))
.sum()
}
pub fn reset(&self) {
self.hits.clear();
self.misses.clear();
}
}
pub fn check_slow_query(metrics: &QueryMetrics, threshold_ms: u64) {
let duration_ms = metrics.duration.as_millis();
if duration_ms > u128::from(threshold_ms) {
warn!(
table = %metrics.table,
operation = %metrics.operation,
duration_ms = %duration_ms,
rows_scanned = %metrics.rows_scanned,
rows_returned = %metrics.rows_returned,
index_used = ?metrics.index_used,
"slow query detected"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_query_metrics_builder() {
let metrics = QueryMetrics::new("users", "select")
.with_rows_scanned(1000)
.with_rows_returned(50)
.with_index("idx_user_id")
.with_duration(Duration::from_millis(25));
assert_eq!(metrics.table, "users");
assert_eq!(metrics.operation, "select");
assert_eq!(metrics.rows_scanned, 1000);
assert_eq!(metrics.rows_returned, 50);
assert_eq!(metrics.index_used, Some("idx_user_id".to_string()));
assert_eq!(metrics.duration, Duration::from_millis(25));
}
#[test]
fn test_index_tracker_hits() {
let tracker = IndexTracker::new();
tracker.record_hit("users", "id");
tracker.record_hit("users", "id");
tracker.record_hit("users", "email");
assert_eq!(tracker.total_hits(), 3);
assert_eq!(tracker.total_misses(), 0);
}
#[test]
fn test_index_tracker_misses() {
let tracker = IndexTracker::new();
tracker.record_miss("users", "name");
tracker.record_miss("users", "name");
tracker.record_miss("orders", "status");
assert_eq!(tracker.total_misses(), 3);
let reports = tracker.report_misses();
assert_eq!(reports.len(), 2);
}
#[test]
fn test_index_tracker_mixed() {
let tracker = IndexTracker::new();
tracker.record_hit("users", "id");
tracker.record_hit("users", "id");
tracker.record_miss("users", "id");
let reports = tracker.report_misses();
assert_eq!(reports.len(), 1);
let report = &reports[0];
assert_eq!(report.table, "users");
assert_eq!(report.column, "id");
assert_eq!(report.hit_count, 2);
assert_eq!(report.miss_count, 1);
}
#[test]
fn test_index_tracker_reset() {
let tracker = IndexTracker::new();
tracker.record_hit("users", "id");
tracker.record_miss("users", "name");
assert_eq!(tracker.total_hits(), 1);
assert_eq!(tracker.total_misses(), 1);
tracker.reset();
assert_eq!(tracker.total_hits(), 0);
assert_eq!(tracker.total_misses(), 0);
}
#[test]
fn test_check_slow_query_below_threshold() {
let metrics = QueryMetrics::new("users", "select").with_duration(Duration::from_millis(50));
check_slow_query(&metrics, 100);
}
#[test]
fn test_check_slow_query_above_threshold() {
let metrics = QueryMetrics::new("users", "select")
.with_rows_scanned(10000)
.with_duration(Duration::from_millis(150));
check_slow_query(&metrics, 100);
}
}