Coverage Report

Created: 2025-12-28 06:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/tracing-appender-0.2.3/src/rolling.rs
Line
Count
Source
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
//! ```
29
use crate::sync::{RwLock, RwLockReadGuard};
30
use std::{
31
    fmt::{self, Debug},
32
    fs::{self, File, OpenOptions},
33
    io::{self, Write},
34
    path::{Path, PathBuf},
35
    sync::atomic::{AtomicUsize, Ordering},
36
};
37
use time::{format_description, Date, Duration, OffsetDateTime, Time};
38
39
mod builder;
40
pub 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
86
pub 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)]
100
pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
101
102
#[derive(Debug)]
103
struct 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
115
impl RollingFileAppender {
116
    /// Creates a new `RollingFileAppender`.
117
    ///
118
    /// A `RollingFileAppender` will have a fixed rotation whose frequency is
119
    /// defined by [`Rotation`][self::Rotation]. The `directory` and
120
    /// `file_name_prefix` arguments determine the location and file name's _prefix_
121
    /// of the log 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
0
    pub fn new(
142
0
        rotation: Rotation,
143
0
        directory: impl AsRef<Path>,
144
0
        filename_prefix: impl AsRef<Path>,
145
0
    ) -> RollingFileAppender {
146
0
        let filename_prefix = filename_prefix
147
0
            .as_ref()
148
0
            .to_str()
149
0
            .expect("filename prefix must be a valid UTF-8 string");
150
0
        Self::builder()
151
0
            .rotation(rotation)
152
0
            .filename_prefix(filename_prefix)
153
0
            .build(directory)
154
0
            .expect("initializing rolling file appender failed")
155
0
    }
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
0
    pub fn builder() -> Builder {
184
0
        Builder::new()
185
0
    }
186
187
0
    fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
188
        let Builder {
189
0
            ref rotation,
190
0
            ref prefix,
191
0
            ref suffix,
192
0
            ref max_files,
193
0
        } = builder;
194
0
        let directory = directory.as_ref().to_path_buf();
195
0
        let now = OffsetDateTime::now_utc();
196
0
        let (state, writer) = Inner::new(
197
0
            now,
198
0
            rotation.clone(),
199
0
            directory,
200
0
            prefix.clone(),
201
0
            suffix.clone(),
202
0
            *max_files,
203
0
        )?;
204
0
        Ok(Self {
205
0
            state,
206
0
            writer,
207
0
            #[cfg(test)]
208
0
            now: Box::new(OffsetDateTime::now_utc),
209
0
        })
210
0
    }
211
212
    #[inline]
213
0
    fn now(&self) -> OffsetDateTime {
214
        #[cfg(test)]
215
        return (self.now)();
216
217
        #[cfg(not(test))]
218
0
        OffsetDateTime::now_utc()
219
0
    }
220
}
221
222
impl io::Write for RollingFileAppender {
223
0
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
224
0
        let now = self.now();
225
0
        let writer = self.writer.get_mut();
226
0
        if let Some(current_time) = self.state.should_rollover(now) {
227
0
            let _did_cas = self.state.advance_date(now, current_time);
228
0
            debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
229
0
            self.state.refresh_writer(now, writer);
230
0
        }
231
0
        writer.write(buf)
232
0
    }
233
234
0
    fn flush(&mut self) -> io::Result<()> {
235
0
        self.writer.get_mut().flush()
236
0
    }
237
}
238
239
impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
240
    type Writer = RollingWriter<'a>;
241
0
    fn make_writer(&'a self) -> Self::Writer {
242
0
        let now = self.now();
243
244
        // Should we try to roll over the log file?
245
0
        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
0
            if self.state.advance_date(now, current_time) {
249
0
                self.state.refresh_writer(now, &mut self.writer.write());
250
0
            }
251
0
        }
252
0
        RollingWriter(self.writer.read())
253
0
    }
254
}
255
256
impl 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
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260
0
        f.debug_struct("RollingFileAppender")
261
0
            .field("state", &self.state)
262
0
            .field("writer", &self.writer)
263
0
            .finish()
