Skip to main content

tracing_appender_localtime/
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 time::{format_description, Date, Duration, OffsetDateTime, 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    offset: time::UtcOffset,
90}
91
92/// A [writer] that writes to a rolling log file.
93///
94/// This is returned by the [`MakeWriter`] implementation for [`RollingFileAppender`].
95///
96/// [writer]: std::io::Write
97/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
98#[derive(Debug)]
99pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
100
101#[derive(Debug)]
102struct Inner {
103    log_directory: PathBuf,
104    log_filename_prefix: Option<String>,
105    log_filename_suffix: Option<String>,
106    date_format: Vec<format_description::FormatItem<'static>>,
107    rotation: Rotation,
108    next_date: AtomicUsize,
109    max_files: Option<usize>,
110}
111
112// === impl RollingFileAppender ===
113
114impl RollingFileAppender {
115    /// Creates a new `RollingFileAppender`.
116    ///
117    /// A `RollingFileAppender` will have a fixed rotation whose frequency is
118    /// defined by [`Rotation`][self::Rotation]. The `directory` and
119    /// `file_name_prefix` arguments determine the location and file name's _prefix_
120    /// of the log file. `RollingFileAppender` will automatically append the current date
121    /// and hour (UTC format) to the file name.
122    ///
123    /// Alternatively, a `RollingFileAppender` can be constructed using one of the following helpers:
124    ///
125    /// - [`Rotation::minutely()`][minutely],
126    /// - [`Rotation::hourly()`][hourly],
127    /// - [`Rotation::daily()`][daily],
128    /// - [`Rotation::never()`][never()]
129    ///
130    /// Additional parameters can be configured using [`RollingFileAppender::builder`].
131    ///
132    /// # Examples
133    ///
134    /// ```rust
135    /// # fn docs() {
136    /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
137    /// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log");
138    /// # }
139    /// ```
140    pub fn new(
141        rotation: Rotation,
142        directory: impl AsRef<Path>,
143        filename_prefix: impl AsRef<Path>,
144    ) -> RollingFileAppender {
145        let filename_prefix = filename_prefix
146            .as_ref()
147            .to_str()
148            .expect("filename prefix must be a valid UTF-8 string");
149        Self::builder()
150            .rotation(rotation)
151            .filename_prefix(filename_prefix)
152            .build(directory)
153            .expect("initializing rolling file appender failed")
154    }
155
156    /// Returns a new [`Builder`] for configuring a `RollingFileAppender`.
157    ///
158    /// The builder interface can be used to set additional configuration
159    /// parameters when constructing a new appender.
160    ///
161    /// Unlike [`RollingFileAppender::new`], the [`Builder::build`] method
162    /// returns a `Result` rather than panicking when the appender cannot be
163    /// initialized. Therefore, the builder interface can also be used when
164    /// appender initialization errors should be handled gracefully.
165    ///
166    /// # Examples
167    ///
168    /// ```rust
169    /// # fn docs() {
170    /// use tracing_appender::rolling::{RollingFileAppender, Rotation};
171    ///
172    /// let file_appender = RollingFileAppender::builder()
173    ///     .rotation(Rotation::HOURLY) // rotate log files once every hour
174    ///     .filename_prefix("myapp") // log file names will be prefixed with `myapp.`
175    ///     .filename_suffix("log") // log file names will be suffixed with `.log`
176    ///     .build("/var/log") // try to build an appender that stores log files in `/var/log`
177    ///     .expect("initializing rolling file appender failed");
178    /// # drop(file_appender);
179    /// # }
180    /// ```
181    #[must_use]
182    pub fn builder() -> Builder {
183        Builder::new()
184    }
185
186    fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
187        let Builder {
188            ref rotation,
189            ref prefix,
190            ref suffix,
191            ref max_files,
192        } = builder;
193
194        use chrono::Local;
195
196        let directory = directory.as_ref().to_path_buf();
197        let offset_in_sec = Local::now().offset().local_minus_utc();
198        let offset = time::UtcOffset::from_whole_seconds(offset_in_sec)
199            .expect("Get localtime offset failed");
200        let now = OffsetDateTime::now_utc().to_offset(offset);
201        // println!("offset is {:?}", now);
202
203        let (state, writer) = Inner::new(
204            now,
205            rotation.clone(),
206            directory,
207            prefix.clone(),
208            suffix.clone(),
209            *max_files,
210        )?;
211        Ok(Self {
212            state,
213            writer,
214            offset,
215        })
216    }
217
218    #[inline]
219    fn now(&self) -> OffsetDateTime {
220        OffsetDateTime::now_utc().to_offset(self.offset)
221    }
222}
223
224impl io::Write for RollingFileAppender {
225    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
226        let now = self.now();
227        let writer = self.writer.get_mut();
228        if let Some(current_time) = self.state.should_rollover(now) {
229            let _did_cas = self.state.advance_date(now, current_time);
230            debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
231            self.state.refresh_writer(now, writer);
232        }
233        writer.write(buf)
234    }
235
236    fn flush(&mut self) -> io::Result<()> {
237        self.writer.get_mut().flush()
238    }
239}
240
241impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
242    type Writer = RollingWriter<'a>;
243    fn make_writer(&'a self) -> Self::Writer {
244        let now = self.now();
245
246        // Should we try to roll over the log file?
247        if let Some(current_time) = self.state.should_rollover(now) {
248            // Did we get the right to lock the file? If not, another thread
249            // did it and we can just make a writer.
250            if self.state.advance_date(now, current_time) {
251                self.state.refresh_writer(now, &mut self.writer.write());
252            }
253        }
254        RollingWriter(self.writer.read())
255    }
256}
257
258impl fmt::Debug for RollingFileAppender {
259    // This manual impl is required because of the `now` field (only present
260    // with `cfg(test)`), which is not `Debug`...
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.debug_struct("RollingFileAppender")
263            .field("state", &self.state)
264            .field("writer", &self.writer)
265            .finish()
266    }
267}
268
269/// Creates a minutely-rotating file appender. This will rotate the log file once per minute.
270///
271/// The appender returned by `rolling::minutely` can be used with `non_blocking` to create
272/// a non-blocking, minutely file appender.
273///
274/// The directory of the log file is specified with the `directory` argument.
275/// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender`
276/// adds the current date, hour, and minute to the log file in UTC.
277///
278/// # Examples
279///
280/// ``` rust
281/// # #[clippy::allow(needless_doctest_main)]
282/// fn main () {
283/// # fn doc() {
284///     let appender = tracing_appender::rolling::minutely("/some/path", "rolling.log");
285///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
286///
287///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
288///
289///     tracing::subscriber::with_default(subscriber.finish(), || {
290///         tracing::event!(tracing::Level::INFO, "Hello");
291///     });
292/// # }
293/// }
294/// ```
295///
296/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH-mm`.
297pub fn minutely(
298    directory: impl AsRef<Path>,
299    file_name_prefix: impl AsRef<Path>,
300) -> RollingFileAppender {
301    RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
302}
303
304/// Creates an hourly-rotating file appender.
305///
306/// The appender returned by `rolling::hourly` can be used with `non_blocking` to create
307/// a non-blocking, hourly file appender.
308///
309/// The directory of the log file is specified with the `directory` argument.
310/// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender`
311/// adds the current date and hour to the log file in UTC.
312///
313/// # Examples
314///
315/// ``` rust
316/// # #[clippy::allow(needless_doctest_main)]
317/// fn main () {
318/// # fn doc() {
319///     let appender = tracing_appender::rolling::hourly("/some/path", "rolling.log");
320///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
321///
322///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
323///
324///     tracing::subscriber::with_default(subscriber.finish(), || {
325///         tracing::event!(tracing::Level::INFO, "Hello");
326///     });
327/// # }
328/// }
329/// ```
330///
331/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`.
332pub fn hourly(
333    directory: impl AsRef<Path>,
334    file_name_prefix: impl AsRef<Path>,
335) -> RollingFileAppender {
336    RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
337}
338
339/// Creates a daily-rotating file appender.
340///
341/// The appender returned by `rolling::daily` can be used with `non_blocking` to create
342/// a non-blocking, daily file appender.
343///
344/// A `RollingFileAppender` has a fixed rotation whose frequency is
345/// defined by [`Rotation`][self::Rotation]. The `directory` and
346/// `file_name_prefix` arguments determine the location and file name's _prefix_
347/// of the log file. `RollingFileAppender` automatically appends the current date in UTC.
348///
349/// # Examples
350///
351/// ``` rust
352/// # #[clippy::allow(needless_doctest_main)]
353/// fn main () {
354/// # fn doc() {
355///     let appender = tracing_appender::rolling::daily("/some/path", "rolling.log");
356///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
357///
358///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
359///
360///     tracing::subscriber::with_default(subscriber.finish(), || {
361///         tracing::event!(tracing::Level::INFO, "Hello");
362///     });
363/// # }
364/// }
365/// ```
366///
367/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`.
368pub fn daily(
369    directory: impl AsRef<Path>,
370    file_name_prefix: impl AsRef<Path>,
371) -> RollingFileAppender {
372    RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
373}
374
375/// Creates a non-rolling file appender.
376///
377/// The appender returned by `rolling::never` can be used with `non_blocking` to create
378/// a non-blocking, non-rotating appender.
379///
380/// The location of the log file will be specified the `directory` passed in.
381/// `file_name` specifies the complete name of the log file (no date or time is appended).
382///
383/// # Examples
384///
385/// ``` rust
386/// # #[clippy::allow(needless_doctest_main)]
387/// fn main () {
388/// # fn doc() {
389///     let appender = tracing_appender::rolling::never("/some/path", "non-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/non-rolling.log`.
402pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
403    RollingFileAppender::new(Rotation::NEVER, directory, file_name)
404}
405
406/// Defines a fixed period for rolling of a log file.
407///
408/// To use a `Rotation`, pick one of the following options:
409///
410/// ### Minutely Rotation
411/// ```rust
412/// # fn docs() {
413/// use tracing_appender::rolling::Rotation;
414/// let rotation = tracing_appender::rolling::Rotation::MINUTELY;
415/// # }
416/// ```
417///
418/// ### Hourly Rotation
419/// ```rust
420/// # fn docs() {
421/// use tracing_appender::rolling::Rotation;
422/// let rotation = tracing_appender::rolling::Rotation::HOURLY;
423/// # }
424/// ```
425///
426/// ### Daily Rotation
427/// ```rust
428/// # fn docs() {
429/// use tracing_appender::rolling::Rotation;
430/// let rotation = tracing_appender::rolling::Rotation::DAILY;
431/// # }
432/// ```
433///
434/// ### No Rotation
435/// ```rust
436/// # fn docs() {
437/// use tracing_appender::rolling::Rotation;
438/// let rotation = tracing_appender::rolling::Rotation::NEVER;
439/// # }
440/// ```
441#[derive(Clone, Eq, PartialEq, Debug)]
442pub struct Rotation(RotationKind);
443
444#[derive(Clone, Eq, PartialEq, Debug)]
445enum RotationKind {
446    Minutely,
447    Hourly,
448    Daily,
449    Never,
450}
451
452impl Rotation {
453    /// Provides an minutely rotation
454    pub const MINUTELY: Self = Self(RotationKind::Minutely);
455    /// Provides an hourly rotation
456    pub const HOURLY: Self = Self(RotationKind::Hourly);
457    /// Provides a daily rotation
458    pub const DAILY: Self = Self(RotationKind::Daily);
459    /// Provides a rotation that never rotates.
460    pub const NEVER: Self = Self(RotationKind::Never);
461
462    pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
463        let unrounded_next_date = match *self {
464            Rotation::MINUTELY => *current_date + Duration::minutes(1),
465            Rotation::HOURLY => *current_date + Duration::hours(1),
466            Rotation::DAILY => *current_date + Duration::days(1),
467            Rotation::NEVER => return None,
468        };
469        Some(self.round_date(&unrounded_next_date))
470    }
471
472    // note that this method will panic if passed a `Rotation::NEVER`.
473    pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime {
474        match *self {
475            Rotation::MINUTELY => {
476                let time = Time::from_hms(date.hour(), date.minute(), 0)
477                    .expect("Invalid time; this is a bug in tracing-appender");
478                date.replace_time(time)
479            }
480            Rotation::HOURLY => {
481                let time = Time::from_hms(date.hour(), 0, 0)
482                    .expect("Invalid time; this is a bug in tracing-appender");
483                date.replace_time(time)
484            }
485            Rotation::DAILY => {
486                let time = Time::from_hms(0, 0, 0)
487                    .expect("Invalid time; this is a bug in tracing-appender");
488                date.replace_time(time)
489            }
490            // Rotation::NEVER is impossible to round.
491            Rotation::NEVER => {
492                unreachable!("Rotation::NEVER is impossible to round.")
493            }
494        }
495    }
496
497    fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
498        match *self {
499            Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
500            Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
501            Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
502            Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
503        }
504        .expect("Unable to create a formatter; this is a bug in tracing-appender")
505    }
506}
507
508// === impl RollingWriter ===
509
510impl io::Write for RollingWriter<'_> {
511    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
512        (&*self.0).write(buf)
513    }
514
515    fn flush(&mut self) -> io::Result<()> {
516        (&*self.0).flush()
517    }
518}
519
520// === impl Inner ===
521
522impl Inner {
523    fn new(
524        now: OffsetDateTime,
525        rotation: Rotation,
526        directory: impl AsRef<Path>,
527        log_filename_prefix: Option<String>,
528        log_filename_suffix: Option<String>,
529        max_files: Option<usize>,
530    ) -> Result<(Self, RwLock<File>), builder::InitError> {
531        let log_directory = directory.as_ref().to_path_buf();
532        let date_format = rotation.date_format();
533        let next_date = rotation.next_date(&now);
534
535        let inner = Inner {
536            log_directory,
537            log_filename_prefix,
538            log_filename_suffix,
539            date_format,
540            next_date: AtomicUsize::new(
541                next_date
542                    .map(|date| date.unix_timestamp() as usize)
543                    .unwrap_or(0),
544            ),
545            rotation,
546            max_files,
547        };
548        let filename = inner.join_date(&now);
549        let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
550        Ok((inner, writer))
551    }
552
553    pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
554        let date = date
555            .format(&self.date_format)
556            .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
557
558        match (
559            &self.rotation,
560            &self.log_filename_prefix,
561            &self.log_filename_suffix,
562        ) {
563            (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
564            (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
565            (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
566            (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
567            (_, Some(filename), None) => format!("{}.{}", filename, date),
568            (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
569            (_, None, None) => date,
570        }
571    }
572
573    fn prune_old_logs(&self, max_files: usize) {
574        let files = fs::read_dir(&self.log_directory).map(|dir| {
575            dir.filter_map(|entry| {
576                let entry = entry.ok()?;
577                let metadata = entry.metadata().ok()?;
578
579                // the appender only creates files, not directories or symlinks,
580                // so we should never delete a dir or symlink.
581                if !metadata.is_file() {
582                    return None;
583                }
584
585                let filename = entry.file_name();
586                // if the filename is not a UTF-8 string, skip it.
587                let filename = filename.to_str()?;
588                if let Some(prefix) = &self.log_filename_prefix {
589                    if !filename.starts_with(prefix) {
590                        return None;
591                    }
592                }
593
594                if let Some(suffix) = &self.log_filename_suffix {
595                    if !filename.ends_with(suffix) {
596                        return None;
597                    }
598                }
599
600                if self.log_filename_prefix.is_none()
601                    && self.log_filename_suffix.is_none()
602                    && Date::parse(filename, &self.date_format).is_err()
603                {
604                    return None;
605                }
606
607                let created = metadata.created().ok()?;
608                Some((entry, created))
609            })
610            .collect::<Vec<_>>()
611        });
612
613        let mut files = match files {
614            Ok(files) => files,
615            Err(error) => {
616                eprintln!("Error reading the log directory/files: {}", error);
617                return;
618            }
619        };
620        if files.len() < max_files {
621            return;
622        }
623
624        // sort the files by their creation timestamps.
625        files.sort_by_key(|(_, created_at)| *created_at);
626
627        // delete files, so that (n-1) files remain, because we will create another log file
628        for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
629            if let Err(error) = fs::remove_file(file.path()) {
630                eprintln!(
631                    "Failed to remove old log file {}: {}",
632                    file.path().display(),
633                    error
634                );
635            }
636        }
637    }
638
639    fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
640        let filename = self.join_date(&now);
641
642        if let Some(max_files) = self.max_files {
643            self.prune_old_logs(max_files);
644        }
645
646        match create_writer(&self.log_directory, &filename) {
647            Ok(new_file) => {
648                if let Err(err) = file.flush() {
649                    eprintln!("Couldn't flush previous writer: {}", err);
650                }
651                *file = new_file;
652            }
653            Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
654        }
655    }
656
657    /// Checks whether or not it's time to roll over the log file.
658    ///
659    /// Rather than returning a `bool`, this returns the current value of
660    /// `next_date` so that we can perform a `compare_exchange` operation with
661    /// that value when setting the next rollover time.
662    ///
663    /// If this method returns `Some`, we should roll to a new log file.
664    /// Otherwise, if this returns we should not rotate the log file.
665    fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
666        let next_date = self.next_date.load(Ordering::Acquire);
667        // if the next date is 0, this appender *never* rotates log files.
668        if next_date == 0 {
669            return None;
670        }
671
672        if date.unix_timestamp() as usize >= next_date {
673            return Some(next_date);
674        }
675
676        None
677    }
678
679    fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
680        let next_date = self
681            .rotation
682            .next_date(&now)
683            .map(|date| date.unix_timestamp() as usize)
684            .unwrap_or(0);
685        self.next_date
686            .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
687            .is_ok()
688    }
689}
690
691fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
692    let path = directory.join(filename);
693    let mut open_options = OpenOptions::new();
694    open_options.append(true).create(true);
695
696    let new_file = open_options.open(path.as_path());
697    if new_file.is_err() {
698        if let Some(parent) = path.parent() {
699            fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
700            return open_options
701                .open(path)
702                .map_err(InitError::ctx("failed to create initial log file"));
703        }
704    }
705
706    new_file.map_err(InitError::ctx("failed to create initial log file"))
707}