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