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
5use std::fs::{File, OpenOptions};
6use std::io::Write;
7use std::path::PathBuf;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Mutex;
10use std::time::{SystemTime, UNIX_EPOCH};
11
12static API_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
13static COMPRESSION_COUNT: AtomicU64 = AtomicU64::new(0);
14static MEMORY_SAVE_COUNT: AtomicU64 = AtomicU64::new(0);
15static TOOL_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
16
17/// Debug logger that writes to file and optionally prints to console
18pub struct DebugLog {
19    file: Option<Mutex<File>>,
20    verbose: bool,
21}
22
23impl DebugLog {
24    /// Create a new debug logger
25    /// Writes to ~/.matrix/debug.log if possible
26    pub fn new(verbose: bool) -> Self {
27        let file = Self::open_log_file().ok().map(Mutex::new);
28        Self { file, verbose }
29    }
30
31    fn open_log_file() -> Result<File, std::io::Error> {
32        let home = std::env::var_os("HOME")
33            .or_else(|| std::env::var_os("USERPROFILE"))
34            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME not set"))?;
35        let mut path = PathBuf::from(home);
36        path.push(".matrix");
37        std::fs::create_dir_all(&path)?;
38        path.push("debug.log");
39        OpenOptions::new()
40            .create(true)
41            .append(true)
42            .open(path)
43    }
44
45    fn timestamp() -> String {
46        let now = SystemTime::now()
47            .duration_since(UNIX_EPOCH)
48            .unwrap_or_default()
49            .as_secs();
50        let secs = now % 60;
51        let mins = (now / 60) % 60;
52        let hours = (now / 3600) % 24;
53        format!("{:02}:{:02}:{:02}", hours, mins, secs)
54    }
55
56    /// Log an API call
57    pub fn api_call(&self, model: &str, input_tokens: u32, cached: bool) {
58        let count = API_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
59        let msg = format!(
60            "[{}] API#{}: model={}, input_tokens={}, cached={}",
61            Self::timestamp(), count, model, input_tokens, cached
62        );
63        self.write(&msg);
64    }
65
66    /// Log compression trigger
67    pub fn compression(&self, original_tokens: u32, compressed_tokens: u32, ratio: f32) {
68        let count = COMPRESSION_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
69        let saved = original_tokens - compressed_tokens;
70        let msg = format!(
71            "[{}] COMPRESSION#{}: original={}, compressed={}, saved={}, ratio={:.1}%",
72            Self::timestamp(), count, original_tokens, compressed_tokens, saved, ratio * 100.0
73        );
74        self.write(&msg);
75    }
76
77    /// Log memory save
78    pub fn memory_save(&self, entries: usize, summary_len: usize) {
79        let count = MEMORY_SAVE_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
80        let msg = format!(
81            "[{}] MEMORY#{}: entries={}, summary_len={}chars",
82            Self::timestamp(), count, entries, summary_len
83        );
84        self.write(&msg);
85    }
86
87    /// Log keyword extraction
88    pub fn keywords_extracted(&self, keywords: &[String], source: &str) {
89        let msg = format!(
90            "[{}] KEYWORDS: {} extracted from {}chars | keywords: {}",
91            Self::timestamp(), 
92            keywords.len(), 
93            source.len(),
94            keywords.join(", ")
95        );
96        self.write(&msg);
97    }
98
99    /// Log tool execution
100    pub fn tool_call(&self, tool: &str, input_preview: &str, result_preview: &str) {
101        let count = TOOL_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
102        let msg = format!(
103            "[{}] TOOL#{}: {} | input: {} | result: {}",
104            Self::timestamp(), count, tool, 
105            truncate(input_preview, 50),
106            truncate(result_preview, 50)
107        );
108        self.write(&msg);
109    }
110
111    /// Log session save
112    pub fn session_save(&self, message_count: usize, total_tokens: u64) {
113        let msg = format!(
114            "[{}] SESSION: messages={}, total_tokens={}",
115            Self::timestamp(), message_count, total_tokens
116        );
117        self.write(&msg);
118    }
119
120    /// Log generic debug message
121    pub fn log(&self, category: &str, message: &str) {
122        let msg = format!("[{}] {}: {}", Self::timestamp(), category, message);
123        self.write(&msg);
124    }
125
126    fn write(&self, msg: &str) {
127        // Write to file
128        if let Some(ref file) = self.file
129            && let Ok(mut f) = file.lock() {
130                let _ = f.write_all(msg.as_bytes());
131                let _ = f.write_all(b"\n");
132            }
133        // Print to console if verbose
134        if self.verbose {
135            println!("{}", msg);
136        }
137    }
138
139    /// Get statistics
140    pub fn stats(&self) -> DebugStats {
141        DebugStats {
142            api_calls: API_CALL_COUNT.load(Ordering::Relaxed),
143            compressions: COMPRESSION_COUNT.load(Ordering::Relaxed),
144            memory_saves: MEMORY_SAVE_COUNT.load(Ordering::Relaxed),
145            tool_calls: TOOL_CALL_COUNT.load(Ordering::Relaxed),
146        }
147    }
148}
149
150fn truncate(s: &str, max: usize) -> String {
151    if s.len() > max {
152        format!("{}...", &s[..max.saturating_sub(3)])
153    } else {
154        s.to_string()
155    }
156}
157
158/// Debug statistics
159#[derive(Debug, Clone)]
160pub struct DebugStats {
161    pub api_calls: u64,
162    pub compressions: u64,
163    pub memory_saves: u64,
164    pub tool_calls: u64,
165}
166
167impl DebugStats {
168    pub fn format(&self) -> String {
169        format!(
170            "API: {} │ Compress: {} │ Memory: {} │ Tools: {}",
171            self.api_calls, self.compressions, self.memory_saves, self.tool_calls
172        )
173    }
174}
175
176/// Global debug logger (lazy initialized)
177static DEBUG_LOG: once_cell::sync::Lazy<DebugLog> = once_cell::sync::Lazy::new(|| {
178    // Try to load .env file first (from current directory)
179    let _ = dotenvy::dotenv();
180    
181    // Also try project-level .matrix/.env
182    if let Ok(cwd) = std::env::current_dir() {
183        let matrix_env = cwd.join(".matrix").join(".env");
184        if matrix_env.exists() {
185            let _ = dotenvy::from_path(&matrix_env);
186        }
187    }
188    
189    let verbose = std::env::var("MATRIXCODE_DEBUG")
190        .map(|v| v == "1" || v == "true" || v == "verbose")
191        .unwrap_or(false);
192    DebugLog::new(verbose)
193});
194
195/// Get the global debug logger
196pub fn debug_log() -> &'static DebugLog {
197    &DEBUG_LOG
198}
199
200/// Convenience macros
201#[macro_export]
202macro_rules! debug_api {
203    ($model:expr, $tokens:expr, $cached:expr) => {
204        $crate::debug::debug_log().api_call($model, $tokens, $cached)
205    };
206}
207
208#[macro_export]
209macro_rules! debug_compress {
210    ($orig:expr, $comp:expr, $ratio:expr) => {
211        $crate::debug::debug_log().compression($orig, $comp, $ratio)
212    };
213}
214
215#[macro_export]
216macro_rules! debug_memory {
217    ($entries:expr, $len:expr) => {
218        $crate::debug::debug_log().memory_save($entries, $len)
219    };
220}
221
222#[macro_export]
223macro_rules! debug_keywords {
224    ($keywords:expr, $source:expr) => {
225        $crate::debug::debug_log().keywords_extracted($keywords, $source)
226    };
227}
228
229#[macro_export]
230macro_rules! debug_tool {
231    ($tool:expr, $input:expr, $result:expr) => {
232        $crate::debug::debug_log().tool_call($tool, $input, $result)
233    };
234}
235
236#[macro_export]
237macro_rules! debug_session {
238    ($msgs:expr, $tokens:expr) => {
239        $crate::debug::debug_log().session_save($msgs, $tokens)
240    };
241}
242
243#[macro_export]
244macro_rules! debug_log_msg {
245    ($cat:expr, $msg:expr) => {
246        $crate::debug::debug_log().log($cat, $msg)
247    };
248}