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