sql_cli/utils/
logging.rs

1use chrono::Local;
2use std::collections::VecDeque;
3use std::sync::{Arc, Mutex, OnceLock};
4use tracing::Level;
5use tracing_subscriber::fmt::MakeWriter;
6
7/// Maximum number of log entries to keep in memory
8const MAX_LOG_ENTRIES: usize = 1000;
9
10/// A log entry with timestamp and message
11#[derive(Debug, Clone)]
12pub struct LogEntry {
13    pub timestamp: String,
14    pub level: String,
15    pub target: String,
16    pub message: String,
17}
18
19impl LogEntry {
20    pub fn new(level: Level, target: &str, message: String) -> Self {
21        Self {
22            timestamp: Local::now().format("%H:%M:%S.%3f").to_string(),
23            level: level.to_string().to_uppercase(),
24            target: target.to_string(),
25            message,
26        }
27    }
28
29    /// Format for display in debug view
30    pub fn format_for_display(&self) -> String {
31        format!(
32            "[{}] {} [{}] {}",
33            self.timestamp, self.level, self.target, self.message
34        )
35    }
36}
37
38/// Thread-safe ring buffer for log entries
39#[derive(Clone)]
40pub struct LogRingBuffer {
41    entries: Arc<Mutex<VecDeque<LogEntry>>>,
42}
43
44impl LogRingBuffer {
45    pub fn new() -> Self {
46        Self {
47            entries: Arc::new(Mutex::new(VecDeque::with_capacity(MAX_LOG_ENTRIES))),
48        }
49    }
50
51    pub fn push(&self, entry: LogEntry) {
52        let mut entries = self.entries.lock().unwrap();
53        if entries.len() >= MAX_LOG_ENTRIES {
54            entries.pop_front();
55        }
56        entries.push_back(entry);
57    }
58
59    pub fn get_recent(&self, count: usize) -> Vec<LogEntry> {
60        let entries = self.entries.lock().unwrap();
61        entries.iter().rev().take(count).rev().cloned().collect()
62    }
63
64    pub fn clear(&self) {
65        let mut entries = self.entries.lock().unwrap();
66        entries.clear();
67    }
68
69    pub fn len(&self) -> usize {
70        let entries = self.entries.lock().unwrap();
71        entries.len()
72    }
73}
74
75/// Custom writer that captures logs to our ring buffer
76pub struct RingBufferWriter {
77    buffer: LogRingBuffer,
78}
79
80impl RingBufferWriter {
81    pub fn new(buffer: LogRingBuffer) -> Self {
82        Self { buffer }
83    }
84}
85
86impl std::io::Write for RingBufferWriter {
87    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
88        // Parse the log message and add to ring buffer
89        if let Ok(message) = std::str::from_utf8(buf) {
90            let message = message.trim();
91            if !message.is_empty() {
92                // The compact format is: "LEVEL target: message"
93                // First, try to extract the level
94                let (level, rest) = if message.starts_with("TRACE ") {
95                    (Level::TRACE, &message[6..])
96                } else if message.starts_with("DEBUG ") {
97                    (Level::DEBUG, &message[6..])
98                } else if message.starts_with("INFO ") {
99                    (Level::INFO, &message[5..])
100                } else if message.starts_with("WARN ") {
101                    (Level::WARN, &message[5..])
102                } else if message.starts_with("ERROR ") {
103                    (Level::ERROR, &message[6..])
104                } else {
105                    // If no level prefix, just store the whole message
106                    self.buffer
107                        .push(LogEntry::new(Level::INFO, "general", message.to_string()));
108                    return Ok(buf.len());
109                };
110
111                // Now parse "target: message" from rest
112                let (target, msg) = if let Some(colon_pos) = rest.find(':') {
113                    let potential_target = &rest[..colon_pos];
114                    // Check if this looks like a target (no spaces)
115                    if !potential_target.contains(' ') {
116                        (potential_target, rest[colon_pos + 1..].trim())
117                    } else {
118                        ("general", rest)
119                    }
120                } else {
121                    ("general", rest)
122                };
123
124                self.buffer
125                    .push(LogEntry::new(level, target, msg.to_string()));
126            }
127        }
128        Ok(buf.len())
129    }
130
131    fn flush(&mut self) -> std::io::Result<()> {
132        Ok(())
133    }
134}
135
136impl<'a> MakeWriter<'a> for RingBufferWriter {
137    type Writer = Self;
138
139    fn make_writer(&'a self) -> Self::Writer {
140        self.clone()
141    }
142}
143
144impl Clone for RingBufferWriter {
145    fn clone(&self) -> Self {
146        Self {
147            buffer: self.buffer.clone(),
148        }
149    }
150}
151
152/// Dual writer that writes to both ring buffer and file
153pub struct DualWriter {
154    buffer: LogRingBuffer,
155    dual_logger: &'static crate::dual_logging::DualLogger,
156}
157
158impl DualWriter {
159    pub fn new(
160        buffer: LogRingBuffer,
161        dual_logger: &'static crate::dual_logging::DualLogger,
162    ) -> Self {
163        Self {
164            buffer,
165            dual_logger,
166        }
167    }
168}
169
170impl std::io::Write for DualWriter {
171    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
172        // Parse the log message
173        if let Ok(message) = std::str::from_utf8(buf) {
174            let message = message.trim();
175            if !message.is_empty() {
176                // The compact format is: "LEVEL target: message"
177                // First, try to extract the level
178                let (level, rest) = if message.starts_with("TRACE ") {
179                    (Level::TRACE, &message[6..])
180                } else if message.starts_with("DEBUG ") {
181                    (Level::DEBUG, &message[6..])
182                } else if message.starts_with("INFO ") {
183                    (Level::INFO, &message[5..])
184                } else if message.starts_with("WARN ") {
185                    (Level::WARN, &message[5..])
186                } else if message.starts_with("ERROR ") {
187                    (Level::ERROR, &message[6..])
188                } else {
189                    // Skip lines that are just timestamps or empty
190                    if message.starts_with("2025-") || message.starts_with("2024-") {
191                        return Ok(buf.len());
192                    }
193                    // If no level prefix, just store the whole message
194                    let entry = LogEntry::new(Level::INFO, "general", message.to_string());
195                    self.buffer.push(entry.clone());
196                    self.dual_logger.log("INFO", "general", message);
197                    return Ok(buf.len());
198                };
199
200                // Now parse "target: message" from rest
201                let (target, msg) = if let Some(colon_pos) = rest.find(':') {
202                    let potential_target = &rest[..colon_pos];
203                    // Check if this looks like a target (no spaces)
204                    if !potential_target.contains(' ') {
205                        (potential_target, rest[colon_pos + 1..].trim())
206                    } else {
207                        ("general", rest)
208                    }
209                } else {
210                    ("general", rest)
211                };
212
213                // Write to ring buffer for F5 display
214                self.buffer
215                    .push(LogEntry::new(level, target, msg.to_string()));
216
217                // Write to file
218                let level_str = match level {
219                    Level::TRACE => "TRACE",
220                    Level::DEBUG => "DEBUG",
221                    Level::INFO => "INFO",
222                    Level::WARN => "WARN",
223                    Level::ERROR => "ERROR",
224                };
225                self.dual_logger.log(level_str, target, msg);
226            }
227        }
228        Ok(buf.len())
229    }
230
231    fn flush(&mut self) -> std::io::Result<()> {
232        self.dual_logger.flush();
233        Ok(())
234    }
235}
236
237impl<'a> MakeWriter<'a> for DualWriter {
238    type Writer = Self;
239
240    fn make_writer(&'a self) -> Self::Writer {
241        Self {
242            buffer: self.buffer.clone(),
243            dual_logger: self.dual_logger,
244        }
245    }
246}
247
248impl Clone for DualWriter {
249    fn clone(&self) -> Self {
250        Self {
251            buffer: self.buffer.clone(),
252            dual_logger: self.dual_logger,
253        }
254    }
255}
256
257/// Global log buffer accessible throughout the application
258static LOG_BUFFER: OnceLock<LogRingBuffer> = OnceLock::new();
259
260/// Initialize the global log buffer
261pub fn init_log_buffer() -> LogRingBuffer {
262    let buffer = LogRingBuffer::new();
263    LOG_BUFFER.set(buffer.clone()).ok();
264    buffer
265}
266
267/// Get the global log buffer
268pub fn get_log_buffer() -> Option<LogRingBuffer> {
269    LOG_BUFFER.get().cloned()
270}
271
272/// Initialize tracing with dual logging (ring buffer + file)
273pub fn init_tracing_with_dual_logging() {
274    use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
275
276    // Initialize the dual logger (ring buffer + file)
277    let dual_logger = crate::dual_logging::init_dual_logger();
278
279    // Initialize the ring buffer for F5 display
280    let buffer = init_log_buffer();
281
282    // Create a custom writer that writes to both ring buffer and file
283    let dual_writer = DualWriter::new(buffer.clone(), dual_logger);
284
285    // Create a subscriber with our dual writer
286    let fmt_layer = fmt::layer()
287        .with_writer(dual_writer)
288        .with_target(true)
289        .with_level(true)
290        .with_ansi(false)
291        .without_time() // We add our own timestamps
292        .compact();
293
294    // Set up env filter - default to TRACE for everything to catch all logs
295    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("trace"));
296
297    tracing_subscriber::registry()
298        .with(filter)
299        .with(fmt_layer)
300        .init();
301
302    // Log initial message
303    tracing::info!(target: "EnhancedTuiApp", "Logging system initialized with dual output");
304}
305
306/// Initialize tracing with our custom ring buffer writer (legacy)
307pub fn init_tracing() -> LogRingBuffer {
308    init_tracing_with_dual_logging();
309    get_log_buffer().unwrap_or_else(|| LogRingBuffer::new())
310}
311
312/// Convenience macros for common operations
313#[macro_export]
314macro_rules! trace_operation {
315    ($op:expr) => {
316        tracing::debug!(target: "operation", "{}", $op);
317    };
318}
319
320#[macro_export]
321macro_rules! trace_query {
322    ($query:expr) => {
323        tracing::info!(target: "query", "Executing: {}", $query);
324    };
325}
326
327#[macro_export]
328macro_rules! trace_buffer_switch {
329    ($from:expr, $to:expr) => {
330        tracing::debug!(target: "buffer", "Switching from buffer {} to {}", $from, $to);
331    };
332}
333
334#[macro_export]
335macro_rules! trace_key {
336    ($key:expr) => {
337        tracing::trace!(target: "input", "Key: {:?}", $key);
338    };
339}