clia_tracing_appender/
rolling.rs

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