264
0
    }
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`.
295
0
pub fn minutely(
296
0
    directory: impl AsRef<Path>,
297
0
    file_name_prefix: impl AsRef<Path>,
298
0
) -> RollingFileAppender {
299
0
    RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
300
0
}
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`.
330
0
pub fn hourly(
331
0
    directory: impl AsRef<Path>,
332
0
    file_name_prefix: impl AsRef<Path>,
333
0
) -> RollingFileAppender {
334
0
    RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
335
0
}
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`][self::Rotation]. The `directory` and
344
/// `file_name_prefix` arguments determine the location and file name's _prefix_
345
/// of the log file. `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-HH`.
366
0
pub fn daily(
367
0
    directory: impl AsRef<Path>,
368
0
    file_name_prefix: impl AsRef<Path>,
369
0
) -> RollingFileAppender {
370
0
    RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
371
0
}
372
373
/// Creates a non-rolling file appender.
374
///
375
/// The appender returned by `rolling::never` can be used with `non_blocking` to create
376
/// a non-blocking, non-rotating appender.
377
///
378
/// The location of the log file will be specified the `directory` passed in.
379
/// `file_name` specifies the complete name of the log file (no date or time is appended).
380
///
381
/// # Examples
382
///
383
/// ``` rust
384
/// # #[clippy::allow(needless_doctest_main)]
385
/// fn main () {
386
/// # fn doc() {
387
///     let appender = tracing_appender::rolling::never("/some/path", "non-rolling.log");
388
///     let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
389
///
390
///     let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
391
///
392
///     tracing::subscriber::with_default(subscriber.finish(), || {
393
///         tracing::event!(tracing::Level::INFO, "Hello");
394
///     });
395
/// # }
396
/// }
397
/// ```
398
///
399
/// This will result in a log file located at `/some/path/non-rolling.log`.
400
0
pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
401
0
    RollingFileAppender::new(Rotation::NEVER, directory, file_name)
402
0
}
403
404
/// Defines a fixed period for rolling of a log file.
405
///
406
/// To use a `Rotation`, pick one of the following options:
407
///
408
/// ### Minutely Rotation
409
/// ```rust
410
/// # fn docs() {
411
/// use tracing_appender::rolling::Rotation;
412
/// let rotation = tracing_appender::rolling::Rotation::MINUTELY;
413
/// # }
414
/// ```
415
///
416
/// ### Hourly Rotation
417
/// ```rust
418
/// # fn docs() {
419
/// use tracing_appender::rolling::Rotation;
420
/// let rotation = tracing_appender::rolling::Rotation::HOURLY;
421
/// # }
422
/// ```
423
///
424
/// ### Daily Rotation
425
/// ```rust
426
/// # fn docs() {
427
/// use tracing_appender::rolling::Rotation;
428
/// let rotation = tracing_appender::rolling::Rotation::DAILY;
429
/// # }
430
/// ```
431
///
432
/// ### No Rotation
433
/// ```rust
434
/// # fn docs() {
435
/// use tracing_appender::rolling::Rotation;
436
/// let rotation = tracing_appender::rolling::Rotation::NEVER;
437
/// # }
438
/// ```
439
#[derive(Clone, Eq, PartialEq, Debug)]
440
pub struct Rotation(RotationKind);
441
442
#[derive(Clone, Eq, PartialEq, Debug)]
443
enum RotationKind {
444
    Minutely,
445
    Hourly,
446
    Daily,
447
    Never,
448
}
449
450
impl Rotation {
451
    /// Provides an minutely rotation
452
    pub const MINUTELY: Self = Self(RotationKind::Minutely);
453
    /// Provides an hourly rotation
454
    pub const HOURLY: Self = Self(RotationKind::Hourly);
455
    /// Provides a daily rotation
456
    pub const DAILY: Self = Self(RotationKind::Daily);
457
    /// Provides a rotation that never rotates.
458
    pub const NEVER: Self = Self(RotationKind::Never);
459
460
0
    pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
461
0
        let unrounded_next_date = match *self {
462
0
            Rotation::MINUTELY => *current_date + Duration::minutes(1),
463
0
            Rotation::HOURLY => *current_date + Duration::hours(1),
464
0
            Rotation::DAILY => *current_date + Duration::days(1),
465
0
            Rotation::NEVER => return None,
466
        };
467
0
        Some(self.round_date(&unrounded_next_date))
468
0
    }
