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