use parking_lot::RwLock;
use std::collections::VecDeque;
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum LogLevel {
#[default]
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
impl LogLevel {
pub fn parse_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"off" | "none" | "0" => Self::Off,
"error" | "1" => Self::Error,
"warn" | "warning" | "2" => Self::Warn,
"info" | "3" => Self::Info,
"debug" | "4" => Self::Debug,
"trace" | "all" | "5" => Self::Trace,
_ => Self::Off,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Off => "OFF",
Self::Error => "ERROR",
Self::Warn => "WARN",
Self::Info => "INFO",
Self::Debug => "DEBUG",
Self::Trace => "TRACE",
}
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryOperation {
Select,
Insert,
Update,
Delete,
Raw,
Transaction,
Unknown,
}
impl QueryOperation {
pub fn from_sql(sql: &str) -> Self {
let sql_upper = sql.trim().to_uppercase();
if sql_upper.starts_with("SELECT") {
Self::Select
} else if sql_upper.starts_with("INSERT") {
Self::Insert
} else if sql_upper.starts_with("UPDATE") {
Self::Update
} else if sql_upper.starts_with("DELETE") {
Self::Delete
} else if sql_upper.starts_with("BEGIN")
|| sql_upper.starts_with("COMMIT")
|| sql_upper.starts_with("ROLLBACK")
{
Self::Transaction
} else {
Self::Unknown
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Select => "SELECT",
Self::Insert => "INSERT",
Self::Update => "UPDATE",
Self::Delete => "DELETE",
Self::Raw => "RAW",
Self::Transaction => "TRANSACTION",
Self::Unknown => "UNKNOWN",
}
}
}
impl fmt::Display for QueryOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct QueryLogEntry {
pub sql: String,
pub params: Vec<String>,
pub operation: QueryOperation,
pub table: Option<String>,
pub duration: Option<Duration>,
pub rows: Option<u64>,
pub success: bool,
pub error: Option<String>,
pub timestamp: std::time::SystemTime,
}
impl QueryLogEntry {
pub fn new(sql: impl Into<String>) -> Self {
let sql = sql.into();
let operation = QueryOperation::from_sql(&sql);
Self {
sql,
params: Vec::new(),
operation,
table: None,
duration: None,
rows: None,
success: true,
error: None,
timestamp: std::time::SystemTime::now(),
}
}
pub fn with_params(mut self, params: Vec<String>) -> Self {
self.params = params;
self
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = Some(table.into());
self
}
pub fn with_duration(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
pub fn with_rows(mut self, rows: u64) -> Self {
self.rows = Some(rows);
self
}
pub fn with_error(mut self, error: impl Into<String>) -> Self {
self.success = false;
self.error = Some(error.into());
self
}
pub fn is_slow(&self, threshold_ms: u64) -> bool {
self.duration
.map(|d| d.as_millis() as u64 >= threshold_ms)
.unwrap_or(false)
}
pub fn format_console(&self) -> String {
let mut output = format!("[TIDE][{}]", self.operation);
if let Some(ref table) = self.table {
output.push_str(&format!(" {}", table));
}
if let Some(duration) = self.duration {
output.push_str(&format!(" ({}ms)", duration.as_millis()));
}
if let Some(rows) = self.rows {
output.push_str(&format!(" [{} rows]", rows));
}
if !self.success {
output.push_str(" FAILED");
if let Some(ref err) = self.error {
output.push_str(&format!(": {}", err));
}
}
output.push_str(&format!("\n SQL: {}", self.sql));
if !self.params.is_empty() {
output.push_str(&format!("\n Params: {:?}", self.params));
}
output
}
}
pub struct QueryTimer {
start: Instant,
sql: String,
table: Option<String>,
}
impl QueryTimer {
pub fn start(sql: impl Into<String>) -> Self {
Self {
start: Instant::now(),
sql: sql.into(),
table: None,
}
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = Some(table.into());
self
}
pub fn stop(&self) -> Duration {
self.start.elapsed()
}
pub fn finish(self) -> QueryLogEntry {
let duration = self.start.elapsed();
let mut entry = QueryLogEntry::new(self.sql).with_duration(duration);
if let Some(table) = self.table {
entry = entry.with_table(table);
}
entry
}
pub fn finish_with_rows(self, rows: u64) -> QueryLogEntry {
self.finish().with_rows(rows)
}
pub fn finish_with_error(self, error: impl Into<String>) -> QueryLogEntry {
self.finish().with_error(error)
}
}
static LOGGER_ENABLED: AtomicBool = AtomicBool::new(false);
static LOGGER_TIMING: AtomicBool = AtomicBool::new(true);
static SLOW_QUERY_THRESHOLD_MS: AtomicU64 = AtomicU64::new(100);
static QUERY_COUNT: AtomicU64 = AtomicU64::new(0);
static SLOW_QUERY_COUNT: AtomicU64 = AtomicU64::new(0);
static TOTAL_QUERY_TIME_MS: AtomicU64 = AtomicU64::new(0);
static LOG_LEVEL: RwLock<LogLevel> = RwLock::new(LogLevel::Off);
static QUERY_HISTORY: RwLock<VecDeque<QueryLogEntry>> = RwLock::new(VecDeque::new());
static HISTORY_LIMIT: RwLock<usize> = RwLock::new(100);
pub struct QueryLogger;
impl QueryLogger {
pub fn global() -> QueryLoggerBuilder {
QueryLoggerBuilder::new()
}
pub fn enable() {
LOGGER_ENABLED.store(true, Ordering::SeqCst);
}
pub fn disable() {
LOGGER_ENABLED.store(false, Ordering::SeqCst);
}
pub fn is_enabled() -> bool {
LOGGER_ENABLED.load(Ordering::SeqCst)
}
pub fn level() -> LogLevel {
*LOG_LEVEL.read()
}
pub fn set_level(level: LogLevel) {
*LOG_LEVEL.write() = level;
}
pub fn log(entry: QueryLogEntry) {
if !Self::is_enabled() {
return;
}
let level = Self::level();
if level == LogLevel::Off {
return;
}
QUERY_COUNT.fetch_add(1, Ordering::SeqCst);
if let Some(duration) = entry.duration {
TOTAL_QUERY_TIME_MS.fetch_add(duration.as_millis() as u64, Ordering::SeqCst);
}
let threshold = SLOW_QUERY_THRESHOLD_MS.load(Ordering::SeqCst);
let is_slow = entry.is_slow(threshold);
if is_slow {
SLOW_QUERY_COUNT.fetch_add(1, Ordering::SeqCst);
}
{
let mut history = QUERY_HISTORY.write();
let limit = *HISTORY_LIMIT.read();
if limit == 0 {
history.clear();
} else {
while history.len() >= limit {
history.pop_front();
}
history.push_back(entry.clone());
}
}
let should_log = match level {
LogLevel::Off => false,
LogLevel::Error => !entry.success,
LogLevel::Warn => !entry.success || is_slow,
LogLevel::Info => !entry.success || is_slow,
LogLevel::Debug => true,
LogLevel::Trace => true,
};
if should_log {
let output = if level == LogLevel::Trace {
entry.format_console()
} else if level >= LogLevel::Debug {
format_debug(&entry)
} else if is_slow {
format_slow(&entry, threshold)
} else {
format_error(&entry)
};
eprintln!("{}", output);
}
}
pub fn log_timed(sql: impl Into<String>, duration: Duration) {
if !Self::is_enabled() {
return;
}
let entry = QueryLogEntry::new(sql).with_duration(duration);
Self::log(entry);
}
pub fn log_error(sql: impl Into<String>, error: impl Into<String>) {
if !Self::is_enabled() {
return;
}
let entry = QueryLogEntry::new(sql).with_error(error);
Self::log(entry);
}
pub fn stats() -> QueryStats {
QueryStats {
total_queries: QUERY_COUNT.load(Ordering::SeqCst),
slow_queries: SLOW_QUERY_COUNT.load(Ordering::SeqCst),
total_time_ms: TOTAL_QUERY_TIME_MS.load(Ordering::SeqCst),
threshold_ms: SLOW_QUERY_THRESHOLD_MS.load(Ordering::SeqCst),
}
}
pub fn reset_stats() {
QUERY_COUNT.store(0, Ordering::SeqCst);
SLOW_QUERY_COUNT.store(0, Ordering::SeqCst);
TOTAL_QUERY_TIME_MS.store(0, Ordering::SeqCst);
}
pub fn history() -> Vec<QueryLogEntry> {
QUERY_HISTORY.read().iter().cloned().collect()
}
pub fn clear_history() {
QUERY_HISTORY.write().clear();
}
pub fn slow_queries() -> Vec<QueryLogEntry> {
let threshold = SLOW_QUERY_THRESHOLD_MS.load(Ordering::SeqCst);
QUERY_HISTORY
.read()
.iter()
.filter(|e| e.is_slow(threshold))
.cloned()
.collect()
}
pub fn init_from_env() {
if let Ok(val) = std::env::var("TIDE_LOG_QUERIES") {
if val == "1" || val.to_lowercase() == "true" {
LOGGER_ENABLED.store(true, Ordering::SeqCst);
}
}
if let Ok(val) = std::env::var("TIDE_LOG_LEVEL") {
let level = LogLevel::parse_str(&val);
*LOG_LEVEL.write() = level;
if level != LogLevel::Off {
LOGGER_ENABLED.store(true, Ordering::SeqCst);
}
}
if let Ok(val) = std::env::var("TIDE_SLOW_QUERY_MS") {
if let Ok(ms) = val.parse::<u64>() {
SLOW_QUERY_THRESHOLD_MS.store(ms, Ordering::SeqCst);
}
}
}
}
pub struct QueryLoggerBuilder {
level: Option<LogLevel>,
timing: Option<bool>,
threshold_ms: Option<u64>,
history_limit: Option<usize>,
}
impl QueryLoggerBuilder {
fn new() -> Self {
Self {
level: None,
timing: None,
threshold_ms: None,
history_limit: None,
}
}
pub fn set_level(mut self, level: LogLevel) -> Self {
self.level = Some(level);
self
}
pub fn enable_timing(mut self, enable: bool) -> Self {
self.timing = Some(enable);
self
}
pub fn set_slow_query_threshold_ms(mut self, ms: u64) -> Self {
self.threshold_ms = Some(ms);
self
}
pub fn set_history_limit(mut self, limit: usize) -> Self {
self.history_limit = Some(limit);
self
}
pub fn enable(self) {
if let Some(level) = self.level {
*LOG_LEVEL.write() = level;
}
if let Some(timing) = self.timing {
LOGGER_TIMING.store(timing, Ordering::SeqCst);
}
if let Some(ms) = self.threshold_ms {
SLOW_QUERY_THRESHOLD_MS.store(ms, Ordering::SeqCst);
}
if let Some(limit) = self.history_limit {
*HISTORY_LIMIT.write() = limit;
}
LOGGER_ENABLED.store(true, Ordering::SeqCst);
}
pub fn disable(self) {
LOGGER_ENABLED.store(false, Ordering::SeqCst);
}
}
#[derive(Debug, Clone, Copy)]
pub struct QueryStats {
pub total_queries: u64,
pub slow_queries: u64,
pub total_time_ms: u64,
pub threshold_ms: u64,
}
impl QueryStats {
pub fn avg_query_time_ms(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
self.total_time_ms as f64 / self.total_queries as f64
}
}
pub fn slow_query_percentage(&self) -> f64 {
if self.total_queries == 0 {
0.0
} else {
(self.slow_queries as f64 / self.total_queries as f64) * 100.0
}
}
}
impl fmt::Display for QueryStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "═══════════════════════════════════════════════════")?;
writeln!(f, "TIDEORM QUERY STATISTICS")?;
writeln!(f, "═══════════════════════════════════════════════════")?;
writeln!(f, "Total Queries: {}", self.total_queries)?;
writeln!(
f,
"Slow Queries: {} ({:.1}%)",
self.slow_queries,
self.slow_query_percentage()
)?;
writeln!(f, "Total Time: {}ms", self.total_time_ms)?;
writeln!(f, "Avg Query Time: {:.2}ms", self.avg_query_time_ms())?;
writeln!(f, "Slow Threshold: {}ms", self.threshold_ms)?;
write!(f, "═══════════════════════════════════════════════════")
}
}
#[derive(Debug, Clone)]
pub struct QueryDebugInfo {
pub table: String,
pub operation: QueryOperation,
pub conditions: Vec<String>,
pub order_by: Vec<String>,
pub group_by: Vec<String>,
pub select: Vec<String>,
pub joins: Vec<String>,
pub limit: Option<u64>,
pub offset: Option<u64>,
pub sql: String,
pub params: Vec<String>,
}
impl QueryDebugInfo {
pub fn new(table: impl Into<String>) -> Self {
Self {
table: table.into(),
operation: QueryOperation::Select,
conditions: Vec::new(),
order_by: Vec::new(),
group_by: Vec::new(),
select: vec!["*".to_string()],
joins: Vec::new(),
limit: None,
offset: None,
sql: String::new(),
params: Vec::new(),
}
}
pub fn with_operation(mut self, op: QueryOperation) -> Self {
self.operation = op;
self
}
pub fn add_condition(&mut self, condition: impl Into<String>) {
self.conditions.push(condition.into());
}
pub fn add_order_by(&mut self, order: impl Into<String>) {
self.order_by.push(order.into());
}
pub fn with_sql(mut self, sql: impl Into<String>) -> Self {
self.sql = sql.into();
self
}
pub fn with_params(mut self, params: Vec<String>) -> Self {
self.params = params;
self
}
}
impl fmt::Display for QueryDebugInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const DEBUG_PREVIEW_BANNER: &str =
"-- DEBUG PREVIEW (not executable, values are approximate)";
writeln!(f, "═══════════════════════════════════════════════════")?;
writeln!(f, "TIDEORM QUERY DEBUG")?;
writeln!(f, "═══════════════════════════════════════════════════")?;
writeln!(f, "Table: {}", self.table)?;
writeln!(f, "Operation: {}", self.operation)?;
if !self.select.is_empty() && self.select != vec!["*".to_string()] {
writeln!(f, "Select: {}", self.select.join(", "))?;
}
if !self.conditions.is_empty() {
writeln!(f, "Conditions:")?;
for cond in &self.conditions {
writeln!(f, " - {}", cond)?;
}
}
if !self.joins.is_empty() {
writeln!(f, "Joins:")?;
for join in &self.joins {
writeln!(f, " - {}", join)?;
}
}
if !self.order_by.is_empty() {
writeln!(f, "Order By: {}", self.order_by.join(", "))?;
}
if !self.group_by.is_empty() {
writeln!(f, "Group By: {}", self.group_by.join(", "))?;
}
if let Some(limit) = self.limit {
write!(f, "Limit: {}", limit)?;
if let Some(offset) = self.offset {
write!(f, " (offset: {})", offset)?;
}
writeln!(f)?;
}
if !self.sql.is_empty() {
writeln!(f, "───────────────────────────────────────────────────")?;
if self.sql.starts_with(DEBUG_PREVIEW_BANNER) {
writeln!(f, "SQL Preview (non-executable):")?;
} else {
writeln!(f, "SQL:")?;
}
for line in self.sql.lines() {
writeln!(f, " {}", line)?;
}
}
if !self.params.is_empty() {
writeln!(f, "Params: {:?}", self.params)?;
}
write!(f, "═══════════════════════════════════════════════════")
}
}
fn format_debug(entry: &QueryLogEntry) -> String {
let timing = entry
.duration
.map(|d| format!(" ({}ms)", d.as_millis()))
.unwrap_or_default();
format!("[TIDE][{}]{} {}", entry.operation, timing, entry.sql)
}
fn format_slow(entry: &QueryLogEntry, threshold: u64) -> String {
let duration_ms = entry.duration.map(|d| d.as_millis() as u64).unwrap_or(0);
format!(
"[TIDE][SLOW QUERY] {} ({}ms > {}ms threshold)\n SQL: {}",
entry.operation, duration_ms, threshold, entry.sql
)
}
fn format_error(entry: &QueryLogEntry) -> String {
let error = entry.error.as_deref().unwrap_or("Unknown error");
format!(
"[TIDE][ERROR] {} failed: {}\n SQL: {}",
entry.operation, error, entry.sql
)
}
#[cfg(test)]
#[path = "testing/logging_tests.rs"]
mod tests;
#[doc(hidden)]
#[macro_export]
macro_rules! tide_info {
($($arg:tt)*) => {
eprintln!("[TideORM] {}", format!($($arg)*));
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! tide_warn {
($($arg:tt)*) => {
eprintln!("[TideORM WARN] {}", format!($($arg)*));
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! tide_debug {
($($arg:tt)*) => {
eprintln!("[TideORM DEBUG] {}", format!($($arg)*));
};
}