469
470
    // note that this method will panic if passed a `Rotation::NEVER`.
471
0
    pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime {
472
0
        match *self {
473
            Rotation::MINUTELY => {
474
0
                let time = Time::from_hms(date.hour(), date.minute(), 0)
475
0
                    .expect("Invalid time; this is a bug in tracing-appender");
476
0
                date.replace_time(time)
477
            }
478
            Rotation::HOURLY => {
479
0
                let time = Time::from_hms(date.hour(), 0, 0)
480
0
                    .expect("Invalid time; this is a bug in tracing-appender");
481
0
                date.replace_time(time)
482
            }
483
            Rotation::DAILY => {
484
0
                let time = Time::from_hms(0, 0, 0)
485
0
                    .expect("Invalid time; this is a bug in tracing-appender");
486
0
                date.replace_time(time)
487
            }
488
            // Rotation::NEVER is impossible to round.
489
            Rotation::NEVER => {
490
0
                unreachable!("Rotation::NEVER is impossible to round.")
491
            }
492
        }
493
0
    }
494
495
0
    fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
496
0
        match *self {
497
0
            Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
498
0
            Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
499
0
            Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
500
0
            Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
501
        }
502
0
        .expect("Unable to create a formatter; this is a bug in tracing-appender")
503
0
    }
504
}
505
506
// === impl RollingWriter ===
507
508
impl io::Write for RollingWriter<'_> {
509
0
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
510
0
        (&*self.0).write(buf)
511
0
    }
512
513
0
    fn flush(&mut self) -> io::Result<()> {
514
0
        (&*self.0).flush()
515
0
    }
