Skip to main content

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