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