516
}
517
518
// === impl Inner ===
519
520
impl Inner {
521
0
    fn new(
522
0
        now: OffsetDateTime,
523
0
        rotation: Rotation,
524
0
        directory: impl AsRef<Path>,
525
0
        log_filename_prefix: Option<String>,
526
0
        log_filename_suffix: Option<String>,
527
0
        max_files: Option<usize>,
528
0
    ) -> Result<(Self, RwLock<File>), builder::InitError> {
529
0
        let log_directory = directory.as_ref().to_path_buf();
530
0
        let date_format = rotation.date_format();
531
0
        let next_date = rotation.next_date(&now);
532
533
0
        let inner = Inner {
534
0
            log_directory,
535
0
            log_filename_prefix,
536
0
            log_filename_suffix,
537
0
            date_format,
538
0
            next_date: AtomicUsize::new(
539
0
                next_date
540
0
                    .map(|date| date.unix_timestamp() as usize)
541
0
                    .unwrap_or(0),
542
            ),
543
0
            rotation,
544
0
            max_files,
545
        };
546
0
        let filename = inner.join_date(&now);
547
0
        let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
548
0
        Ok((inner, writer))
549
0
    }
550
551
0
    pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
552
0
        let date = date
553
0
            .format(&self.date_format)
554
0
            .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
555
556
        match (
557
0
            &self.rotation,
558
0
            &self.log_filename_prefix,
559
0
            &self.log_filename_suffix,
560
        ) {
561
0
            (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
562
0
            (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
563
0
            (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
564
0
            (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
565
0
            (_, Some(filename), None) => format!("{}.{}", filename, date),
566
0
            (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
567
0
            (_, None, None) => date,
568
        }
569
0
    }
570
571
0
    fn prune_old_logs(&self, max_files: usize) {
572
0
        let files = fs::read_dir(&self.log_directory).map(|dir| {
573
0
            dir.filter_map(|entry| {
574
0
                let entry = entry.ok()?;
575
0
                let metadata = entry.metadata().ok()?;
576
577
                // the appender only creates files, not directories or symlinks,
578
                // so we should never delete a dir or symlink.
579
0
                if !metadata.is_file() {
580
0
                    return None;
581
0
                }
582
583
0
                let filename = entry.file_name();
584
                // if the filename is not a UTF-8 string, skip it.
585
0
                let filename = filename.to_str()?;
586
0
                if let Some(prefix) = &self.log_filename_prefix {
587
0
                    if !filename.starts_with(prefix) {
588
0
                        return None;
589
0
                    }
590
0
                }
591
592
0
                if let Some(suffix) = &self.log_filename_suffix {
593
0
                    if !filename.ends_with(suffix) {
594
0
                        return None;
595
0
                    }
596
0
                }
597
598
0
                if self.log_filename_prefix.is_none()
599
0
                    && self.log_filename_suffix.is_none()
600
0
                    && Date::parse(filename, &self.date_format).is_err()
601
                {
602
0
                    return None;
603
0
                }
604
605
0
                let created = metadata.created().ok()?;
606
0
                Some((entry, created))
607
0
            })
608
0
            .collect::<Vec<_>>()
609
0
        });
610
611
0
        let mut files = match files {
612
0
            Ok(files) => files,
613
0
            Err(error) => {
614
0
                eprintln!("Error reading the log directory/files: {}", error);
615
0
                return;
616
            }
617
        };
618
0
        if files.len() < max_files {
619
0
            return;
620
0
        }
621
622
        // sort the files by their creation timestamps.
623
0
        files.sort_by_key(|(_, created_at)| *created_at);
624
625
        // delete files, so that (n-1) files remain, because we will create another log file
626
0
        for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
627
0
            if let Err(error) = fs::remove_file(file.path()) {
628
0
                eprintln!(
629
0
                    "Failed to remove old log file {}: {}",
630
0
                    file.path().display(),
631
0
                    error
632
0
                );
633
0
            }
634
        }
635
0
    }
636
637
0
    fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
638
0
        let filename = self.join_date(&now);
639
640
0
        if let Some(max_files) = self.max_files {
641
0
            self.prune_old_logs(max_files);
642
0
        }
643
644
0
        match create_writer(&self.log_directory, &filename) {
645
0
            Ok(new_file) => {
646
0
                if let Err(err) = file.flush() {
647
0
                    eprintln!("Couldn't flush previous writer: {}", err);
648
0
                }
649
0
                *file = new_file;
650
            }
651
0
            Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
652
        }
653
0
    }
654
655
    /// Checks whether or not it's time to roll over the log file.
656
    ///
657
    /// Rather than returning a `bool`, this returns the current value of
658
    /// `next_date` so that we can perform a `compare_exchange` operation with
659
    /// that value when setting the next rollover time.
660
    ///
661
    /// If this method returns `Some`, we should roll to a new log file.
662
    /// Otherwise, if this returns we should not rotate the log file.
663
0
    fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
664
0
        let next_date = self.next_date.load(Ordering::Acquire);
665
        // if the next date is 0, this appender *never* rotates log files.
666
0
        if next_date == 0 {
667
0
            return None;
668
0
        }
669
670
0
        if date.unix_timestamp() as usize >= next_date {
671
0
            return Some(next_date);
672
0
        }
673
674
0
        None
675
0
    }
676
677
0
    fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
678
0
        let next_date = self
679
0
            .rotation
680
0
            .next_date(&now)
681
0
            .map(|date| date.unix_timestamp() as usize)
682
0
            .unwrap_or(0);
683
0
        self.next_date
684
0
            .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
685
0
            .is_ok()
686
0
    }
687
}
688
689
0
fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
690
0
    let path = directory.join(filename);
691
0
    let mut open_options = OpenOptions::new();
692
0
    open_options.append(true).create(true);
693
694
0
    let new_file = open_options.open(path.as_path());
695
0
    if new_file.is_err() {
696
0
        if let Some(parent) = path.parent() {
697
0
            fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
698
0
            return open_options
699
0
                .open(path)
700
0
                .map_err(InitError::ctx("failed to create initial log file"));
701
0
        }
702
0
    }
703
704
0
    new_file.map_err(InitError::ctx("failed to create initial log file"))
