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_or_else(|_| PathBuf::from("C:\\temp"), PathBuf::from)
19 .join("sql-cli")
20 } else {
21 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
34pub 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 let _ = std::fs::create_dir_all(&log_dir);
54
55 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 let latest_path = log_dir.join("latest.log");
62
63 #[cfg(unix)]
64 {
65 let _ = std::fs::remove_file(&latest_path); let _ = std::os::unix::fs::symlink(&log_path, &latest_path);
68 }
69
70 #[cfg(windows)]
71 {
72 let pointer_content = format!("Current log file: {}\n", log_path.display());
75 let _ = std::fs::write(&latest_path, pointer_content);
76
77 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 let log_file = OpenOptions::new()
90 .create(true)
91 .append(true)
92 .open(&log_path)
93 .ok();
94
95 Self {
98 ring_buffer: LogRingBuffer::new(),
99 log_file: Arc::new(Mutex::new(log_file)),
100 log_path,
101 }
102 }
103
104 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 self.ring_buffer.push(entry.clone());
120
121 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(); }
131 }
132
133 if std::env::var("SQL_CLI_DEBUG").is_ok() {
135 eprintln!("{}", entry.format_for_display());
136 }
137 }
138
139 #[must_use]
141 pub fn ring_buffer(&self) -> &LogRingBuffer {
142 &self.ring_buffer
143 }
144
145 #[must_use]
147 pub fn log_path(&self) -> &PathBuf {
148 &self.log_path
149 }
150
151 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
161pub fn init_dual_logger() -> &'static DualLogger {
163 DUAL_LOGGER.get_or_init(DualLogger::new)
164}
165
166pub fn get_dual_logger() -> Option<&'static DualLogger> {
168 DUAL_LOGGER.get()
169}
170
171#[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#[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}