use chrono::Local;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, OnceLock};
use crate::logging::{LogEntry, LogRingBuffer};
static DUAL_LOGGER: OnceLock<DualLogger> = OnceLock::new();
fn get_log_dir() -> PathBuf {
if cfg!(target_os = "windows") {
std::env::var("LOCALAPPDATA")
.or_else(|_| std::env::var("TEMP"))
.map_or_else(|_| PathBuf::from("C:\\temp"), PathBuf::from)
.join("sql-cli")
} else {
if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home)
.join(".local")
.join("share")
.join("sql-cli")
.join("logs")
} else {
PathBuf::from("/tmp").join("sql-cli")
}
}
}
pub struct DualLogger {
ring_buffer: LogRingBuffer,
log_file: Arc<Mutex<Option<File>>>,
log_path: PathBuf,
}
impl Default for DualLogger {
fn default() -> Self {
Self::new()
}
}
impl DualLogger {
#[must_use]
pub fn new() -> Self {
let log_dir = get_log_dir();
let _ = std::fs::create_dir_all(&log_dir);
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let log_filename = format!("sql-cli_{timestamp}.log");
let log_path = log_dir.join(&log_filename);
let latest_path = log_dir.join("latest.log");
#[cfg(unix)]
{
let _ = std::fs::remove_file(&latest_path); let _ = std::os::unix::fs::symlink(&log_path, &latest_path);
}
#[cfg(windows)]
{
let pointer_content = format!("Current log file: {}\n", log_path.display());
let _ = std::fs::write(&latest_path, pointer_content);
let tail_script = log_dir.join("tail-latest.bat");
let script_content = format!(
"@echo off\necho Tailing: {}\ntype \"{}\" && timeout /t 2 >nul && goto :loop\n:loop\ntype \"{}\" 2>nul\ntimeout /t 1 >nul\ngoto :loop",
log_path.display(),
log_path.display(),
log_path.display()
);
let _ = std::fs::write(&tail_script, script_content);
}
let log_file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok();
Self {
ring_buffer: LogRingBuffer::new(),
log_file: Arc::new(Mutex::new(log_file)),
log_path,
}
}
pub fn log(&self, level: &str, target: &str, message: &str) {
let entry = LogEntry::new(
match level {
"ERROR" => tracing::Level::ERROR,
"WARN" => tracing::Level::WARN,
"INFO" => tracing::Level::INFO,
"DEBUG" => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
},
target,
message.to_string(),
);
self.ring_buffer.push(entry.clone());
if let Ok(mut file_opt) = self.log_file.lock() {
if let Some(ref mut file) = *file_opt {
let log_line = format!(
"[{}] {} [{}] {}\n",
entry.timestamp, entry.level, entry.target, entry.message
);
let _ = file.write_all(log_line.as_bytes());
let _ = file.flush(); }
}
if std::env::var("SQL_CLI_DEBUG").is_ok() {
eprintln!("{}", entry.format_for_display());
}
}
#[must_use]
pub fn ring_buffer(&self) -> &LogRingBuffer {
&self.ring_buffer
}
#[must_use]
pub fn log_path(&self) -> &PathBuf {
&self.log_path
}
pub fn flush(&self) {
if let Ok(mut file_opt) = self.log_file.lock() {
if let Some(ref mut file) = *file_opt {
let _ = file.flush();
}
}
}
}
pub fn init_dual_logger() -> &'static DualLogger {
DUAL_LOGGER.get_or_init(DualLogger::new)
}
pub fn get_dual_logger() -> Option<&'static DualLogger> {
DUAL_LOGGER.get()
}
#[macro_export]
macro_rules! dual_log {
($level:expr, $target:expr, $($arg:tt)*) => {{
if let Some(logger) = $crate::dual_logging::get_dual_logger() {
logger.log($level, $target, &format!($($arg)*));
}
}};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {{ dual_log!("ERROR", module_path!(), $($arg)*); }};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {{ dual_log!("WARN", module_path!(), $($arg)*); }};
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {{ dual_log!("INFO", module_path!(), $($arg)*); }};
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{ dual_log!("DEBUG", module_path!(), $($arg)*); }};
}