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