sql_cli/utils/
dual_logging.rs1use 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
9static DUAL_LOGGER: OnceLock<DualLogger> = OnceLock::new();
11
12fn get_log_dir() -> PathBuf {
14 if cfg!(target_os = "windows") {
15 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 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
35pub 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 let _ = std::fs::create_dir_all(&log_dir);
48
49 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 let latest_path = log_dir.join("latest.log");
56
57 #[cfg(unix)]
58 {
59 let _ = std::fs::remove_file(&latest_path); let _ = std::os::unix::fs::symlink(&log_path, &latest_path);
62 }
63
64 #[cfg(windows)]
65 {
66 let pointer_content = format!("Current log file: {}\n", log_path.display());
69 let _ = std::fs::write(&latest_path, pointer_content);
70
71 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 let log_file = OpenOptions::new()
84 .create(true)
85 .append(true)
86 .open(&log_path)
87 .ok();
88
89 Self {
92 ring_buffer: LogRingBuffer::new(),
93 log_file: Arc::new(Mutex::new(log_file)),
94 log_path,
95 }
96 }
97
98 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 self.ring_buffer.push(entry.clone());
114
115 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(); }
125 }
126
127 if std::env::var("SQL_CLI_DEBUG").is_ok() {
129 eprintln!("{}", entry.format_for_display());
130 }
131 }
132
133 pub fn ring_buffer(&self) -> &LogRingBuffer {
135 &self.ring_buffer
136 }
137
138 pub fn log_path(&self) -> &PathBuf {
140 &self.log_path
141 }
142
143 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
153pub fn init_dual_logger() -> &'static DualLogger {
155 DUAL_LOGGER.get_or_init(|| DualLogger::new())
156}
157
158pub fn get_dual_logger() -> Option<&'static DualLogger> {
160 DUAL_LOGGER.get()
161}
162
163#[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#[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}