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