tracing_appender/
rolling.rs

1//! A rolling file appender.
2//!
3//! Creates a new log file at a fixed frequency as defined by [`Rotation`].
4//! Logs will be written to this file for the duration of the period and will automatically roll over
5//! to the newly created log file once the time period has elapsed.
6//!
7//! The log file is created at the specified directory and file name prefix which *may* be appended with
8//! the date and time.
9//!
10//! The following helpers are available for creating a rolling file appender.
11//!
12//! - [`Rotation::minutely()`][minutely]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH-mm`
13//!   will be created minutely (once per minute)
14//! - [`Rotation::hourly()`][hourly]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH`
15//!   will be created hourly
16//! - [`Rotation::daily()`][daily]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd`
17//!   will be created daily
18//! - [`Rotation::never()`][never()]: This will result in log file located at `some_directory/log_file_name`
19//!
20//!
21//! # Examples
22//!
23//! ```rust
24//! # fn docs() {
25//! use tracing_appender::rolling::{RollingFileAppender, Rotation};
26//! let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log");
27//! # }
28//! ```
29use crate::sync::{RwLock, RwLockReadGuard};
30use std::{
31    fmt::{self, Debug},
32    fs::{self, File, OpenOptions},
33    io::{self, Write},
34    path::{Path, PathBuf},
35    sync::atomic::{AtomicUsize, Ordering},
36};
37use time::{format_description, Date, Duration, OffsetDateTime, PrimitiveDateTime, Time};
38
39mod builder;
40pub use builder::{Builder, InitError};
41
42/// A file appender with the ability to rotate log files at a fixed schedule.
43///
44/// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will
45/// block on write operations. It may be used with [`NonBlocking`] to perform
46/// writes without blocking the current thread.
47///
48/// Additionally, `RollingFileAppender` also implements the [`MakeWriter`]
49/// trait from `tracing-subscriber`, so it may also be used
50/// directly, without [`NonBlocking`].
51///
52/// [write]: std::io::Write
53/// [`NonBlocking`]: super::non_blocking::NonBlocking
54///
55/// # Examples
56///
57/// Rolling a log file once every hour:
58///
59/// ```rust
60/// # fn docs() {
61/// let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix");
62/// # }
63/// ```
64///
65/// Combining a `RollingFileAppender` with another [`MakeWriter`] implementation:
66///
67/// ```rust
68/// # fn docs() {
69/// use tracing_subscriber::fmt::writer::MakeWriterExt;
70///
71/// // Log all events to a rolling log file.
72/// let logfile = tracing_appender::rolling::hourly("/logs", "myapp-logs");
73///
74/// // Log `INFO` and above to stdout.
75/// let stdout = std::io::stdout.with_max_level(tracing::Level::INFO);
76///
77/// tracing_subscriber::fmt()
78///     // Combine the stdout and log file `MakeWriter`s into one
79///     // `MakeWriter` that writes to both
80///     .with_writer(stdout.and(logfile))
81///     .init();
82/// # }
83/// ```
84///
85/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
86pub struct RollingFileAppender {
87    state: Inner,
88    writer: RwLock<File>,
89    #[cfg(test)]
90    now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
91}
92
93/// A [writer] that writes to a rolling log file.
94///
95/// This is returned by the [`MakeWriter`] implementation for [`RollingFileAppender`].
96///
97/// [writer]: std::io::Write
98/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
99#[derive(Debug)]
100pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
101
102#[derive(Debug)]
103struct Inner {
104    log_directory: PathBuf,
105    log_filename_prefix: Option<String>,
106    log_filename_suffix: Option<String>,
107    date_format: Vec<format_description::FormatItem<'static>>,
108    rotation: Rotation,
109    next_date: AtomicUsize,
110    max_files: Option<usize>,
111}
112
113// === impl RollingFileAppender ===
114
115impl RollingFileAppender {
116    /// Creates a new `RollingFileAppender`.
117    ///
118    /// A `RollingFileAppender` will have a fixed rotation whose frequency is
119    /// defined by [`Rotation`]. The `directory` and `file_name_prefix`
120    /// arguments determine the location and file name's _prefix_ of the log
121    /// file. `RollingFileAppender` will automatically append the current date
122    /// and hour (UTC format) to the file name.
123    ///
124    /// Alternatively, a `RollingFileAppender` can be constructed using one of the following helpers:
125    ///
126    /// - [`Rotation::minutely()`][minutely],
127    /// - [`Rotation::hourly()`][hourly],
128    /// - [`Rotation::daily()`][daily],
129    /// - [`Rotation::never()`][never()]
130    ///
131    /// Additional parameters can be configured using [`RollingFileAppender::builder`].
132    ///
133    /// # Examples
134    ///
135    /// ```rust
136    /// # fn docs() {
137    /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
138    /// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log");
139    /// # }
140    /// ```
141    pub fn new(
142        rotation: Rotation,
143        directory: impl AsRef<Path>,
144        filename_prefix: impl AsRef<Path>,
145    ) -> RollingFileAppender {
146        let filename_prefix = filename_prefix
147            .as_ref()
148            .to_str()
149            .expect("filename prefix must be a valid UTF-8 string");
150        Self::builder()
151            .rotation(rotation)
152            .filename_prefix(filename_prefix)
153            .build(directory)
154            .expect("initializing rolling file appender failed")
155    }
156
157    /// Returns a new [`Builder`] for configuring a `RollingFileAppender`.
158    ///
159    /// The builder interface can be used to set additional configuration
160    /// parameters when constructing a new appender.
161    ///
162    /// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method
163    /// returns a `Result` rather than panicking when the appender cannot be
164    /// initialized. Therefore, the builder interface can also be used when
165    /// appender initialization errors should be handled gracefully.
166    ///
167    /// # Examples
168    ///
169    /// ```rust
170    /// # fn docs() {
171    /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
172    ///
173    /// let file_appender = RollingFileAppender::builder()
174    ///     .rotation(Rotation::HOURLY) // rotate log files once every hour
175    ///     .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
176    ///     .filename_suffix("log") // log file names will be suffixed with `.log`
177    ///     .build("/var/log") // try to build an appender that stores log files in `/var/log`
178    ///     .expect("initializing rolling file appender failed");
179    /// # drop(file_appender);
180    /// # }
181    /// ```
182    #[must_use]
183    pub fn builder() -> Builder {
184        Builder::new()
185    }
186
187    fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
188        let Builder {
189            ref rotation,
190            ref prefix,
191            ref suffix,
192            ref max_files,
193        } = builder;
194        let directory = directory.as_ref().to_path_buf();
195        let now = OffsetDateTime::now_utc();
196        let (state, writer) = Inner::new(
197            now,
198            rotation.clone(),
199            directory,
200            prefix.clone(),
201            suffix.clone(),
202            *max_files,
203        )?;
204        Ok(Self {
205            state,
206            writer,
207            #[cfg(test)]
208            now: Box::new(OffsetDateTime::now_utc),
209        })
210    }
211
212    #[inline]
213    fn now(&self) -> OffsetDateTime {
214        #[cfg(test)]
215        return (self.now)();
216
217        #[cfg(not(test))]
218        OffsetDateTime::now_utc()
219    }
220}
221
222impl io::Write for RollingFileAppender {
223    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
224        let now = self.now();
225        let writer = self.writer.get_mut();
226        if let Some(current_time) = self.state.should_rollover(now) {
227            let _did_cas = self.state.advance_date(now, current_time);
228            debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
229            self.state.refresh_writer(now, writer);
230        }
231        writer.write(buf)
232    }
233
234    fn flush(&mut self) -> io::Result<()> {
235        self.writer.get_mut().flush()
236    }
237}
238
239impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
240    type Writer = RollingWriter<'a>;
241    fn make_writer(&'a self) -> Self::Writer {
242        let now = self.now();
243
244        // Should we try to roll over the log file?
245        if let Some(current_time) = self.state.should_rollover(now) {
246            // Did we get the right to lock the file? If not, another thread
247            // did it and we can just make a writer.
248            if self.state.advance_date(now, current_time) {
249                self.state.refresh_writer(now, &mut self.writer.write());
250            }
251        }
252        RollingWriter(self.writer.read())
253    }
254}
255
256impl fmt::Debug for RollingFileAppender {
257    // This manual impl is required because of the `now` field (only present
258    // with `cfg(test)`), which is not `Debug`...
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        f.debug_struct("RollingFileAppender")
261            .field("state", &self.state)
262            .field("writer", &self.writer)
263            .finish()
264    }
265}
266
267/// Creates a minutely-rotating file appender. This will rotate the log file once per minute.
268///
269/// The appender returned by `rolling::minutely` can be used with `non_blocking` to create
270/// a non-blocking, minutely file appender.
271///
272/// The directory of the log file is specified with the `directory` argument.
273/// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender`
274/// adds the current date, hour, and minute to the log file in UTC.
275///
276/// # Examples
277///
278/// ``` rust
279/// # #[clippy::allow(needless_doctest_main)]
280/// fn main () {
281/// # fn doc() {
282///     let appender = tracing_appender::rolling::minutely("/some/path", "rolling.log");
283///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
284///
285///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
286///
287///     tracing::subscriber::with_default(subscriber.finish(), || {
288///         tracing::event!(tracing::Level::INFO, "Hello");
289///     });
290/// # }
291/// }
292/// ```
293///
294/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH-mm`.
295pub fn minutely(
296    directory: impl AsRef<Path>,
297    file_name_prefix: impl AsRef<Path>,
298) -> RollingFileAppender {
299    RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
300}
301
302/// Creates an hourly-rotating file appender.
303///
304/// The appender returned by `rolling::hourly` can be used with `non_blocking` to create
305/// a non-blocking, hourly file appender.
306///
307/// The directory of the log file is specified with the `directory` argument.
308/// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender`
309/// adds the current date and hour to the log file in UTC.
310///
311/// # Examples
312///
313/// ``` rust
314/// # #[clippy::allow(needless_doctest_main)]
315/// fn main () {
316/// # fn doc() {
317///     let appender = tracing_appender::rolling::hourly("/some/path", "rolling.log");
318///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
319///
320///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
321///
322///     tracing::subscriber::with_default(subscriber.finish(), || {
323///         tracing::event!(tracing::Level::INFO, "Hello");
324///     });
325/// # }
326/// }
327/// ```
328///
329/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`.
330pub fn hourly(
331    directory: impl AsRef<Path>,
332    file_name_prefix: impl AsRef<Path>,
333) -> RollingFileAppender {
334    RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
335}
336
337/// Creates a daily-rotating file appender.
338///
339/// The appender returned by `rolling::daily` can be used with `non_blocking` to create
340/// a non-blocking, daily file appender.
341///
342/// A `RollingFileAppender` has a fixed rotation whose frequency is
343/// defined by [`Rotation`]. The `directory` and `file_name_prefix`
344/// arguments determine the location and file name's _prefix_ of the log file.
345/// `RollingFileAppender` automatically appends the current date in UTC.
346///
347/// # Examples
348///
349/// ``` rust
350/// # #[clippy::allow(needless_doctest_main)]
351/// fn main () {
352/// # fn doc() {
353///     let appender = tracing_appender::rolling::daily("/some/path", "rolling.log");
354///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
355///
356///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
357///
358///     tracing::subscriber::with_default(subscriber.finish(), || {
359///         tracing::event!(tracing::Level::INFO, "Hello");
360///     });
361/// # }
362/// }
363/// ```
364///
365/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
366pub fn daily(
367    directory: impl AsRef<Path>,
368    file_name_prefix: impl AsRef<Path>,
369) -> RollingFileAppender {
370    RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
371}
372
373/// Creates a weekly-rotating file appender. The logs will rotate every Sunday at midnight UTC.
374///
375/// The appender returned by `rolling::weekly` can be used with `non_blocking` to create
376/// a non-blocking, weekly file appender.
377///
378/// A `RollingFileAppender` has a fixed rotation whose frequency is
379/// defined by [`Rotation`]. The `directory` and `file_name_prefix` arguments
380/// determine the location and file name's _prefix_ of the log file.
381/// `RollingFileAppender` automatically appends the current date in UTC.
382///
383/// # Examples
384///
385/// ``` rust
386/// # #[clippy::allow(needless_doctest_main)]
387/// fn main () {
388/// # fn doc() {
389///     let appender = tracing_appender::rolling::weekly("/some/path", "rolling.log");
390///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
391///
392///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
393///
394///     tracing::subscriber::with_default(subscriber.finish(), || {
395///         tracing::event!(tracing::Level::INFO, "Hello");
396///     });
397/// # }
398/// }
399/// ```
400///
401/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
402pub fn weekly(
403    directory: impl AsRef<Path>,
404    file_name_prefix: impl AsRef<Path>,
405) -> RollingFileAppender {
406    RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
407}
408
409/// Creates a non-rolling file appender.
410///
411/// The appender returned by `rolling::never` can be used with `non_blocking` to create
412/// a non-blocking, non-rotating appender.
413///
414/// The location of the log file will be specified the `directory` passed in.
415/// `file_name` specifies the complete name of the log file (no date or time is appended).
416///
417/// # Examples
418///
419/// ``` rust
420/// # #[clippy::allow(needless_doctest_main)]
421/// fn main () {
422/// # fn doc() {
423///     let appender = tracing_appender::rolling::never("/some/path", "non-rolling.log");
424///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
425///
426///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
427///
428///     tracing::subscriber::with_default(subscriber.finish(), || {
429///         tracing::event!(tracing::Level::INFO, "Hello");
430///     });
431/// # }
432/// }
433/// ```
434///
435/// This will result in a log file located at `/some/path/non-rolling.log`.
436pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
437    RollingFileAppender::new(Rotation::NEVER, directory, file_name)
438}
439
440/// Defines a fixed period for rolling of a log file.
441///
442/// To use a `Rotation`, pick one of the following options:
443///
444/// ### Minutely Rotation
445/// ```rust
446/// # fn docs() {
447/// use tracing_appender::rolling::Rotation;
448/// let rotation = tracing_appender::rolling::Rotation::MINUTELY;
449/// # }
450/// ```
451///
452/// ### Hourly Rotation
453/// ```rust
454/// # fn docs() {
455/// use tracing_appender::rolling::Rotation;
456/// let rotation = tracing_appender::rolling::Rotation::HOURLY;
457/// # }
458/// ```
459///
460/// ### Daily Rotation
461/// ```rust
462/// # fn docs() {
463/// use tracing_appender::rolling::Rotation;
464/// let rotation = tracing_appender::rolling::Rotation::DAILY;
465/// # }
466/// ```
467///
468/// ### Weekly Rotation
469/// ```rust
470/// # fn docs() {
471/// use tracing_appender::rolling::Rotation;
472/// let rotation = tracing_appender::rolling::Rotation::WEEKLY;
473/// # }
474/// ```
475///
476/// ### No Rotation
477/// ```rust
478/// # fn docs() {
479/// use tracing_appender::rolling::Rotation;
480/// let rotation = tracing_appender::rolling::Rotation::NEVER;
481/// # }
482/// ```
483#[derive(Clone, Eq, PartialEq, Debug)]
484pub struct Rotation(RotationKind);
485
486#[derive(Clone, Eq, PartialEq, Debug)]
487enum RotationKind {
488    Minutely,
489    Hourly,
490    Daily,
491    Weekly,
492    Never,
493}
494
495impl Rotation {
496    /// Provides a minutely rotation.
497    pub const MINUTELY: Self = Self(RotationKind::Minutely);
498    /// Provides an hourly rotation.
499    pub const HOURLY: Self = Self(RotationKind::Hourly);
500    /// Provides a daily rotation.
501    pub const DAILY: Self = Self(RotationKind::Daily);
502    /// Provides a weekly rotation that rotates every Sunday at midnight UTC.
503    pub const WEEKLY: Self = Self(RotationKind::Weekly);
504    /// Provides a rotation that never rotates.
505    pub const NEVER: Self = Self(RotationKind::Never);
506
507    /// Determines the next date that we should round to or `None` if `self` uses [`Rotation::NEVER`].
508    pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
509        let unrounded_next_date = match *self {
510            Rotation::MINUTELY => *current_date + Duration::minutes(1),
511            Rotation::HOURLY => *current_date + Duration::hours(1),
512            Rotation::DAILY => *current_date + Duration::days(1),
513            Rotation::WEEKLY => *current_date + Duration::weeks(1),
514            Rotation::NEVER => return None,
515        };
516        Some(self.round_date(unrounded_next_date))
517    }
518
519    /// Rounds the date towards the past using the [`Rotation`] interval.
520    ///
521    /// # Panics
522    ///
523    /// This method will panic if `self`` uses [`Rotation::NEVER`].
524    pub(crate) fn round_date(&self, date: OffsetDateTime) -> OffsetDateTime {
525        match *self {
526            Rotation::MINUTELY => {
527                let time = Time::from_hms(date.hour(), date.minute(), 0)
528                    .expect("Invalid time; this is a bug in tracing-appender");
529                date.replace_time(time)
530            }
531            Rotation::HOURLY => {
532                let time = Time::from_hms(date.hour(), 0, 0)
533                    .expect("Invalid time; this is a bug in tracing-appender");
534                date.replace_time(time)
535            }
536            Rotation::DAILY => {
537                let time = Time::from_hms(0, 0, 0)
538                    .expect("Invalid time; this is a bug in tracing-appender");
539                date.replace_time(time)
540            }
541            Rotation::WEEKLY => {
542                let zero_time = Time::from_hms(0, 0, 0)
543                    .expect("Invalid time; this is a bug in tracing-appender");
544
545                let days_since_sunday = date.weekday().number_days_from_sunday();
546                let date = date - Duration::days(days_since_sunday.into());
547                date.replace_time(zero_time)
548            }
549            // Rotation::NEVER is impossible to round.
550            Rotation::NEVER => {
551                unreachable!("Rotation::NEVER is impossible to round.")
552            }
553        }
554    }
555
556    fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
557        match *self {
558            Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
559            Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
560            Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
561            Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
562            Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
563        }
564        .expect("Unable to create a formatter; this is a bug in tracing-appender")
565    }
566}
567
568// === impl RollingWriter ===
569
570impl io::Write for RollingWriter<'_> {
571    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
572        (&*self.0).write(buf)
573    }
574
575    fn flush(&mut self) -> io::Result<()> {
576        (&*self.0).flush()
577    }
578}
579
580// === impl Inner ===
581
582impl Inner {
583    fn new(
584        now: OffsetDateTime,
585        rotation: Rotation,
586        directory: impl AsRef<Path>,
587        log_filename_prefix: Option<String>,
588        log_filename_suffix: Option<String>,
589        max_files: Option<usize>,
590    ) -> Result<(Self, RwLock<File>), builder::InitError> {
591        let log_directory = directory.as_ref().to_path_buf();
592        let date_format = rotation.date_format();
593        let next_date = rotation.next_date(&now);
594
595        let inner = Inner {
596            log_directory,
597            log_filename_prefix,
598            log_filename_suffix,
599            date_format,
600            next_date: AtomicUsize::new(
601                next_date
602                    .map(|date| date.unix_timestamp() as usize)
603                    .unwrap_or(0),
604            ),
605            rotation,
606            max_files,
607        };
608
609        if let Some(max_files) = max_files {
610            inner.prune_old_logs(max_files);
611        }
612
613        let filename = inner.join_date(&now);
614        let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
615        Ok((inner, writer))
616    }
617
618    /// Returns the full filename for the provided date, using [`Rotation`] to round accordingly.
619    pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
620        let date = if let Rotation::NEVER = self.rotation {
621            date.format(&self.date_format)
622                .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
623        } else {
624            self.rotation
625                .round_date(*date)
626                .format(&self.date_format)
627                .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
628        };
629
630        match (
631            &self.rotation,
632            &self.log_filename_prefix,
633            &self.log_filename_suffix,
634        ) {
635            (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
636            (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
637            (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
638            (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
639            (_, Some(filename), None) => format!("{}.{}", filename, date),
640            (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
641            (_, None, None) => date,
642        }
643    }
644
645    fn prune_old_logs(&self, max_files: usize) {
646        let files = fs::read_dir(&self.log_directory).map(|dir| {
647            dir.filter_map(|entry| {
648                let entry = entry.ok()?;
649                let metadata = entry.metadata().ok()?;
650
651                // the appender only creates files, not directories or symlinks,
652                // so we should never delete a dir or symlink.
653                if !metadata.is_file() {
654                    return None;
655                }
656
657                let filename = entry.file_name();
658                // if the filename is not a UTF-8 string, skip it.
659                let filename = filename.to_str()?;
660                if let Some(prefix) = &self.log_filename_prefix {
661                    if !filename.starts_with(prefix) {
662                        return None;
663                    }
664                }
665
666                if let Some(suffix) = &self.log_filename_suffix {
667                    if !filename.ends_with(suffix) {
668                        return None;
669                    }
670                }
671
672                if self.log_filename_prefix.is_none()
673                    && self.log_filename_suffix.is_none()
674                    && Date::parse(filename, &self.date_format).is_err()
675                {
676                    return None;
677                }
678
679                let created = metadata.created().ok().or_else(|| {
680                    let mut datetime = filename;
681                    if let Some(prefix) = &self.log_filename_prefix {
682                        datetime = datetime.strip_prefix(prefix)?;
683                        datetime = datetime.strip_prefix('.')?;
684                    }
685                    if let Some(suffix) = &self.log_filename_suffix {
686                        datetime = datetime.strip_suffix(suffix)?;
687                        datetime = datetime.strip_suffix('.')?;
688                    }
689
690                    Some(
691                        PrimitiveDateTime::parse(datetime, &self.date_format)
692                            .ok()?
693                            .assume_utc()
694                            .into(),
695                    )
696                })?;
697                Some((entry, created))
698            })
699            .collect::<Vec<_>>()
700        });
701
702        let mut files = match files {
703            Ok(files) => files,
704            Err(error) => {
705                eprintln!("Error reading the log directory/files: {}", error);
706                return;
707            }
708        };
709        if files.len() < max_files {
710            return;
711        }
712
713        // sort the files by their creation timestamps.
714        files.sort_by_key(|(_, created_at)| *created_at);
715
716        // delete files, so that (n-1) files remain, because we will create another log file
717        for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
718            if let Err(error) = fs::remove_file(file.path()) {
719                eprintln!(
720                    "Failed to remove old log file {}: {}",
721                    file.path().display(),
722                    error
723                );
724            }
725        }
726    }
727
728    fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
729        let filename = self.join_date(&now);
730
731        if let Some(max_files) = self.max_files {
732            self.prune_old_logs(max_files);
733        }
734
735        match create_writer(&self.log_directory, &filename) {
736            Ok(new_file) => {
737                if let Err(err) = file.flush() {
738                    eprintln!("Couldn't flush previous writer: {}", err);
739                }
740                *file = new_file;
741            }
742            Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
743        }
744    }
745
746    /// Checks whether or not it's time to roll over the log file.
747    ///
748    /// Rather than returning a `bool`, this returns the current value of
749    /// `next_date` so that we can perform a `compare_exchange` operation with
750    /// that value when setting the next rollover time.
751    ///
752    /// If this method returns `Some`, we should roll to a new log file.
753    /// Otherwise, if this returns we should not rotate the log file.
754    fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
755        let next_date = self.next_date.load(Ordering::Acquire);
756        // if the next date is 0, this appender *never* rotates log files.
757        if next_date == 0 {
758            return None;
759        }
760
761        if date.unix_timestamp() as usize >= next_date {
762            return Some(next_date);
763        }
764
765        None
766    }
767
768    fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
769        let next_date = self
770            .rotation
771            .next_date(&now)
772            .map(|date| date.unix_timestamp() as usize)
773            .unwrap_or(0);
774        self.next_date
775            .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
776            .is_ok()
777    }
778}
779
780fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
781    let path = directory.join(filename);
782    let mut open_options = OpenOptions::new();
783    open_options.append(true).create(true);
784
785    let new_file = open_options.open(path.as_path());
786    if new_file.is_err() {
787        if let Some(parent) = path.parent() {
788            fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
789            return open_options
790                .open(path)
791                .map_err(InitError::ctx("failed to create initial log file"));
792        }
793    }
794
795    new_file.map_err(InitError::ctx("failed to create initial log file"))
796}
797
798#[cfg(test)]
799mod test {
800    use super::*;
801    use std::fs;
802    use std::io::Write;
803
804    fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
805        let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
806
807        for entry in dir_contents {
808            let path = entry.expect("Expected dir entry").path();
809            let file = fs::read_to_string(&path).expect("Failed to read file");
810            println!("path={}\nfile={:?}", path.display(), file);
811
812            if file.as_str() == expected_value {
813                return true;
814            }
815        }
816
817        false
818    }
819
820    fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
821        appender
822            .write_all(msg.as_bytes())
823            .expect("Failed to write to appender");
824        appender.flush().expect("Failed to flush!");
825    }
826
827    fn test_appender(rotation: Rotation, file_prefix: &str) {
828        let directory = tempfile::tempdir().expect("failed to create tempdir");
829        let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
830
831        let expected_value = "Hello";
832        write_to_log(&mut appender, expected_value);
833        assert!(find_str_in_log(directory.path(), expected_value));
834
835        directory
836            .close()
837            .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
838    }
839
840    #[test]
841    fn write_minutely_log() {
842        test_appender(Rotation::MINUTELY, "minutely.log");
843    }
844
845    #[test]
846    fn write_hourly_log() {
847        test_appender(Rotation::HOURLY, "hourly.log");
848    }
849
850    #[test]
851    fn write_daily_log() {
852        test_appender(Rotation::DAILY, "daily.log");
853    }
854
855    #[test]
856    fn write_weekly_log() {
857        test_appender(Rotation::WEEKLY, "weekly.log");
858    }
859
860    #[test]
861    fn write_never_log() {
862        test_appender(Rotation::NEVER, "never.log");
863    }
864
865    #[test]
866    fn test_rotations() {
867        // per-minute basis
868        let now = OffsetDateTime::now_utc();
869        let next = Rotation::MINUTELY.next_date(&now).unwrap();
870        assert_eq!((now + Duration::MINUTE).minute(), next.minute());
871
872        // per-hour basis
873        let now = OffsetDateTime::now_utc();
874        let next = Rotation::HOURLY.next_date(&now).unwrap();
875        assert_eq!((now + Duration::HOUR).hour(), next.hour());
876
877        // per-day basis
878        let now = OffsetDateTime::now_utc();
879        let next = Rotation::DAILY.next_date(&now).unwrap();
880        assert_eq!((now + Duration::DAY).day(), next.day());
881
882        // per-week basis
883        let now = OffsetDateTime::now_utc();
884        let now_rounded = Rotation::WEEKLY.round_date(now);
885        let next = Rotation::WEEKLY.next_date(&now).unwrap();
886        assert!(now_rounded < next);
887
888        // never
889        let now = OffsetDateTime::now_utc();
890        let next = Rotation::NEVER.next_date(&now);
891        assert!(next.is_none());
892    }
893
894    #[test]
895    fn test_join_date() {
896        struct TestCase {
897            expected: &'static str,
898            rotation: Rotation,
899            prefix: Option<&'static str>,
900            suffix: Option<&'static str>,
901            now: OffsetDateTime,
902        }
903
904        let format = format_description::parse(
905            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
906         sign:mandatory]:[offset_minute]:[offset_second]",
907        )
908        .unwrap();
909        let directory = tempfile::tempdir().expect("failed to create tempdir");
910
911        let test_cases = vec![
912            TestCase {
913                expected: "my_prefix.2025-02-16.log",
914                rotation: Rotation::WEEKLY,
915                prefix: Some("my_prefix"),
916                suffix: Some("log"),
917                now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
918            },
919            // Make sure weekly rotation rounds to the preceding year when appropriate
920            TestCase {
921                expected: "my_prefix.2024-12-29.log",
922                rotation: Rotation::WEEKLY,
923                prefix: Some("my_prefix"),
924                suffix: Some("log"),
925                now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
926            },
927            TestCase {
928                expected: "my_prefix.2025-02-17.log",
929                rotation: Rotation::DAILY,
930                prefix: Some("my_prefix"),
931                suffix: Some("log"),
932                now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
933            },
934            TestCase {
935                expected: "my_prefix.2025-02-17-10.log",
936                rotation: Rotation::HOURLY,
937                prefix: Some("my_prefix"),
938                suffix: Some("log"),
939                now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
940            },
941            TestCase {
942                expected: "my_prefix.2025-02-17-10-01.log",
943                rotation: Rotation::MINUTELY,
944                prefix: Some("my_prefix"),
945                suffix: Some("log"),
946                now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
947            },
948            TestCase {
949                expected: "my_prefix.log",
950                rotation: Rotation::NEVER,
951                prefix: Some("my_prefix"),
952                suffix: Some("log"),
953                now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
954            },
955        ];
956
957        for test_case in test_cases {
958            let (inner, _) = Inner::new(
959                test_case.now,
960                test_case.rotation.clone(),
961                directory.path(),
962                test_case.prefix.map(ToString::to_string),
963                test_case.suffix.map(ToString::to_string),
964                None,
965            )
966            .unwrap();
967            let path = inner.join_date(&test_case.now);
968
969            assert_eq!(path, test_case.expected);
970        }
971    }
972
973    #[test]
974    #[should_panic(
975        expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
976    )]
977    fn test_never_date_rounding() {
978        let now = OffsetDateTime::now_utc();
979        let _ = Rotation::NEVER.round_date(now);
980    }
981
982    #[test]
983    fn test_path_concatenation() {
984        let format = format_description::parse(
985            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
986         sign:mandatory]:[offset_minute]:[offset_second]",
987        )
988        .unwrap();
989        let directory = tempfile::tempdir().expect("failed to create tempdir");
990
991        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
992
993        struct TestCase {
994            expected: &'static str,
995            rotation: Rotation,
996            prefix: Option<&'static str>,
997            suffix: Option<&'static str>,
998        }
999
1000        let test = |TestCase {
1001                        expected,
1002                        rotation,
1003                        prefix,
1004                        suffix,
1005                    }| {
1006            let (inner, _) = Inner::new(
1007                now,
1008                rotation.clone(),
1009                directory.path(),
1010                prefix.map(ToString::to_string),
1011                suffix.map(ToString::to_string),
1012                None,
1013            )
1014            .unwrap();
1015            let path = inner.join_date(&now);
1016            assert_eq!(
1017                expected, path,
1018                "rotation = {:?}, prefix = {:?}, suffix = {:?}",
1019                rotation, prefix, suffix
1020            );
1021        };
1022
1023        let test_cases = vec![
1024            // prefix only
1025            TestCase {
1026                expected: "app.log.2020-02-01-10-01",
1027                rotation: Rotation::MINUTELY,
1028                prefix: Some("app.log"),
1029                suffix: None,
1030            },
1031            TestCase {
1032                expected: "app.log.2020-02-01-10",
1033                rotation: Rotation::HOURLY,
1034                prefix: Some("app.log"),
1035                suffix: None,
1036            },
1037            TestCase {
1038                expected: "app.log.2020-02-01",
1039                rotation: Rotation::DAILY,
1040                prefix: Some("app.log"),
1041                suffix: None,
1042            },
1043            TestCase {
1044                expected: "app.log",
1045                rotation: Rotation::NEVER,
1046                prefix: Some("app.log"),
1047                suffix: None,
1048            },
1049            // prefix and suffix
1050            TestCase {
1051                expected: "app.2020-02-01-10-01.log",
1052                rotation: Rotation::MINUTELY,
1053                prefix: Some("app"),
1054                suffix: Some("log"),
1055            },
1056            TestCase {
1057                expected: "app.2020-02-01-10.log",
1058                rotation: Rotation::HOURLY,
1059                prefix: Some("app"),
1060                suffix: Some("log"),
1061            },
1062            TestCase {
1063                expected: "app.2020-02-01.log",
1064                rotation: Rotation::DAILY,
1065                prefix: Some("app"),
1066                suffix: Some("log"),
1067            },
1068            TestCase {
1069                expected: "app.log",
1070                rotation: Rotation::NEVER,
1071                prefix: Some("app"),
1072                suffix: Some("log"),
1073            },
1074            // suffix only
1075            TestCase {
1076                expected: "2020-02-01-10-01.log",
1077                rotation: Rotation::MINUTELY,
1078                prefix: None,
1079                suffix: Some("log"),
1080            },
1081            TestCase {
1082                expected: "2020-02-01-10.log",
1083                rotation: Rotation::HOURLY,
1084                prefix: None,
1085                suffix: Some("log"),
1086            },
1087            TestCase {
1088                expected: "2020-02-01.log",
1089                rotation: Rotation::DAILY,
1090                prefix: None,
1091                suffix: Some("log"),
1092            },
1093            TestCase {
1094                expected: "log",
1095                rotation: Rotation::NEVER,
1096                prefix: None,
1097                suffix: Some("log"),
1098            },
1099        ];
1100        for test_case in test_cases {
1101            test(test_case)
1102        }
1103    }
1104
1105    #[test]
1106    fn test_make_writer() {
1107        use std::sync::{Arc, Mutex};
1108        use tracing_subscriber::prelude::*;
1109
1110        let format = format_description::parse(
1111            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1112         sign:mandatory]:[offset_minute]:[offset_second]",
1113        )
1114        .unwrap();
1115
1116        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1117        let directory = tempfile::tempdir().expect("failed to create tempdir");
1118        let (state, writer) = Inner::new(
1119            now,
1120            Rotation::HOURLY,
1121            directory.path(),
1122            Some("test_make_writer".to_string()),
1123            None,
1124            None,
1125        )
1126        .unwrap();
1127
1128        let clock = Arc::new(Mutex::new(now));
1129        let now = {
1130            let clock = clock.clone();
1131            Box::new(move || *clock.lock().unwrap())
1132        };
1133        let appender = RollingFileAppender { state, writer, now };
1134        let default = tracing_subscriber::fmt()
1135            .without_time()
1136            .with_level(false)
1137            .with_target(false)
1138            .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1139            .with_writer(appender)
1140            .finish()
1141            .set_default();
1142
1143        tracing::info!("file 1");
1144
1145        // advance time by one second
1146        (*clock.lock().unwrap()) += Duration::seconds(1);
1147
1148        tracing::info!("file 1");
1149
1150        // advance time by one hour
1151        (*clock.lock().unwrap()) += Duration::hours(1);
1152
1153        tracing::info!("file 2");
1154
1155        // advance time by one second
1156        (*clock.lock().unwrap()) += Duration::seconds(1);
1157
1158        tracing::info!("file 2");
1159
1160        drop(default);
1161
1162        let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1163        println!("dir={:?}", dir_contents);
1164        for entry in dir_contents {
1165            println!("entry={:?}", entry);
1166            let path = entry.expect("Expected dir entry").path();
1167            let file = fs::read_to_string(&path).expect("Failed to read file");
1168            println!("path={}\nfile={:?}", path.display(), file);
1169
1170            match path
1171                .extension()
1172                .expect("found a file without a date!")
1173                .to_str()
1174                .expect("extension should be UTF8")
1175            {
1176                "2020-02-01-10" => {
1177                    assert_eq!("file 1\nfile 1\n", file);
1178                }
1179                "2020-02-01-11" => {
1180                    assert_eq!("file 2\nfile 2\n", file);
1181                }
1182                x => panic!("unexpected date {}", x),
1183            }
1184        }
1185    }
1186
1187    #[test]
1188    fn test_max_log_files() {
1189        use std::sync::{Arc, Mutex};
1190        use tracing_subscriber::prelude::*;
1191
1192        let format = format_description::parse(
1193            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1194         sign:mandatory]:[offset_minute]:[offset_second]",
1195        )
1196        .unwrap();
1197
1198        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1199        let directory = tempfile::tempdir().expect("failed to create tempdir");
1200        let (state, writer) = Inner::new(
1201            now,
1202            Rotation::HOURLY,
1203            directory.path(),
1204            Some("test_max_log_files".to_string()),
1205            None,
1206            Some(2),
1207        )
1208        .unwrap();
1209
1210        let clock = Arc::new(Mutex::new(now));
1211        let now = {
1212            let clock = clock.clone();
1213            Box::new(move || *clock.lock().unwrap())
1214        };
1215        let appender = RollingFileAppender { state, writer, now };
1216        let default = tracing_subscriber::fmt()
1217            .without_time()
1218            .with_level(false)
1219            .with_target(false)
1220            .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1221            .with_writer(appender)
1222            .finish()
1223            .set_default();
1224
1225        tracing::info!("file 1");
1226
1227        // advance time by one second
1228        (*clock.lock().unwrap()) += Duration::seconds(1);
1229
1230        tracing::info!("file 1");
1231
1232        // advance time by one hour
1233        (*clock.lock().unwrap()) += Duration::hours(1);
1234
1235        // depending on the filesystem, the creation timestamp's resolution may
1236        // be as coarse as one second, so we need to wait a bit here to ensure
1237        // that the next file actually is newer than the old one.
1238        std::thread::sleep(std::time::Duration::from_secs(1));
1239
1240        tracing::info!("file 2");
1241
1242        // advance time by one second
1243        (*clock.lock().unwrap()) += Duration::seconds(1);
1244
1245        tracing::info!("file 2");
1246
1247        // advance time by one hour
1248        (*clock.lock().unwrap()) += Duration::hours(1);
1249
1250        // again, sleep to ensure that the creation timestamps actually differ.
1251        std::thread::sleep(std::time::Duration::from_secs(1));
1252
1253        tracing::info!("file 3");
1254
1255        // advance time by one second
1256        (*clock.lock().unwrap()) += Duration::seconds(1);
1257
1258        tracing::info!("file 3");
1259
1260        drop(default);
1261
1262        let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1263        println!("dir={:?}", dir_contents);
1264
1265        for entry in dir_contents {
1266            println!("entry={:?}", entry);
1267            let path = entry.expect("Expected dir entry").path();
1268            let file = fs::read_to_string(&path).expect("Failed to read file");
1269            println!("path={}\nfile={:?}", path.display(), file);
1270
1271            match path
1272                .extension()
1273                .expect("found a file without a date!")
1274                .to_str()
1275                .expect("extension should be UTF8")
1276            {
1277                "2020-02-01-10" => {
1278                    panic!("this file should have been pruned already!");
1279                }
1280                "2020-02-01-11" => {
1281                    assert_eq!("file 2\nfile 2\n", file);
1282                }
1283                "2020-02-01-12" => {
1284                    assert_eq!("file 3\nfile 3\n", file);
1285                }
1286                x => panic!("unexpected date {}", x),
1287            }
1288        }
1289    }
1290}