sql_cli/utils/
dual_logging.rs

1use chrono::Local;
2use std::fs::{File, OpenOptions};
3use std::io::Write;
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex, OnceLock};
6
7use crate::logging::{LogEntry, LogRingBuffer};
8
9/// Global dual logger instance
10static DUAL_LOGGER: OnceLock<DualLogger> = OnceLock::new();
11
12/// Cross-platform log directory
13fn get_log_dir() -> PathBuf {
14    if cfg!(target_os = "windows") {
15        // Windows: Use %TEMP% or %LOCALAPPDATA%
16        std::env::var("LOCALAPPDATA")
17            .or_else(|_| std::env::var("TEMP"))
18            .map(PathBuf::from)
19            .unwrap_or_else(|_| PathBuf::from("C:\\temp"))
20            .join("sql-cli")
21    } else {
22        // Unix-like: Use /tmp or $HOME/.local/share
23        if let Ok(home) = std::env::var("HOME") {
24            PathBuf::from(home)
25                .join(".local")
26                .join("share")
27                .join("sql-cli")
28                .join("logs")
29        } else {
30            PathBuf::from("/tmp").join("sql-cli")
31        }
32    }
33}
34
35/// Dual logger that writes to both ring buffer and file
36pub struct DualLogger {
37    ring_buffer: LogRingBuffer,
38    log_file: Arc<Mutex<Option<File>>>,
39    log_path: PathBuf,
40}
41
42impl DualLogger {
43    pub fn new() -> Self {
44        let log_dir = get_log_dir();
45
46        // Create log directory if it doesn't exist
47        let _ = std::fs::create_dir_all(&log_dir);
48
49        // Create timestamped log file
50        let timestamp = Local::now().format("%Y%m%d_%H%M%S");
51        let log_filename = format!("sql-cli_{}.log", timestamp);
52        let log_path = log_dir.join(&log_filename);
53
54        // Create a "latest.log" pointer - different approach for different OS
55        let latest_path = log_dir.join("latest.log");
56
57        #[cfg(unix)]
58        {
59            // On Unix, use symlink (doesn't require elevated privileges)
60            let _ = std::fs::remove_file(&latest_path); // Remove old symlink
61            let _ = std::os::unix::fs::symlink(&log_path, &latest_path);
62        }
63
64        #[cfg(windows)]
65        {
66            // On Windows, write a text file with the path to the actual log
67            // This avoids needing admin rights for symlinks
68            let pointer_content = format!("Current log file: {}\n", log_path.display());
69            let _ = std::fs::write(&latest_path, pointer_content);
70
71            // Also create a batch file for easy tailing
72            let tail_script = log_dir.join("tail-latest.bat");
73            let script_content = format!(
74                "@echo off\necho Tailing: {}\ntype \"{}\" && timeout /t 2 >nul && goto :loop\n:loop\ntype \"{}\" 2>nul\ntimeout /t 1 >nul\ngoto :loop",
75                log_path.display(),
76                log_path.display(),
77                log_path.display()
78            );
79            let _ = std::fs::write(&tail_script, script_content);
80        }
81
82        // Open log file
83        let log_file = OpenOptions::new()
84            .create(true)
85            .append(true)
86            .open(&log_path)
87            .ok();
88
89        // Don't print here - let main.rs handle the announcement
90
91        Self {
92            ring_buffer: LogRingBuffer::new(),
93            log_file: Arc::new(Mutex::new(log_file)),
94            log_path,
95        }
96    }
97
98    /// Log a message to both ring buffer and file
99    pub fn log(&self, level: &str, target: &str, message: &str) {
100        let entry = LogEntry::new(
101            match level {
102                "ERROR" => tracing::Level::ERROR,
103                "WARN" => tracing::Level::WARN,
104                "INFO" => tracing::Level::INFO,
105                "DEBUG" => tracing::Level::DEBUG,
106                _ => tracing::Level::TRACE,
107            },
108            target,
109            message.to_string(),
110        );
111
112        // To ring buffer (for F5 display)
113        self.ring_buffer.push(entry.clone());
114
115        // To file (for persistent history)
116        if let Ok(mut file_opt) = self.log_file.lock() {
117            if let Some(ref mut file) = *file_opt {
118                let log_line = format!(
119                    "[{}] {} [{}] {}\n",
120                    entry.timestamp, entry.level, entry.target, entry.message
121                );
122                let _ = file.write_all(log_line.as_bytes());
123                let _ = file.flush(); // Important for crash debugging!
124            }
125        }
126
127        // Also to stderr if DEBUG env var set
128        if std::env::var("SQL_CLI_DEBUG").is_ok() {
129            eprintln!("{}", entry.format_for_display());
130        }
131    }
132
133    /// Get the ring buffer for F5 display
134    pub fn ring_buffer(&self) -> &LogRingBuffer {
135        &self.ring_buffer
136    }
137
138    /// Get the log file path
139    pub fn log_path(&self) -> &PathBuf {
140        &self.log_path
141    }
142
143    /// Force flush the log file
144    pub fn flush(&self) {
145        if let Ok(mut file_opt) = self.log_file.lock() {
146            if let Some(ref mut file) = *file_opt {
147                let _ = file.flush();
148            }
149        }
150    }
151}
152
153/// Initialize the global dual logger
154pub fn init_dual_logger() -> &'static DualLogger {
155    DUAL_LOGGER.get_or_init(|| DualLogger::new())
156}
157
158/// Get the global dual logger
159pub fn get_dual_logger() -> Option<&'static DualLogger> {
160    DUAL_LOGGER.get()
161}
162
163/// Convenience macro for logging
164#[macro_export]
165macro_rules! dual_log {
166    ($level:expr, $target:expr, $($arg:tt)*) => {{
167        if let Some(logger) = $crate::dual_logging::get_dual_logger() {
168            logger.log($level, $target, &format!($($arg)*));
169        }
170    }};
171}
172
173/// Log at different levels
174#[macro_export]
175macro_rules! log_error {
176    ($($arg:tt)*) => {{ dual_log!("ERROR", module_path!(), $($arg)*); }};
177}
178
179#[macro_export]
180macro_rules! log_warn {
181    ($($arg:tt)*) => {{ dual_log!("WARN", module_path!(), $($arg)*); }};
182}
183
184#[macro_export]
185macro_rules! log_info {
186    ($($arg:tt)*) => {{ dual_log!("INFO", module_path!(), $($arg)*); }};
187}
188
189#[macro_export]
190macro_rules! log_debug {
191    ($($arg:tt)*) => {{ dual_log!("DEBUG", module_path!(), $($arg)*); }};
192}