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]    | [`LevelFilter::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    /// [`FullFormatter`]: crate::formatter::FullFormatter
183    /// [error_handler]: RotatingFileSinkBuilder::error_handler
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**, and defaults to `0`.
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**, and defaults to `false`.
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**, and defaults to the value consistent
797    /// with `std`.
798    #[must_use]
799    pub fn capacity(mut self, capacity: usize) -> Self {
800        self.capacity = Some(capacity);
801        self
802    }
803
804    // Prop
805    //
806
807    /// Specifies a log level filter.
808    ///
809    /// This parameter is **optional**, and defaults to [`LevelFilter::All`].
810    #[must_use]
811    pub fn level_filter(self, level_filter: LevelFilter) -> Self {
812        self.prop.set_level_filter(level_filter);
813        self
814    }
815
816    /// Specifies a formatter.
817    ///
818    /// This parameter is **optional**, and defaults to [`FullFormatter`].
819    ///
820    /// [`FullFormatter`]: crate::formatter::FullFormatter
821    #[must_use]
822    pub fn formatter<F>(self, formatter: F) -> Self
823    where
824        F: Formatter + 'static,
825    {
826        self.prop.set_formatter(formatter);
827        self
828    }
829
830    /// Specifies an error handler.
831    ///
832    /// This parameter is **optional**, and defaults to
833    /// [`ErrorHandler::default()`].
834    #[must_use]
835    pub fn error_handler<F: Into<ErrorHandler>>(self, handler: F) -> Self {
836        self.prop.set_error_handler(handler);
837        self
838    }
839}
840
841impl<ArgRP> RotatingFileSinkBuilder<(), ArgRP> {
842    #[doc(hidden)]
843    #[deprecated(note = "\n\n\
844        builder compile-time error:\n\
845        - missing required parameter `base_path`\n\n\
846    ")]
847    pub fn build(self, _: Infallible) {}
848
849    #[doc(hidden)]
850    #[deprecated(note = "\n\n\
851        builder compile-time error:\n\
852        - missing required parameter `base_path`\n\n\
853    ")]
854    pub fn build_arc(self, _: Infallible) {}
855}
856
857impl RotatingFileSinkBuilder<PathBuf, ()> {
858    #[doc(hidden)]
859    #[deprecated(note = "\n\n\
860        builder compile-time error:\n\
861        - missing required parameter `rotation_policy`\n\n\
862    ")]
863    pub fn build(self, _: Infallible) {}
864
865    #[doc(hidden)]
866    #[deprecated(note = "\n\n\
867        builder compile-time error:\n\
868        - missing required parameter `rotation_policy`\n\n\
869    ")]
870    pub fn build_arc(self, _: Infallible) {}
871}
872
873impl RotatingFileSinkBuilder<PathBuf, RotationPolicy> {
874    /// Builds a [`RotatingFileSink`].
875    ///
876    /// # Error
877    ///
878    /// If the argument `rotation_policy` is invalid, or an error occurs opening
879    /// the file, [`Error::CreateDirectory`] or [`Error::OpenFile`] will be
880    /// returned.
881    pub fn build(self) -> Result<RotatingFileSink> {
882        self.build_with_initial_time(None)
883    }
884
885    /// Builds a `Arc<RotatingFileSink>`.
886    ///
887    /// This is a shorthand method for `.build().map(Arc::new)`.
888    pub fn build_arc(self) -> Result<Arc<RotatingFileSink>> {
889        self.build().map(Arc::new)
890    }
891
892    fn build_with_initial_time(self, override_now: Option<SystemTime>) -> Result<RotatingFileSink> {
893        self.rotation_policy
894            .validate()
895            .map_err(|err| Error::InvalidArgument(InvalidArgumentError::RotationPolicy(err)))?;
896
897        let rotator = match self.rotation_policy {
898            RotationPolicy::FileSize(max_size) => RotatorKind::FileSize(RotatorFileSize::new(
899                self.base_path,
900                max_size,
901                self.max_files,
902                self.rotate_on_open,
903                None,
904            )?),
905            RotationPolicy::Daily { hour, minute } => {
906                RotatorKind::TimePoint(RotatorTimePoint::new(
907                    override_now,
908                    self.base_path,
909                    TimePoint::Daily { hour, minute },
910                    self.max_files,
911                    self.rotate_on_open,
912                    None,
913                )?)
914            }
915            RotationPolicy::Hourly => RotatorKind::TimePoint(RotatorTimePoint::new(
916                override_now,
917                self.base_path,
918                TimePoint::Hourly,
919                self.max_files,
920                self.rotate_on_open,
921                None,
922            )?),
923            RotationPolicy::Period(duration) => RotatorKind::TimePoint(RotatorTimePoint::new(
924                override_now,
925                self.base_path,
926                TimePoint::Period(duration),
927                self.max_files,
928                self.rotate_on_open,
929                None,
930            )?),
931        };
932
933        let res = RotatingFileSink {
934            prop: self.prop,
935            rotator,
936        };
937
938        Ok(res)
939    }
940}
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945    use crate::{prelude::*, test_utils::*, Level, Record};
946
947    static BASE_LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
948        let path = TEST_LOGS_PATH.join("rotating_file_sink");
949        if !path.exists() {
950            _ = fs::create_dir(&path);
951        }
952        path
953    });
954
955    const SECOND_1: Duration = Duration::from_secs(1);
956
957    mod policy_file_size {
958        use super::*;
959
960        static LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
961            let path = BASE_LOGS_PATH.join("policy_file_size");
962            fs::create_dir_all(&path).unwrap();
963            path
964        });
965
966        #[test]
967        fn calc_file_path() {
968            let calc = |base_path, index| {
969                RotatorFileSize::calc_file_path(base_path, index)
970                    .to_str()
971                    .unwrap()
972                    .to_string()
973            };
974
975            #[cfg(not(windows))]
976            let run = || {
977                assert_eq!(calc("/tmp/test.log", 0), "/tmp/test.log");
978                assert_eq!(calc("/tmp/test", 0), "/tmp/test");
979
980                assert_eq!(calc("/tmp/test.log", 1), "/tmp/test_1.log");
981                assert_eq!(calc("/tmp/test", 1), "/tmp/test_1");
982
983                assert_eq!(calc("/tmp/test.log", 23), "/tmp/test_23.log");
984                assert_eq!(calc("/tmp/test", 23), "/tmp/test_23");
985            };
986
987            #[cfg(windows)]
988            let run = || {
989                assert_eq!(calc("D:\\tmp\\test.txt", 0), "D:\\tmp\\test.txt");
990                assert_eq!(calc("D:\\tmp\\test", 0), "D:\\tmp\\test");
991
992                assert_eq!(calc("D:\\tmp\\test.txt", 1), "D:\\tmp\\test_1.txt");
993                assert_eq!(calc("D:\\tmp\\test", 1), "D:\\tmp\\test_1");
994
995                assert_eq!(calc("D:\\tmp\\test.txt", 23), "D:\\tmp\\test_23.txt");
996                assert_eq!(calc("D:\\tmp\\test", 23), "D:\\tmp\\test_23");
997            };
998
999            run();
1000        }
1001
1002        #[test]
1003        fn rotate() {
1004            let base_path = LOGS_PATH.join("test.log");
1005
1006            let build = |clean, rotate_on_open| {
1007                if clean {
1008                    fs::remove_dir_all(LOGS_PATH.as_path()).unwrap();
1009                    if !LOGS_PATH.exists() {
1010                        fs::create_dir(LOGS_PATH.as_path()).unwrap();
1011                    }
1012                }
1013
1014                let sink = RotatingFileSink::builder()
1015                    .base_path(LOGS_PATH.join(&base_path))
1016                    .rotation_policy(RotationPolicy::FileSize(16))
1017                    .max_files(3)
1018                    .rotate_on_open(rotate_on_open)
1019                    .formatter(NoModFormatter::new())
1020                    .build_arc()
1021                    .unwrap();
1022                let logger = build_test_logger(|b| b.sink(sink.clone()));
1023                logger.set_level_filter(LevelFilter::All);
1024                (sink, logger)
1025            };
1026
1027            let index_to_path =
1028                |index| RotatorFileSize::calc_file_path(PathBuf::from(&base_path), index);
1029
1030            let file_exists = |index| index_to_path(index).exists();
1031            let files_exists_4 = || {
1032                (
1033                    file_exists(0),
1034                    file_exists(1),
1035                    file_exists(2),
1036                    file_exists(3),
1037                )
1038            };
1039
1040            let read_file = |index| fs::read_to_string(index_to_path(index)).ok();
1041            let read_file_4 = || (read_file(0), read_file(1), read_file(2), read_file(3));
1042
1043            const STR_4: &str = "abcd";
1044            const STR_5: &str = "abcde";
1045
1046            {
1047                let (sink, logger) = build(true, false);
1048
1049                assert_eq!(files_exists_4(), (true, false, false, false));
1050                assert_eq!(sink._current_size(), 0);
1051
1052                info!(logger: logger, "{}", STR_4);
1053                assert_eq!(files_exists_4(), (true, false, false, false));
1054                assert_eq!(sink._current_size(), 4);
1055
1056                info!(logger: logger, "{}", STR_4);
1057                assert_eq!(files_exists_4(), (true, false, false, false));
1058                assert_eq!(sink._current_size(), 8);
1059
1060                info!(logger: logger, "{}", STR_4);
1061                assert_eq!(files_exists_4(), (true, false, false, false));
1062                assert_eq!(sink._current_size(), 12);
1063
1064                info!(logger: logger, "{}", STR_4);
1065                assert_eq!(files_exists_4(), (true, false, false, false));
1066                assert_eq!(sink._current_size(), 16);
1067
1068                info!(logger: logger, "{}", STR_4);
1069                assert_eq!(files_exists_4(), (true, true, false, false));
1070                assert_eq!(sink._current_size(), 4);
1071            }
1072            assert_eq!(
1073                read_file_4(),
1074                (
1075                    Some("abcd".to_string()),
1076                    Some("abcdabcdabcdabcd".to_string()),
1077                    None,
1078                    None
1079                )
1080            );
1081
1082            {
1083                let (sink, logger) = build(true, false);
1084
1085                assert_eq!(files_exists_4(), (true, false, false, false));
1086                assert_eq!(sink._current_size(), 0);
1087
1088                info!(logger: logger, "{}", STR_4);
1089                info!(logger: logger, "{}", STR_4);
1090                info!(logger: logger, "{}", STR_4);
1091                assert_eq!(files_exists_4(), (true, false, false, false));
1092                assert_eq!(sink._current_size(), 12);
1093
1094                info!(logger: logger, "{}", STR_5);
1095                assert_eq!(files_exists_4(), (true, true, false, false));
1096                assert_eq!(sink._current_size(), 5);
1097            }
1098            assert_eq!(
1099                read_file_4(),
1100                (
1101                    Some("abcde".to_string()),
1102                    Some("abcdabcdabcd".to_string()),
1103                    None,
1104                    None
1105                )
1106            );
1107
1108            // test `rotate_on_open` == false
1109            {
1110                let (sink, logger) = build(false, false);
1111
1112                assert_eq!(files_exists_4(), (true, true, false, false));
1113                assert_eq!(sink._current_size(), 5);
1114
1115                info!(logger: logger, "{}", STR_5);
1116                assert_eq!(files_exists_4(), (true, true, false, false));
1117                assert_eq!(sink._current_size(), 10);
1118            }
1119            assert_eq!(
1120                read_file_4(),
1121                (
1122                    Some("abcdeabcde".to_string()),
1123                    Some("abcdabcdabcd".to_string()),
1124                    None,
1125                    None
1126                )
1127            );
1128
1129            // test `rotate_on_open` == true
1130            {
1131                let (sink, logger) = build(false, true);
1132
1133                assert_eq!(files_exists_4(), (true, true, true, false));
1134                assert_eq!(sink._current_size(), 0);
1135
1136                info!(logger: logger, "{}", STR_5);
1137                assert_eq!(files_exists_4(), (true, true, true, false));
1138                assert_eq!(sink._current_size(), 5);
1139            }
1140            assert_eq!(
1141                read_file_4(),
1142                (
1143                    Some("abcde".to_string()),
1144                    Some("abcdeabcde".to_string()),
1145                    Some("abcdabcdabcd".to_string()),
1146                    None
1147                )
1148            );
1149
1150            // test `max_files`
1151            {
1152                let (sink, logger) = build(false, true);
1153
1154                assert_eq!(files_exists_4(), (true, true, true, false));
1155                assert_eq!(sink._current_size(), 0);
1156
1157                info!(logger: logger, "{}", STR_4);
1158                assert_eq!(files_exists_4(), (true, true, true, false));
1159                assert_eq!(sink._current_size(), 4);
1160            }
1161            assert_eq!(
1162                read_file_4(),
1163                (
1164                    Some("abcd".to_string()),
1165                    Some("abcde".to_string()),
1166                    Some("abcdeabcde".to_string()),
1167                    None
1168                )
1169            );
1170        }
1171    }
1172
1173    mod policy_time_point {
1174        use super::*;
1175
1176        static LOGS_PATH: Lazy<PathBuf> = Lazy::new(|| {
1177            let path = BASE_LOGS_PATH.join("policy_time_point");
1178            _ = fs::remove_dir_all(&path);
1179            fs::create_dir_all(&path).unwrap();
1180            path
1181        });
1182
1183        #[track_caller]
1184        fn assert_files_count(file_name_prefix: &str, expected: usize) {
1185            let paths = fs::read_dir(LOGS_PATH.clone()).unwrap();
1186
1187            let mut filenames = Vec::new();
1188            let actual = paths.fold(0_usize, |mut count, entry| {
1189                let filename = entry.unwrap().file_name();
1190                if filename.to_string_lossy().starts_with(file_name_prefix) {
1191                    count += 1;
1192                    filenames.push(filename);
1193                }
1194                count
1195            });
1196            println!("found files: {filenames:?}");
1197            assert_eq!(actual, expected)
1198        }
1199
1200        #[test]
1201        fn calc_file_path() {
1202            let system_time = Local.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap().into();
1203
1204            let calc_daily = |base_path| {
1205                RotatorTimePoint::calc_file_path(
1206                    base_path,
1207                    TimePoint::Daily { hour: 8, minute: 9 },
1208                    system_time,
1209                )
1210                .to_str()
1211                .unwrap()
1212                .to_string()
1213            };
1214
1215            let calc_hourly = |base_path| {
1216                RotatorTimePoint::calc_file_path(base_path, TimePoint::Hourly, system_time)
1217                    .to_str()
1218                    .unwrap()
1219                    .to_string()
1220            };
1221
1222            let calc_period = |base_path| {
1223                RotatorTimePoint::calc_file_path(
1224                    base_path,
1225                    TimePoint::Period(10 * MINUTE_1),
1226                    system_time,
1227                )
1228                .to_str()
1229                .unwrap()
1230                .to_string()
1231            };
1232
1233            #[cfg(not(windows))]
1234            let run = || {
1235                assert_eq!(calc_daily("/tmp/test.log"), "/tmp/test_2012-03-04.log");
1236                assert_eq!(calc_daily("/tmp/test"), "/tmp/test_2012-03-04");
1237
1238                assert_eq!(calc_hourly("/tmp/test.log"), "/tmp/test_2012-03-04_05.log");
1239                assert_eq!(calc_hourly("/tmp/test"), "/tmp/test_2012-03-04_05");
1240
1241                assert_eq!(
1242                    calc_period("/tmp/test.log"),
1243                    "/tmp/test_2012-03-04_05-06.log"
1244                );
1245                assert_eq!(calc_period("/tmp/test"), "/tmp/test_2012-03-04_05-06");
1246            };
1247
1248            #[cfg(windows)]
1249            #[rustfmt::skip]
1250            let run = || {
1251                assert_eq!(calc_daily("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04.txt");
1252                assert_eq!(calc_daily("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04");
1253
1254                assert_eq!(calc_hourly("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04_05.txt");
1255                assert_eq!(calc_hourly("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04_05");
1256
1257                assert_eq!(calc_period("D:\\tmp\\test.txt"), "D:\\tmp\\test_2012-03-04_05-06.txt");
1258                assert_eq!(calc_period("D:\\tmp\\test"), "D:\\tmp\\test_2012-03-04_05-06");
1259            };
1260
1261            run();
1262        }
1263
1264        #[test]
1265        fn rotate() {
1266            let build = |rotate_on_open| {
1267                let hourly_sink = RotatingFileSink::builder()
1268                    .base_path(LOGS_PATH.join("hourly.log"))
1269                    .rotation_policy(RotationPolicy::Hourly)
1270                    .rotate_on_open(rotate_on_open)
1271                    .build_arc()
1272                    .unwrap();
1273
1274                let period_sink = RotatingFileSink::builder()
1275                    .base_path(LOGS_PATH.join("period.log"))
1276                    .rotation_policy(RotationPolicy::Period(HOUR_1 + 2 * MINUTE_1 + 3 * SECOND_1))
1277                    .rotate_on_open(rotate_on_open)
1278                    .build_arc()
1279                    .unwrap();
1280
1281                let local_time_now = Local::now();
1282                let daily_sink = RotatingFileSink::builder()
1283                    .base_path(LOGS_PATH.join("daily.log"))
1284                    .rotation_policy(RotationPolicy::Daily {
1285                        hour: local_time_now.hour(),
1286                        minute: local_time_now.minute(),
1287                    })
1288                    .rotate_on_open(rotate_on_open)
1289                    .build_arc()
1290                    .unwrap();
1291
1292                let sinks: [Arc<dyn Sink>; 3] = [hourly_sink, period_sink, daily_sink];
1293                let logger = build_test_logger(|b| b.sinks(sinks));
1294                logger.set_level_filter(LevelFilter::All);
1295                logger
1296            };
1297
1298            {
1299                let logger = build(true);
1300                let mut record = Record::new(Level::Info, "test log message", None, None, &[]);
1301                let initial_time = record.time();
1302
1303                assert_files_count("hourly", 1);
1304                assert_files_count("period", 1);
1305                assert_files_count("daily", 1);
1306
1307                logger.log(&record);
1308                assert_files_count("hourly", 1);
1309                assert_files_count("period", 1);
1310                assert_files_count("daily", 1);
1311
1312                record.set_time(record.time() + HOUR_1 + SECOND_1);
1313                logger.log(&record);
1314                assert_files_count("hourly", 2);
1315                assert_files_count("period", 1);
1316                assert_files_count("daily", 1);
1317
1318                record.set_time(record.time() + HOUR_1 + SECOND_1);
1319                logger.log(&record);
1320                assert_files_count("hourly", 3);
1321                assert_files_count("period", 2);
1322                assert_files_count("daily", 1);
1323
1324                record.set_time(record.time() + SECOND_1);
1325                logger.log(&record);
1326                assert_files_count("hourly", 3);
1327                assert_files_count("period", 2);
1328                assert_files_count("daily", 1);
1329
1330                record.set_time(initial_time + DAY_1 + SECOND_1);
1331                logger.log(&record);
1332                assert_files_count("hourly", 4);
1333                assert_files_count("period", 3);
1334                assert_files_count("daily", 2);
1335            }
1336        }
1337
1338        // This test may only detect issues if the system time zone is not UTC.
1339        #[test]
1340        fn respect_local_tz() {
1341            let prefix = "respect_local_tz";
1342
1343            let initial_time = Local // FixedOffset::east_opt(8 * 3600).unwrap()
1344                .with_ymd_and_hms(2024, 8, 29, 11, 45, 14)
1345                .unwrap();
1346
1347            let logger = {
1348                let daily_sink = RotatingFileSink::builder()
1349                    .base_path(LOGS_PATH.join(format!("{prefix}.log")))
1350                    .rotation_policy(RotationPolicy::Daily { hour: 0, minute: 0 })
1351                    .rotate_on_open(true)
1352                    .build_with_initial_time(Some(initial_time.to_utc().into()))
1353                    .unwrap();
1354
1355                build_test_logger(|b| b.sink(Arc::new(daily_sink)).level_filter(LevelFilter::All))
1356            };
1357
1358            {
1359                let mut record = Record::new(Level::Info, "test log message", None, None, &[]);
1360
1361                assert_files_count(prefix, 1);
1362
1363                record.set_time(initial_time.to_utc().into());
1364                logger.log(&record);
1365                assert_files_count(prefix, 1);
1366
1367                record.set_time(record.time() + HOUR_1 + SECOND_1);
1368                logger.log(&record);
1369                assert_files_count(prefix, 1);
1370
1371                record.set_time(record.time() + HOUR_1 + SECOND_1);
1372                logger.log(&record);
1373                assert_files_count(prefix, 1);
1374
1375                record.set_time(
1376                    initial_time
1377                        .with_day(30)
1378                        .unwrap()
1379                        .with_hour(0)
1380                        .unwrap()
1381                        .with_minute(1)
1382                        .unwrap()
1383                        .to_utc()
1384                        .into(),
1385                );
1386                logger.log(&record);
1387                assert_files_count(prefix, 2);
1388
1389                record.set_time(record.time() + HOUR_1 + SECOND_1);
1390                logger.log(&record);
1391                assert_files_count(prefix, 2);
1392
1393                record.set_time(
1394                    initial_time
1395                        .with_day(30)
1396                        .unwrap()
1397                        .with_hour(8)
1398                        .unwrap()
1399                        .with_minute(2)
1400                        .unwrap()
1401                        .to_utc()
1402                        .into(),
1403                );
1404                logger.log(&record);
1405                assert_files_count(prefix, 2);
1406
1407                record.set_time(record.time() + HOUR_1 + SECOND_1);
1408                logger.log(&record);
1409                assert_files_count(prefix, 2);
1410
1411                record.set_time(
1412                    initial_time
1413                        .with_day(31)
1414                        .unwrap()
1415                        .with_hour(0)
1416                        .unwrap()
1417                        .to_utc()
1418                        .into(),
1419                );
1420                logger.log(&record);
1421                assert_files_count(prefix, 3);
1422
1423                record.set_time(record.time() + HOUR_1 + SECOND_1);
1424                logger.log(&record);
1425                assert_files_count(prefix, 3);
1426            }
1427        }
1428    }
1429
1430    #[test]
1431    fn test_builder_optional_params() {
1432        // workaround for the missing `no_run` attribute
1433        let _ = || {
1434            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1435                .base_path("/path/to/base_log_file")
1436                .rotation_policy(RotationPolicy::Hourly)
1437                // .max_files(100)
1438                // .rotate_on_open(true)
1439                .build();
1440
1441            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1442                .base_path("/path/to/base_log_file")
1443                .rotation_policy(RotationPolicy::Hourly)
1444                .max_files(100)
1445                // .rotate_on_open(true)
1446                .build();
1447
1448            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1449                .base_path("/path/to/base_log_file")
1450                .rotation_policy(RotationPolicy::Hourly)
1451                // .max_files(100)
1452                .rotate_on_open(true)
1453                .build();
1454
1455            let _: Result<RotatingFileSink> = RotatingFileSink::builder()
1456                .base_path("/path/to/base_log_file")
1457                .rotation_policy(RotationPolicy::Hourly)
1458                .max_files(100)
1459                .rotate_on_open(true)
1460                .build();
1461        };
1462    }
1463
1464    #[test]
1465    fn test_invalid_rotation_policy() {
1466        use RotationPolicy::*;
1467
1468        fn daily(hour: u32, minute: u32) -> RotationPolicy {
1469            Daily { hour, minute }
1470        }
1471        fn period(duration: Duration) -> RotationPolicy {
1472            Period(duration)
1473        }
1474
1475        assert!(FileSize(1).validate().is_ok());
1476        assert!(FileSize(1024).validate().is_ok());
1477        assert!(FileSize(u64::MAX).validate().is_ok());
1478        assert!(FileSize(0).validate().is_err());
1479
1480        assert!(daily(0, 0).validate().is_ok());
1481        assert!(daily(15, 30).validate().is_ok());
1482        assert!(daily(23, 59).validate().is_ok());
1483        assert!(daily(24, 59).validate().is_err());
1484        assert!(daily(23, 60).validate().is_err());
1485        assert!(daily(24, 60).validate().is_err());
1486
1487        assert!(period(Duration::from_secs(0)).validate().is_err());
1488        assert!(period(SECOND_1).validate().is_err());
1489        assert!(period(59 * SECOND_1).validate().is_err());
1490        assert!(period(MINUTE_1).validate().is_ok());
1491        assert!(period(HOUR_1).validate().is_ok());
1492        assert!(period(HOUR_1 + MINUTE_1 + SECOND_1).validate().is_ok());
1493        assert!(period(60 * HOUR_1 + MINUTE_1 + SECOND_1).validate().is_ok());
1494        assert!(period(2 * DAY_1 + 60 * HOUR_1 + MINUTE_1 + SECOND_1)
1495            .validate()
1496            .is_ok());
1497    }
1498}