rust_loguru/handler/
file.rs

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
14/// A handler that writes log records to a file
15pub 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                // Open a new file handle for the clone
49                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                    // Close the current file
164                    *file_guard = None;
165
166                    // Remove the oldest log file if it exists
167                    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                    // Rotate existing files
173                    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                    // Rename current file to .1
182                    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                    // Open a new file
197                    *file_guard = Some(
198                        OpenOptions::new()
199                            .create(true)
200                            .append(true)
201                            .open(&self.path)?,
202                    );
203
204                    // Flush the new file
205                    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        // Trim whitespace and check again
429        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); // Small size to trigger rotation
492
493        // Write enough data to trigger rotation
494        let record = Record::new(
495            LogLevel::Info,
496            "A".repeat(200).as_str(), // Write more than max_size bytes
497            Some("test_module".to_string()),
498            Some("test.rs".to_string()),
499            Some(42),
500        );
501        assert!(handler.handle(&record).is_ok());
502
503        // Write to the new file
504        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        // Verify the rotated file exists
514        let rotated_path = format!("{}.1", log_path.to_string_lossy());
515        assert!(Path::new(&rotated_path).exists());
516
517        // Verify the new file contains only the new message
518        let contents = fs::read_to_string(&log_path)?;
519        assert!(!contents.contains(&"A".repeat(200)));
520        assert!(contents.contains("New message"));
521
522        // Verify the rotated file contains the old message
523        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        // Close the file handle before making it read-only
537        handler.file = Mutex::new(None);
538
539        // Make the file read-only to simulate a write error
540        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}