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