flexi_logger/writers/
file_log_writer.rs

1#![allow(clippy::module_name_repetitions)]
2mod builder;
3mod config;
4mod infix_filter;
5mod rotation_config;
6mod state;
7mod state_handle;
8mod threads;
9
10pub use self::builder::{ArcFileLogWriter, FileLogWriterBuilder, FileLogWriterHandle};
11pub use self::config::FileLogWriterConfig;
12pub(crate) use infix_filter::InfixFilter;
13
14use self::{rotation_config::RotationConfig, state::State, state_handle::StateHandle};
15use crate::{
16    writers::LogWriter, DeferredNow, EffectiveWriteMode, FileSpec, FlexiLoggerError,
17    FormatFunction, LogfileSelector,
18};
19use log::Record;
20use std::path::PathBuf;
21
22const WINDOWS_LINE_ENDING: &[u8] = b"\r\n";
23const UNIX_LINE_ENDING: &[u8] = b"\n";
24
25/// A configurable [`LogWriter`] implementation that writes to a file or a sequence of files.
26///
27/// See [writers](crate::writers) for usage guidance.
28#[derive(Debug)]
29pub struct FileLogWriter {
30    // the state needs to be mutable; since `Log.log()` requires an unmutable self,
31    // which translates into a non-mutating `LogWriter::write()`,
32    // we need internal mutability and thread-safety.
33    state_handle: StateHandle,
34    max_log_level: log::LevelFilter,
35}
36impl FileLogWriter {
37    fn new(
38        state: State,
39        max_log_level: log::LevelFilter,
40        format_function: FormatFunction,
41    ) -> FileLogWriter {
42        let state_handle = match state.config().write_mode.effective_write_mode() {
43            EffectiveWriteMode::Direct
44            | EffectiveWriteMode::BufferAndFlushWith(_)
45            | EffectiveWriteMode::BufferDontFlushWith(_) => {
46                StateHandle::new_sync(state, format_function)
47            }
48
49            #[cfg(feature = "async")]
50            EffectiveWriteMode::AsyncWith {
51                pool_capa,
52                message_capa,
53                flush_interval: _,
54            } => StateHandle::new_async(pool_capa, message_capa, state, format_function),
55        };
56
57        FileLogWriter {
58            state_handle,
59            max_log_level,
60        }
61    }
62
63    /// Instantiates a builder for `FileLogWriter`.
64    #[must_use]
65    pub fn builder(file_spec: FileSpec) -> FileLogWriterBuilder {
66        FileLogWriterBuilder::new(file_spec)
67    }
68
69    /// Returns a reference to its configured output format function.
70    #[must_use]
71    #[inline]
72    pub fn format(&self) -> FormatFunction {
73        self.state_handle.format_function()
74    }
75
76    pub(crate) fn plain_write(&self, buffer: &[u8]) -> std::result::Result<usize, std::io::Error> {
77        self.state_handle.plain_write(buffer)
78    }
79
80    /// Replaces parts of the configuration of the file log writer.
81    ///
82    /// Note that the write mode and the format function cannot be reset and
83    /// that the provided `FileLogWriterBuilder` must have the same values for these as the
84    /// current `FileLogWriter`.
85    ///
86    /// # Errors
87    ///
88    /// `FlexiLoggerError::Reset` if a reset was tried with a different write mode.
89    ///
90    /// `FlexiLoggerError::Io` if the specified path doesn't work.
91    ///
92    /// `FlexiLoggerError::OutputBadDirectory` if the specified path is not a directory.
93    ///
94    /// `FlexiLoggerError::Poison` if some mutex is poisoned.
95    pub fn reset(&self, flwb: &FileLogWriterBuilder) -> Result<(), FlexiLoggerError> {
96        self.state_handle.reset(flwb)
97    }
98
99    /// Returns the current configuration of the file log writer
100    ///
101    /// # Errors
102    ///
103    /// `FlexiLoggerError::Poison` if some mutex is poisoned.
104    pub fn config(&self) -> Result<FileLogWriterConfig, FlexiLoggerError> {
105        self.state_handle.config()
106    }
107
108    /// Makes the `FileLogWriter` re-open the current log file.
109    ///
110    /// `FileLogWriter` expects that nobody else modifies the file to which it writes,
111    /// and offers capabilities to rotate, compress, and clean up log files.
112    ///
113    /// However, if you use tools like linux' `logrotate`
114    /// to rename or delete the current output file, you need to inform the `FileLogWriter` about
115    /// such actions by calling this method. Otherwise the `FileLogWriter` will not stop
116    /// writing to the renamed or even deleted file!
117    ///
118    /// # Example
119    ///
120    /// `logrotate` e.g. can be configured to send a `SIGHUP` signal to your program. You should
121    /// handle `SIGHUP` in your program explicitly,
122    /// e.g. using a crate like [`ctrlc`](https://docs.rs/ctrlc/latest/ctrlc/),
123    /// and call this function from the registered signal handler.
124    ///
125    /// # Errors
126    ///
127    /// `FlexiLoggerError::Poison` if some mutex is poisoned.
128    pub fn reopen_outputfile(&self) -> Result<(), FlexiLoggerError> {
129        self.state_handle.reopen_outputfile()
130    }
131
132    /// Trigger an extra log file rotation.
133    ///
134    /// Does nothing if rotation is not configured.
135    ///
136    /// # Errors
137    ///
138    /// `FlexiLoggerError::Poison` if some mutex is poisoned.
139    ///
140    /// IO errors.
141    pub fn rotate(&self) -> Result<(), FlexiLoggerError> {
142        self.state_handle.rotate()
143    }
144
145    /// Returns the list of existing log files according to the current `FileSpec`.
146    ///
147    /// The list includes the current log file and the compressed files, if they exist.
148    ///
149    /// # Errors
150    ///
151    /// `FlexiLoggerError::Poison` if some mutex is poisoned.
152    pub fn existing_log_files(
153        &self,
154        selector: &LogfileSelector,
155    ) -> Result<Vec<PathBuf>, FlexiLoggerError> {
156        self.state_handle.existing_log_files(selector)
157    }
158}
159
160impl LogWriter for FileLogWriter {
161    #[inline]
162    fn write(&self, now: &mut DeferredNow, record: &Record) -> std::io::Result<()> {
163        if record.level() <= self.max_log_level {
164            self.state_handle.write(now, record)
165        } else {
166            Ok(())
167        }
168    }
169
170    #[inline]
171    fn flush(&self) -> std::io::Result<()> {
172        self.state_handle.flush()
173    }
174
175    #[inline]
176    fn max_log_level(&self) -> log::LevelFilter {
177        self.max_log_level
178    }
179
180    fn reopen_output(&self) -> Result<(), FlexiLoggerError> {
181        self.reopen_outputfile()
182    }
183
184    fn rotate(&self) -> Result<(), FlexiLoggerError> {
185        self.state_handle.rotate()
186    }
187
188    fn validate_logs(&self, expected: &[(&'static str, &'static str, &'static str)]) {
189        self.state_handle.validate_logs(expected);
190    }
191
192    fn shutdown(&self) {
193        self.state_handle.shutdown();
194    }
195}
196
197impl Drop for FileLogWriter {
198    fn drop(&mut self) {
199        self.shutdown();
200    }
201}
202
203#[cfg(test)]
204mod test {
205    #[cfg(feature = "async")]
206    use crate::ZERO_DURATION;
207    use crate::{writers::LogWriter, Cleanup, Criterion, DeferredNow, FileSpec, Naming, WriteMode};
208    use chrono::Local;
209    use std::ops::Add;
210    use std::path::{Path, PathBuf};
211    use std::time::Duration;
212
213    const DIRECTORY: &str = r"log_files/rotate";
214    const ONE: &str = "ONE";
215    const TWO: &str = "TWO";
216    const THREE: &str = "THREE";
217    const FOUR: &str = "FOUR";
218    const FIVE: &str = "FIVE";
219    const SIX: &str = "SIX";
220    const SEVEN: &str = "SEVEN";
221    const EIGHT: &str = "EIGHT";
222    const NINE: &str = "NINE";
223
224    const FMT_DASHES_U_DASHES: &str = "%Y-%m-%d_%H-%M-%S";
225
226    #[test]
227    fn test_rotate_no_append_numbers() {
228        // we use timestamp as discriminant to allow repeated runs
229        let ts =
230            String::from("false-numbers-") + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
231        let naming = Naming::Numbers;
232
233        // ensure we start with -/-/-
234        assert!(not_exists("00000", &ts));
235        assert!(not_exists("00001", &ts));
236        assert!(not_exists("CURRENT", &ts));
237
238        // ensure this produces -/-/ONE
239        write_loglines(false, naming, &ts, &[ONE]);
240        assert!(not_exists("00000", &ts));
241        assert!(not_exists("00001", &ts));
242        assert!(contains("CURRENT", &ts, ONE));
243
244        // ensure this produces ONE/-/TWO
245        write_loglines(false, naming, &ts, &[TWO]);
246        assert!(contains("00000", &ts, ONE));
247        assert!(not_exists("00001", &ts));
248        assert!(contains("CURRENT", &ts, TWO));
249
250        // ensure this also produces ONE/-/TWO
251        remove("CURRENT", &ts);
252        assert!(not_exists("CURRENT", &ts));
253        write_loglines(false, naming, &ts, &[TWO]);
254        assert!(contains("00000", &ts, ONE));
255        assert!(not_exists("00001", &ts));
256        assert!(contains("CURRENT", &ts, TWO));
257
258        // ensure this produces ONE/TWO/THREE
259        write_loglines(false, naming, &ts, &[THREE]);
260        assert!(contains("00000", &ts, ONE));
261        assert!(contains("00001", &ts, TWO));
262        assert!(contains("CURRENT", &ts, THREE));
263    }
264
265    #[test]
266    fn test_rotate_with_append_numbers() {
267        // we use timestamp as discriminant to allow repeated runs
268        let ts =
269            String::from("true-numbers-") + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
270        let naming = Naming::Numbers;
271
272        // ensure we start with -/-/-
273        assert!(not_exists("00000", &ts));
274        assert!(not_exists("00001", &ts));
275        assert!(not_exists("CURRENT", &ts));
276
277        // ensure this produces 12/-/3
278        write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
279        assert!(contains("00000", &ts, ONE));
280        assert!(contains("00000", &ts, TWO));
281        assert!(not_exists("00001", &ts));
282        assert!(contains("CURRENT", &ts, THREE));
283
284        // ensure this produces 12/34/56
285        write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
286        assert!(contains("00000", &ts, ONE));
287        assert!(contains("00000", &ts, TWO));
288        assert!(contains("00001", &ts, THREE));
289        assert!(contains("00001", &ts, FOUR));
290        assert!(contains("CURRENT", &ts, FIVE));
291        assert!(contains("CURRENT", &ts, SIX));
292
293        // ensure this also produces 12/34/56
294        remove("CURRENT", &ts);
295        remove("00001", &ts);
296        assert!(not_exists("CURRENT", &ts));
297        write_loglines(true, naming, &ts, &[THREE, FOUR, FIVE, SIX]);
298        assert!(contains("00000", &ts, ONE));
299        assert!(contains("00000", &ts, TWO));
300        assert!(contains("00001", &ts, THREE));
301        assert!(contains("00001", &ts, FOUR));
302        assert!(contains("CURRENT", &ts, FIVE));
303        assert!(contains("CURRENT", &ts, SIX));
304
305        // ensure this produces 12/34/56/78/9
306        write_loglines(true, naming, &ts, &[SEVEN, EIGHT, NINE]);
307        assert!(contains("00002", &ts, FIVE));
308        assert!(contains("00002", &ts, SIX));
309        assert!(contains("00003", &ts, SEVEN));
310        assert!(contains("00003", &ts, EIGHT));
311        assert!(contains("CURRENT", &ts, NINE));
312    }
313
314    #[test]
315    fn test_rotate_no_append_timestamps() {
316        // we use timestamp as discriminant to allow repeated runs
317        let ts_discr = String::from("false-timestamps-")
318            + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
319
320        let basename = String::from(DIRECTORY).add("/").add(
321            &Path::new(&std::env::args().next().unwrap())
322                .file_stem().unwrap(/*cannot fail*/)
323                .to_string_lossy(),
324        );
325        let naming = Naming::Timestamps;
326
327        println!("{} ensure we start with -/-/-", chrono::Local::now());
328        assert!(list_rotated_files(&basename, &ts_discr).is_empty());
329        assert!(not_exists("CURRENT", &ts_discr));
330
331        println!("{} ensure this produces -/-/ONE", chrono::Local::now());
332        write_loglines(false, naming, &ts_discr, &[ONE]);
333        assert!(list_rotated_files(&basename, &ts_discr).is_empty());
334        assert!(contains("CURRENT", &ts_discr, ONE));
335
336        std::thread::sleep(Duration::from_secs(2));
337        println!("{} ensure this produces ONE/-/TWO", chrono::Local::now());
338        write_loglines(false, naming, &ts_discr, &[TWO]);
339        assert_eq!(list_rotated_files(&basename, &ts_discr).len(), 1);
340        assert!(contains("CURRENT", &ts_discr, TWO));
341
342        std::thread::sleep(Duration::from_secs(2));
343        println!(
344            "{} ensure this produces ONE/TWO/THREE",
345            chrono::Local::now()
346        );
347        write_loglines(false, naming, &ts_discr, &[THREE]);
348        assert_eq!(list_rotated_files(&basename, &ts_discr).len(), 2);
349        assert!(contains("CURRENT", &ts_discr, THREE));
350    }
351
352    #[test]
353    fn test_rotate_with_append_timestamps() {
354        // we use timestamp as discriminant to allow repeated runs
355        let ts = String::from("true-timestamps-")
356            + &Local::now().format(FMT_DASHES_U_DASHES).to_string();
357
358        let basename = String::from(DIRECTORY).add("/").add(
359            &Path::new(&std::env::args().next().unwrap())
360                .file_stem().unwrap(/*cannot fail*/)
361                .to_string_lossy(),
362        );
363        let naming = Naming::Timestamps;
364
365        // ensure we start with -/-/-
366        assert!(list_rotated_files(&basename, &ts).is_empty());
367        assert!(not_exists("CURRENT", &ts));
368
369        // ensure this produces 12/-/3
370        write_loglines(true, naming, &ts, &[ONE, TWO, THREE]);
371        assert_eq!(list_rotated_files(&basename, &ts).len(), 1);
372        assert!(contains("CURRENT", &ts, THREE));
373
374        // ensure this produces 12/34/56
375        write_loglines(true, naming, &ts, &[FOUR, FIVE, SIX]);
376        assert!(contains("CURRENT", &ts, FIVE));
377        assert!(contains("CURRENT", &ts, SIX));
378        assert_eq!(list_rotated_files(&basename, &ts).len(), 2);
379
380        // ensure this produces 12/34/56/78/9
381        write_loglines(true, naming, &ts, &[SEVEN, EIGHT, NINE]);
382        assert_eq!(list_rotated_files(&basename, &ts).len(), 4);
383        assert!(contains("CURRENT", &ts, NINE));
384    }
385
386    #[test]
387    fn issue_38() {
388        const NUMBER_OF_FILES: usize = 5;
389        const NUMBER_OF_PSEUDO_PROCESSES: usize = 11;
390        const ISSUE_38: &str = "issue_38";
391        const LOG_FOLDER: &str = "log_files/issue_38";
392
393        for _ in 0..NUMBER_OF_PSEUDO_PROCESSES {
394            let flwb = crate::writers::file_log_writer::FileLogWriter::builder(
395                FileSpec::default()
396                    .directory(LOG_FOLDER)
397                    .discriminant(ISSUE_38),
398            )
399            .rotate(
400                Criterion::Size(500),
401                Naming::Timestamps,
402                Cleanup::KeepLogFiles(NUMBER_OF_FILES),
403            )
404            .o_append(false);
405
406            #[cfg(feature = "async")]
407            let flwb = flwb.write_mode(WriteMode::AsyncWith {
408                pool_capa: 5,
409                message_capa: 400,
410                flush_interval: ZERO_DURATION,
411            });
412
413            let flw = flwb.try_build().unwrap();
414
415            // write some lines, but not enough to rotate
416            for i in 0..4 {
417                flw.write(
418                    &mut DeferredNow::new(),
419                    &log::Record::builder()
420                        .args(format_args!("{i}"))
421                        .level(log::Level::Error)
422                        .target("myApp")
423                        .file(Some("server.rs"))
424                        .line(Some(144))
425                        .module_path(Some("server"))
426                        .build(),
427                )
428                .unwrap();
429            }
430            flw.flush().ok();
431        }
432
433        // give the cleanup thread a short moment of time
434        std::thread::sleep(Duration::from_millis(50));
435
436        let fn_pattern = String::with_capacity(180)
437            .add(
438                &String::from(LOG_FOLDER).add("/").add(
439                    &Path::new(&std::env::args().next().unwrap())
440            .file_stem().unwrap(/*cannot fail*/)
441            .to_string_lossy(),
442                ),
443            )
444            .add("_")
445            .add(ISSUE_38)
446            .add("_r[0-9]*")
447            .add(".log");
448
449        assert_eq!(
450            glob::glob(&fn_pattern)
451                .unwrap()
452                .filter_map(Result::ok)
453                .count(),
454            NUMBER_OF_FILES
455        );
456    }
457
458    #[test]
459    fn test_reset() {
460        #[cfg(not(feature = "async"))]
461        let write_mode = WriteMode::BufferDontFlushWith(4);
462        #[cfg(feature = "async")]
463        let write_mode = WriteMode::AsyncWith {
464            pool_capa: 7,
465            message_capa: 8,
466            flush_interval: ZERO_DURATION,
467        };
468        let flw = super::FileLogWriter::builder(
469            FileSpec::default()
470                .directory(DIRECTORY)
471                .discriminant("test_reset-1"),
472        )
473        .rotate(
474            Criterion::Size(28),
475            Naming::Numbers,
476            Cleanup::KeepLogFiles(20),
477        )
478        .append()
479        .write_mode(write_mode)
480        .try_build()
481        .unwrap();
482
483        flw.write(
484            &mut DeferredNow::new(),
485            &log::Record::builder()
486                .args(format_args!("{}", "test_reset-1"))
487                .level(log::Level::Error)
488                .target("test_reset")
489                .file(Some("server.rs"))
490                .line(Some(144))
491                .module_path(Some("server"))
492                .build(),
493        )
494        .unwrap();
495
496        println!("FileLogWriter {flw:?}");
497
498        flw.reset(
499            &super::FileLogWriter::builder(
500                FileSpec::default()
501                    .directory(DIRECTORY)
502                    .discriminant("test_reset-2"),
503            )
504            .rotate(
505                Criterion::Size(28),
506                Naming::Numbers,
507                Cleanup::KeepLogFiles(20),
508            )
509            .write_mode(write_mode),
510        )
511        .unwrap();
512        flw.write(
513            &mut DeferredNow::new(),
514            &log::Record::builder()
515                .args(format_args!("{}", "test_reset-2"))
516                .level(log::Level::Error)
517                .target("test_reset")
518                .file(Some("server.rs"))
519                .line(Some(144))
520                .module_path(Some("server"))
521                .build(),
522        )
523        .unwrap();
524        println!("FileLogWriter {flw:?}");
525
526        assert!(flw
527            .reset(
528                &super::FileLogWriter::builder(
529                    FileSpec::default()
530                        .directory(DIRECTORY)
531                        .discriminant("test_reset-3"),
532                )
533                .rotate(
534                    Criterion::Size(28),
535                    Naming::Numbers,
536                    Cleanup::KeepLogFiles(20),
537                )
538                .write_mode(WriteMode::Direct),
539            )
540            .is_err());
541    }
542
543    #[test]
544    fn test_max_log_level() {
545        let spec = FileSpec::default()
546            .directory(DIRECTORY)
547            .discriminant("test_max_log_level-1")
548            .suppress_basename()
549            .suppress_timestamp();
550        let flw = super::FileLogWriter::builder(spec.clone())
551            .max_level(log::LevelFilter::Warn)
552            .write_mode(WriteMode::Direct)
553            .try_build()
554            .unwrap();
555
556        let write_msg = |level, msg| {
557            flw.write(
558                &mut DeferredNow::new(),
559                &log::Record::builder()
560                    .args(format_args!("{msg}"))
561                    .level(level)
562                    .target("test_max_log_level")
563                    .file(Some("server.rs"))
564                    .line(Some(144))
565                    .module_path(Some("server"))
566                    .build(),
567            )
568            .unwrap();
569        };
570
571        write_msg(log::Level::Trace, "trace message");
572        write_msg(log::Level::Debug, "debug message");
573        write_msg(log::Level::Info, "info message");
574        write_msg(log::Level::Warn, "warn message");
575        write_msg(log::Level::Error, "error message");
576
577        let log_contents = std::fs::read_to_string(spec.as_pathbuf(None)).unwrap();
578
579        assert!(!log_contents.contains("trace message"));
580        assert!(!log_contents.contains("debug message"));
581        assert!(!log_contents.contains("info message"));
582        assert!(log_contents.contains("warn message"));
583        assert!(log_contents.contains("error message"));
584    }
585
586    fn remove(s: &str, discr: &str) {
587        std::fs::remove_file(get_hackyfilepath(s, discr)).unwrap();
588    }
589
590    fn not_exists(s: &str, discr: &str) -> bool {
591        !get_hackyfilepath(s, discr).exists()
592    }
593
594    fn contains(s: &str, discr: &str, text: &str) -> bool {
595        match std::fs::read_to_string(get_hackyfilepath(s, discr)) {
596            Err(_) => false,
597            Ok(s) => s.contains(text),
598        }
599    }
600
601    fn get_hackyfilepath(infix: &str, discr: &str) -> Box<Path> {
602        let arg0 = std::env::args().next().unwrap();
603        let mut s_filename = Path::new(&arg0)
604            .file_stem()
605            .unwrap()
606            .to_string_lossy()
607            .to_string();
608        s_filename += "_";
609        s_filename += discr;
610        s_filename += "_r";
611        s_filename += infix;
612        s_filename += ".log";
613        let mut path_buf = PathBuf::from(DIRECTORY);
614        path_buf.push(s_filename);
615        path_buf.into_boxed_path()
616    }
617
618    fn write_loglines(append: bool, naming: Naming, discr: &str, texts: &[&'static str]) {
619        let flw = get_file_log_writer(append, naming, discr);
620        for text in texts {
621            flw.write(
622                &mut DeferredNow::new(),
623                &log::Record::builder()
624                    .args(format_args!("{text}"))
625                    .level(log::Level::Error)
626                    .target("myApp")
627                    .file(Some("server.rs"))
628                    .line(Some(144))
629                    .module_path(Some("server"))
630                    .build(),
631            )
632            .unwrap();
633        }
634    }
635
636    fn get_file_log_writer(
637        append: bool,
638        naming: Naming,
639        discr: &str,
640    ) -> crate::writers::FileLogWriter {
641        super::FileLogWriter::builder(FileSpec::default().directory(DIRECTORY).discriminant(discr))
642            .rotate(
643                Criterion::Size(if append { 28 } else { 10 }),
644                naming,
645                Cleanup::Never,
646            )
647            .o_append(append)
648            .try_build()
649            .unwrap()
650    }
651
652    fn list_rotated_files(basename: &str, discr: &str) -> Vec<String> {
653        let fn_pattern = String::with_capacity(180)
654            .add(basename)
655            .add("_")
656            .add(discr)
657            .add("_r2[0-9]*") // Year 3000 problem!!!
658            .add(".log");
659
660        glob::glob(&fn_pattern)
661            .unwrap()
662            .map(|r| r.unwrap().into_os_string().to_string_lossy().to_string())
663            .collect()
664    }
665}