1use crate::formatters::Formatter;
2use crate::level::LogLevel;
3use crate::record::Record;
4use flate2::write::GzEncoder;
5use flate2::Compression;
6use std::fmt;
7use std::fs::{File, OpenOptions};
8use std::io::{self, Write};
9use std::path::Path;
10use std::sync::Mutex;
11
12use super::{Handler, HandlerFilter};
13
14pub struct FileHandler {
16 level: LogLevel,
17 enabled: bool,
18 formatter: Formatter,
19 file: Mutex<Option<File>>,
20 path: String,
21 max_size: Option<usize>,
22 max_files: Option<usize>,
23 compress: bool,
24 filter: Option<HandlerFilter>,
25 batch_buffer: Mutex<Vec<Record>>,
26 batch_size: Option<usize>,
27}
28
29impl fmt::Debug for FileHandler {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.debug_struct("FileHandler")
32 .field("level", &self.level)
33 .field("enabled", &self.enabled)
34 .field("formatter", &self.formatter)
35 .field("path", &self.path)
36 .field("max_size", &self.max_size)
37 .field("max_files", &self.max_files)
38 .field("compress", &self.compress)
39 .field("batch_size", &self.batch_size)
40 .finish()
41 }
42}
43
44impl Clone for FileHandler {
45 fn clone(&self) -> Self {
46 let file = if let Ok(guard) = self.file.lock() {
47 if guard.is_some() {
48 OpenOptions::new()
50 .create(true)
51 .append(true)
52 .open(&self.path)
53 .ok()
54 .map(|f| Mutex::new(Some(f)))
55 .unwrap_or_else(|| Mutex::new(None))
56 } else {
57 Mutex::new(None)
58 }
59 } else {
60 Mutex::new(None)
61 };
62
63 Self {
64 level: self.level,
65 enabled: self.enabled,
66 formatter: self.formatter.clone(),
67 file,
68 path: self.path.clone(),
69 max_size: self.max_size,
70 max_files: self.max_files,
71 compress: self.compress,
72 filter: self.filter.clone(),
73 batch_buffer: Mutex::new({
74 let buffer_guard = self.batch_buffer.lock().unwrap();
75 buffer_guard.clone()
76 }),
77 batch_size: self.batch_size,
78 }
79 }
80}
81
82impl FileHandler {
83 pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
84 let path = path.as_ref().to_string_lossy().into_owned();
85 let file = OpenOptions::new().create(true).append(true).open(&path)?;
86
87 Ok(Self {
88 level: LogLevel::Info,
89 enabled: true,
90 formatter: Formatter::text(),
91 file: Mutex::new(Some(file)),
92 path,
93 max_size: None,
94 max_files: None,
95 compress: false,
96 filter: None,
97 batch_buffer: Mutex::new(Vec::new()),
98 batch_size: None,
99 })
100 }
101
102 pub fn with_level(mut self, level: LogLevel) -> Self {
103 self.level = level;
104 self
105 }
106
107 pub fn with_formatter(mut self, formatter: Formatter) -> Self {
108 self.formatter = formatter;
109 self
110 }
111
112 pub fn with_colors(mut self, use_colors: bool) -> Self {
113 self.formatter = self.formatter.with_colors(use_colors);
114 self
115 }
116
117 pub fn with_pattern(self, pattern: impl Into<String>) -> Self {
118 let mut handler = self;
119 let formatter = handler.formatter.with_pattern(pattern);
120 handler.formatter = formatter;
121 handler
122 }
123
124 pub fn with_format<F>(mut self, format_fn: F) -> Self
125 where
126 F: Fn(&Record) -> String + Send + Sync + 'static,
127 {
128 self.formatter = self.formatter.with_format(format_fn);
129 self
130 }
131
132 pub fn with_rotation(mut self, max_size: usize, max_files: usize) -> Self {
133 self.max_size = Some(max_size);
134 self.max_files = Some(max_files);
135 self
136 }
137
138 pub fn with_filter(mut self, filter: HandlerFilter) -> Self {
139 self.filter = Some(filter);
140 self
141 }
142
143 pub fn with_compression(mut self, compress: bool) -> Self {
144 self.compress = compress;
145 self
146 }
147
148 pub fn with_batching(mut self, batch_size: usize) -> Self {
149 self.batch_size = Some(batch_size);
150 self
151 }
152
153 fn rotate_if_needed(&self) -> io::Result<()> {
154 if let (Some(max_size), Some(max_files)) = (self.max_size, self.max_files) {
155 let mut file_guard = self
156 .file
157 .lock()
158 .map_err(|_| io::Error::new(io::ErrorKind::Other, "Failed to lock file mutex"))?;
159
160 if let Some(file) = file_guard.as_ref() {
161 let metadata = file.metadata()?;
162 if metadata.len() as usize >= max_size {
163 *file_guard = None;
165
166 let oldest_log = format!("{}.{}", self.path, max_files);
168 if Path::new(&oldest_log).exists() {
169 std::fs::remove_file(&oldest_log)?;
170 }
171
172 for i in (1..max_files).rev() {
174 let old_path = format!("{}.{}", self.path, i);
175 let new_path = format!("{}.{}", self.path, i + 1);
176 if Path::new(&old_path).exists() {
177 std::fs::rename(&old_path, &new_path)?;
178 }
179 }
180
181 if Path::new(&self.path).exists() {
183 let rotated_path = format!("{}.1", self.path);
184 std::fs::rename(&self.path, &rotated_path)?;
185 if self.compress {
186 let mut input = File::open(&rotated_path)?;
187 let gz_path = format!("{}.gz", rotated_path);
188 let mut encoder =
189 GzEncoder::new(File::create(&gz_path)?, Compression::default());
190 std::io::copy(&mut input, &mut encoder)?;
191 encoder.finish()?;
192 std::fs::remove_file(&rotated_path)?;
193 }
194 }
195
196 *file_guard = Some(
198 OpenOptions::new()
199 .create(true)
200 .append(true)
201 .open(&self.path)?,
202 );
203
204 if let Some(file) = file_guard.as_mut() {
206 file.flush()?;
207 }
208 }
209 }
210 }
211 Ok(())
212 }
213}
214
215impl Handler for FileHandler {
216 fn handle(&self, record: &Record) -> Result<(), String> {
217 if !self.enabled || record.level() < self.level {
218 return Ok(());
219 }
220 if let Some(filter) = &self.filter {
221 if !(filter)(record) {
222 return Ok(());
223 }
224 }
225 if let Some(batch_size) = self.batch_size {
226 let mut buffer = self.batch_buffer.lock().unwrap();
227 buffer.push(record.clone());
228 if buffer.len() >= batch_size {
229 let batch = buffer.drain(..).collect::<Vec<_>>();
230 drop(buffer);
231 return self.handle_batch(&batch);
232 }
233 return Ok(());
234 }
235 let formatted = self.formatter.format(record);
236 if let Err(e) = self.rotate_if_needed() {
237 return Err(format!("Failed to rotate log file: {}", e));
238 }
239 let mut file_guard = self
240 .file
241 .lock()
242 .map_err(|e| format!("Failed to lock file mutex: {}", e))?;
243 if let Some(file) = file_guard.as_mut() {
244 match write!(file, "{}", formatted) {
245 Ok(_) => Ok(()),
246 Err(e) => {
247 if e.kind() == io::ErrorKind::PermissionDenied {
249 Err(format!("Permission denied: {}", e))
250 } else {
251 Err(format!("Failed to write to file: {}", e))
252 }
253 }
254 }
255 } else {
256 Err("No file handle available".to_string())
257 }
258 }
259
260 fn level(&self) -> LogLevel {
261 self.level
262 }
263
264 fn set_level(&mut self, level: LogLevel) {
265 self.level = level;
266 }
267
268 fn is_enabled(&self) -> bool {
269 self.enabled
270 }
271
272 fn set_enabled(&mut self, enabled: bool) {
273 self.enabled = enabled;
274 }
275
276 fn formatter(&self) -> &Formatter {
277 &self.formatter
278 }
279
280 fn set_formatter(&mut self, formatter: Formatter) {
281 self.formatter = formatter;
282 }
283
284 fn set_filter(&mut self, filter: Option<HandlerFilter>) {
285 self.filter = filter;
286 }
287
288 fn filter(&self) -> Option<&HandlerFilter> {
289 self.filter.as_ref()
290 }
291
292 fn handle_batch(&self, records: &[Record]) -> Result<(), String> {
293 let mut file_guard = self
294 .file
295 .lock()
296 .map_err(|e| format!("Failed to lock file mutex: {}", e))?;
297 for record in records {
298 if !self.enabled || record.level() < self.level {
299 continue;
300 }
301 if let Some(filter) = &self.filter {
302 if !(filter)(record) {
303 continue;
304 }
305 }
306 let formatted = self.formatter.format(record);
307 if let Err(e) = self.rotate_if_needed() {
308 return Err(format!("Failed to rotate log file: {}", e));
309 }
310 if let Some(file) = file_guard.as_mut() {
311 if let Err(e) = write!(file, "{}", formatted) {
312 return Err(format!("Failed to write to file: {}", e));
313 }
314 }
315 }
316 Ok(())
317 }
318
319 fn init(&mut self) -> Result<(), String> {
320 Ok(())
321 }
322
323 fn flush(&self) -> Result<(), String> {
324 let mut file_guard = self.file.lock().unwrap();
325 if let Some(file) = file_guard.as_mut() {
326 file.flush()
327 .map_err(|e| format!("Failed to flush file: {}", e))?;
328 }
329 Ok(())
330 }
331
332 fn shutdown(&mut self) -> Result<(), String> {
333 self.flush()
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use std::fs;
341 use tempfile::TempDir;
342
343 #[test]
344 fn test_file_handler_creation() -> io::Result<()> {
345 let temp_dir = TempDir::new()?;
346 let log_path = temp_dir.path().join("test.log");
347 let handler = FileHandler::new(log_path.to_str().unwrap())?;
348
349 assert_eq!(handler.level(), LogLevel::Info);
350 assert!(handler.is_enabled());
351 assert_eq!(handler.path, log_path.to_str().unwrap());
352 Ok(())
353 }
354
355 #[test]
356 fn test_file_handler_level_filtering() -> io::Result<()> {
357 let temp_dir = TempDir::new()?;
358 let log_path = temp_dir.path().join("test.log");
359 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
360 handler.set_level(LogLevel::Warning);
361
362 let info_record = Record::new(
363 LogLevel::Info,
364 "Info message",
365 Some("test_module".to_string()),
366 Some("test.rs".to_string()),
367 Some(42),
368 );
369 let warning_record = Record::new(
370 LogLevel::Warning,
371 "Warning message",
372 Some("test_module".to_string()),
373 Some("test.rs".to_string()),
374 Some(42),
375 );
376
377 assert!(handler.handle(&info_record).is_ok());
378 assert!(handler.handle(&warning_record).is_ok());
379
380 let contents = fs::read_to_string(log_path)?;
381 assert!(!contents.contains("Info message"));
382 assert!(contents.contains("Warning message"));
383 Ok(())
384 }
385
386 #[test]
387 fn test_file_handler_disabled() -> io::Result<()> {
388 let temp_dir = TempDir::new()?;
389 let log_path = temp_dir.path().join("test.log");
390 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
391 handler.set_enabled(false);
392
393 let record = Record::new(
394 LogLevel::Info,
395 "Test message",
396 Some("test_module".to_string()),
397 Some("test.rs".to_string()),
398 Some(42),
399 );
400
401 assert!(handler.handle(&record).is_ok());
402 let contents = fs::read_to_string(log_path)?;
403 assert!(contents.is_empty());
404 Ok(())
405 }
406
407 #[test]
408 fn test_file_handler_formatting() -> io::Result<()> {
409 let temp_dir = TempDir::new()?;
410 let log_path = temp_dir.path().join("test.log");
411 let handler = FileHandler::new(log_path.to_str().unwrap())?
412 .with_pattern("{level} - {message}")
413 .with_colors(false);
414
415 let record = Record::new(
416 LogLevel::Info,
417 "Test message",
418 Some("test_module".to_string()),
419 Some("test.rs".to_string()),
420 Some(42),
421 );
422
423 assert!(handler.handle(&record).is_ok());
424 let contents = fs::read_to_string(log_path)?;
425 println!("File contents: '{}'", contents);
426 println!("File contents length: {}", contents.len());
427 println!("File contents bytes: {:?}", contents.as_bytes());
428
429 let trimmed_contents = contents.trim();
431 println!("Trimmed contents: '{}'", trimmed_contents);
432 assert!(trimmed_contents.contains("INFO - Test message"));
433 Ok(())
434 }
435
436 #[test]
437 fn test_file_handler_metadata() -> io::Result<()> {
438 let temp_dir = TempDir::new()?;
439 let log_path = temp_dir.path().join("test.log");
440 let handler = FileHandler::new(log_path.to_str().unwrap())?;
441
442 let record = Record::new(
443 LogLevel::Info,
444 "Test message",
445 Some("test_module".to_string()),
446 Some("test.rs".to_string()),
447 Some(42),
448 )
449 .with_metadata("key1", "value1")
450 .with_metadata("key2", "value2");
451
452 assert!(handler.handle(&record).is_ok());
453 let contents = fs::read_to_string(log_path)?;
454 assert!(contents.contains("key1=value1"));
455 assert!(contents.contains("key2=value2"));
456 Ok(())
457 }
458
459 #[test]
460 fn test_file_handler_structured_data() -> io::Result<()> {
461 let temp_dir = TempDir::new()?;
462 let log_path = temp_dir.path().join("test.log");
463 let handler = FileHandler::new(log_path.to_str().unwrap())?;
464
465 let data = serde_json::json!({
466 "user_id": 123,
467 "action": "login"
468 });
469
470 let record = Record::new(
471 LogLevel::Info,
472 "Structured data test",
473 Some("test_module".to_string()),
474 Some("test.rs".to_string()),
475 Some(42),
476 )
477 .with_structured_data("data", &data)
478 .unwrap();
479
480 assert!(handler.handle(&record).is_ok());
481 let contents = fs::read_to_string(log_path)?;
482 assert!(contents.contains("data="));
483 assert!(contents.contains(r#""user_id":123"#));
484 assert!(contents.contains(r#""action":"login""#));
485 Ok(())
486 }
487
488 #[test]
489 fn test_file_handler_rotation() -> io::Result<()> {
490 let temp_dir = TempDir::new()?;
491 let log_path = temp_dir.path().join("test.log");
492 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_rotation(100, 3); let record = Record::new(
496 LogLevel::Info,
497 "A".repeat(200).as_str(), Some("test_module".to_string()),
499 Some("test.rs".to_string()),
500 Some(42),
501 );
502 assert!(handler.handle(&record).is_ok());
503
504 let new_record = Record::new(
506 LogLevel::Info,
507 "New message",
508 Some("test_module".to_string()),
509 Some("test.rs".to_string()),
510 Some(42),
511 );
512 assert!(handler.handle(&new_record).is_ok());
513
514 let rotated_path = format!("{}.1", log_path.to_string_lossy());
516 assert!(Path::new(&rotated_path).exists());
517
518 let contents = fs::read_to_string(&log_path)?;
520 assert!(!contents.contains(&"A".repeat(200)));
521 assert!(contents.contains("New message"));
522
523 let rotated_contents = fs::read_to_string(&rotated_path)?;
525 assert!(rotated_contents.contains(&"A".repeat(200)));
526 assert!(!rotated_contents.contains("New message"));
527
528 Ok(())
529 }
530
531 #[test]
532 fn test_file_handler_write_error() -> io::Result<()> {
533 let temp_dir = TempDir::new()?;
534 let log_path = temp_dir.path().join("test.log");
535 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
536
537 handler.file = Mutex::new(None);
539
540 let mut perms = fs::metadata(&log_path)?.permissions();
542 perms.set_readonly(true);
543 fs::set_permissions(&log_path, perms)?;
544
545 let record = Record::new(
546 LogLevel::Info,
547 "Test message",
548 Some("test_module".to_string()),
549 Some("test.rs".to_string()),
550 Some(42),
551 );
552
553 assert!(handler.handle(&record).is_err());
554 Ok(())
555 }
556
557 #[test]
558 fn test_file_handler_filtering() -> io::Result<()> {
559 let temp_dir = TempDir::new()?;
560 let log_path = temp_dir.path().join("test.log");
561 let filter = std::sync::Arc::new(|record: &Record| record.message().contains("pass"));
562 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_filter(filter);
563 let record1 = Record::new(
564 LogLevel::Info,
565 "should pass",
566 None::<String>,
567 None::<String>,
568 None,
569 );
570 let record2 = Record::new(
571 LogLevel::Info,
572 "should fail",
573 None::<String>,
574 None::<String>,
575 None,
576 );
577 assert!(handler.handle(&record1).is_ok());
578 assert!(handler.handle(&record2).is_ok());
579 let contents = fs::read_to_string(log_path)?;
580 assert!(contents.contains("should pass"));
581 assert!(!contents.contains("should fail"));
582 Ok(())
583 }
584
585 #[test]
586 fn test_file_handler_batch() -> io::Result<()> {
587 let temp_dir = TempDir::new()?;
588 let log_path = temp_dir.path().join("test.log");
589 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_batching(2);
590 let record1 = Record::new(LogLevel::Info, "msg1", None::<String>, None::<String>, None);
591 let record2 = Record::new(LogLevel::Info, "msg2", None::<String>, None::<String>, None);
592 assert!(handler.handle(&record1).is_ok());
593 assert!(handler.handle(&record2).is_ok());
594 let contents = fs::read_to_string(log_path)?;
595 assert!(contents.contains("msg1"));
596 assert!(contents.contains("msg2"));
597 Ok(())
598 }
599
600 #[test]
601 fn test_file_handler_compression() -> io::Result<()> {
602 let temp_dir = TempDir::new()?;
603 let log_path = temp_dir.path().join("test.log");
604 let handler = FileHandler::new(log_path.to_str().unwrap())?
605 .with_rotation(100, 2)
606 .with_compression(true);
607 let record1 = Record::new(
608 LogLevel::Info,
609 "A".repeat(200).as_str(),
610 None::<String>,
611 None::<String>,
612 None,
613 );
614 let record2 = Record::new(
615 LogLevel::Info,
616 "B".repeat(200).as_str(),
617 None::<String>,
618 None::<String>,
619 None,
620 );
621 assert!(handler.handle(&record1).is_ok());
622 assert!(handler.handle(&record2).is_ok());
623 handler.flush().unwrap();
624 let rotated_gz = format!("{}.1.gz", log_path.to_string_lossy());
625 assert!(Path::new(&rotated_gz).exists());
626 Ok(())
627 }
628}