705
0
}
706
707
#[cfg(test)]
708
mod test {
709
    use super::*;
710
    use std::fs;
711
    use std::io::Write;
712
713
    fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
714
        let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
715
716
        for entry in dir_contents {
717
            let path = entry.expect("Expected dir entry").path();
718
            let file = fs::read_to_string(&path).expect("Failed to read file");
719
            println!("path={}\nfile={:?}", path.display(), file);
720
721
            if file.as_str() == expected_value {
722
                return true;
723
            }
724
        }
725
726
        false
727
    }
728
729
    fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
730
        appender
731
            .write_all(msg.as_bytes())
732
            .expect("Failed to write to appender");
733
        appender.flush().expect("Failed to flush!");
734
    }
735
736
    fn test_appender(rotation: Rotation, file_prefix: &str) {
737
        let directory = tempfile::tempdir().expect("failed to create tempdir");
738
        let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
739
740
        let expected_value = "Hello";
741
        write_to_log(&mut appender, expected_value);
742
        assert!(find_str_in_log(directory.path(), expected_value));
743
744
        directory
745
            .close()
746
            .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
747
    }
748
749
    #[test]
750
    fn write_minutely_log() {
751
        test_appender(Rotation::HOURLY, "minutely.log");
752
    }
753
754
    #[test]
755
    fn write_hourly_log() {
756
        test_appender(Rotation::HOURLY, "hourly.log");
757
    }
758
759
    #[test]
760
    fn write_daily_log() {
761
        test_appender(Rotation::DAILY, "daily.log");
762
    }
763
764
    #[test]
765
    fn write_never_log() {
766
        test_appender(Rotation::NEVER, "never.log");
767
    }
768
769
    #[test]
770
    fn test_rotations() {
771
        // per-minute basis
772
        let now = OffsetDateTime::now_utc();
773
        let next = Rotation::MINUTELY.next_date(&now).unwrap();
774
        assert_eq!((now + Duration::MINUTE).minute(), next.minute());
775
776
        // per-hour basis
777
        let now = OffsetDateTime::now_utc();
778
        let next = Rotation::HOURLY.next_date(&now).unwrap();
779
        assert_eq!((now + Duration::HOUR).hour(), next.hour());
780
781
        // daily-basis
782
        let now = OffsetDateTime::now_utc();
783
        let next = Rotation::DAILY.next_date(&now).unwrap();
784
        assert_eq!((now + Duration::DAY).day(), next.day());
785
786
        // never
787
        let now = OffsetDateTime::now_utc();
788
        let next = Rotation::NEVER.next_date(&now);
789
        assert!(next.is_none());
790
    }
791
792
    #[test]
793
    #[should_panic(
794
        expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
795
    )]
796
    fn test_never_date_rounding() {
797
        let now = OffsetDateTime::now_utc();
798
        let _ = Rotation::NEVER.round_date(&now);
799
    }
800
801
    #[test]
802
    fn test_path_concatenation() {
803
        let format = format_description::parse(
804
            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
805
         sign:mandatory]:[offset_minute]:[offset_second]",
806
        )
807
        .unwrap();
808
        let directory = tempfile::tempdir().expect("failed to create tempdir");
809
810
        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
811
812
        struct TestCase {
813
            expected: &'static str,
814
            rotation: Rotation,
815
            prefix: Option<&'static str>,
816
            suffix: Option<&'static str>,
817
        }
818
819
        let test = |TestCase {
820
                        expected,
821
                        rotation,
822
                        prefix,
823
                        suffix,
824
                    }| {
825
            let (inner, _) = Inner::new(
826
                now,
827
                rotation.clone(),
828
                directory.path(),
829
                prefix.map(ToString::to_string),
830
                suffix.map(ToString::to_string),
831
                None,
832
            )
833
            .unwrap();
834
            let path = inner.join_date(&now);
835
            assert_eq!(
836
                expected, path,
837
                "rotation = {:?}, prefix = {:?}, suffix = {:?}",
838
                rotation, prefix, suffix
839
            );
840
        };
