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, HandlerError, 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::template(
91 "{timestamp} {level} {module} {location} {message} {metadata} {data}",
92 ),
93 file: Mutex::new(Some(file)),
94 path,
95 max_size: None,
96 max_files: None,
97 compress: false,
98 filter: None,
99 batch_buffer: Mutex::new(Vec::new()),
100 batch_size: None,
101 })
102 }
103
104 pub fn with_level(mut self, level: LogLevel) -> Self {
105 self.level = level;
106 self
107 }
108
109 pub fn with_formatter(mut self, formatter: Formatter) -> Self {
110 self.formatter = formatter;
111 self
112 }
113
114 pub fn with_colors(mut self, use_colors: bool) -> Self {
115 self.formatter = self.formatter.with_colors(use_colors);
116 self
117 }
118
119 pub fn with_pattern(mut self, pattern: impl Into<String>) -> Self {
120 self.formatter = Formatter::template(pattern);
121 self
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(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
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<(), HandlerError> {
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().map_err(|e| {
227 HandlerError::IoError(io::Error::new(io::ErrorKind::Other, e.to_string()))
228 })?;
229 buffer.push(record.clone());
230 if buffer.len() >= batch_size {
231 let batch = buffer.drain(..).collect::<Vec<_>>();
232 drop(buffer);
233 return self.handle_batch(&batch);
234 }
235 return Ok(());
236 }
237 let formatted = self.formatter.format(record);
238 if let Err(e) = self.rotate_if_needed() {
239 return Err(HandlerError::IoError(e));
240 }
241 let mut file_guard = self.file.lock().map_err(|e| {
242 HandlerError::IoError(io::Error::new(io::ErrorKind::Other, e.to_string()))
243 })?;
244 if let Some(ref mut file) = *file_guard {
245 write!(file, "{}", formatted).map_err(HandlerError::IoError)?;
246 file.flush().map_err(HandlerError::IoError)?;
247 Ok(())
248 } else {
249 Err(HandlerError::NotInitialized)
250 }
251 }
252
253 fn level(&self) -> LogLevel {
254 self.level
255 }
256
257 fn set_level(&mut self, level: LogLevel) {
258 self.level = level;
259 }
260
261 fn is_enabled(&self) -> bool {
262 self.enabled
263 }
264
265 fn set_enabled(&mut self, enabled: bool) {
266 self.enabled = enabled;
267 }
268
269 fn formatter(&self) -> &Formatter {
270 &self.formatter
271 }
272
273 fn set_formatter(&mut self, formatter: Formatter) {
274 self.formatter = formatter;
275 }
276
277 fn set_filter(&mut self, filter: Option<HandlerFilter>) {
278 self.filter = filter;
279 }
280
281 fn filter(&self) -> Option<&HandlerFilter> {
282 self.filter.as_ref()
283 }
284
285 fn handle_batch(&self, records: &[Record]) -> Result<(), HandlerError> {
286 let mut file_guard = self.file.lock().map_err(|e| {
287 HandlerError::IoError(io::Error::new(io::ErrorKind::Other, e.to_string()))
288 })?;
289 for record in records {
290 if !self.enabled || record.level() < self.level {
291 continue;
292 }
293 if let Some(filter) = &self.filter {
294 if !(filter)(record) {
295 continue;
296 }
297 }
298 let formatted = self.formatter.format(record);
299 if let Err(e) = self.rotate_if_needed() {
300 return Err(HandlerError::IoError(e));
301 }
302 if let Some(ref mut file) = file_guard.as_mut() {
303 write!(file, "{}", formatted).map_err(HandlerError::IoError)?;
304 }
305 }
306 if let Some(ref mut file) = file_guard.as_mut() {
307 file.flush().map_err(HandlerError::IoError)?;
308 }
309 Ok(())
310 }
311
312 fn init(&mut self) -> Result<(), HandlerError> {
313 let file = OpenOptions::new()
314 .create(true)
315 .append(true)
316 .open(&self.path)
317 .map_err(HandlerError::IoError)?;
318 *self.file.lock().unwrap() = Some(file);
319 Ok(())
320 }
321
322 fn flush(&self) -> Result<(), HandlerError> {
323 if let Some(ref mut file) = self.file.lock().unwrap().as_mut() {
324 file.flush().map_err(HandlerError::IoError)?;
325 Ok(())
326 } else {
327 Err(HandlerError::NotInitialized)
328 }
329 }
330
331 fn shutdown(&mut self) -> Result<(), HandlerError> {
332 self.flush()
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339 use std::fs;
340 use tempfile::TempDir;
341
342 #[test]
343 fn test_file_handler_creation() -> io::Result<()> {
344 let temp_dir = TempDir::new()?;
345 let log_path = temp_dir.path().join("test.log");
346 let handler = FileHandler::new(log_path.to_str().unwrap())?;
347
348 assert_eq!(handler.level(), LogLevel::Info);
349 assert!(handler.is_enabled());
350 assert_eq!(handler.path, log_path.to_str().unwrap());
351 Ok(())
352 }
353
354 #[test]
355 fn test_file_handler_level_filtering() -> io::Result<()> {
356 let temp_dir = TempDir::new()?;
357 let log_path = temp_dir.path().join("test.log");
358 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
359 handler.set_level(LogLevel::Warning);
360
361 let info_record = Record::new(
362 LogLevel::Info,
363 "Info message",
364 Some("test_module".to_string()),
365 Some("test.rs".to_string()),
366 Some(42),
367 );
368 let warning_record = Record::new(
369 LogLevel::Warning,
370 "Warning message",
371 Some("test_module".to_string()),
372 Some("test.rs".to_string()),
373 Some(42),
374 );
375
376 assert!(handler.handle(&info_record).is_ok());
377 assert!(handler.handle(&warning_record).is_ok());
378
379 let contents = fs::read_to_string(log_path)?;
380 assert!(!contents.contains("Info message"));
381 assert!(contents.contains("Warning message"));
382 Ok(())
383 }
384
385 #[test]
386 fn test_file_handler_disabled() -> io::Result<()> {
387 let temp_dir = TempDir::new()?;
388 let log_path = temp_dir.path().join("test.log");
389 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
390 handler.set_enabled(false);
391
392 let record = Record::new(
393 LogLevel::Info,
394 "Test message",
395 Some("test_module".to_string()),
396 Some("test.rs".to_string()),
397 Some(42),
398 );
399
400 assert!(handler.handle(&record).is_ok());
401 let contents = fs::read_to_string(log_path)?;
402 assert!(contents.is_empty());
403 Ok(())
404 }
405
406 #[test]
407 fn test_file_handler_formatting() -> io::Result<()> {
408 let temp_dir = TempDir::new()?;
409 let log_path = temp_dir.path().join("test.log");
410 let handler = FileHandler::new(log_path.to_str().unwrap())?
411 .with_pattern("{level} - {message}")
412 .with_colors(false);
413
414 let record = Record::new(
415 LogLevel::Info,
416 "Test message",
417 Some("test_module".to_string()),
418 Some("test.rs".to_string()),
419 Some(42),
420 );
421
422 assert!(handler.handle(&record).is_ok());
423 let contents = fs::read_to_string(log_path)?;
424 println!("File contents: '{}'", contents);
425 println!("File contents length: {}", contents.len());
426 println!("File contents bytes: {:?}", contents.as_bytes());
427
428 let trimmed_contents = contents.trim();
430 println!("Trimmed contents: '{}'", trimmed_contents);
431 assert!(trimmed_contents.contains("INFO - Test message"));
432 Ok(())
433 }
434
435 #[test]
436 fn test_file_handler_metadata() -> io::Result<()> {
437 let temp_dir = TempDir::new()?;
438 let log_path = temp_dir.path().join("test.log");
439 let handler = FileHandler::new(log_path.to_str().unwrap())?;
440
441 let record = Record::new(
442 LogLevel::Info,
443 "Test message",
444 Some("test_module".to_string()),
445 Some("test.rs".to_string()),
446 Some(42),
447 )
448 .with_metadata("key1", "value1")
449 .with_metadata("key2", "value2");
450
451 assert!(handler.handle(&record).is_ok());
452 let contents = fs::read_to_string(log_path)?;
453 assert!(contents.contains("key1=value1"));
454 assert!(contents.contains("key2=value2"));
455 Ok(())
456 }
457
458 #[test]
459 fn test_file_handler_structured_data() -> io::Result<()> {
460 let temp_dir = TempDir::new()?;
461 let log_path = temp_dir.path().join("test.log");
462 let handler = FileHandler::new(log_path.to_str().unwrap())?;
463
464 let data = serde_json::json!({
465 "user_id": 123,
466 "action": "login"
467 });
468
469 let record = Record::new(
470 LogLevel::Info,
471 "Structured data test",
472 Some("test_module".to_string()),
473 Some("test.rs".to_string()),
474 Some(42),
475 )
476 .with_structured_data("data", &data)
477 .unwrap();
478
479 assert!(handler.handle(&record).is_ok());
480 let contents = fs::read_to_string(log_path)?;
481 assert!(contents.contains("data="));
482 assert!(contents.contains(r#""user_id":123"#));
483 assert!(contents.contains(r#""action":"login""#));
484 Ok(())
485 }
486
487 #[test]
488 fn test_file_handler_rotation() -> io::Result<()> {
489 let temp_dir = TempDir::new()?;
490 let log_path = temp_dir.path().join("test.log");
491 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_rotation(100, 3); let record = Record::new(
495 LogLevel::Info,
496 "A".repeat(200).as_str(), Some("test_module".to_string()),
498 Some("test.rs".to_string()),
499 Some(42),
500 );
501 assert!(handler.handle(&record).is_ok());
502
503 let new_record = Record::new(
505 LogLevel::Info,
506 "New message",
507 Some("test_module".to_string()),
508 Some("test.rs".to_string()),
509 Some(42),
510 );
511 assert!(handler.handle(&new_record).is_ok());
512
513 let rotated_path = format!("{}.1", log_path.to_string_lossy());
515 assert!(Path::new(&rotated_path).exists());
516
517 let contents = fs::read_to_string(&log_path)?;
519 assert!(!contents.contains(&"A".repeat(200)));
520 assert!(contents.contains("New message"));
521
522 let rotated_contents = fs::read_to_string(&rotated_path)?;
524 assert!(rotated_contents.contains(&"A".repeat(200)));
525 assert!(!rotated_contents.contains("New message"));
526
527 Ok(())
528 }
529
530 #[test]
531 fn test_file_handler_write_error() -> io::Result<()> {
532 let temp_dir = TempDir::new()?;
533 let log_path = temp_dir.path().join("test.log");
534 let mut handler = FileHandler::new(log_path.to_str().unwrap())?;
535
536 handler.file = Mutex::new(None);
538
539 let mut perms = fs::metadata(&log_path)?.permissions();
541 perms.set_readonly(true);
542 fs::set_permissions(&log_path, perms)?;
543
544 let record = Record::new(
545 LogLevel::Info,
546 "Test message",
547 Some("test_module".to_string()),
548 Some("test.rs".to_string()),
549 Some(42),
550 );
551
552 assert!(handler.handle(&record).is_err());
553 Ok(())
554 }
555
556 #[test]
557 fn test_file_handler_filtering() -> io::Result<()> {
558 let temp_dir = TempDir::new()?;
559 let log_path = temp_dir.path().join("test.log");
560 let filter = std::sync::Arc::new(|record: &Record| record.message().contains("pass"));
561 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_filter(filter);
562 let record1 = Record::new(
563 LogLevel::Info,
564 "should pass",
565 None::<String>,
566 None::<String>,
567 None,
568 );
569 let record2 = Record::new(
570 LogLevel::Info,
571 "should fail",
572 None::<String>,
573 None::<String>,
574 None,
575 );
576 assert!(handler.handle(&record1).is_ok());
577 assert!(handler.handle(&record2).is_ok());
578 let contents = fs::read_to_string(log_path)?;
579 assert!(contents.contains("should pass"));
580 assert!(!contents.contains("should fail"));
581 Ok(())
582 }
583
584 #[test]
585 fn test_file_handler_batch() -> io::Result<()> {
586 let temp_dir = TempDir::new()?;
587 let log_path = temp_dir.path().join("test.log");
588 let handler = FileHandler::new(log_path.to_str().unwrap())?.with_batching(2);
589 let record1 = Record::new(LogLevel::Info, "msg1", None::<String>, None::<String>, None);
590 let record2 = Record::new(LogLevel::Info, "msg2", None::<String>, None::<String>, None);
591 assert!(handler.handle(&record1).is_ok());
592 assert!(handler.handle(&record2).is_ok());
593 let contents = fs::read_to_string(log_path)?;
594 assert!(contents.contains("msg1"));
595 assert!(contents.contains("msg2"));
596 Ok(())
597 }
598
599 #[test]
600 fn test_file_handler_compression() -> io::Result<()> {
601 let temp_dir = TempDir::new()?;
602 let log_path = temp_dir.path().join("test.log");
603 let handler = FileHandler::new(log_path.to_str().unwrap())?
604 .with_rotation(100, 2)
605 .with_compression(true);
606 let record1 = Record::new(
607 LogLevel::Info,
608 "A".repeat(200).as_str(),
609 None::<String>,
610 None::<String>,
611 None,
612 );
613 let record2 = Record::new(
614 LogLevel::Info,
615 "B".repeat(200).as_str(),
616 None::<String>,
617 None::<String>,
618 None,
619 );
620 assert!(handler.handle(&record1).is_ok());
621 assert!(handler.handle(&record2).is_ok());
622 handler.flush().unwrap();
623 let rotated_gz = format!("{}.1.gz", log_path.to_string_lossy());
624 assert!(Path::new(&rotated_gz).exists());
625 Ok(())
626 }
627}