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, 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::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                    // 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<(), 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                    // Check if it's a permission error
248                    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        // Trim whitespace and check again
430        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); // Small size to trigger rotation
493
494        // Write enough data to trigger rotation
495        let record = Record::new(
496            LogLevel::Info,
497            "A".repeat(200).as_str(), // Write more than max_size bytes
498            Some("test_module".to_string()),
499            Some("test.rs".to_string()),
500            Some(42),
501        );
502        assert!(handler.handle(&record).is_ok());
503
504        // Write to the new file
505        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        // Verify the rotated file exists
515        let rotated_path = format!("{}.1", log_path.to_string_lossy());
516        assert!(Path::new(&rotated_path).exists());
517
518        // Verify the new file contains only the new message
519        let contents = fs::read_to_string(&log_path)?;
520        assert!(!contents.contains(&"A".repeat(200)));
521        assert!(contents.contains("New message"));
522
523        // Verify the rotated file contains the old message
524        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        // Close the file handle before making it read-only
538        handler.file = Mutex::new(None);
539
540        // Make the file read-only to simulate a write error
541        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}