1use crate::sync::{RwLock, RwLockReadGuard};
30use std::{
31 fmt::{self, Debug},
32 fs::{self, File, OpenOptions},
33 io::{self, Write},
34 path::{Path, PathBuf},
35 sync::atomic::{AtomicUsize, Ordering},
36};
37use time::{format_description, Date, Duration, OffsetDateTime, Time};
38
39mod builder;
40pub use builder::{Builder, InitError};
41
42pub struct RollingFileAppender {
87 state: Inner,
88 writer: RwLock<File>,
89 #[cfg(test)]
90 now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
91}
92
93#[derive(Debug)]
100pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
101
102#[derive(Debug)]
103struct Inner {
104 log_directory: PathBuf,
105 log_filename_prefix: Option<String>,
106 log_filename_suffix: Option<String>,
107 date_format: Vec<format_description::FormatItem<'static>>,
108 rotation: Rotation,
109 next_date: AtomicUsize,
110 max_files: Option<usize>,
111}
112
113impl RollingFileAppender {
116 pub fn new(
142 rotation: Rotation,
143 directory: impl AsRef<Path>,
144 filename_prefix: impl AsRef<Path>,
145 ) -> RollingFileAppender {
146 let filename_prefix = filename_prefix
147 .as_ref()
148 .to_str()
149 .expect("filename prefix must be a valid UTF-8 string");
150 Self::builder()
151 .rotation(rotation)
152 .filename_prefix(filename_prefix)
153 .build(directory)
154 .expect("initializing rolling file appender failed")
155 }
156
157 #[must_use]
183 pub fn builder() -> Builder {
184 Builder::new()
185 }
186
187 fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
188 let Builder {
189 ref rotation,
190 ref prefix,
191 ref suffix,
192 ref max_files,
193 } = builder;
194 let directory = directory.as_ref().to_path_buf();
195 let now = OffsetDateTime::now_utc();
196 let (state, writer) = Inner::new(
197 now,
198 rotation.clone(),
199 directory,
200 prefix.clone(),
201 suffix.clone(),
202 *max_files,
203 )?;
204 Ok(Self {
205 state,
206 writer,
207 #[cfg(test)]
208 now: Box::new(OffsetDateTime::now_utc),
209 })
210 }
211
212 #[inline]
213 fn now(&self) -> OffsetDateTime {
214 #[cfg(test)]
215 return (self.now)();
216
217 #[cfg(not(test))]
218 OffsetDateTime::now_utc()
219 }
220}
221
222impl io::Write for RollingFileAppender {
223 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
224 let now = self.now();
225 let writer = self.writer.get_mut();
226 if let Some(current_time) = self.state.should_rollover(now) {
227 let _did_cas = self.state.advance_date(now, current_time);
228 debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
229 self.state.refresh_writer(now, writer);
230 }
231 writer.write(buf)
232 }
233
234 fn flush(&mut self) -> io::Result<()> {
235 self.writer.get_mut().flush()
236 }
237}
238
239impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
240 type Writer = RollingWriter<'a>;
241 fn make_writer(&'a self) -> Self::Writer {
242 let now = self.now();
243
244 if let Some(current_time) = self.state.should_rollover(now) {
246 if self.state.advance_date(now, current_time) {
249 self.state.refresh_writer(now, &mut self.writer.write());
250 }
251 }
252 RollingWriter(self.writer.read())
253 }
254}
255
256impl fmt::Debug for RollingFileAppender {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260 f.debug_struct("RollingFileAppender")
261 .field("state", &self.state)
262 .field("writer", &self.writer)
263 .finish()
264 }
265}
266
267pub fn minutely(
296 directory: impl AsRef<Path>,
297 file_name_prefix: impl AsRef<Path>,
298) -> RollingFileAppender {
299 RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
300}
301
302pub fn hourly(
331 directory: impl AsRef<Path>,
332 file_name_prefix: impl AsRef<Path>,
333) -> RollingFileAppender {
334 RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
335}
336
337pub fn daily(
367 directory: impl AsRef<Path>,
368 file_name_prefix: impl AsRef<Path>,
369) -> RollingFileAppender {
370 RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
371}
372
373pub fn weekly(
403 directory: impl AsRef<Path>,
404 file_name_prefix: impl AsRef<Path>,
405) -> RollingFileAppender {
406 RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
407}
408
409pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
437 RollingFileAppender::new(Rotation::NEVER, directory, file_name)
438}
439
440#[derive(Clone, Eq, PartialEq, Debug)]
484pub struct Rotation(RotationKind);
485
486#[derive(Clone, Eq, PartialEq, Debug)]
487enum RotationKind {
488 Minutely,
489 Hourly,
490 Daily,
491 Weekly,
492 Never,
493}
494
495impl Rotation {
496 pub const MINUTELY: Self = Self(RotationKind::Minutely);
498 pub const HOURLY: Self = Self(RotationKind::Hourly);
500 pub const DAILY: Self = Self(RotationKind::Daily);
502 pub const WEEKLY: Self = Self(RotationKind::Weekly);
504 pub const NEVER: Self = Self(RotationKind::Never);
506
507 pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
509 let unrounded_next_date = match *self {
510 Rotation::MINUTELY => *current_date + Duration::minutes(1),
511 Rotation::HOURLY => *current_date + Duration::hours(1),
512 Rotation::DAILY => *current_date + Duration::days(1),
513 Rotation::WEEKLY => *current_date + Duration::weeks(1),
514 Rotation::NEVER => return None,
515 };
516 Some(self.round_date(unrounded_next_date))
517 }
518
519 pub(crate) fn round_date(&self, date: OffsetDateTime) -> OffsetDateTime {
525 match *self {
526 Rotation::MINUTELY => {
527 let time = Time::from_hms(date.hour(), date.minute(), 0)
528 .expect("Invalid time; this is a bug in tracing-appender");
529 date.replace_time(time)
530 }
531 Rotation::HOURLY => {
532 let time = Time::from_hms(date.hour(), 0, 0)
533 .expect("Invalid time; this is a bug in tracing-appender");
534 date.replace_time(time)
535 }
536 Rotation::DAILY => {
537 let time = Time::from_hms(0, 0, 0)
538 .expect("Invalid time; this is a bug in tracing-appender");
539 date.replace_time(time)
540 }
541 Rotation::WEEKLY => {
542 let zero_time = Time::from_hms(0, 0, 0)
543 .expect("Invalid time; this is a bug in tracing-appender");
544
545 let days_since_sunday = date.weekday().number_days_from_sunday();
546 let date = date - Duration::days(days_since_sunday.into());
547 date.replace_time(zero_time)
548 }
549 Rotation::NEVER => {
551 unreachable!("Rotation::NEVER is impossible to round.")
552 }
553 }
554 }
555
556 fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
557 match *self {
558 Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
559 Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
560 Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
561 Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
562 Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
563 }
564 .expect("Unable to create a formatter; this is a bug in tracing-appender")
565 }
566}
567
568impl io::Write for RollingWriter<'_> {
571 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
572 (&*self.0).write(buf)
573 }
574
575 fn flush(&mut self) -> io::Result<()> {
576 (&*self.0).flush()
577 }
578}
579
580impl Inner {
583 fn new(
584 now: OffsetDateTime,
585 rotation: Rotation,
586 directory: impl AsRef<Path>,
587 log_filename_prefix: Option<String>,
588 log_filename_suffix: Option<String>,
589 max_files: Option<usize>,
590 ) -> Result<(Self, RwLock<File>), builder::InitError> {
591 let log_directory = directory.as_ref().to_path_buf();
592 let date_format = rotation.date_format();
593 let next_date = rotation.next_date(&now);
594
595 let inner = Inner {
596 log_directory,
597 log_filename_prefix,
598 log_filename_suffix,
599 date_format,
600 next_date: AtomicUsize::new(
601 next_date
602 .map(|date| date.unix_timestamp() as usize)
603 .unwrap_or(0),
604 ),
605 rotation,
606 max_files,
607 };
608 let filename = inner.join_date(&now);
609 let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
610 Ok((inner, writer))
611 }
612
613 pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
615 let date = if let Rotation::NEVER = self.rotation {
616 date.format(&self.date_format)
617 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
618 } else {
619 self.rotation
620 .round_date(*date)
621 .format(&self.date_format)
622 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
623 };
624
625 match (
626 &self.rotation,
627 &self.log_filename_prefix,
628 &self.log_filename_suffix,
629 ) {
630 (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
631 (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
632 (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
633 (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
634 (_, Some(filename), None) => format!("{}.{}", filename, date),
635 (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
636 (_, None, None) => date,
637 }
638 }
639
640 fn prune_old_logs(&self, max_files: usize) {
641 let files = fs::read_dir(&self.log_directory).map(|dir| {
642 dir.filter_map(|entry| {
643 let entry = entry.ok()?;
644 let metadata = entry.metadata().ok()?;
645
646 if !metadata.is_file() {
649 return None;
650 }
651
652 let filename = entry.file_name();
653 let filename = filename.to_str()?;
655 if let Some(prefix) = &self.log_filename_prefix {
656 if !filename.starts_with(prefix) {
657 return None;
658 }
659 }
660
661 if let Some(suffix) = &self.log_filename_suffix {
662 if !filename.ends_with(suffix) {
663 return None;
664 }
665 }
666
667 if self.log_filename_prefix.is_none()
668 && self.log_filename_suffix.is_none()
669 && Date::parse(filename, &self.date_format).is_err()
670 {
671 return None;
672 }
673
674 let created = metadata.created().ok()?;
675 Some((entry, created))
676 })
677 .collect::<Vec<_>>()
678 });
679
680 let mut files = match files {
681 Ok(files) => files,
682 Err(error) => {
683 eprintln!("Error reading the log directory/files: {}", error);
684 return;
685 }
686 };
687 if files.len() < max_files {
688 return;
689 }
690
691 files.sort_by_key(|(_, created_at)| *created_at);
693
694 for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
696 if let Err(error) = fs::remove_file(file.path()) {
697 eprintln!(
698 "Failed to remove old log file {}: {}",
699 file.path().display(),
700 error
701 );
702 }
703 }
704 }
705
706 fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
707 let filename = self.join_date(&now);
708
709 if let Some(max_files) = self.max_files {
710 self.prune_old_logs(max_files);
711 }
712
713 match create_writer(&self.log_directory, &filename) {
714 Ok(new_file) => {
715 if let Err(err) = file.flush() {
716 eprintln!("Couldn't flush previous writer: {}", err);
717 }
718 *file = new_file;
719 }
720 Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
721 }
722 }
723
724 fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
733 let next_date = self.next_date.load(Ordering::Acquire);
734 if next_date == 0 {
736 return None;
737 }
738
739 if date.unix_timestamp() as usize >= next_date {
740 return Some(next_date);
741 }
742
743 None
744 }
745
746 fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
747 let next_date = self
748 .rotation
749 .next_date(&now)
750 .map(|date| date.unix_timestamp() as usize)
751 .unwrap_or(0);
752 self.next_date
753 .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
754 .is_ok()
755 }
756}
757
758fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
759 let path = directory.join(filename);
760 let mut open_options = OpenOptions::new();
761 open_options.append(true).create(true);
762
763 let new_file = open_options.open(path.as_path());
764 if new_file.is_err() {
765 if let Some(parent) = path.parent() {
766 fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
767 return open_options
768 .open(path)
769 .map_err(InitError::ctx("failed to create initial log file"));
770 }
771 }
772
773 new_file.map_err(InitError::ctx("failed to create initial log file"))
774}
775
776#[cfg(test)]
777mod test {
778 use super::*;
779 use std::fs;
780 use std::io::Write;
781
782 fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
783 let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
784
785 for entry in dir_contents {
786 let path = entry.expect("Expected dir entry").path();
787 let file = fs::read_to_string(&path).expect("Failed to read file");
788 println!("path={}\nfile={:?}", path.display(), file);
789
790 if file.as_str() == expected_value {
791 return true;
792 }
793 }
794
795 false
796 }
797
798 fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
799 appender
800 .write_all(msg.as_bytes())
801 .expect("Failed to write to appender");
802 appender.flush().expect("Failed to flush!");
803 }
804
805 fn test_appender(rotation: Rotation, file_prefix: &str) {
806 let directory = tempfile::tempdir().expect("failed to create tempdir");
807 let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
808
809 let expected_value = "Hello";
810 write_to_log(&mut appender, expected_value);
811 assert!(find_str_in_log(directory.path(), expected_value));
812
813 directory
814 .close()
815 .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
816 }
817
818 #[test]
819 fn write_minutely_log() {
820 test_appender(Rotation::MINUTELY, "minutely.log");
821 }
822
823 #[test]
824 fn write_hourly_log() {
825 test_appender(Rotation::HOURLY, "hourly.log");
826 }
827
828 #[test]
829 fn write_daily_log() {
830 test_appender(Rotation::DAILY, "daily.log");
831 }
832
833 #[test]
834 fn write_weekly_log() {
835 test_appender(Rotation::WEEKLY, "weekly.log");
836 }
837
838 #[test]
839 fn write_never_log() {
840 test_appender(Rotation::NEVER, "never.log");
841 }
842
843 #[test]
844 fn test_rotations() {
845 let now = OffsetDateTime::now_utc();
847 let next = Rotation::MINUTELY.next_date(&now).unwrap();
848 assert_eq!((now + Duration::MINUTE).minute(), next.minute());
849
850 let now = OffsetDateTime::now_utc();
852 let next = Rotation::HOURLY.next_date(&now).unwrap();
853 assert_eq!((now + Duration::HOUR).hour(), next.hour());
854
855 let now = OffsetDateTime::now_utc();
857 let next = Rotation::DAILY.next_date(&now).unwrap();
858 assert_eq!((now + Duration::DAY).day(), next.day());
859
860 let now = OffsetDateTime::now_utc();
862 let now_rounded = Rotation::WEEKLY.round_date(now);
863 let next = Rotation::WEEKLY.next_date(&now).unwrap();
864 assert!(now_rounded < next);
865
866 let now = OffsetDateTime::now_utc();
868 let next = Rotation::NEVER.next_date(&now);
869 assert!(next.is_none());
870 }
871
872 #[test]
873 fn test_join_date() {
874 struct TestCase {
875 expected: &'static str,
876 rotation: Rotation,
877 prefix: Option<&'static str>,
878 suffix: Option<&'static str>,
879 now: OffsetDateTime,
880 }
881
882 let format = format_description::parse(
883 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
884 sign:mandatory]:[offset_minute]:[offset_second]",
885 )
886 .unwrap();
887 let directory = tempfile::tempdir().expect("failed to create tempdir");
888
889 let test_cases = vec![
890 TestCase {
891 expected: "my_prefix.2025-02-16.log",
892 rotation: Rotation::WEEKLY,
893 prefix: Some("my_prefix"),
894 suffix: Some("log"),
895 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
896 },
897 TestCase {
899 expected: "my_prefix.2024-12-29.log",
900 rotation: Rotation::WEEKLY,
901 prefix: Some("my_prefix"),
902 suffix: Some("log"),
903 now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
904 },
905 TestCase {
906 expected: "my_prefix.2025-02-17.log",
907 rotation: Rotation::DAILY,
908 prefix: Some("my_prefix"),
909 suffix: Some("log"),
910 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
911 },
912 TestCase {
913 expected: "my_prefix.2025-02-17-10.log",
914 rotation: Rotation::HOURLY,
915 prefix: Some("my_prefix"),
916 suffix: Some("log"),
917 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
918 },
919 TestCase {
920 expected: "my_prefix.2025-02-17-10-01.log",
921 rotation: Rotation::MINUTELY,
922 prefix: Some("my_prefix"),
923 suffix: Some("log"),
924 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
925 },
926 TestCase {
927 expected: "my_prefix.log",
928 rotation: Rotation::NEVER,
929 prefix: Some("my_prefix"),
930 suffix: Some("log"),
931 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
932 },
933 ];
934
935 for test_case in test_cases {
936 let (inner, _) = Inner::new(
937 test_case.now,
938 test_case.rotation.clone(),
939 directory.path(),
940 test_case.prefix.map(ToString::to_string),
941 test_case.suffix.map(ToString::to_string),
942 None,
943 )
944 .unwrap();
945 let path = inner.join_date(&test_case.now);
946
947 assert_eq!(path, test_case.expected);
948 }
949 }
950
951 #[test]
952 #[should_panic(
953 expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
954 )]
955 fn test_never_date_rounding() {
956 let now = OffsetDateTime::now_utc();
957 let _ = Rotation::NEVER.round_date(now);
958 }
959
960 #[test]
961 fn test_path_concatenation() {
962 let format = format_description::parse(
963 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
964 sign:mandatory]:[offset_minute]:[offset_second]",
965 )
966 .unwrap();
967 let directory = tempfile::tempdir().expect("failed to create tempdir");
968
969 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
970
971 struct TestCase {
972 expected: &'static str,
973 rotation: Rotation,
974 prefix: Option<&'static str>,
975 suffix: Option<&'static str>,
976 }
977
978 let test = |TestCase {
979 expected,
980 rotation,
981 prefix,
982 suffix,
983 }| {
984 let (inner, _) = Inner::new(
985 now,
986 rotation.clone(),
987 directory.path(),
988 prefix.map(ToString::to_string),
989 suffix.map(ToString::to_string),
990 None,
991 )
992 .unwrap();
993 let path = inner.join_date(&now);
994 assert_eq!(
995 expected, path,
996 "rotation = {:?}, prefix = {:?}, suffix = {:?}",
997 rotation, prefix, suffix
998 );
999 };
1000
1001 let test_cases = vec![
1002 TestCase {
1004 expected: "app.log.2020-02-01-10-01",
1005 rotation: Rotation::MINUTELY,
1006 prefix: Some("app.log"),
1007 suffix: None,
1008 },
1009 TestCase {
1010 expected: "app.log.2020-02-01-10",
1011 rotation: Rotation::HOURLY,
1012 prefix: Some("app.log"),
1013 suffix: None,
1014 },
1015 TestCase {
1016 expected: "app.log.2020-02-01",
1017 rotation: Rotation::DAILY,
1018 prefix: Some("app.log"),
1019 suffix: None,
1020 },
1021 TestCase {
1022 expected: "app.log",
1023 rotation: Rotation::NEVER,
1024 prefix: Some("app.log"),
1025 suffix: None,
1026 },
1027 TestCase {
1029 expected: "app.2020-02-01-10-01.log",
1030 rotation: Rotation::MINUTELY,
1031 prefix: Some("app"),
1032 suffix: Some("log"),
1033 },
1034 TestCase {
1035 expected: "app.2020-02-01-10.log",
1036 rotation: Rotation::HOURLY,
1037 prefix: Some("app"),
1038 suffix: Some("log"),
1039 },
1040 TestCase {
1041 expected: "app.2020-02-01.log",
1042 rotation: Rotation::DAILY,
1043 prefix: Some("app"),
1044 suffix: Some("log"),
1045 },
1046 TestCase {
1047 expected: "app.log",
1048 rotation: Rotation::NEVER,
1049 prefix: Some("app"),
1050 suffix: Some("log"),
1051 },
1052 TestCase {
1054 expected: "2020-02-01-10-01.log",
1055 rotation: Rotation::MINUTELY,
1056 prefix: None,
1057 suffix: Some("log"),
1058 },
1059 TestCase {
1060 expected: "2020-02-01-10.log",
1061 rotation: Rotation::HOURLY,
1062 prefix: None,
1063 suffix: Some("log"),
1064 },
1065 TestCase {
1066 expected: "2020-02-01.log",
1067 rotation: Rotation::DAILY,
1068 prefix: None,
1069 suffix: Some("log"),
1070 },
1071 TestCase {
1072 expected: "log",
1073 rotation: Rotation::NEVER,
1074 prefix: None,
1075 suffix: Some("log"),
1076 },
1077 ];
1078 for test_case in test_cases {
1079 test(test_case)
1080 }
1081 }
1082
1083 #[test]
1084 fn test_make_writer() {
1085 use std::sync::{Arc, Mutex};
1086 use tracing_subscriber::prelude::*;
1087
1088 let format = format_description::parse(
1089 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1090 sign:mandatory]:[offset_minute]:[offset_second]",
1091 )
1092 .unwrap();
1093
1094 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1095 let directory = tempfile::tempdir().expect("failed to create tempdir");
1096 let (state, writer) = Inner::new(
1097 now,
1098 Rotation::HOURLY,
1099 directory.path(),
1100 Some("test_make_writer".to_string()),
1101 None,
1102 None,
1103 )
1104 .unwrap();
1105
1106 let clock = Arc::new(Mutex::new(now));
1107 let now = {
1108 let clock = clock.clone();
1109 Box::new(move || *clock.lock().unwrap())
1110 };
1111 let appender = RollingFileAppender { state, writer, now };
1112 let default = tracing_subscriber::fmt()
1113 .without_time()
1114 .with_level(false)
1115 .with_target(false)
1116 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1117 .with_writer(appender)
1118 .finish()
1119 .set_default();
1120
1121 tracing::info!("file 1");
1122
1123 (*clock.lock().unwrap()) += Duration::seconds(1);
1125
1126 tracing::info!("file 1");
1127
1128 (*clock.lock().unwrap()) += Duration::hours(1);
1130
1131 tracing::info!("file 2");
1132
1133 (*clock.lock().unwrap()) += Duration::seconds(1);
1135
1136 tracing::info!("file 2");
1137
1138 drop(default);
1139
1140 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1141 println!("dir={:?}", dir_contents);
1142 for entry in dir_contents {
1143 println!("entry={:?}", entry);
1144 let path = entry.expect("Expected dir entry").path();
1145 let file = fs::read_to_string(&path).expect("Failed to read file");
1146 println!("path={}\nfile={:?}", path.display(), file);
1147
1148 match path
1149 .extension()
1150 .expect("found a file without a date!")
1151 .to_str()
1152 .expect("extension should be UTF8")
1153 {
1154 "2020-02-01-10" => {
1155 assert_eq!("file 1\nfile 1\n", file);
1156 }
1157 "2020-02-01-11" => {
1158 assert_eq!("file 2\nfile 2\n", file);
1159 }
1160 x => panic!("unexpected date {}", x),
1161 }
1162 }
1163 }
1164
1165 #[test]
1166 fn test_max_log_files() {
1167 use std::sync::{Arc, Mutex};
1168 use tracing_subscriber::prelude::*;
1169
1170 let format = format_description::parse(
1171 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1172 sign:mandatory]:[offset_minute]:[offset_second]",
1173 )
1174 .unwrap();
1175
1176 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1177 let directory = tempfile::tempdir().expect("failed to create tempdir");
1178 let (state, writer) = Inner::new(
1179 now,
1180 Rotation::HOURLY,
1181 directory.path(),
1182 Some("test_max_log_files".to_string()),
1183 None,
1184 Some(2),
1185 )
1186 .unwrap();
1187
1188 let clock = Arc::new(Mutex::new(now));
1189 let now = {
1190 let clock = clock.clone();
1191 Box::new(move || *clock.lock().unwrap())
1192 };
1193 let appender = RollingFileAppender { state, writer, now };
1194 let default = tracing_subscriber::fmt()
1195 .without_time()
1196 .with_level(false)
1197 .with_target(false)
1198 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1199 .with_writer(appender)
1200 .finish()
1201 .set_default();
1202
1203 tracing::info!("file 1");
1204
1205 (*clock.lock().unwrap()) += Duration::seconds(1);
1207
1208 tracing::info!("file 1");
1209
1210 (*clock.lock().unwrap()) += Duration::hours(1);
1212
1213 std::thread::sleep(std::time::Duration::from_secs(1));
1217
1218 tracing::info!("file 2");
1219
1220 (*clock.lock().unwrap()) += Duration::seconds(1);
1222
1223 tracing::info!("file 2");
1224
1225 (*clock.lock().unwrap()) += Duration::hours(1);
1227
1228 std::thread::sleep(std::time::Duration::from_secs(1));
1230
1231 tracing::info!("file 3");
1232
1233 (*clock.lock().unwrap()) += Duration::seconds(1);
1235
1236 tracing::info!("file 3");
1237
1238 drop(default);
1239
1240 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1241 println!("dir={:?}", dir_contents);
1242
1243 for entry in dir_contents {
1244 println!("entry={:?}", entry);
1245 let path = entry.expect("Expected dir entry").path();
1246 let file = fs::read_to_string(&path).expect("Failed to read file");
1247 println!("path={}\nfile={:?}", path.display(), file);
1248
1249 match path
1250 .extension()
1251 .expect("found a file without a date!")
1252 .to_str()
1253 .expect("extension should be UTF8")
1254 {
1255 "2020-02-01-10" => {
1256 panic!("this file should have been pruned already!");
1257 }
1258 "2020-02-01-11" => {
1259 assert_eq!("file 2\nfile 2\n", file);
1260 }
1261 "2020-02-01-12" => {
1262 assert_eq!("file 3\nfile 3\n", file);
1263 }
1264 x => panic!("unexpected date {}", x),
1265 }
1266 }
1267 }
1268}