Skip to main content

matrixcode_core/
debug.rs

1//! Debug logging for MatrixCode operations
2//!
3//! Tracks: API calls, compression, memory saves, tool executions
4//!
5//! Features:
6//! - Debug mode toggle: only logs when debug mode is enabled
7//! - Session-based files: each session gets its own log file
8//! - Auto rotation: rotates when file exceeds size limit
9
10use std::fs::{self, File, OpenOptions};
11use std::io::Write;
12use std::path::PathBuf;
13use std::sync::Mutex;
14use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use crate::constants::MATRIX_DIR;
18use crate::event::AgentEvent;
19use crate::truncate::truncate_with_suffix;
20use tokio::sync::mpsc;
21
22static API_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
23static COMPRESSION_COUNT: AtomicU64 = AtomicU64::new(0);
24static MEMORY_SAVE_COUNT: AtomicU64 = AtomicU64::new(0);
25static TOOL_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
26
27/// Maximum log file size before rotation (10 MB)
28const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024;
29
30/// Debug logger that writes to file
31pub struct DebugLog {
32    file: Mutex<Option<File>>,
33    current_file: Mutex<Option<PathBuf>>,
34    enabled: AtomicBool,
35    session_id: Mutex<Option<String>>,
36}
37
38impl Default for DebugLog {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl DebugLog {
45    /// Create a new debug logger
46    /// Initially disabled, call enable() to start logging
47    pub fn new() -> Self {
48        Self {
49            file: Mutex::new(None),
50            current_file: Mutex::new(None),
51            enabled: AtomicBool::new(false),
52            session_id: Mutex::new(None),
53        }
54    }
55
56    /// Enable debug logging with a session ID
57    /// Creates a session-specific log file
58    pub fn enable(&self, session_id: Option<&str>) {
59        self.enabled.store(true, Ordering::Relaxed);
60
61        // Generate session ID if not provided
62        let sid = session_id
63            .map(|s| s.to_string())
64            .unwrap_or_else(|| Self::generate_session_id());
65
66        if let Ok(mut guard) = self.session_id.lock() {
67            *guard = Some(sid.clone());
68        }
69
70        // Create new log file for this session
71        if let Ok(mut file_guard) = self.file.lock() {
72            if let Ok(mut path_guard) = self.current_file.lock() {
73                match Self::open_session_log_file(&sid, 0) {
74                    Ok((file, path)) => {
75                        *file_guard = Some(file);
76                        *path_guard = Some(path);
77                    }
78                    Err(e) => {
79                        eprintln!("Failed to create debug log file: {}", e);
80                    }
81                }
82            }
83        }
84    }
85
86    /// Disable debug logging
87    pub fn disable(&self) {
88        self.enabled.store(false, Ordering::Relaxed);
89        if let Ok(mut guard) = self.file.lock() {
90            *guard = None;
91        }
92    }
93
94    /// Check if debug logging is enabled
95    pub fn is_enabled(&self) -> bool {
96        self.enabled.load(Ordering::Relaxed)
97    }
98
99    /// Generate a timestamp-based session ID
100    fn generate_session_id() -> String {
101        let now = SystemTime::now()
102            .duration_since(UNIX_EPOCH)
103            .unwrap_or_default()
104            .as_secs();
105        format!("session_{}", now)
106    }
107
108    /// Get log directory path
109    fn get_log_dir() -> Result<PathBuf, std::io::Error> {
110        let home = std::env::var_os("HOME")
111            .or_else(|| std::env::var_os("USERPROFILE"))
112            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME not set"))?;
113        let mut path = PathBuf::from(home);
114        path.push(MATRIX_DIR);
115        path.push("logs");
116        fs::create_dir_all(&path)?;
117        Ok(path)
118    }
119
120    /// Open a session-specific log file
121    fn open_session_log_file(
122        session_id: &str,
123        rotation: u32,
124    ) -> Result<(File, PathBuf), std::io::Error> {
125        let mut path = Self::get_log_dir()?;
126        let filename = if rotation == 0 {
127            format!("{}.log", session_id)
128        } else {
129            format!("{}.{}.log", session_id, rotation)
130        };
131        path.push(&filename);
132        let file = OpenOptions::new().create(true).append(true).open(&path)?;
133        Ok((file, path))
134    }
135
136    /// Rotate log file if it exceeds size limit
137    fn rotate_if_needed(&self) {
138        if let Ok(mut file_guard) = self.file.lock()
139            && let Ok(mut path_guard) = self.current_file.lock()
140            && let Ok(session_guard) = self.session_id.lock()
141        {
142            if let (Some(file), Some(_path), Some(session_id)) = (
143                file_guard.as_mut(),
144                path_guard.as_ref(),
145                session_guard.as_ref(),
146            ) {
147                // Check file size
148                if let Ok(metadata) = file.metadata() {
149                    if metadata.len() > MAX_LOG_SIZE {
150                        // Find next available rotation number
151                        let mut rotation = 1u32;
152                        loop {
153                            let mut new_path = Self::get_log_dir().unwrap_or_default();
154                            let filename = format!("{}.{}.log", session_id, rotation);
155                            new_path.push(&filename);
156                            if !new_path.exists() {
157                                break;
158                            }
159                            rotation += 1;
160                        }
161
162                        // Open new rotated file
163                        if let Ok((new_file, new_path)) =
164                            Self::open_session_log_file(session_id, rotation)
165                        {
166                            *file_guard = Some(new_file);
167                            *path_guard = Some(new_path);
168                        }
169                    }
170                }
171            }
172        }
173    }
174
175    /// Clean up old log files (keep last N sessions)
176    pub fn cleanup_old_logs(keep_count: usize) -> Result<(), std::io::Error> {
177        let log_dir = Self::get_log_dir()?;
178        let mut log_files: Vec<_> = fs::read_dir(&log_dir)?
179            .filter_map(|e| e.ok())
180            .filter(|e| {
181                e.path()
182                    .extension()
183                    .map(|ext| ext == "log")
184                    .unwrap_or(false)
185            })
186            .collect();
187
188        // Sort by modification time (oldest first)
189        log_files.sort_by_key(|e| {
190            e.metadata()
191                .and_then(|m| m.modified())
192                .unwrap_or(SystemTime::UNIX_EPOCH)
193        });
194
195        // Remove oldest files beyond keep_count
196        let total_sessions = log_files.len();
197        if total_sessions > keep_count {
198            for entry in log_files.iter().take(total_sessions - keep_count) {
199                let _ = fs::remove_file(entry.path());
200            }
201        }
202
203        Ok(())
204    }
205
206    fn timestamp() -> String {
207        let now = SystemTime::now()
208            .duration_since(UNIX_EPOCH)
209            .unwrap_or_default()
210            .as_secs();
211        let secs = now % 60;
212        let mins = (now / 60) % 60;
213        let hours = (now / 3600) % 24;
214        format!("{:02}:{:02}:{:02}", hours, mins, secs)
215    }
216
217    /// Log an API call
218    pub fn api_call(&self, model: &str, input_tokens: u32, cached: bool) {
219        if !self.is_enabled() {
220            return;
221        }
222        let count = API_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
223        let msg = format!(
224            "API#{}: model={}, input_tokens={}, cached={}",
225            count, model, input_tokens, cached
226        );
227        self.write_log("API", &msg);
228    }
229
230    /// Log compression trigger
231    pub fn compression(&self, original_tokens: u32, compressed_tokens: u32, ratio: f32) {
232        if !self.is_enabled() {
233            return;
234        }
235        let count = COMPRESSION_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
236        let saved = original_tokens - compressed_tokens;
237        let msg = format!(
238            "COMPRESSION#{}: original={}, compressed={}, saved={}, ratio={:.1}%",
239            count,
240            original_tokens,
241            compressed_tokens,
242            saved,
243            ratio * 100.0
244        );
245        self.write_log("COMPRESS", &msg);
246    }
247
248    /// Log memory save
249    pub fn memory_save(&self, entries: usize, summary_len: usize) {
250        if !self.is_enabled() {
251            return;
252        }
253        let count = MEMORY_SAVE_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
254        let msg = format!(
255            "MEMORY#{}: entries={}, summary_len={}chars",
256            count, entries, summary_len
257        );
258        self.write_log("MEMORY", &msg);
259    }
260
261    /// Log keyword extraction
262    pub fn keywords_extracted(&self, keywords: &[String], source: &str) {
263        if !self.is_enabled() {
264            return;
265        }
266        let msg = format!(
267            "{} extracted from {}chars | keywords: {}",
268            keywords.len(),
269            source.len(),
270            keywords.join(", ")
271        );
272        self.write_log("KEYWORDS", &msg);
273    }
274
275    /// Log tool execution
276    pub fn tool_call(&self, tool: &str, input_preview: &str, result_preview: &str) {
277        if !self.is_enabled() {
278            return;
279        }
280        let count = TOOL_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
281        let msg = format!(
282            "TOOL#{}: {} | input: {} | result: {}",
283            count,
284            tool,
285            truncate(input_preview, 50),
286            truncate(result_preview, 50)
287        );
288        self.write_log("TOOL", &msg);
289    }
290
291    /// Log session save
292    pub fn session_save(&self, message_count: usize, total_tokens: u64) {
293        if !self.is_enabled() {
294            return;
295        }
296        let msg = format!(
297            "SESSION: messages={}, total_tokens={}",
298            message_count, total_tokens
299        );
300        self.write_log("SESSION", &msg);
301    }
302
303    /// Log generic debug message
304    pub fn log(&self, category: &str, message: &str) {
305        if !self.is_enabled() {
306            return;
307        }
308        self.write_log(category, message);
309    }
310
311    /// Log API request body (for debug)
312    pub fn api_request(&self, url: &str, body: &str) {
313        if !self.is_enabled() {
314            return;
315        }
316        // Write full content to file
317        let body_preview = if body.len() > 5000 {
318            truncate_with_suffix(body, 5000)
319        } else {
320            body.to_string()
321        };
322        let msg = format!(
323            "API_REQUEST: url={}\n---REQUEST_BODY---\n{}\n---END---",
324            url, body_preview
325        );
326        self.write(&format!("[{}] {}", Self::timestamp(), msg));
327
328        // Send brief summary to debug panel
329        let panel_msg = format!("url={} | body_len={}chars", url, body.len());
330        self.send_debug_event("API_REQUEST", &panel_msg);
331    }
332
333    /// Log API response (for debug)
334    pub fn api_response(&self, status: u16, body: &str) {
335        if !self.is_enabled() {
336            return;
337        }
338        // Write full content to file
339        let body_preview = if body.len() > 10000 {
340            truncate_with_suffix(body, 10000)
341        } else {
342            body.to_string()
343        };
344        let msg = format!(
345            "API_RESPONSE: status={}\n---RESPONSE_BODY---\n{}\n---END---",
346            status, body_preview
347        );
348        self.write(&format!("[{}] {}", Self::timestamp(), msg));
349
350        // Send brief summary to debug panel
351        let panel_msg = format!("status={} | body_len={}chars", status, body.len());
352        self.send_debug_event("API_RESPONSE", &panel_msg);
353    }
354
355    /// Log streaming chunk (for debug, limited)
356    pub fn stream_chunk(&self, chunk_type: &str, content: &str) {
357        if !self.is_enabled() {
358            return;
359        }
360        // Only log small chunks to avoid flooding
361        let preview = if content.len() > 200 {
362            truncate_with_suffix(content, 200)
363        } else {
364            content.to_string()
365        };
366        let msg = format!(
367            "[{}] STREAM_CHUNK: type={} | {}",
368            Self::timestamp(),
369            chunk_type,
370            preview
371        );
372        self.write(&msg);
373    }
374
375    /// Log AI memory extraction (keyword extraction with fast model)
376    pub fn memory_ai_keywords(
377        &self,
378        model: &str,
379        keywords_count: usize,
380        source_len: usize,
381        used_ai: bool,
382    ) {
383        if !self.is_enabled() {
384            return;
385        }
386        let method = if used_ai { "AI" } else { "rule" };
387        let msg = format!(
388            "MEMORY_AI_KEYWORDS: model={}, method={}, keywords={}, source_len={}chars",
389            model, method, keywords_count, source_len
390        );
391        self.write_log("MEMORY", &msg);
392    }
393
394    /// Log AI memory detection (memory extraction from response)
395    pub fn memory_ai_detection(
396        &self,
397        model: &str,
398        entries_count: usize,
399        text_len: usize,
400        used_ai: bool,
401    ) {
402        if !self.is_enabled() {
403            return;
404        }
405        let method = if used_ai { "AI" } else { "rule" };
406        let msg = format!(
407            "MEMORY_AI_DETECT: model={}, method={}, entries={}, text_len={}chars",
408            model, method, entries_count, text_len
409        );
410        self.write_log("MEMORY", &msg);
411    }
412
413    fn write(&self, msg: &str) {
414        // Check if debug mode is enabled
415        if !self.is_enabled() {
416            return;
417        }
418
419        // Write to file only, don't print to console (would mess up TUI)
420        if let Ok(mut guard) = self.file.lock() {
421            if let Some(ref mut file) = *guard {
422                let _ = file.write_all(msg.as_bytes());
423                let _ = file.write_all(b"\n");
424                let _ = file.flush();
425            }
426        }
427
428        // Check for rotation after write
429        self.rotate_if_needed();
430    }
431
432    /// Write log with category and send event to TUI debug panel
433    fn write_log(&self, category: &str, message: &str) {
434        if !self.is_enabled() {
435            return;
436        }
437
438        let msg = format!("[{}] {}: {}", Self::timestamp(), category, message);
439        self.write(&msg);
440
441        // Send event to TUI debug panel
442        self.send_debug_event(category, message);
443    }
444
445    /// Send debug event to TUI panel only (no file write)
446    fn send_debug_event(&self, category: &str, message: &str) {
447        if let Ok(guard) = DEBUG_EVENT_SENDER.lock()
448            && let Some(ref sender) = *guard
449        {
450            let _ = sender.try_send(AgentEvent::debug_log(category, message));
451        }
452    }
453
454    /// Get statistics
455    pub fn stats(&self) -> DebugStats {
456        DebugStats {
457            api_calls: API_CALL_COUNT.load(Ordering::Relaxed),
458            compressions: COMPRESSION_COUNT.load(Ordering::Relaxed),
459            memory_saves: MEMORY_SAVE_COUNT.load(Ordering::Relaxed),
460            tool_calls: TOOL_CALL_COUNT.load(Ordering::Relaxed),
461        }
462    }
463}
464
465fn truncate(s: &str, max: usize) -> String {
466    truncate_with_suffix(s, max)
467}
468
469/// Debug statistics
470#[derive(Debug, Clone)]
471pub struct DebugStats {
472    pub api_calls: u64,
473    pub compressions: u64,
474    pub memory_saves: u64,
475    pub tool_calls: u64,
476}
477
478impl DebugStats {
479    pub fn format(&self) -> String {
480        format!(
481            "API: {} │ Compress: {} │ Memory: {} │ Tools: {}",
482            self.api_calls, self.compressions, self.memory_saves, self.tool_calls
483        )
484    }
485}
486
487/// Global debug logger (lazy initialized)
488static DEBUG_LOG: once_cell::sync::Lazy<DebugLog> = once_cell::sync::Lazy::new(|| {
489    // Try to load .env file first (from current directory)
490    let _ = dotenvy::dotenv();
491
492    // Also try project-level .matrix/.env
493    if let Ok(cwd) = std::env::current_dir() {
494        let matrix_env = cwd.join(MATRIX_DIR).join(".env");
495        if matrix_env.exists() {
496            let _ = dotenvy::from_path(&matrix_env);
497        }
498    }
499
500    DebugLog::new()
501});
502
503/// Global event sender for TUI debug panel
504static DEBUG_EVENT_SENDER: once_cell::sync::Lazy<Mutex<Option<mpsc::Sender<AgentEvent>>>> =
505    once_cell::sync::Lazy::new(|| Mutex::new(None));
506
507/// Get the global debug logger
508pub fn debug_log() -> &'static DebugLog {
509    &DEBUG_LOG
510}
511
512/// Enable debug logging with optional session ID
513pub fn enable_debug_logging(session_id: Option<&str>) {
514    DEBUG_LOG.enable(session_id);
515}
516
517/// Disable debug logging
518pub fn disable_debug_logging() {
519    DEBUG_LOG.disable();
520}
521
522/// Check if debug logging is enabled
523pub fn is_debug_enabled() -> bool {
524    DEBUG_LOG.is_enabled()
525}
526
527/// Set event sender for TUI debug panel
528/// This allows debug logs to be displayed in the TUI debug panel
529pub fn set_debug_event_sender(sender: mpsc::Sender<AgentEvent>) {
530    if let Ok(mut guard) = DEBUG_EVENT_SENDER.lock() {
531        *guard = Some(sender);
532    }
533}
534
535/// Convenience macros
536#[macro_export]
537macro_rules! debug_api {
538    ($model:expr, $tokens:expr, $cached:expr) => {
539        $crate::debug::debug_log().api_call($model, $tokens, $cached)
540    };
541}
542
543#[macro_export]
544macro_rules! debug_compress {
545    ($orig:expr, $comp:expr, $ratio:expr) => {
546        $crate::debug::debug_log().compression($orig, $comp, $ratio)
547    };
548}
549
550#[macro_export]
551macro_rules! debug_memory {
552    ($entries:expr, $len:expr) => {
553        $crate::debug::debug_log().memory_save($entries, $len)
554    };
555}
556
557#[macro_export]
558macro_rules! debug_keywords {
559    ($keywords:expr, $source:expr) => {
560        $crate::debug::debug_log().keywords_extracted($keywords, $source)
561    };
562}
563
564#[macro_export]
565macro_rules! debug_tool {
566    ($tool:expr, $input:expr, $result:expr) => {
567        $crate::debug::debug_log().tool_call($tool, $input, $result)
568    };
569}
570
571#[macro_export]
572macro_rules! debug_session {
573    ($msgs:expr, $tokens:expr) => {
574        $crate::debug::debug_log().session_save($msgs, $tokens)
575    };
576}
577
578#[macro_export]
579macro_rules! debug_log_msg {
580    ($cat:expr, $msg:expr) => {
581        $crate::debug::debug_log().log($cat, $msg)
582    };
583}
584
585#[macro_export]
586macro_rules! debug_api_request {
587    ($url:expr, $body:expr) => {
588        $crate::debug::debug_log().api_request($url, $body)
589    };
590}
591
592#[macro_export]
593macro_rules! debug_api_response {
594    ($status:expr, $body:expr) => {
595        $crate::debug::debug_log().api_response($status, $body)
596    };
597}
598
599#[macro_export]
600macro_rules! debug_stream_chunk {
601    ($type:expr, $content:expr) => {
602        $crate::debug::debug_log().stream_chunk($type, $content)
603    };
604}
605
606#[macro_export]
607macro_rules! debug_memory_ai_keywords {
608    ($model:expr, $count:expr, $len:expr, $ai:expr) => {
609        $crate::debug::debug_log().memory_ai_keywords($model, $count, $len, $ai)
610    };
611}
612
613#[macro_export]
614macro_rules! debug_memory_ai_detect {
615    ($model:expr, $count:expr, $len:expr, $ai:expr) => {
616        $crate::debug::debug_log().memory_ai_detection($model, $count, $len, $ai)
617    };
618}