spdlog/sink/
rotating_file_sink.rs

1//! Provides a rotating file sink.
2
3use std::{
4    collections::LinkedList,
5    convert::Infallible,
6    ffi::OsString,
7    fs::{self, File},
8    hash::Hash,
9    io::{BufWriter, Write},
10    path::{Path, PathBuf},
11    result::Result as StdResult,
12    time::{Duration, SystemTime},
13};
14
15use chrono::prelude::*;
16
17use crate::{
18    error::InvalidArgumentError,
19    formatter::FormatterContext,
20    sink::{helper, Sink},
21    sync::*,
22    utils, Error, Record, Result, StringBuf,
23};
24
25/// Rotation policies for [`RotatingFileSink`].
26///
27/// Rotation policy defines when and how to split logs into multiple files,
28/// during which new log files may be created and old log files may be deleted.
29///
30/// # Error
31///
32/// Note that some parameters have range requirements, functions that receive it
33/// will return an error if the requirements are not met.
34///
35/// # Examples
36///
37/// ```
38/// use spdlog::sink::RotationPolicy;
39///
40/// // Rotating every 10 MB file.
41/// RotationPolicy::FileSize(1024 * 1024 * 10);
42///
43/// // Rotating every day at 15:30.
44/// RotationPolicy::Daily { hour: 15, minute: 30 };
45///
46/// // Rotating every hour.
47/// RotationPolicy::Hourly;
48///
49/// // Rotating every 6 hour.
50/// # use std::time::Duration;
51/// RotationPolicy::Period(Duration::from_secs(6 * 60 * 60));
52/// ```
53#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
54pub enum RotationPolicy {
55    /// Rotating to a new log file when the size of the current log file exceeds
56    /// the given limit.
57    FileSize(
58        /// Maximum file size (in bytes). Range: (0, u64::MAX].
59        u64,
60    ),
61    /// Rotating to a new log file at a specified time point within a day.
62    Daily {
63        /// Hour of the time point. Range: [0, 23].
64        hour: u32,
65        /// Minute of the time point. Range: [0, 59].
66        minute: u32,
67    },
68    /// Rotating to a new log file at minute 0 of each hour.
69    Hourly,
70    /// Rotating to a new log file after given period (greater then 1 minute) is
71    /// passed.
72    Period(
73        /// Period to the next rotation. Range: [1 minute, Duration::MAX].
74        Duration,
75    ),
76}
77
78const SECONDS_PER_MINUTE: u64 = 60;
79const SECONDS_PER_HOUR: u64 = 60 * SECONDS_PER_MINUTE;
80const SECONDS_PER_DAY: u64 = 24 * SECONDS_PER_HOUR;
81const MINUTE_1: Duration = Duration::from_secs(SECONDS_PER_MINUTE);
82const HOUR_1: Duration = Duration::from_secs(SECONDS_PER_HOUR);
83const DAY_1: Duration = Duration::from_secs(SECONDS_PER_DAY);
84
85trait Rotator {
86    #[allow(clippy::ptr_arg)]
87    fn log(&self, record: &Record, string_buf: &StringBuf) -> Result<()>;
88    fn flush(&self) -> Result<()>;
89    fn drop_flush(&mut self) -> Result<()> {
90        self.flush()
91    }
92}
93
94enum RotatorKind {
95    FileSize(RotatorFileSize),
96    TimePoint(RotatorTimePoint),
97}
98
99struct RotatorFileSize {
100    base_path: PathBuf,
101    max_size: u64,
102    max_files: usize,
103    inner: SpinMutex<RotatorFileSizeInner>,
104}
105
106struct RotatorFileSizeInner {
107    file: Option<BufWriter<File>>,
108    current_size: u64,
109}
110
111struct RotatorTimePoint {
112    base_path: PathBuf,
113    time_point: TimePoint,
114    max_files: usize,
115    inner: SpinMutex<RotatorTimePointInner>,
116}
117
118#[derive(Copy, Clone)]
119enum TimePoint {
120    Daily { hour: u32, minute: u32 },
121    Hourly,
122    Period(Duration),
123}
124
125struct RotatorTimePointInner {
126    file: BufWriter<File>,
127    rotation_time_point: SystemTime,
128    file_paths: Option<LinkedList<PathBuf>>,
129}
130
131/// A sink with a file as the target, split files according to the rotation
132/// policy.
133///
134/// A service program running for a long time may continuously write logs to a
135/// single file, which makes the logs hard to view and manage.
136/// `RotatingFileSink` is designed for this usage scenario. It automatically
137/// splits logs into one or more files and can be configured to automatically
138/// delete old files to save disk space. The operation of splitting logs into
139/// multiple files and optionally deleting old files is called **rotation**. The
140/// **rotation policy** determines when and how log files are created or
141/// deleted.
142///
143/// # Examples
144///
145/// See [./examples] directory.
146///
147/// [./examples]: https://github.com/SpriteOvO/spdlog-rs/tree/main/spdlog/examples
148pub struct RotatingFileSink {
149    common_impl: helper::CommonImpl,
150    rotator: RotatorKind,
151}
152
153/// #
154#[doc = include_str!("../include/doc/generic-builder-note.md")]
155pub struct RotatingFileSinkBuilder<ArgBP, ArgRP> {
156    common_builder_impl: helper::CommonBuilderImpl,
157    base_path: ArgBP,
158    rotation_policy: ArgRP,
159    max_files: usize,
160    rotate_on_open: bool,
161}
162
163impl RotatingFileSink {
164    /// Gets a builder of `RotatingFileSink` with default parameters:
165    ///
166    /// | Parameter         | Default Value           |
167    /// |-------------------|-------------------------|
168    /// | [level_filter]    | `All`                   |
169    /// | [formatter]       | `FullFormatter`         |
170    /// | [error_handler]   | [default error handler] |
171    /// |                   |                         |
172    /// | [base_path]       | *must be specified*     |
173    /// | [rotation_policy] | *must be specified*     |
174    /// | [max_files]       | `0`                     |
175    /// | [rotate_on_open]  | `false`                 |
176    ///
177    /// [level_filter]: RotatingFileSinkBuilder::level_filter
178    /// [formatter]: RotatingFileSinkBuilder::formatter
179    /// [error_handler]: RotatingFileSinkBuilder::error_handler
180    /// [default error handler]: error/index.html#default-error-handler
181    /// [base_path]: RotatingFileSinkBuilder::base_path
182    /// [rotation_policy]: RotatingFileSinkBuilder::rotation_policy
183    /// [max_files]: RotatingFileSinkBuilder::max_files
184    /// [rotate_on_open]: RotatingFileSinkBuilder::rotate_on_open
185    #[must_use]
186    pub fn builder() -> RotatingFileSinkBuilder<(), ()> {
187        RotatingFileSinkBuilder {
188            common_builder_impl: helper::CommonBuilderImpl::new(),
189            base_path: (),
190            rotation_policy: (),
191            max_files: 0,
192            rotate_on_open: false,
193        }
194    }
195
196    /// Constructs a `RotatingFileSink`.
197    ///
198    /// The parameter `max_files` specifies the maximum number of files. If the
199    /// number of existing files reaches this parameter, the oldest file will be
200    /// deleted on the next rotation. Pass `0` for no limit.
201    ///
202    /// The parameter `rotate_on_open` specifies whether to rotate files once
203    /// when constructing `RotatingFileSink`. For the [`RotationPolicy::Daily`],
204    /// [`RotationPolicy::Hourly`], and [`RotationPolicy::Period`] rotation
205    /// policies, it may truncate the contents of the existing file if the
206    /// parameter is `true`, since the file name is a time point and not an
207    /// index.
208    ///
209    /// # Error
210    ///
211    /// If an error occurs opening the file, [`Error::CreateDirectory`] or
212    /// [`Error::OpenFile`] will be returned.
213    ///
214    /// # Panics
215    ///
216    /// Panics if the parameter `rotation_policy` is invalid. See the
217    /// documentation of [`RotationPolicy`] for requirements.
218    #[deprecated(
219        since = "0.3.0",
220        note = "it may be removed in the future, use `RotatingFileSink::builder()` instead"
221    )]
222    pub fn new<P>(
223        base_path: P,
224        rotation_policy: RotationPolicy,
225        max_files: usize,
226        rotate_on_open: bool,
227    ) -> Result<Self>
228    where
229        P: Into<PathBuf>,
230    {
231        Self::builder()
232            .base_path(base_path)
233            .rotation_policy(rotation_policy)
234            .max_files(max_files)
235            .rotate_on_open(rotate_on_open)
236            .build()
237    }
238
239    #[cfg(test)]
240    #[must_use]
241    fn _current_size(&self) -> u64 {
242        if let RotatorKind::FileSize(rotator) = &self.rotator {
243            rotator.inner.lock().current_size
244        } else {
245            panic!();
246        }
247    }
248}
249
250impl Sink for RotatingFileSink {
251    fn log(&self, record: &Record) -> Result<()> {
252        let mut string_buf = StringBuf::new();
253        let mut ctx = FormatterContext::new();
254        self.common_impl
255            .formatter
256            .read()
257            .format(record, &mut string_buf, &mut ctx)?;
258
259        self.rotator.log(record, &string_buf)
260    }
261
262    fn flush(&self) -> Result<()> {
263        self.rotator.flush()
264    }
265
266    helper::common_impl!(@Sink: common_impl);
267}
268
269impl Drop for RotatingFileSink {
270    fn drop(&mut self) {
271        if let Err(err) = self.rotator.drop_flush() {
272            self.common_impl
273                .non_returnable_error("RotatingFileSink", err)
274        }
275    }
276}
277
278impl RotationPolicy {
279    fn validate(&self) -> StdResult<(), String> {
280        match self {
281            Self::FileSize(max_size) => {
282                if *max_size == 0 {
283                    return Err(format!(
284                        "policy 'file size' expect `max_size` to be (0, u64::MAX] but got {}",
285                        *max_size
286                    ));
287                }
288            }
289            Self::Daily { hour, minute } => {
290                if *hour > 23 || *minute > 59 {
291                    return Err(format!(
292                        "policy 'daily' expect `(hour, minute)` to be ([0, 23], [0, 59]) but got ({}, {})",
293                        *hour, *minute
294                    ));
295                }
296            }
297            Self::Hourly => {}
298            Self::Period(duration) => {
299                if *duration < MINUTE_1 {
300                    return Err(format!(
301                        "policy 'period' expect duration greater then 1 minute but got {:?}",
302                        *duration
303                    ));
304                }
305            }
306        }
307        Ok(())
308    }
309}
310
311impl Rotator for RotatorKind {
312    fn log(&self, record: &Record, string_buf: &StringBuf) -> Result<()> {
313        match self {
314            Self::FileSize(rotator) => rotator.log(record, string_buf),
315            Self::TimePoint(rotator) => rotator.log(record, string_buf),
316        }
317    }
318
319    fn flush(&self) -> Result<()> {
320        match self {
321            Self::FileSize(rotator) => rotator.flush(),
322            Self::TimePoint(rotator) => rotator.flush(),
323        }
324    }
325
326    fn drop_flush(&mut self) -> Result<()> {
327        match self {
328            Self::FileSize(rotator) => rotator.drop_flush(),
329            Self::TimePoint(rotator) => rotator.drop_flush(),
330        }
331    }
332}
333
334impl RotatorFileSize {
335    fn new(
336        base_path: PathBuf,
337        max_size: u64,
338        max_files: usize,
339        rotate_on_open: bool,
340    ) -> Result<Self> {
341        let file = utils::open_file(&base_path, false)?;
342        let current_size = file.metadata().map_err(Error::QueryFileMetadata)?.len();
343
344        let res = Self {
345            base_path,
346            max_size,
347            max_files,
348            inner: SpinMutex::new(RotatorFileSizeInner::new(file, current_size)),
349        };
350
351        if rotate_on_open && current_size > 0 {
352            res.rotate(&mut res.inner.lock())?;
353            res.inner.lock().current_size = 0;
354        }
355
356        Ok(res)
357    }
358
359    fn reopen(&self) -> Result<File> {
360        // always truncate
361        utils::open_file(&self.base_path, true)
362    }
363
364    fn rotate(&self, opened_file: &mut SpinMutexGuard<RotatorFileSizeInner>) -> Result<()> {
365        let inner = || {
366            for i in (1..self.max_files).rev() {
367                let src = Self::calc_file_path(&self.base_path, i - 1);
368                if !src.exists() {
369                    continue;
370                }
371
372                let dst = Self::calc_file_path(&self.base_path, i);
373                if dst.exists() {
374                    fs::remove_file(&dst).map_err(Error::RemoveFile)?;
375                }
376
377                fs::rename(src, dst).map_err(Error::RenameFile)?;
378            }
379            Ok(())
380        };
381
382        opened_file.file = None;
383
384        let res = inner();
385        if res.is_err() {
386            opened_file.current_size = 0;
387        }
388
389        opened_file.file = Some(BufWriter::new(self.reopen()?));
390
391        res
392    }
393
394    #[must_use]
395    fn calc_file_path(base_path: impl AsRef<Path>, index: usize) -> PathBuf {
396        let base_path = base_path.as_ref();
397
398        if index == 0 {
399            return base_path.to_owned();
400        }
401
402        let mut file_name = base_path
403            .file_stem()
404            .map(|s| s.to_owned())
405            .unwrap_or_else(|| OsString::from(""));
406
407        let externsion = base_path.extension();
408
409        // append index
410        file_name.push(format!("_{}", index));
411
412        let mut path = base_path.to_owned();
413        path.set_file_name(file_name);
414        if let Some(externsion) = externsion {
415            path.set_extension(externsion);
416        }
417
418        path
419    }
420
421    // if `self.inner.file` is `None`, try to reopen the file.
422    fn lock_inner(&self) -> Result<SpinMutexGuard<RotatorFileSizeInner>> {
423        let mut inner = self.inner.lock();
424        if inner.file.is_none() {
425            inner.file = Some(BufWriter::new(self.reopen()?));
426        }
427        Ok(inner)
428    }
429}
430
431impl Rotator for RotatorFileSize {
432    fn log(&self, _record: &Record, string_buf: &StringBuf) -> Result<()> {
433        let mut inner = self.lock_inner()?;
434
435        inner.current_size += string_buf.len() as u64;
436        if inner.current_size > self.max_size {
437            self.rotate(&mut inner)?;
438            inner.current_size = string_buf.len() as u64;
439        }
440
441        inner
442            .file
443            .as_mut()
444            .unwrap()
445            .write_all(string_buf.as_bytes())
446            .map_err(Error::WriteRecord)
447    }
448
449    fn flush(&self) -> Result<()> {
450        self.lock_inner()?
451            .file
452            .as_mut()
453            .unwrap()
454            .flush()
455            .map_err(Error::FlushBuffer)
456    }
457
458    fn drop_flush(&mut self) -> Result<()> {
459        let mut inner = self.inner.lock();
460        if let Some(file) = inner.file.as_mut() {
461            file.flush().map_err(Error::FlushBuffer)
462        } else {
463            Ok(())
464        }
465    }
466}
467
468impl RotatorFileSizeInner {
469    #[must_use]
470    fn new(file: File, current_size: u64) -> Self {
471        Self {
472            file: Some(BufWriter::new(file)),
473            current_size,
474        }
475    }
476}
477
478impl RotatorTimePoint {
479    fn new(
480        override_now: Option<SystemTime>,
481        base_path: PathBuf,
482        time_point: TimePoint,
483        max_files: usize,
484        truncate: bool,
485    ) -> Result<Self> {
486        let now = override_now.unwrap_or_else(SystemTime::now);
487        let file_path = Self::calc_file_path(base_path.as_path(), time_point, now);
488        let file = utils::open_file(file_path, truncate)?;
489
490        let inner = RotatorTimePointInner {
491            file: BufWriter::new(file),
492            rotation_time_point: Self::next_rotation_time_point(time_point, now),
493            file_paths: None,
494        };
495
496        let mut res = Self {
497            base_path,
498            time_point,
499            max_files,
500            inner: SpinMutex::new(inner),
501        };
502
503        res.init_previous_file_paths(max_files, now);
504
505        Ok(res)
506    }
507
508    fn init_previous_file_paths(&mut self, max_files: usize, mut now: SystemTime) {
509        if max_files > 0 {
510            let mut file_paths = LinkedList::new();
511
512            for _ in 0..max_files {
513                let file_path = Self::calc_file_path(&self.base_path, self.time_point, now);
514
515                if !file_path.exists() {
516                    break;
517                }
518
519                file_paths.push_front(file_path);
520                now = now.checked_sub(self.time_point.delta_std()).unwrap()
521            }
522
523            self.inner.get_mut().file_paths = Some(file_paths);
524        }
525    }
526
527    // a little expensive, should only be called when rotation is needed or in
528    // constructor.
529    #[must_use]
530    fn next_rotation_time_point(time_point: TimePoint, now: SystemTime) -> SystemTime {
531        let now: DateTime<Local> = now.into();
532        let mut rotation_time = now;
533
534        match time_point {
535            TimePoint::Daily { hour, minute } => {
536                rotation_time = rotation_time
537                    .with_hour(hour)
538                    .unwrap()
539                    .with_minute(minute)
540                    .unwrap()
541                    .with_second(0)
542                    .unwrap()
543                    .with_nanosecond(0)
544                    .unwrap()
545            }
546            TimePoint::Hourly => {
547                rotation_time = rotation_time
548                    .with_minute(0)
549                    .unwrap()
550                    .with_second(0)
551                    .unwrap()
552                    .with_nanosecond(0)
553                    .unwrap()
554            }
555            TimePoint::Period { .. } => {}
556        };
557
558        if rotation_time <= now {
559            rotation_time = rotation_time
560                .checked_add_signed(time_point.delta_chrono())
561                .unwrap();
562        }
563        rotation_time.into()
564    }
565
566    fn push_new_remove_old(
567        &self,
568        new: PathBuf,
569        inner: &mut SpinMutexGuard<RotatorTimePointInner>,
570    ) -> Result<()> {
571        let file_paths = inner.file_paths.as_mut().unwrap();
572
573        while file_paths.len() >= self.max_files {
574            let old = file_paths.pop_front().unwrap();
575            if old.exists() {
576                fs::remove_file(old).map_err(Error::RemoveFile)?;
577            }
578        }
579        file_paths.push_back(new);
580
581        Ok(())
582    }
583
584    #[must_use]
585    fn calc_file_path(
586        base_path: impl AsRef<Path>,
587        time_point: TimePoint,
588        system_time: SystemTime,
589    ) -> PathBuf {
590        let base_path = base_path.as_ref();
591        let local_time: DateTime<Local> = system_time.into();
592
593        let mut file_name = base_path
594            .file_stem()
595            .map(|s| s.to_owned())
596            .unwrap_or_else(|| OsString::from(""));
597
598        let externsion = base_path.extension();
599
600        match time_point {
601            TimePoint::Daily { .. } => {
602                // append y-m-d
603                file_name.push(format!(
604                    "_{}-{:02}-{:02}",
605                    local_time.year(),
606                    local_time.month(),
607                    local_time.day()
608                ));
609            }
610            TimePoint::Hourly => {
611                // append y-m-d_h
612                file_name.push(format!(
613                    "_{}-{:02}-{:02}_{:02}",
614                    local_time.year(),
615                    local_time.month(),
616                    local_time.day(),
617                    local_time.hour()
618                ));
619            }
620            TimePoint::Period { .. } => {
621                // append y-m-d_h-m
622                file_name.push(format!(
623                    "_{}-{:02}-{:02}_{:02}-{:02}",
624                    local_time.year(),
625                    local_time.month(),
626                    local_time.day(),
627                    local_time.hour(),
628                    local_time.minute()
629                ));
630            }
631        }
632
633        let mut path = base_path.to_owned();
634        path.set_file_name(file_name);
635        if let Some(externsion) = externsion {
636            path.set_extension(externsion);
637        }
638
639        path
640    }
641}
642
643impl Rotator for RotatorTimePoint {
644    fn log(&self, record: &Record, string_buf: &StringBuf) -> Result<()> {
645        let mut inner = self.inner.lock();
646
647        let mut file_path = None;
648        let record_time = record.time();
649        let should_rotate = record_time >= inner.rotation_time_point;
650
651        if should_rotate {
652            file_path = Some(Self::calc_file_path(
653                &self.base_path,
654                self.time_point,
655                record_time,
656            ));
657            inner.file = BufWriter::new(utils::open_file(file_path.as_ref().unwrap(), true)?);
658            inner.rotation_time_point =
659                Self::next_rotation_time_point(self.time_point, record_time);
660        }
661
662        inner
663            .file
664            .write_all(string_buf.as_bytes())
665            .map_err(Error::WriteRecord)?;
666
667        if should_rotate && inner.file_paths.is_some() {
668            self.push_new_remove_old(file_path.unwrap(), &mut inner)?;
669        }
670
671        Ok(())
672    }
673
674    fn flush(&self) -> Result<()> {
675        self.inner.lock().file.flush().map_err(Error::FlushBuffer)
676    }
677}
678
679impl TimePoint {
680    #[must_use]
681    fn delta_std(&self) -> Duration {
682        match self {
683            Self::Daily { .. } => DAY_1,
684            Self::Hourly { .. } => HOUR_1,
685            Self::Period(duration) => *duration,
686        }
687    }
688
689    #[must_use]
690    fn delta_chrono(&self) -> chrono::Duration {
691        match self {
692            Self::Daily { .. } => chrono::Duration::days(1),
693            Self::Hourly { .. } => chrono::Duration::hours(1),
694            Self::Period(duration) => chrono::Duration::from_std(*duration).unwrap(),
695        }
696    }
697}
698
699impl<ArgBP, ArgRP> RotatingFileSinkBuilder<ArgBP, ArgRP> {
700    /// Specifies the base path of the log file.
701    ///
702    /// The path needs to be suffixed with an extension, if you expect the
703    /// rotated eventual file names to contain the extension.
704    ///
705    /// If there is an extension, the different rotation policies will insert
706    /// relevant information in the front of the extension. If there is not
707    /// an extension, it will be appended to the end.
708    ///
709    /// Supposes the given base path is `/path/to/base_file.log`, the eventual
710    /// file names may look like the following:
711    ///
712    /// - `/path/to/base_file_1.log`
713    /// - `/path/to/base_file_2.log`
714    /// - `/path/to/base_file_2022-03-23.log`
715    /// - `/path/to/base_file_2022-03-24.log`
716    /// - `/path/to/base_file_2022-03-23_03.log`
717    /// - `/path/to/base_file_2022-03-23_04.log`
718    ///
719    /// This parameter is **required**.
720    #[must_use]
721    pub fn base_path<P>(self, base_path: P) -> RotatingFileSinkBuilder<PathBuf, ArgRP>
722    where
723        P: Into<PathBuf>,
724    {
725        RotatingFileSinkBuilder {
726            common_builder_impl: self.common_builder_impl,
727            base_path: base_path.into(),
728            rotation_policy: self.rotation_policy,
729            max_files: self.max_files,
730            rotate_on_open: self.rotate_on_open,
731        }
732    }
733
734    /// Specifies the rotation policy.
735    ///
736    /// This parameter is **required**.
737    #[must_use]
738    pub fn rotation_policy(
739        self,
740        rotation_policy: RotationPolicy,
741    ) -> RotatingFileSinkBuilder<ArgBP, RotationPolicy> {
742        RotatingFileSinkBuilder {
743            common_builder_impl: self.common_builder_impl,
744            base_path: self.base_path,
745            rotation_policy,
746            max_files: self.max_files,
747            rotate_on_open: self.rotate_on_open,
748        }
749    }
750
751    /// Specifies the maximum number of files.
752    ///
753    /// If the number of existing files reaches this parameter, the oldest file
754    /// will be deleted on the next rotation.
755    ///
756    /// Specify `0` for no limit.
757    ///
758    /// This parameter is **optional**.
759    #[must_use]
760    pub fn max_files(mut self, max_files: usize) -> Self {
761        self.max_files = max_files;
762        self
763    }
764
765    /// Specifies whether to rotate files once when constructing
766    /// `RotatingFileSink`.
767    ///
768    /// For the [`RotationPolicy::Daily`], [`RotationPolicy::Hourly`], and
769    /// [`RotationPolicy::Period`] rotation policies, it may truncate the
770    /// contents of the existing file if the parameter is `true`, since the
771    /// file name is a time point and not an index.
772    ///
773    /// This parameter is **optional**.
774    #[must_use]
775    pub fn rotate_on_open(mut self, rotate_on_open: bool) -> Self {
776        self.rotate_on_open = rotate_on_open;
777        self
778    }
779
780    helper::common_impl!(@SinkBuilder: common_builder_impl);
781}
782
783impl<ArgRP> RotatingFileSinkBuilder<(), ArgRP> {
784    #[doc(hidden)]
785    #[deprecated(note = "\n\n\
786        builder compile-time error:\n\
787        - missing required parameter `base_path`\n\n\
788    ")]
789    pub fn build(self, _: Infallible) {}
790}
791
792impl RotatingFileSinkBuilder<PathBuf, ()> {
793    #[doc(hidden)]
794    #[deprecated(note = "\n\n\
795        builder compile-time error:\n\
796        - missing required parameter `rotation_policy`\n\n\
797    ")]
798    pub fn build(self, _: Infallible) {}
799}
800
801impl RotatingFileSinkBuilder<PathBuf, RotationPolicy> {
802    /// Builds a [`RotatingFileSink`].
803    ///
804    /// # Error
805    ///
806    /// If the argument `rotation_policy` is invalid, or an error occurs opening
807    /// the file, [`Error::CreateDirectory`] or [`Error::OpenFile`] will be
808    /// returned.
809    pub fn build(self) -> Result<RotatingFileSink> {
810        self.build_with_initial_time(None)
811    }
812
813    fn build_with_initial_time(self, override_now: Option<SystemTime>) -> Result<RotatingFileSink> {
814        self.rotation_policy
815            .validate()
816            .map_err(|err| Error::InvalidArgument(InvalidArgumentError::RotationPolicy(err)))?;
817
818        let rotator = match self.rotation_policy {
819            RotationPolicy::FileSize(max_size) => RotatorKind::FileSize(RotatorFileSize::new(
820                self.base_path,
821                max_size,
822                self.max_files,
823                self.rotate_on_open,
824            )?),
825            RotationPolicy::Daily { hour, minute } => {
826                RotatorKind::TimePoint(RotatorTimePoint::new(
827                    override_now,
828                    self.base_path,
829                    TimePoint::Daily { hour, minute },
830                    self.max_files,
831                    self.rotate_on_open,
832                )?)
833            }
834            RotationPolicy::Hourly => RotatorKind::TimePoint(RotatorTimePoint::new(
835                override_now,
836                self.base_path,
837                TimePoint::Hourly,
838                self.max_files,
839                self.rotate_on_open,
840            )?),
841            RotationPolicy::Period(duration) => RotatorKind::TimePoint(RotatorTimePoint::new(
842                override_now,
843                self.base_path,
844                TimePoint::Period(duration),
845                self.max_files,
846                self.rotate_on_open,
847            )?),
848        };
849
850        let res = RotatingFileSink {
851            common_impl: helper::CommonImpl::from_builder(self.common_builder_impl),
852            rotator,
853        };
854
855        Ok(res)
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862    use crate::{prelude::*, test_utils::*, Level, Record};
863
864    static BASE_LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
865        let path = TEST_LOGS_PATH.join("rotating_file_sink");
866        if !path.exists() {
867            _ = fs::create_dir(&path);
868        }
869        path
870    });
871
872    const SECOND_1: Duration = Duration::from_secs(1);
873
874    mod policy_file_size {
875        use super::*;
876
877        static LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
878            let path = BASE_LOGS_PATH.join("policy_file_size");
879            fs::create_dir_all(&path).unwrap();
880            path
881        });
882
883        #[test]
884        fn calc_file_path() {
885            let calc = |base_path, index| {
886                RotatorFileSize::calc_file_path(base_path, index)
887                    .to_str()
888                    .unwrap()
889                    .to_string()
890            };
891
892            #[cfg(not(windows))]
893            let run = || {
894                assert_eq!(calc("/tmp/test.log", 0), "/tmp/test.log");
895                assert_eq!(calc("/tmp/test", 0), "/tmp/test");
896
897                assert_eq!(calc("/tmp/test.log", 1), "/tmp/test_1.log");
898                assert_eq!(calc("/tmp/test", 1), "/tmp/test_1");
899
900                assert_eq!(calc("/tmp/test.log", 23), "/tmp/test_23.log");
901                assert_eq!(calc("/tmp/test", 23), "/tmp/test_23");
902            };
903
904            #[cfg(windows)]
905            let run = || {
906                assert_eq!(calc("D:\\tmp\\test.txt", 0), "D:\\tmp\\test.txt");
907                assert_eq!(calc("D:\\tmp\\test", 0), "D:\\tmp\\test");
908
909                assert_eq!(calc("D:\\tmp\\test.txt", 1), "D:\\tmp\\test_1.txt");
910                assert_eq!(calc("D:\\tmp\\test", 1), "D:\\tmp\\test_1");
911
912                assert_eq!(calc("D:\\tmp\\test.txt", 23), "D:\\tmp\\test_23.txt");
913                assert_eq!(calc("D:\\tmp\\test", 23), "D:\\tmp\\test_23");
914            };
915
916            run();
917        }
918
919        #[test]
920        fn rotate() {
921            let base_path = LOGS_PATH.join("test.log");
922
923            let build = |clean, rotate_on_open| {
924                if clean {
925                    fs::remove_dir_all(LOGS_PATH.as_path()).unwrap();
926                    if !LOGS_PATH.exists() {
927                        fs::create_dir(LOGS_PATH.as_path()).unwrap();
928                    }
929                }
930
931                let formatter = Box::new(NoModFormatter::new());
932                let sink = RotatingFileSink::builder()
933                    .base_path(LOGS_PATH.join(&base_path))
934                    .rotation_policy(RotationPolicy::FileSize(16))
935                    .max_files(3)
936                    .rotate_on_open(rotate_on_open)
937                    .build()
938                    .unwrap();
939                sink.set_formatter(formatter);
940                let sink = Arc::new(sink);
941                let logger = build_test_logger(|b| b.sink(sink.clone()));
942                logger.set_level_filter(LevelFilter::All);
943                (sink, logger)
944            };
945
946            let index_to_path =
947                |index| RotatorFileSize::calc_file_path(PathBuf::from(&base_path), index);
948
949            let file_exists = |index| index_to_path(index).exists();
950            let files_exists_4 = || {
951                (
952                    file_exists(0),
953                    file_exists(1),
954                    file_exists(2),
955                    file_exists(3),
956                )
957            };
958
959            let read_file = |index| fs::read_to_string(index_to_path(index)).ok();
960            let read_file_4 = || (read_file(0), read_file(1), read_file(2), read_file(3));
961
962            const STR_4: &str = "abcd";
963            const STR_5: &str = "abcde";
964
965            {
966                let (sink, logger) = build(true, false);
967
968                assert_eq!(files_exists_4(), (true, false, false, false));
969                assert_eq!(sink._current_size(), 0);
970
971                info!(logger: logger, "{}", STR_4);
972                assert_eq!(files_exists_4(), (true, false, false, false));
973                assert_eq!(sink._current_size(), 4);
974
975                info!(logger: logger, "{}", STR_4);
976                assert_eq!(files_exists_4(), (true, false, false, false));
977                assert_eq!(sink._current_size(), 8);
978
979                info!(logger: logger, "{}", STR_4);
980                assert_eq!(files_exists_4(), (true, false, false, false));
981                assert_eq!(sink._current_size(), 12);
982
983                info!(logger: logger, "{}", STR_4);
984                assert_eq!(files_exists_4(), (true, false, false, false));
985                assert_eq!(sink._current_size(), 16);
986
987                info!(logger: logger, "{}", STR_4);
988                assert_eq!(files_exists_4(), (true, true, false, false));
989                assert_eq!(sink._current_size(), 4);
990            }
991            assert_eq!(
992                read_file_4(),
993                (
994                    Some("abcd".to_string()),
995                    Some("abcdabcdabcdabcd".to_string()),
996                    None,
997                    None
998                )
999            );
1000
1001            {
1002                let (sink, logger) = build(true, false);
1003
1004                assert_eq!(files_exists_4(), (true, false, false, false));
1005                assert_eq!(sink._current_size(), 0);
1006
1007                info!(logger: logger, "{}", STR_4);
1008                info!(logger: logger, "{}", STR_4);
1009                info!(logger: logger, "{}", STR_4);
1010                assert_eq!(files_exists_4(), (true, false, false, false));
1011                assert_eq!(sink._current_size(), 12);
1012
1013                info!(logger: logger, "{}", STR_5);
1014                assert_eq!(files_exists_4(), (true, true, false, false));
1015                assert_eq!(sink._current_size(), 5);
1016            }
1017            assert_eq!(
1018                read_file_4(),
1019                (
1020                    Some("abcde".to_string()),
1021                    Some("abcdabcdabcd".to_string()),
1022                    None,
1023                    None
1024                )
1025            );
1026
1027            // test `rotate_on_open` == false
1028            {
1029                let (sink, logger) = build(false, false);
1030
1031                assert_eq!(files_exists_4(), (true, true, false, false));
1032                assert_eq!(sink._current_size(), 5);
1033
1034                info!(logger: logger, "{}", STR_5);
1035                assert_eq!(files_exists_4(), (true, true, false, false));
1036                assert_eq!(sink._current_size(), 10);
1037            }
1038            assert_eq!(
1039                read_file_4(),
1040                (
1041                    Some("abcdeabcde".to_string()),
1042                    Some("abcdabcdabcd".to_string()),
1043                    None,
1044                    None
1045                )
1046            );
1047
1048            // test `rotate_on_open` == true
1049            {
1050                let (sink, logger) = build(false, true);
1051
1052                assert_eq!(files_exists_4(), (true, true, true, false));
1053                assert_eq!(sink._current_size(), 0);
1054
1055                info!(logger: logger, "{}", STR_5);
1056                assert_eq!(files_exists_4(), (true, true, true, false));
1057                assert_eq!(sink._current_size(), 5);
1058            }
1059            assert_eq!(
1060                read_file_4(),
1061                (
1062                    Some("abcde".to_string()),
1063                    Some("abcdeabcde".to_string()),
1064                    Some("abcdabcdabcd".to_string()),
1065                    None
1066                )
1067            );
1068
1069            // test `max_files`
1070            {
1071                let (sink, logger) = build(false, true);
1072
1073                assert_eq!(files_exists_4(), (true, true, true, false));
1074                assert_eq!(sink._current_size(), 0);
1075
1076                info!(logger: logger, "{}", STR_4);
1077                assert_eq!(files_exists_4(), (true, true, true, false));
1078                assert_eq!(sink._current_size(), 4);
1079            }
1080            assert_eq!(
1081                read_file_4(),
1082                (
1083                    Some("abcd".to_string()),
1084                    Some("abcde".to_string()),
1085                    Some("abcdeabcde".to_string()),
1086                    None
1087                )
1088            );
1089        }
1090    }
1091
1092    mod policy_time_point {
1093        use super::*;
1094
1095        static LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
1096            let path = BASE_LOGS_PATH.join("policy_time_point");
1097            _ = fs::remove_dir_all(&path);
1098            fs::create_dir_all(&path).unwrap();
1099            path
1100        });
1101
1102        #[track_caller]
1103        fn assert_files_count(file_name_prefix: &str, expected: usize) {
1104            let paths = fs::read_dir(LOGS_PATH.clone()).unwrap();
1105
1106            let mut filenames = Vec::new();
1107            let actual = paths.fold(0_usize, |mut count, entry| {
1108                let filename = entry.unwrap().file_name();
1109                if filename.to_string_lossy().starts_with(file_name_prefix) {
1110                    count += 1;
1111                    filenames.push(filename);
1112                }
1113                count
1114            });
1115            println!("found files: {:?}", filenames);
1116            assert_eq!(actual, expected)
1117        }
1118
1119        #[test]
1120        fn calc_file_path() {
1121            let system_time = Local.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap().into();
1122
1123            let calc_daily = |base_path| {
1124                RotatorTimePoint::calc_file_path(
1125                    base_path,
1126                    TimePoint::Daily { hour: 8, minute: 9 },
1127                    system_time,
1128                )
1129                .to_str()
1130                .unwrap()
1131                .to_string()
1132            };
1133
1134            let calc_hourly = |base_path| {
1135                RotatorTimePoint::calc_file_path(base_path, TimePoint::Hourly, system_time)
1136                    .to_str()
1137                    .unwrap()
1138                    .to_string()
1139            };
1140
1141            let calc_period = |base_path| {
1142                RotatorTimePoint::calc_file_path(
1143                    base_path,
1144                    TimePoint::Period(10 * MINUTE_1),
1145                    system_time,
1146                )
1147                .to_str()
1148                .unwrap()
1149                .to_string()
1150            };
1151
1152            #[cfg(not(windows))]
1153            let run = || {
1154                assert_eq!(calc_daily("/tmp/test.log"), "/tmp/test_2012-03-04.log");
1155                assert_eq!(calc_daily("/tmp/test"), "/tmp/test_2012-03-04");
1156
1157                assert_eq!(calc_hourly("/tmp/test.log"), "/tmp/test_2012-03-04_05.log");
1158                assert_eq!(calc_hourly("/tmp/test"), "/tmp/test_2012-03-04_05");
1159
1160                assert_eq!(
1161                    calc_period("/tmp/test.log"),
1162                    "/tmp/test_2012-03-04_05-06.log"
1163                );
1164                assert_eq!(calc_period("/tmp/test"), "/tmp/test_2012-03-04_05-06");
1165            };
1166
1167            #[cfg(windows)]
1168            #[rustfmt::skip]
1169            let run = || {
1170                assert_eq!(calc_daily("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04.txt");
1171                assert_eq!(calc_daily("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04");
1172
1173                assert_eq!(calc_hourly("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04_05.txt");
1174                assert_eq!(calc_hourly("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04_05");
1175
1176                assert_eq!(calc_period("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04_05-06.txt");
1177                assert_eq!(calc_period("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04_05-06");
1178            };
1179
1180            run();
1181        }
1182
1183        #[test]
1184        fn rotate() {
1185            let build = |rotate_on_open| {
1186                let hourly_sink = RotatingFileSink::builder()
1187                    .base_path(LOGS_PATH.join("hourly.log"))
1188                    .rotation_policy(RotationPolicy::Hourly)
1189                    .rotate_on_open(rotate_on_open)
1190                    .build()
1191                    .unwrap();
1192
1193                let period_sink = RotatingFileSink::builder()
1194                    .base_path(LOGS_PATH.join("period.log"))
1195                    .rotation_policy(RotationPolicy::Period(HOUR_1 + 2 * MINUTE_1 + 3 * SECOND_1))
1196                    .rotate_on_open(rotate_on_open)
1197                    .build()
1198                    .unwrap();
1199
1200                let local_time_now = Local::now();
1201                let daily_sink = RotatingFileSink::builder()
1202                    .base_path(LOGS_PATH.join("daily.log"))
1203                    .rotation_policy(RotationPolicy::Daily {
1204                        hour: local_time_now.hour(),
1205                        minute: local_time_now.minute(),
1206                    })
1207                    .rotate_on_open(rotate_on_open)
1208                    .build()
1209                    .unwrap();
1210
1211                let sinks: [Arc<dyn Sink>; 3] = [
1212                    Arc::new(hourly_sink),
1213                    Arc::new(period_sink),
1214                    Arc::new(daily_sink),
1215                ];
1216                let logger = build_test_logger(|b| b.sinks(sinks));
1217                logger.set_level_filter(LevelFilter::All);
1218                logger
1219            };
1220
1221            {
1222                let logger = build(true);
1223                let mut record = Record::new(Level::Info, "test log message", None, None);
1224                let initial_time = record.time();
1225
1226                assert_files_count("hourly", 1);
1227                assert_files_count("period", 1);
1228                assert_files_count("daily", 1);
1229
1230                logger.log(&record);
1231                assert_files_count("hourly", 1);
1232                assert_files_count("period", 1);
1233                assert_files_count("daily", 1);
1234
1235                record.set_time(record.time() + HOUR_1 + SECOND_1);
1236                logger.log(&record);
1237                assert_files_count("hourly", 2);
1238                assert_files_count("period", 1);
1239                assert_files_count("daily", 1);
1240
1241                record.set_time(record.time() + HOUR_1 + SECOND_1);
1242                logger.log(&record);
1243                assert_files_count("hourly", 3);
1244                assert_files_count("period", 2);
1245                assert_files_count("daily", 1);
1246
1247                record.set_time(record.time() + SECOND_1);
1248                logger.log(&record);
1249                assert_files_count("hourly", 3);
1250                assert_files_count("period", 2);
1251                assert_files_count("daily", 1);
1252
1253                record.set_time(initial_time + DAY_1 + SECOND_1);
1254                logger.log(&record);
1255                assert_files_count("hourly", 4);
1256                assert_files_count("period", 3);
1257                assert_files_count("daily", 2);
1258            }
1259        }
1260
1261        // This test may only detect issues if the system time zone is not UTC.
1262        #[test]
1263        fn respect_local_tz() {
1264            let prefix = "respect_local_tz";
1265
1266            let initial_time = Local // FixedOffset::east_opt(8 * 3600).unwrap()
1267                .with_ymd_and_hms(2024, 8, 29, 11, 45, 14)
1268                .unwrap();
1269
1270            let logger = {
1271                let daily_sink = RotatingFileSink::builder()
1272                    .base_path(LOGS_PATH.join(format!("{prefix}.log")))
1273                    .rotation_policy(RotationPolicy::Daily { hour: 0, minute: 0 })
1274                    .rotate_on_open(true)
1275                    .build_with_initial_time(Some(initial_time.to_utc().into()))
1276                    .unwrap();
1277
1278                build_test_logger(|b| b.sink(Arc::new(daily_sink)).level_filter(LevelFilter::All))
1279            };
1280
1281            {
1282                let mut record = Record::new(Level::Info, "test log message", None, None);
1283
1284                assert_files_count(prefix, 1);
1285
1286                record.set_time(initial_time.to_utc().into());
1287                logger.log(&record);
1288                assert_files_count(prefix, 1);
1289
1290                record.set_time(record.time() + HOUR_1 + SECOND_1);
1291                logger.log(&record);
1292                assert_files_count(prefix, 1);
1293
1294                record.set_time(record.time() + HOUR_1 + SECOND_1);
1295                logger.log(&record);
1296                assert_files_count(prefix, 1);
1297
1298                record.set_time(
1299                    initial_time
1300                        .with_day(30)
1301                        .unwrap()
1302                        .with_hour(0)
1303                        .unwrap()
1304                        .with_minute(1)
1305                        .unwrap()
1306                        .to_utc()
1307                        .into(),
1308                );
1309                logger.log(&record);
1310                assert_files_count(prefix, 2);
1311
1312                record.set_time(record.time() + HOUR_1 + SECOND_1);
1313                logger.log(&record);
1314                assert_files_count(prefix, 2);
1315
1316                record.set_time(
1317                    initial_time
1318                        .with_day(30)
1319                        .unwrap()
1320                        .with_hour(8)
1321                        .unwrap()
1322                        .with_minute(2)
1323                        .unwrap()
1324                        .to_utc()
1325                        .into(),
1326                );
1327                logger.log(&record);
1328                assert_files_count(prefix, 2);
1329
1330                record.set_time(record.time() + HOUR_1 + SECOND_1);
1331                logger.log(&record);
1332                assert_files_count(prefix, 2);
1333
1334                record.set_time(
1335                    initial_time
1336                        .with_day(31)
1337                        .unwrap()
1338                        .with_hour(0)
1339                        .unwrap()
1340                        .to_utc()
1341                        .into(),
1342                );
1343                logger.log(&record);
1344                assert_files_count(prefix, 3);
1345
1346                record.set_time(record.time() + HOUR_1 + SECOND_1);
1347                logger.log(&record);
1348                assert_files_count(prefix, 3);
1349            }
1350        }
1351    }
1352
1353    #[test]
1354    fn test_builder_optional_params() {
1355        // workaround for the missing `no_run` attribute
1356        let _ = || {
1357            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1358                .base_path("/path/to/base_log_file")
1359                .rotation_policy(RotationPolicy::Hourly)
1360                // .max_files(100)
1361                // .rotate_on_open(true)
1362                .build();
1363
1364            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1365                .base_path("/path/to/base_log_file")
1366                .rotation_policy(RotationPolicy::Hourly)
1367                .max_files(100)
1368                // .rotate_on_open(true)
1369                .build();
1370
1371            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1372                .base_path("/path/to/base_log_file")
1373                .rotation_policy(RotationPolicy::Hourly)
1374                // .max_files(100)
1375                .rotate_on_open(true)
1376                .build();
1377
1378            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1379                .base_path("/path/to/base_log_file")
1380                .rotation_policy(RotationPolicy::Hourly)
1381                .max_files(100)
1382                .rotate_on_open(true)
1383                .build();
1384        };
1385    }
1386
1387    #[test]
1388    fn test_invalid_rotation_policy() {
1389        use RotationPolicy::*;
1390
1391        fn daily(hour: u32, minute: u32) -> RotationPolicy {
1392            Daily { hour, minute }
1393        }
1394        fn period(duration: Duration) -> RotationPolicy {
1395            Period(duration)
1396        }
1397
1398        assert!(FileSize(1).validate().is_ok());
1399        assert!(FileSize(1024).validate().is_ok());
1400        assert!(FileSize(u64::MAX).validate().is_ok());
1401        assert!(FileSize(0).validate().is_err());
1402
1403        assert!(daily(0, 0).validate().is_ok());
1404        assert!(daily(15, 30).validate().is_ok());
1405        assert!(daily(23, 59).validate().is_ok());
1406        assert!(daily(24, 59).validate().is_err());
1407        assert!(daily(23, 60).validate().is_err());
1408        assert!(daily(24, 60).validate().is_err());
1409
1410        assert!(period(Duration::from_secs(0)).validate().is_err());
1411        assert!(period(SECOND_1).validate().is_err());
1412        assert!(period(59 * SECOND_1).validate().is_err());
1413        assert!(period(MINUTE_1).validate().is_ok());
1414        assert!(period(HOUR_1).validate().is_ok());
1415        assert!(period(HOUR_1 + MINUTE_1 + SECOND_1).validate().is_ok());
1416        assert!(period(60 * HOUR_1 + MINUTE_1 + SECOND_1).validate().is_ok());
1417        assert!(period(2 * DAY_1 + 60 * HOUR_1 + MINUTE_1 + SECOND_1)
1418            .validate()
1419            .is_ok());
1420    }
1421}