1use 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
19pub struct DebugLog {
21 file: Option<Mutex<File>>,
22 verbose: bool,
23}
24
25impl DebugLog {
26 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 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 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 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 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 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 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 pub fn log(&self, category: &str, message: &str) {
137 let msg = format!("[{}] {}: {}", Self::timestamp(), category, message);
138 self.write(&msg);
139 }
140
141 pub fn api_request(&self, url: &str, body: &str) {
143 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 pub fn api_response(&self, status: u16, body: &str) {
160 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 pub fn stream_chunk(&self, chunk_type: &str, content: &str) {
177 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 fn write(&self, msg: &str) {
193 if let Some(ref file) = self.file
195 && let Ok(mut f) = file.lock()
196 {
197 let _ = f.write_all(msg.as_bytes());
198 let _ = f.write_all(b"\n");
199 }
200 if self.verbose {
202 println!("{}", msg);
203 }
204 }
205
206 pub fn stats(&self) -> DebugStats {
208 DebugStats {
209 api_calls: API_CALL_COUNT.load(Ordering::Relaxed),
210 compressions: COMPRESSION_COUNT.load(Ordering::Relaxed),
211 memory_saves: MEMORY_SAVE_COUNT.load(Ordering::Relaxed),
212 tool_calls: TOOL_CALL_COUNT.load(Ordering::Relaxed),
213 }
214 }
215}
216
217fn truncate(s: &str, max: usize) -> String {
218 truncate_with_suffix(s, max)
219}
220
221#[derive(Debug, Clone)]
223pub struct DebugStats {
224 pub api_calls: u64,
225 pub compressions: u64,
226 pub memory_saves: u64,
227 pub tool_calls: u64,
228}
229
230impl DebugStats {
231 pub fn format(&self) -> String {
232 format!(
233 "API: {} │ Compress: {} │ Memory: {} │ Tools: {}",
234 self.api_calls, self.compressions, self.memory_saves, self.tool_calls
235 )
236 }
237}
238
239static DEBUG_LOG: once_cell::sync::Lazy<DebugLog> = once_cell::sync::Lazy::new(|| {
241 let _ = dotenvy::dotenv();
243
244 if let Ok(cwd) = std::env::current_dir() {
246 let matrix_env = cwd.join(".matrix").join(".env");
247 if matrix_env.exists() {
248 let _ = dotenvy::from_path(&matrix_env);
249 }
250 }
251
252 let verbose = std::env::var("MATRIXCODE_DEBUG")
253 .map(|v| v == "1" || v == "true" || v == "verbose")
254 .unwrap_or(false);
255 DebugLog::new(verbose)
256});
257
258pub fn debug_log() -> &'static DebugLog {
260 &DEBUG_LOG
261}
262
263#[macro_export]
265macro_rules! debug_api {
266 ($model:expr, $tokens:expr, $cached:expr) => {
267 $crate::debug::debug_log().api_call($model, $tokens, $cached)
268 };
269}
270
271#[macro_export]
272macro_rules! debug_compress {
273 ($orig:expr, $comp:expr, $ratio:expr) => {
274 $crate::debug::debug_log().compression($orig, $comp, $ratio)
275 };
276}
277
278#[macro_export]
279macro_rules! debug_memory {
280 ($entries:expr, $len:expr) => {
281 $crate::debug::debug_log().memory_save($entries, $len)
282 };
283}
284
285#[macro_export]
286macro_rules! debug_keywords {
287 ($keywords:expr, $source:expr) => {
288 $crate::debug::debug_log().keywords_extracted($keywords, $source)
289 };
290}
291
292#[macro_export]
293macro_rules! debug_tool {
294 ($tool:expr, $input:expr, $result:expr) => {
295 $crate::debug::debug_log().tool_call($tool, $input, $result)
296 };
297}
298
299#[macro_export]
300macro_rules! debug_session {
301 ($msgs:expr, $tokens:expr) => {
302 $crate::debug::debug_log().session_save($msgs, $tokens)
303 };
304}
305
306#[macro_export]
307macro_rules! debug_log_msg {
308 ($cat:expr, $msg:expr) => {
309 $crate::debug::debug_log().log($cat, $msg)
310 };
311}
312
313#[macro_export]
314macro_rules! debug_api_request {
315 ($url:expr, $body:expr) => {
316 $crate::debug::debug_log().api_request($url, $body)
317 };
318}
319
320#[macro_export]
321macro_rules! debug_api_response {
322 ($status:expr, $body:expr) => {
323 $crate::debug::debug_log().api_response($status, $body)
324 };
325}
326
327#[macro_export]
328macro_rules! debug_stream_chunk {
329 ($type:expr, $content:expr) => {
330 $crate::debug::debug_log().stream_chunk($type, $content)
331 };
332}