use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, error, info, warn};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DbContext {
pub user_id: Option<Uuid>,
pub request_id: Option<String>,
pub session_id: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub custom_fields: HashMap<String, String>,
}
impl DbContext {
pub fn with_user_id(mut self, user_id: Uuid) -> Self {
self.user_id = Some(user_id);
self
}
pub fn with_request_id(mut self, request_id: String) -> Self {
self.request_id = Some(request_id);
self
}
pub fn with_session_id(mut self, session_id: String) -> Self {
self.session_id = Some(session_id);
self
}
pub fn with_ip_address(mut self, ip_address: String) -> Self {
self.ip_address = Some(ip_address);
self
}
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = Some(user_agent);
self
}
pub fn add_field(mut self, key: String, value: String) -> Self {
self.custom_fields.insert(key, value);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryLogEntry {
pub timestamp: DateTime<Utc>,
pub level: LogLevel,
pub operation: String,
pub table: String,
pub duration_ms: u64,
pub rows_affected: Option<u64>,
pub success: bool,
pub error: Option<String>,
pub context: DbContext,
pub sql: Option<String>,
}
impl QueryLogEntry {
pub fn new(operation: String, table: String, duration_ms: u64) -> Self {
Self {
timestamp: Utc::now(),
level: LogLevel::Info,
operation,
table,
duration_ms,
rows_affected: None,
success: true,
error: None,
context: DbContext::default(),
sql: None,
}
}
pub fn with_level(mut self, level: LogLevel) -> Self {
self.level = level;
self
}
pub fn with_rows_affected(mut self, rows: u64) -> Self {
self.rows_affected = Some(rows);
self
}
pub fn with_error(mut self, error: String) -> Self {
self.success = false;
self.error = Some(error);
self.level = LogLevel::Error;
self
}
pub fn with_context(mut self, context: DbContext) -> Self {
self.context = context;
self
}
pub fn with_sql(mut self, sql: String) -> Self {
self.sql = Some(sql);
self
}
pub fn log(&self) {
let user_id = self.context.user_id.map(|id| id.to_string());
let request_id = self.context.request_id.as_deref();
match self.level {
LogLevel::Debug => {
debug!(
operation = %self.operation,
table = %self.table,
duration_ms = self.duration_ms,
user_id = ?user_id,
request_id = ?request_id,
success = self.success,
"Database query executed"
);
}
LogLevel::Info => {
info!(
operation = %self.operation,
table = %self.table,
duration_ms = self.duration_ms,
user_id = ?user_id,
request_id = ?request_id,
success = self.success,
"Database query executed"
);
}
LogLevel::Warn => {
warn!(
operation = %self.operation,
table = %self.table,
duration_ms = self.duration_ms,
user_id = ?user_id,
request_id = ?request_id,
success = self.success,
error = ?self.error,
"Database query warning"
);
}
LogLevel::Error => {
error!(
operation = %self.operation,
table = %self.table,
duration_ms = self.duration_ms,
user_id = ?user_id,
request_id = ?request_id,
success = self.success,
error = ?self.error,
"Database query failed"
);
}
}
}
}
#[derive(Debug, Clone)]
pub struct AnomalyDetector {
baseline_avg_ms: f64,
std_dev_threshold: f64,
min_samples: usize,
}
impl Default for AnomalyDetector {
fn default() -> Self {
Self {
baseline_avg_ms: 100.0,
std_dev_threshold: 3.0,
min_samples: 10,
}
}
}
impl AnomalyDetector {
pub fn new(baseline_avg_ms: f64, std_dev_threshold: f64) -> Self {
Self {
baseline_avg_ms,
std_dev_threshold,
min_samples: 10,
}
}
pub fn is_anomalous(&self, duration_ms: u64, samples: &[u64]) -> bool {
if samples.len() < self.min_samples {
return duration_ms as f64 > self.baseline_avg_ms * 2.0;
}
let mean = samples.iter().sum::<u64>() as f64 / samples.len() as f64;
let variance = samples
.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>()
/ samples.len() as f64;
let std_dev = variance.sqrt();
duration_ms as f64 > mean + (self.std_dev_threshold * std_dev)
}
pub fn update_baseline(&mut self, samples: &[u64]) {
if samples.is_empty() {
return;
}
self.baseline_avg_ms = samples.iter().sum::<u64>() as f64 / samples.len() as f64;
}
}
#[derive(Debug)]
pub struct PerformanceTracker {
samples: Arc<RwLock<HashMap<String, Vec<u64>>>>,
detector: AnomalyDetector,
}
impl Default for PerformanceTracker {
fn default() -> Self {
Self::new(AnomalyDetector::default())
}
}
impl PerformanceTracker {
pub fn new(detector: AnomalyDetector) -> Self {
Self {
samples: Arc::new(RwLock::new(HashMap::new())),
detector,
}
}
pub fn record(&self, operation: &str, table: &str, duration_ms: u64) {
let key = format!("{}:{}", operation, table);
let mut samples = self.samples.write();
let entry = samples.entry(key).or_default();
if entry.len() >= 100 {
entry.remove(0);
}
entry.push(duration_ms);
}
pub fn check_anomaly(&self, operation: &str, table: &str, duration_ms: u64) -> bool {
let key = format!("{}:{}", operation, table);
let samples = self.samples.read();
if let Some(history) = samples.get(&key) {
self.detector.is_anomalous(duration_ms, history)
} else {
false
}
}
pub fn get_stats(&self, operation: &str, table: &str) -> Option<QueryStats> {
let key = format!("{}:{}", operation, table);
let samples = self.samples.read();
samples.get(&key).map(|history| {
let count = history.len();
let sum: u64 = history.iter().sum();
let avg = if count > 0 { sum / count as u64 } else { 0 };
let min = history.iter().min().copied().unwrap_or(0);
let max = history.iter().max().copied().unwrap_or(0);
QueryStats {
operation: operation.to_string(),
table: table.to_string(),
count,
avg_duration_ms: avg,
min_duration_ms: min,
max_duration_ms: max,
}
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryStats {
pub operation: String,
pub table: String,
pub count: usize,
pub avg_duration_ms: u64,
pub min_duration_ms: u64,
pub max_duration_ms: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_db_context_builder() {
let user_id = Uuid::new_v4();
let context = DbContext::default()
.with_user_id(user_id)
.with_request_id("req-123".to_string())
.with_session_id("sess-456".to_string())
.with_ip_address("127.0.0.1".to_string())
.with_user_agent("test-agent".to_string())
.add_field("custom".to_string(), "value".to_string());
assert_eq!(context.user_id, Some(user_id));
assert_eq!(context.request_id, Some("req-123".to_string()));
assert_eq!(context.session_id, Some("sess-456".to_string()));
assert_eq!(context.ip_address, Some("127.0.0.1".to_string()));
assert_eq!(context.user_agent, Some("test-agent".to_string()));
assert_eq!(
context.custom_fields.get("custom"),
Some(&"value".to_string())
);
}
#[test]
fn test_query_log_entry() {
let entry = QueryLogEntry::new("SELECT".to_string(), "users".to_string(), 50)
.with_rows_affected(10)
.with_sql("SELECT * FROM users".to_string());
assert_eq!(entry.operation, "SELECT");
assert_eq!(entry.table, "users");
assert_eq!(entry.duration_ms, 50);
assert_eq!(entry.rows_affected, Some(10));
assert!(entry.success);
assert_eq!(entry.sql, Some("SELECT * FROM users".to_string()));
}
#[test]
fn test_query_log_entry_with_error() {
let entry = QueryLogEntry::new("INSERT".to_string(), "users".to_string(), 100)
.with_error("Constraint violation".to_string());
assert!(!entry.success);
assert_eq!(entry.error, Some("Constraint violation".to_string()));
assert_eq!(entry.level, LogLevel::Error);
}
#[test]
fn test_anomaly_detector_with_insufficient_samples() {
let detector = AnomalyDetector::default();
let samples = vec![100, 110, 105];
assert!(!detector.is_anomalous(150, &samples));
assert!(detector.is_anomalous(300, &samples));
}
#[test]
fn test_anomaly_detector_with_sufficient_samples() {
let detector = AnomalyDetector::new(100.0, 3.0);
let samples: Vec<u64> = vec![95, 100, 105, 98, 102, 99, 101, 103, 97, 100, 104, 96];
assert!(!detector.is_anomalous(105, &samples));
assert!(detector.is_anomalous(500, &samples));
}
#[test]
fn test_anomaly_detector_update_baseline() {
let mut detector = AnomalyDetector::default();
let samples = vec![200, 210, 205, 215];
detector.update_baseline(&samples);
assert!((detector.baseline_avg_ms - 207.5).abs() < 0.1);
}
#[test]
fn test_performance_tracker_record() {
let tracker = PerformanceTracker::default();
tracker.record("SELECT", "users", 100);
tracker.record("SELECT", "users", 110);
tracker.record("SELECT", "users", 105);
let stats = tracker.get_stats("SELECT", "users").unwrap();
assert_eq!(stats.count, 3);
assert_eq!(stats.min_duration_ms, 100);
assert_eq!(stats.max_duration_ms, 110);
}
#[test]
fn test_performance_tracker_check_anomaly() {
let tracker = PerformanceTracker::default();
for i in 0..20 {
tracker.record("SELECT", "users", 95 + (i % 10) as u64);
}
assert!(!tracker.check_anomaly("SELECT", "users", 105));
assert!(tracker.check_anomaly("SELECT", "users", 1000));
}
#[test]
fn test_performance_tracker_max_samples() {
let tracker = PerformanceTracker::default();
for i in 0..150 {
tracker.record("SELECT", "users", i);
}
let stats = tracker.get_stats("SELECT", "users").unwrap();
assert_eq!(stats.count, 100); }
#[test]
fn test_query_log_entry_log() {
let entry = QueryLogEntry::new("SELECT".to_string(), "users".to_string(), 50);
entry.log();
}
}