841
842
        let test_cases = vec![
843
            // prefix only
844
            TestCase {
845
                expected: "app.log.2020-02-01-10-01",
846
                rotation: Rotation::MINUTELY,
847
                prefix: Some("app.log"),
848
                suffix: None,
849
            },
850
            TestCase {
851
                expected: "app.log.2020-02-01-10",
852
                rotation: Rotation::HOURLY,
853
                prefix: Some("app.log"),
854
                suffix: None,
855
            },
856
            TestCase {
857
                expected: "app.log.2020-02-01",
858
                rotation: Rotation::DAILY,
859
                prefix: Some("app.log"),
860
                suffix: None,
861
            },
862
            TestCase {
863
                expected: "app.log",
864
                rotation: Rotation::NEVER,
865
                prefix: Some("app.log"),
866
                suffix: None,
867
            },
868
            // prefix and suffix
869
            TestCase {
870
                expected: "app.2020-02-01-10-01.log",
871
                rotation: Rotation::MINUTELY,
872
                prefix: Some("app"),
873
                suffix: Some("log"),
874
            },
875
            TestCase {
876
                expected: "app.2020-02-01-10.log",
877
                rotation: Rotation::HOURLY,
878
                prefix: Some("app"),
879
                suffix: Some("log"),
880
            },
881
            TestCase {
882
                expected: "app.2020-02-01.log",
883
                rotation: Rotation::DAILY,
884
                prefix: Some("app"),
885
                suffix: Some("log"),
886
            },
887
            TestCase {
888
                expected: "app.log",
889
                rotation: Rotation::NEVER,
890
                prefix: Some("app"),
891
                suffix: Some("log"),
892
            },
893
            // suffix only
894
            TestCase {
895
                expected: "2020-02-01-10-01.log",
896
                rotation: Rotation::MINUTELY,
897
                prefix: None,
898
                suffix: Some("log"),
899
            },
900
            TestCase {
901
                expected: "2020-02-01-10.log",
902
                rotation: Rotation::HOURLY,
903
                prefix: None,
904
                suffix: Some("log"),
905
            },
906
            TestCase {
907
                expected: "2020-02-01.log",
908
                rotation: Rotation::DAILY,
909
                prefix: None,
910
                suffix: Some("log"),
911
            },
912
            TestCase {
913
                expected: "log",
914
                rotation: Rotation::NEVER,
915
                prefix: None,
916
                suffix: Some("log"),
917
            },
918
        ];
919
        for test_case in test_cases {
920
            test(test_case)
921
        }
922
    }
923
924
    #[test]
925
    fn test_make_writer() {
926
        use std::sync::{Arc, Mutex};
927
        use tracing_subscriber::prelude::*;
928
929
        let format = format_description::parse(
930
            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
931
         sign:mandatory]:[offset_minute]:[offset_second]",
932
        )
933
        .unwrap();
934
935
        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
936
        let directory = tempfile::tempdir().expect("failed to create tempdir");
937
        let (state, writer) = Inner::new(
938
            now,
939
            Rotation::HOURLY,
940
            directory.path(),
941
            Some("test_make_writer".to_string()),
942
            None,
943
            None,
944
        )
945
        .unwrap();
946
947
        let clock = Arc::new(Mutex::new(now));
948
        let now = {
949
            let clock = clock.clone();
950
            Box::new(move || *clock.lock().unwrap())
951
        };
952
        let appender = RollingFileAppender { state, writer, now };
953
        let default = tracing_subscriber::fmt()
954
            .without_time()
955
            .with_level(false)
956
            .with_target(false)
957
            .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
958
            .with_writer(appender)
959
            .finish()
960
            .set_default();
961
962
        tracing::info!("file 1");
963
964
        // advance time by one second
965
        (*clock.lock().unwrap()) += Duration::seconds(1);
966
967
        tracing::info!("file 1");
968
969
        // advance time by one hour
970
        (*clock.lock().unwrap()) += Duration::hours(1);
971
972
        tracing::info!("file 2");
973
974
        // advance time by one second
975
        (*clock.lock().unwrap()) += Duration::seconds(1);
976
977
        tracing::info!("file 2");
978
979
        drop(default);
980
981
        let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
982
        println!("dir={:?}", dir_contents);
