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::Mutex;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12use crate::truncate::truncate_with_suffix;
13
14static API_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
15static COMPRESSION_COUNT: AtomicU64 = AtomicU64::new(0);
16static MEMORY_SAVE_COUNT: AtomicU64 = AtomicU64::new(0);
17static TOOL_CALL_COUNT: AtomicU64 = AtomicU64::new(0);
18
19/// Debug logger that writes to file and optionally prints to console
20pub struct DebugLog {
21    file: Option<Mutex<File>>,
22    verbose: bool,
23}
24
25impl DebugLog {
26    /// Create a new debug logger
27    /// Writes to ~/.matrix/debug.log if possible
28    pub fn new(verbose: bool) -> Self {
29        let file = Self::open_log_file().ok().map(Mutex::new);
30        Self { file, verbose }
31    }
32
33    fn open_log_file() -> Result<File, std::io::Error> {
34        let home = std::env::var_os("HOME")
35            .or_else(|| std::env::var_os("USERPROFILE"))
36            .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "HOME not set"))?;
37        let mut path = PathBuf::from(home);
38        path.push(".matrix");
39        std::fs::create_dir_all(&path)?;
40        path.push("debug.log");
41        OpenOptions::new().create(true).append(true).open(path)
42    }
43
44    fn timestamp() -> String {
45        let now = SystemTime::now()
46            .duration_since(UNIX_EPOCH)
47            .unwrap_or_default()
48            .as_secs();
49        let secs = now % 60;
50        let mins = (now / 60) % 60;
51        let hours = (now / 3600) % 24;
52        format!("{:02}:{:02}:{:02}", hours, mins, secs)
53    }
54
55    /// Log an API call
56    pub fn api_call(&self, model: &str, input_tokens: u32, cached: bool) {
57        let count = API_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
58        let msg = format!(
59            "[{}] API#{}: model={}, input_tokens={}, cached={}",
60            Self::timestamp(),
61            count,
62            model,
63            input_tokens,
64            cached
65        );
66        self.write(&msg);
67    }
68
69    /// Log compression trigger
70    pub fn compression(&self, original_tokens: u32, compressed_tokens: u32, ratio: f32) {
71        let count = COMPRESSION_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
72        let saved = original_tokens - compressed_tokens;
73        let msg = format!(
74            "[{}] COMPRESSION#{}: original={}, compressed={}, saved={}, ratio={:.1}%",
75            Self::timestamp(),
76            count,
77            original_tokens,
78            compressed_tokens,
79            saved,
80            ratio * 100.0
81        );
82        self.write(&msg);
83    }
84
85    /// Log memory save
86    pub fn memory_save(&self, entries: usize, summary_len: usize) {
87        let count = MEMORY_SAVE_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
88        let msg = format!(
89            "[{}] MEMORY#{}: entries={}, summary_len={}chars",
90            Self::timestamp(),
91            count,
92            entries,
93            summary_len
94        );
95        self.write(&msg);
96    }
97
98    /// Log keyword extraction
99    pub fn keywords_extracted(&self, keywords: &[String], source: &str) {
100        let msg = format!(
101            "[{}] KEYWORDS: {} extracted from {}chars | keywords: {}",
102            Self::timestamp(),
103            keywords.len(),
104            source.len(),
105            keywords.join(", ")
106        );
107        self.write(&msg);
108    }
109
110    /// Log tool execution
111    pub fn tool_call(&self, tool: &str, input_preview: &str, result_preview: &str) {
112        let count = TOOL_CALL_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
113        let msg = format!(
114            "[{}] TOOL#{}: {} | input: {} | result: {}",
115            Self::timestamp(),
116            count,
117            tool,
118            truncate(input_preview, 50),
119            truncate(result_preview, 50)
120        );
121        self.write(&msg);
122    }
123
124    /// Log session save
125    pub fn session_save(&self, message_count: usize, total_tokens: u64) {
126        let msg = format!(
127            "[{}] SESSION: messages={}, total_tokens={}",
128            Self::timestamp(),
129            message_count,
130            total_tokens
131        );
132        self.write(&msg);
133    }
134
135    /// Log generic debug message
136    pub fn log(&self, category: &str, message: &str) {
137        let msg = format!("[{}] {}: {}", Self::timestamp(), category, message);
138        self.write(&msg);
139    }
140
141    /// Log API request body (for debug)
142    pub fn api_request(&self, url: &str, body: &str) {
143        // Truncate large bodies for readability
144        let body_preview = if body.len() > 5000 {
145            truncate_with_suffix(body, 5000)
146        } else {
147            body.to_string()
148        };
149        let msg = format!(
150            "[{}] API_REQUEST: url={}\n---REQUEST_BODY---\n{}\n---END---",
151            Self::timestamp(),
152            url,
153            body_preview
154        );
155        self.write(&msg);
156    }
157
158    /// Log API response (for debug)
159    pub fn api_response(&self, status: u16, body: &str) {
160        // Truncate large responses
161        let body_preview = if body.len() > 10000 {
162            truncate_with_suffix(body, 10000)
163        } else {
164            body.to_string()
165        };
166        let msg = format!(
167            "[{}] API_RESPONSE: status={}\n---RESPONSE_BODY---\n{}\n---END---",
168            Self::timestamp(),
169            status,
170            body_preview
171        );
172        self.write(&msg);
173    }
174
175    /// Log streaming chunk (for debug, limited)
176    pub fn stream_chunk(&self, chunk_type: &str, content: &str) {
177        // Only log small chunks to avoid flooding
178        let preview = if content.len() > 200 {
179            truncate_with_suffix(content, 200)
180        } else {
181            content.to_string()
182        };
183        let msg = format!(
184            "[{}] STREAM_CHUNK: type={} | {}",
185            Self::timestamp(),
186            chunk_type,
187            preview
188        );
189        self.write(&msg);
190    }
191
192    /// Log AI memory extraction (keyword extraction with fast model)
193    pub fn memory_ai_keywords(&self, model: &str, keywords_count: usize, source_len: usize, used_ai: bool) {
194        let method = if used_ai { "AI" } else { "rule" };
195        let msg = format!(
196            "[{}] MEMORY_AI_KEYWORDS: model={}, method={}, keywords={}, source_len={}chars",
197            Self::timestamp(),
198            model,
199            method,
200            keywords_count,
201            source_len
202        );
203        self.write(&msg);
204    }
205
206    /// Log AI memory detection (memory extraction from response)
207    pub fn memory_ai_detection(&self, model: &str, entries_count: usize, text_len: usize, used_ai: bool) {
208        let method = if used_ai { "AI" } else { "rule" };
209        let msg = format!(
210            "[{}] MEMORY_AI_DETECT: model={}, method={}, entries={}, text_len={}chars",
211            Self::timestamp(),
212            model,
213            method,
214            entries_count,
215            text_len
216        );
217        self.write(&msg);
218    }
219
220    fn write(&self, msg: &str) {
221        // Write to file
222        if let Some(ref file) = self.file
223            && let Ok(mut f) = file.lock()
224        {
225            let _ = f.write_all(msg.as_bytes());
226            let _ = f.write_all(b"\n");
227        }
228        // Print to console if verbose
229        if self.verbose {
230            println!("{}", msg);
231        }
232    }
233
234    /// Get statistics
235    pub fn stats(&self) -> DebugStats {
236        DebugStats {
237            api_calls: API_CALL_COUNT.load(Ordering::Relaxed),
238            compressions: COMPRESSION_COUNT.load(Ordering::Relaxed),
239            memory_saves: MEMORY_SAVE_COUNT.load(Ordering::Relaxed),
240            tool_calls: TOOL_CALL_COUNT.load(Ordering::Relaxed),
241        }
242    }
243}
244
245fn truncate(s: &str, max: usize) -> String {
246    truncate_with_suffix(s, max)
247}
248
249/// Debug statistics
250#[derive(Debug, Clone)]
251pub struct DebugStats {
252    pub api_calls: u64,
253    pub compressions: u64,
254    pub memory_saves: u64,
255    pub tool_calls: u64,
256}
257
258impl DebugStats {
259    pub fn format(&self) -> String {
260        format!(
261            "API: {} │ Compress: {} │ Memory: {} │ Tools: {}",
262            self.api_calls, self.compressions, self.memory_saves, self.tool_calls
263        )
264    }
265}
266
267/// Global debug logger (lazy initialized)
268static DEBUG_LOG: once_cell::sync::Lazy<DebugLog> = once_cell::sync::Lazy::new(|| {
269    // Try to load .env file first (from current directory)
270    let _ = dotenvy::dotenv();
271
272    // Also try project-level .matrix/.env
273    if let Ok(cwd) = std::env::current_dir() {
274        let matrix_env = cwd.join(".matrix").join(".env");
275        if matrix_env.exists() {
276            let _ = dotenvy::from_path(&matrix_env);
277        }
278    }
279
280    let verbose = std::env::var("MATRIXCODE_DEBUG")
281        .map(|v| v == "1" || v == "true" || v == "verbose")
282        .unwrap_or(false);
283    DebugLog::new(verbose)
284});
285
286/// Get the global debug logger
287pub fn debug_log() -> &'static DebugLog {
288    &DEBUG_LOG
289}
290
291/// Convenience macros
292#[macro_export]
293macro_rules! debug_api {
294    ($model:expr, $tokens:expr, $cached:expr) => {
295        $crate::debug::debug_log().api_call($model, $tokens, $cached)
296    };
297}
298
299#[macro_export]
300macro_rules! debug_compress {
301    ($orig:expr, $comp:expr, $ratio:expr) => {
302        $crate::debug::debug_log().compression($orig, $comp, $ratio)
303    };
304}
305
306#[macro_export]
307macro_rules! debug_memory {
308    ($entries:expr, $len:expr) => {
309        $crate::debug::debug_log().memory_save($entries, $len)
310    };
311}
312
313#[macro_export]
314macro_rules! debug_keywords {
315    ($keywords:expr, $source:expr) => {
316        $crate::debug::debug_log().keywords_extracted($keywords, $source)
317    };
318}
319
320#[macro_export]
321macro_rules! debug_tool {
322    ($tool:expr, $input:expr, $result:expr) => {
323        $crate::debug::debug_log().tool_call($tool, $input, $result)
324    };
325}
326
327#[macro_export]
328macro_rules! debug_session {
329    ($msgs:expr, $tokens:expr) => {
330        $crate::debug::debug_log().session_save($msgs, $tokens)
331    };
332}
333
334#[macro_export]
335macro_rules! debug_log_msg {
336    ($cat:expr, $msg:expr) => {
337        $crate::debug::debug_log().log($cat, $msg)
338    };
339}
340
341#[macro_export]
342macro_rules! debug_api_request {
343    ($url:expr, $body:expr) => {
344        $crate::debug::debug_log().api_request($url, $body)
345    };
346}
347
348#[macro_export]
349macro_rules! debug_api_response {
350    ($status:expr, $body:expr) => {
351        $crate::debug::debug_log().api_response($status, $body)
352    };
353}
354
355#[macro_export]
356macro_rules! debug_stream_chunk {
357    ($type:expr, $content:expr) => {
358        $crate::debug::debug_log().stream_chunk($type, $content)
359    };
360}
361
362#[macro_export]
363macro_rules! debug_memory_ai_keywords {
364    ($model:expr, $count:expr, $len:expr, $ai:expr) => {
365        $crate::debug::debug_log().memory_ai_keywords($model, $count, $len, $ai)
366    };
367}
368
369#[macro_export]
370macro_rules! debug_memory_ai_detect {
371    ($model:expr, $count:expr, $len:expr, $ai:expr) => {
372        $crate::debug::debug_log().memory_ai_detection($model, $count, $len, $ai)
373    };
374}