1use 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
27const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024;
29
30pub 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 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 pub fn enable(&self, session_id: Option<&str>) {
59 self.enabled.store(true, Ordering::Relaxed);
60
61 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 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 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 pub fn is_enabled(&self) -> bool {
96 self.enabled.load(Ordering::Relaxed)
97 }
98
99 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 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 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 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 if let Ok(metadata) = file.metadata() {
149 if metadata.len() > MAX_LOG_SIZE {
150 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 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 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 log_files.sort_by_key(|e| {
190 e.metadata()
191 .and_then(|m| m.modified())
192 .unwrap_or(SystemTime::UNIX_EPOCH)
193 });
194
195 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 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 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 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 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 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 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 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 pub fn api_request(&self, url: &str, body: &str) {
313 if !self.is_enabled() {
314 return;
315 }
316 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 let panel_msg = format!("url={} | body_len={}chars", url, body.len());
330 self.send_debug_event("API_REQUEST", &panel_msg);
331 }
332
333 pub fn api_response(&self, status: u16, body: &str) {
335 if !self.is_enabled() {
336 return;
337 }
338 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 let panel_msg = format!("status={} | body_len={}chars", status, body.len());
352 self.send_debug_event("API_RESPONSE", &panel_msg);
353 }
354
355 pub fn stream_chunk(&self, chunk_type: &str, content: &str) {
357 if !self.is_enabled() {
358 return;
359 }
360 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 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 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 if !self.is_enabled() {
416 return;
417 }
418
419 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 self.rotate_if_needed();
430 }
431
432 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 self.send_debug_event(category, message);
443 }
444
445 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 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#[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
487static DEBUG_LOG: once_cell::sync::Lazy<DebugLog> = once_cell::sync::Lazy::new(|| {
489 let _ = dotenvy::dotenv();
491
492 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
503static DEBUG_EVENT_SENDER: once_cell::sync::Lazy<Mutex<Option<mpsc::Sender<AgentEvent>>>> =
505 once_cell::sync::Lazy::new(|| Mutex::new(None));
506
507pub fn debug_log() -> &'static DebugLog {
509 &DEBUG_LOG
510}
511
512pub fn enable_debug_logging(session_id: Option<&str>) {
514 DEBUG_LOG.enable(session_id);
515}
516
517pub fn disable_debug_logging() {
519 DEBUG_LOG.disable();
520}
521
522pub fn is_debug_enabled() -> bool {
524 DEBUG_LOG.is_enabled()
525}
526
527pub 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#[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}