983
        for entry in dir_contents {
984
            println!("entry={:?}", entry);
985
            let path = entry.expect("Expected dir entry").path();
986
            let file = fs::read_to_string(&path).expect("Failed to read file");
987
            println!("path={}\nfile={:?}", path.display(), file);
988
989
            match path
990
                .extension()
991
                .expect("found a file without a date!")
992
                .to_str()
993
                .expect("extension should be UTF8")
994
            {
995
                "2020-02-01-10" => {
996
                    assert_eq!("file 1\nfile 1\n", file);
997
                }
998
                "2020-02-01-11" => {
999
                    assert_eq!("file 2\nfile 2\n", file);
1000
                }
1001
                x => panic!("unexpected date {}", x),
1002
            }
1003
        }
1004
    }
1005
1006
    #[test]
1007
    fn test_max_log_files() {
1008
        use std::sync::{Arc, Mutex};
1009
        use tracing_subscriber::prelude::*;
1010
1011
        let format = format_description::parse(
1012
            "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1013
         sign:mandatory]:[offset_minute]:[offset_second]",
1014
        )
1015
        .unwrap();
1016
1017
        let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1018
        let directory = tempfile::tempdir().expect("failed to create tempdir");
1019
        let (state, writer) = Inner::new(
1020
            now,
1021
            Rotation::HOURLY,
1022
            directory.path(),
1023
            Some("test_max_log_files".to_string()),
1024
            None,
1025
            Some(2),
1026
        )
1027
        .unwrap();
1028
1029
        let clock = Arc::new(Mutex::new(now));
1030
        let now = {
1031
            let clock = clock.clone();
1032
            Box::new(move || *clock.lock().unwrap())
1033
        };
1034
        let appender = RollingFileAppender { state, writer, now };
1035
        let default = tracing_subscriber::fmt()
1036
            .without_time()
1037
            .with_level(false)
1038
            .with_target(false)
1039
            .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1040
            .with_writer(appender)
1041
            .finish()
1042
            .set_default();
1043
1044
        tracing::info!("file 1");
1045
1046
        // advance time by one second
1047
        (*clock.lock().unwrap()) += Duration::seconds(1);
1048
1049
        tracing::info!("file 1");
1050
1051
        // advance time by one hour
1052
        (*clock.lock().unwrap()) += Duration::hours(1);
1053
1054
        // depending on the filesystem, the creation timestamp's resolution may
1055
        // be as coarse as one second, so we need to wait a bit here to ensure
1056
        // that the next file actually is newer than the old one.
1057
        std::thread::sleep(std::time::Duration::from_secs(1));
1058
1059
        tracing::info!("file 2");
1060
1061
        // advance time by one second
1062
        (*clock.lock().unwrap()) += Duration::seconds(1);
1063
1064
        tracing::info!("file 2");
1065
1066
        // advance time by one hour
1067
        (*clock.lock().unwrap()) += Duration::hours(1);
1068
1069
        // again, sleep to ensure that the creation timestamps actually differ.
1070
        std::thread::sleep(std::time::Duration::from_secs(1));
1071
1072
        tracing::info!("file 3");
1073
1074
        // advance time by one second
1075
        (*clock.lock().unwrap()) += Duration::seconds(1);
1076
1077
        tracing::info!("file 3");
1078
1079
        drop(default);
1080
1081
        let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1082
        println!("dir={:?}", dir_contents);
1083
1084
        for entry in dir_contents {
1085
            println!("entry={:?}", entry);
1086
            let path = entry.expect("Expected dir entry").path();
1087
            let file = fs::read_to_string(&path).expect("Failed to read file");
1088
            println!("path={}\nfile={:?}", path.display(), file);
1089
1090
            match path
1091
                .extension()
1092
                .expect("found a file without a date!")
1093
                .to_str()
1094
                .expect("extension should be UTF8")
1095
            {
1096
                "2020-02-01-10" => {
1097
                    panic!("this file should have been pruned already!");
1098
                }
1099
                "2020-02-01-11" => {
1100
                    assert_eq!("file 2\nfile 2\n", file);
1101
                }
1102
                "2020-02-01-12" => {
1103
                    assert_eq!("file 3\nfile 3\n", file);
1104
                }
1105
                x => panic!("unexpected date {}", x),
1106
            }
1107
        }
1108
    }
1109
}