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::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
288
289    // Initialize the dual logger (ring buffer + file)
290    let dual_logger = crate::dual_logging::init_dual_logger();
291
292    // Initialize the ring buffer for F5 display
293    let buffer = init_log_buffer();
294
295    // Create a custom writer that writes to both ring buffer and file
296    let dual_writer = DualWriter::new(buffer.clone(), dual_logger);
297
298    // Create a subscriber with our dual writer
299    let fmt_layer = fmt::layer()
300        .with_writer(dual_writer)
301        .with_target(true)
302        .with_level(true)
303        .with_ansi(false)
304        .without_time() // We add our own timestamps
305        .compact();
306
307    // Set up env filter - default to TRACE for everything to catch all logs
308    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("trace"));
309
310    tracing_subscriber::registry()
311        .with(filter)
312        .with(fmt_layer)
313        .init();
314
315    // Log initial message
316    tracing::info!(target: "EnhancedTuiApp", "Logging system initialized with dual output");
317}
318
319/// Initialize tracing with our custom ring buffer writer (legacy)
320#[must_use]
321pub fn init_tracing() -> LogRingBuffer {
322    init_tracing_with_dual_logging();
323    get_log_buffer().unwrap_or_default()
324}
325
326/// Convenience macros for common operations
327#[macro_export]
328macro_rules! trace_operation {
329    ($op:expr) => {
330        tracing::debug!(target: "operation", "{}", $op);
331    };
332}
333
334#[macro_export]
335macro_rules! trace_query {
336    ($query:expr) => {
337        tracing::info!(target: "query", "Executing: {}", $query);
338    };
339}
340
341#[macro_export]
342macro_rules! trace_buffer_switch {
343    ($from:expr, $to:expr) => {
344        tracing::debug!(target: "buffer", "Switching from buffer {} to {}", $from, $to);
345    };
346}
347
348#[macro_export]
349macro_rules! trace_key {
350    ($key:expr) => {
351        tracing::trace!(target: "input", "Key: {:?}", $key);
352    };
353}