1use super::loggable::Loggable;
7use crate::checkpoint::timestamp;
8use crate::json_parser::printer::Printable;
9use crate::logger::{
10 Colors, ARROW, BOX_BL, BOX_BR, BOX_H, BOX_TL, BOX_TR, BOX_V, CHECK, CROSS, INFO, WARN,
11};
12use crate::workspace::Workspace;
13use std::fs::{self, OpenOptions};
14use std::io::{IsTerminal, Write};
15use std::path::Path;
16use std::sync::Arc;
17
18use crate::logger::output::strip_ansi_codes;
19
20pub struct Logger {
25 colors: Colors,
26 log_file: Option<String>,
28 workspace: Option<Arc<dyn Workspace>>,
30 workspace_log_path: Option<String>,
32}
33
34impl Logger {
35 pub const fn new(colors: Colors) -> Self {
37 Self {
38 colors,
39 log_file: None,
40 workspace: None,
41 workspace_log_path: None,
42 }
43 }
44
45 pub fn with_log_file(mut self, path: &str) -> Self {
55 self.log_file = Some(path.to_string());
56 self
57 }
58
59 pub fn with_workspace_log(
70 mut self,
71 workspace: Arc<dyn Workspace>,
72 relative_path: &str,
73 ) -> Self {
74 self.workspace = Some(workspace);
75 self.workspace_log_path = Some(relative_path.to_string());
76 self
77 }
78
79 fn log_to_file(&self, msg: &str) {
81 let clean_msg = strip_ansi_codes(msg);
83
84 if let (Some(workspace), Some(ref path)) = (&self.workspace, &self.workspace_log_path) {
86 let path = std::path::Path::new(path);
87 if let Some(parent) = path.parent() {
89 let _ = workspace.create_dir_all(parent);
90 }
91 let _ = workspace.append_bytes(path, format!("{clean_msg}\n").as_bytes());
93 return;
94 }
95
96 if let Some(ref path) = self.log_file {
98 if let Some(parent) = Path::new(path).parent() {
99 let _ = fs::create_dir_all(parent);
100 }
101 if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
102 let _ = writeln!(file, "{clean_msg}");
103 let _ = file.flush();
104 let _ = file.sync_all();
106 }
107 }
108 }
109
110 pub fn info(&self, msg: &str) {
112 let c = &self.colors;
113 println!(
114 "{}[{}]{} {}{}{} {}",
115 c.dim(),
116 timestamp(),
117 c.reset(),
118 c.blue(),
119 INFO,
120 c.reset(),
121 msg
122 );
123 self.log_to_file(&format!("[{}] [INFO] {}", timestamp(), msg));
124 }
125
126 pub fn success(&self, msg: &str) {
128 let c = &self.colors;
129 println!(
130 "{}[{}]{} {}{}{} {}{}{}",
131 c.dim(),
132 timestamp(),
133 c.reset(),
134 c.green(),
135 CHECK,
136 c.reset(),
137 c.green(),
138 msg,
139 c.reset()
140 );
141 self.log_to_file(&format!("[{}] [OK] {}", timestamp(), msg));
142 }
143
144 pub fn warn(&self, msg: &str) {
146 let c = &self.colors;
147 println!(
148 "{}[{}]{} {}{}{} {}{}{}",
149 c.dim(),
150 timestamp(),
151 c.reset(),
152 c.yellow(),
153 WARN,
154 c.reset(),
155 c.yellow(),
156 msg,
157 c.reset()
158 );
159 self.log_to_file(&format!("[{}] [WARN] {}", timestamp(), msg));
160 }
161
162 pub fn error(&self, msg: &str) {
164 let c = &self.colors;
165 eprintln!(
166 "{}[{}]{} {}{}{} {}{}{}",
167 c.dim(),
168 timestamp(),
169 c.reset(),
170 c.red(),
171 CROSS,
172 c.reset(),
173 c.red(),
174 msg,
175 c.reset()
176 );
177 self.log_to_file(&format!("[{}] [ERROR] {}", timestamp(), msg));
178 }
179
180 pub fn step(&self, msg: &str) {
182 let c = &self.colors;
183 println!(
184 "{}[{}]{} {}{}{} {}",
185 c.dim(),
186 timestamp(),
187 c.reset(),
188 c.magenta(),
189 ARROW,
190 c.reset(),
191 msg
192 );
193 self.log_to_file(&format!("[{}] [STEP] {}", timestamp(), msg));
194 }
195
196 pub fn header(&self, title: &str, color_fn: fn(Colors) -> &'static str) {
203 let c = self.colors;
204 let color = color_fn(c);
205 let width = 60;
206 let title_len = title.chars().count();
207 let padding = (width - title_len - 2) / 2;
208
209 println!();
210 println!(
211 "{}{}{}{}{}{}",
212 color,
213 c.bold(),
214 BOX_TL,
215 BOX_H.to_string().repeat(width),
216 BOX_TR,
217 c.reset()
218 );
219 println!(
220 "{}{}{}{}{}{}{}{}{}{}",
221 color,
222 c.bold(),
223 BOX_V,
224 " ".repeat(padding),
225 c.white(),
226 title,
227 color,
228 " ".repeat(width - padding - title_len),
229 BOX_V,
230 c.reset()
231 );
232 println!(
233 "{}{}{}{}{}{}",
234 color,
235 c.bold(),
236 BOX_BL,
237 BOX_H.to_string().repeat(width),
238 BOX_BR,
239 c.reset()
240 );
241 }
242
243 pub fn subheader(&self, title: &str) {
245 let c = &self.colors;
246 println!();
247 println!("{}{}{} {}{}", c.bold(), c.blue(), ARROW, title, c.reset());
248 println!("{}{}──{}", c.dim(), "─".repeat(title.len()), c.reset());
249 }
250}
251
252impl Default for Logger {
253 fn default() -> Self {
254 Self::new(Colors::new())
255 }
256}
257
258impl Loggable for Logger {
261 fn log(&self, msg: &str) {
262 self.log_to_file(msg);
263 }
264
265 fn info(&self, msg: &str) {
266 let c = &self.colors;
267 println!(
268 "{}[{}]{} {}{}{} {}",
269 c.dim(),
270 timestamp(),
271 c.reset(),
272 c.blue(),
273 INFO,
274 c.reset(),
275 msg
276 );
277 self.log(&format!("[{}] [INFO] {msg}", timestamp()));
278 }
279
280 fn success(&self, msg: &str) {
281 let c = &self.colors;
282 println!(
283 "{}[{}]{} {}{}{} {}{}{}",
284 c.dim(),
285 timestamp(),
286 c.reset(),
287 c.green(),
288 CHECK,
289 c.reset(),
290 c.green(),
291 msg,
292 c.reset()
293 );
294 self.log(&format!("[{}] [OK] {msg}", timestamp()));
295 }
296
297 fn warn(&self, msg: &str) {
298 let c = &self.colors;
299 println!(
300 "{}[{}]{} {}{}{} {}{}{}",
301 c.dim(),
302 timestamp(),
303 c.reset(),
304 c.yellow(),
305 WARN,
306 c.reset(),
307 c.yellow(),
308 msg,
309 c.reset()
310 );
311 self.log(&format!("[{}] [WARN] {msg}", timestamp()));
312 }
313
314 fn error(&self, msg: &str) {
315 let c = &self.colors;
316 eprintln!(
317 "{}[{}]{} {}{}{} {}{}{}",
318 c.dim(),
319 timestamp(),
320 c.reset(),
321 c.red(),
322 CROSS,
323 c.reset(),
324 c.red(),
325 msg,
326 c.reset()
327 );
328 self.log(&format!("[{}] [ERROR] {msg}", timestamp()));
329 }
330
331 fn header(&self, title: &str, color_fn: fn(Colors) -> &'static str) {
332 let c = self.colors;
336 let color = color_fn(c);
337 let width = 60;
338 let title_len = title.chars().count();
339 let padding = (width - title_len - 2) / 2;
340
341 println!();
342 println!(
343 "{}{}{}{}{}{}",
344 color,
345 c.bold(),
346 BOX_TL,
347 BOX_H.to_string().repeat(width),
348 BOX_TR,
349 c.reset()
350 );
351 println!(
352 "{}{}{}{}{}{}{}{}{}{}",
353 color,
354 c.bold(),
355 BOX_V,
356 " ".repeat(padding),
357 c.white(),
358 title,
359 color,
360 " ".repeat(width - padding - title_len),
361 BOX_V,
362 c.reset()
363 );
364 println!(
365 "{}{}{}{}{}{}",
366 color,
367 c.bold(),
368 BOX_BL,
369 BOX_H.to_string().repeat(width),
370 BOX_BR,
371 c.reset()
372 );
373 }
374}
375
376impl std::io::Write for Logger {
379 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
380 std::io::stdout().write(buf)
382 }
383
384 fn flush(&mut self) -> std::io::Result<()> {
385 std::io::stdout().flush()
386 }
387}
388
389impl Printable for Logger {
390 fn is_terminal(&self) -> bool {
391 std::io::stdout().is_terminal()
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 #[cfg(feature = "test-utils")]
402 mod workspace_tests {
403 use super::super::*;
404 use crate::workspace::MemoryWorkspace;
405
406 #[test]
407 fn test_logger_with_workspace_writes_to_file() {
408 let workspace = Arc::new(MemoryWorkspace::new_test());
409 let logger = Logger::new(Colors::new())
410 .with_workspace_log(workspace.clone(), ".agent/logs/test.log");
411
412 Loggable::info(&logger, "test message");
414
415 let content = workspace.get_file(".agent/logs/test.log").unwrap();
417 assert!(content.contains("test message"));
418 assert!(content.contains("[INFO]"));
419 }
420
421 #[test]
422 fn test_logger_with_workspace_strips_ansi_codes() {
423 let workspace = Arc::new(MemoryWorkspace::new_test());
424 let logger = Logger::new(Colors::new())
425 .with_workspace_log(workspace.clone(), ".agent/logs/test.log");
426
427 logger.log("[INFO] \x1b[31mcolored\x1b[0m message");
429
430 let content = workspace.get_file(".agent/logs/test.log").unwrap();
431 assert!(content.contains("colored message"));
432 assert!(!content.contains("\x1b["));
433 }
434
435 #[test]
436 fn test_logger_with_workspace_creates_parent_dirs() {
437 let workspace = Arc::new(MemoryWorkspace::new_test());
438 let logger = Logger::new(Colors::new())
439 .with_workspace_log(workspace.clone(), ".agent/logs/nested/deep/test.log");
440
441 Loggable::info(&logger, "nested log");
442
443 assert!(workspace.exists(std::path::Path::new(".agent/logs/nested/deep")));
445 let content = workspace
446 .get_file(".agent/logs/nested/deep/test.log")
447 .unwrap();
448 assert!(content.contains("nested log"));
449 }
450 }
451}