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