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, PrimitiveDateTime, 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
609 if let Some(max_files) = max_files {
610 inner.prune_old_logs(max_files);
611 }
612
613 let filename = inner.join_date(&now);
614 let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
615 Ok((inner, writer))
616 }
617
618 pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
620 let date = if let Rotation::NEVER = self.rotation {
621 date.format(&self.date_format)
622 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
623 } else {
624 self.rotation
625 .round_date(*date)
626 .format(&self.date_format)
627 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
628 };
629
630 match (
631 &self.rotation,
632 &self.log_filename_prefix,
633 &self.log_filename_suffix,
634 ) {
635 (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
636 (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
637 (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
638 (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
639 (_, Some(filename), None) => format!("{}.{}", filename, date),
640 (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
641 (_, None, None) => date,
642 }
643 }
644
645 fn prune_old_logs(&self, max_files: usize) {
646 let files = fs::read_dir(&self.log_directory).map(|dir| {
647 dir.filter_map(|entry| {
648 let entry = entry.ok()?;
649 let metadata = entry.metadata().ok()?;
650
651 if !metadata.is_file() {
654 return None;
655 }
656
657 let filename = entry.file_name();
658 let filename = filename.to_str()?;
660 if let Some(prefix) = &self.log_filename_prefix {
661 if !filename.starts_with(prefix) {
662 return None;
663 }
664 }
665
666 if let Some(suffix) = &self.log_filename_suffix {
667 if !filename.ends_with(suffix) {
668 return None;
669 }
670 }
671
672 if self.log_filename_prefix.is_none()
673 && self.log_filename_suffix.is_none()
674 && Date::parse(filename, &self.date_format).is_err()
675 {
676 return None;
677 }
678
679 let created = metadata.created().ok().or_else(|| {
680 let mut datetime = filename;
681 if let Some(prefix) = &self.log_filename_prefix {
682 datetime = datetime.strip_prefix(prefix)?;
683 datetime = datetime.strip_prefix('.')?;
684 }
685 if let Some(suffix) = &self.log_filename_suffix {
686 datetime = datetime.strip_suffix(suffix)?;
687 datetime = datetime.strip_suffix('.')?;
688 }
689
690 Some(
691 PrimitiveDateTime::parse(datetime, &self.date_format)
692 .ok()?
693 .assume_utc()
694 .into(),
695 )
696 })?;
697 Some((entry, created))
698 })
699 .collect::<Vec<_>>()
700 });
701
702 let mut files = match files {
703 Ok(files) => files,
704 Err(error) => {
705 eprintln!("Error reading the log directory/files: {}", error);
706 return;
707 }
708 };
709 if files.len() < max_files {
710 return;
711 }
712
713 files.sort_by_key(|(_, created_at)| *created_at);
715
716 for (file, _) in files.iter().take(files.len() - (max_files - 1)) {
718 if let Err(error) = fs::remove_file(file.path()) {
719 eprintln!(
720 "Failed to remove old log file {}: {}",
721 file.path().display(),
722 error
723 );
724 }
725 }
726 }
727
728 fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
729 let filename = self.join_date(&now);
730
731 if let Some(max_files) = self.max_files {
732 self.prune_old_logs(max_files);
733 }
734
735 match create_writer(&self.log_directory, &filename) {
736 Ok(new_file) => {
737 if let Err(err) = file.flush() {
738 eprintln!("Couldn't flush previous writer: {}", err);
739 }
740 *file = new_file;
741 }
742 Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
743 }
744 }
745
746 fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
755 let next_date = self.next_date.load(Ordering::Acquire);
756 if next_date == 0 {
758 return None;
759 }
760
761 if date.unix_timestamp() as usize >= next_date {
762 return Some(next_date);
763 }
764
765 None
766 }
767
768 fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
769 let next_date = self
770 .rotation
771 .next_date(&now)
772 .map(|date| date.unix_timestamp() as usize)
773 .unwrap_or(0);
774 self.next_date
775 .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
776 .is_ok()
777 }
778}
779
780fn create_writer(directory: &Path, filename: &str) -> Result<File, InitError> {
781 let path = directory.join(filename);
782 let mut open_options = OpenOptions::new();
783 open_options.append(true).create(true);
784
785 let new_file = open_options.open(path.as_path());
786 if new_file.is_err() {
787 if let Some(parent) = path.parent() {
788 fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
789 return open_options
790 .open(path)
791 .map_err(InitError::ctx("failed to create initial log file"));
792 }
793 }
794
795 new_file.map_err(InitError::ctx("failed to create initial log file"))
796}
797
798#[cfg(test)]
799mod test {
800 use super::*;
801 use std::fs;
802 use std::io::Write;
803
804 fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
805 let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
806
807 for entry in dir_contents {
808 let path = entry.expect("Expected dir entry").path();
809 let file = fs::read_to_string(&path).expect("Failed to read file");
810 println!("path={}\nfile={:?}", path.display(), file);
811
812 if file.as_str() == expected_value {
813 return true;
814 }
815 }
816
817 false
818 }
819
820 fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
821 appender
822 .write_all(msg.as_bytes())
823 .expect("Failed to write to appender");
824 appender.flush().expect("Failed to flush!");
825 }
826
827 fn test_appender(rotation: Rotation, file_prefix: &str) {
828 let directory = tempfile::tempdir().expect("failed to create tempdir");
829 let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
830
831 let expected_value = "Hello";
832 write_to_log(&mut appender, expected_value);
833 assert!(find_str_in_log(directory.path(), expected_value));
834
835 directory
836 .close()
837 .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
838 }
839
840 #[test]
841 fn write_minutely_log() {
842 test_appender(Rotation::MINUTELY, "minutely.log");
843 }
844
845 #[test]
846 fn write_hourly_log() {
847 test_appender(Rotation::HOURLY, "hourly.log");
848 }
849
850 #[test]
851 fn write_daily_log() {
852 test_appender(Rotation::DAILY, "daily.log");
853 }
854
855 #[test]
856 fn write_weekly_log() {
857 test_appender(Rotation::WEEKLY, "weekly.log");
858 }
859
860 #[test]
861 fn write_never_log() {
862 test_appender(Rotation::NEVER, "never.log");
863 }
864
865 #[test]
866 fn test_rotations() {
867 let now = OffsetDateTime::now_utc();
869 let next = Rotation::MINUTELY.next_date(&now).unwrap();
870 assert_eq!((now + Duration::MINUTE).minute(), next.minute());
871
872 let now = OffsetDateTime::now_utc();
874 let next = Rotation::HOURLY.next_date(&now).unwrap();
875 assert_eq!((now + Duration::HOUR).hour(), next.hour());
876
877 let now = OffsetDateTime::now_utc();
879 let next = Rotation::DAILY.next_date(&now).unwrap();
880 assert_eq!((now + Duration::DAY).day(), next.day());
881
882 let now = OffsetDateTime::now_utc();
884 let now_rounded = Rotation::WEEKLY.round_date(now);
885 let next = Rotation::WEEKLY.next_date(&now).unwrap();
886 assert!(now_rounded < next);
887
888 let now = OffsetDateTime::now_utc();
890 let next = Rotation::NEVER.next_date(&now);
891 assert!(next.is_none());
892 }
893
894 #[test]
895 fn test_join_date() {
896 struct TestCase {
897 expected: &'static str,
898 rotation: Rotation,
899 prefix: Option<&'static str>,
900 suffix: Option<&'static str>,
901 now: OffsetDateTime,
902 }
903
904 let format = format_description::parse(
905 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
906 sign:mandatory]:[offset_minute]:[offset_second]",
907 )
908 .unwrap();
909 let directory = tempfile::tempdir().expect("failed to create tempdir");
910
911 let test_cases = vec![
912 TestCase {
913 expected: "my_prefix.2025-02-16.log",
914 rotation: Rotation::WEEKLY,
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 {
921 expected: "my_prefix.2024-12-29.log",
922 rotation: Rotation::WEEKLY,
923 prefix: Some("my_prefix"),
924 suffix: Some("log"),
925 now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
926 },
927 TestCase {
928 expected: "my_prefix.2025-02-17.log",
929 rotation: Rotation::DAILY,
930 prefix: Some("my_prefix"),
931 suffix: Some("log"),
932 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
933 },
934 TestCase {
935 expected: "my_prefix.2025-02-17-10.log",
936 rotation: Rotation::HOURLY,
937 prefix: Some("my_prefix"),
938 suffix: Some("log"),
939 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
940 },
941 TestCase {
942 expected: "my_prefix.2025-02-17-10-01.log",
943 rotation: Rotation::MINUTELY,
944 prefix: Some("my_prefix"),
945 suffix: Some("log"),
946 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
947 },
948 TestCase {
949 expected: "my_prefix.log",
950 rotation: Rotation::NEVER,
951 prefix: Some("my_prefix"),
952 suffix: Some("log"),
953 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
954 },
955 ];
956
957 for test_case in test_cases {
958 let (inner, _) = Inner::new(
959 test_case.now,
960 test_case.rotation.clone(),
961 directory.path(),
962 test_case.prefix.map(ToString::to_string),
963 test_case.suffix.map(ToString::to_string),
964 None,
965 )
966 .unwrap();
967 let path = inner.join_date(&test_case.now);
968
969 assert_eq!(path, test_case.expected);
970 }
971 }
972
973 #[test]
974 #[should_panic(
975 expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
976 )]
977 fn test_never_date_rounding() {
978 let now = OffsetDateTime::now_utc();
979 let _ = Rotation::NEVER.round_date(now);
980 }
981
982 #[test]
983 fn test_path_concatenation() {
984 let format = format_description::parse(
985 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
986 sign:mandatory]:[offset_minute]:[offset_second]",
987 )
988 .unwrap();
989 let directory = tempfile::tempdir().expect("failed to create tempdir");
990
991 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
992
993 struct TestCase {
994 expected: &'static str,
995 rotation: Rotation,
996 prefix: Option<&'static str>,
997 suffix: Option<&'static str>,
998 }
999
1000 let test = |TestCase {
1001 expected,
1002 rotation,
1003 prefix,
1004 suffix,
1005 }| {
1006 let (inner, _) = Inner::new(
1007 now,
1008 rotation.clone(),
1009 directory.path(),
1010 prefix.map(ToString::to_string),
1011 suffix.map(ToString::to_string),
1012 None,
1013 )
1014 .unwrap();
1015 let path = inner.join_date(&now);
1016 assert_eq!(
1017 expected, path,
1018 "rotation = {:?}, prefix = {:?}, suffix = {:?}",
1019 rotation, prefix, suffix
1020 );
1021 };
1022
1023 let test_cases = vec![
1024 TestCase {
1026 expected: "app.log.2020-02-01-10-01",
1027 rotation: Rotation::MINUTELY,
1028 prefix: Some("app.log"),
1029 suffix: None,
1030 },
1031 TestCase {
1032 expected: "app.log.2020-02-01-10",
1033 rotation: Rotation::HOURLY,
1034 prefix: Some("app.log"),
1035 suffix: None,
1036 },
1037 TestCase {
1038 expected: "app.log.2020-02-01",
1039 rotation: Rotation::DAILY,
1040 prefix: Some("app.log"),
1041 suffix: None,
1042 },
1043 TestCase {
1044 expected: "app.log",
1045 rotation: Rotation::NEVER,
1046 prefix: Some("app.log"),
1047 suffix: None,
1048 },
1049 TestCase {
1051 expected: "app.2020-02-01-10-01.log",
1052 rotation: Rotation::MINUTELY,
1053 prefix: Some("app"),
1054 suffix: Some("log"),
1055 },
1056 TestCase {
1057 expected: "app.2020-02-01-10.log",
1058 rotation: Rotation::HOURLY,
1059 prefix: Some("app"),
1060 suffix: Some("log"),
1061 },
1062 TestCase {
1063 expected: "app.2020-02-01.log",
1064 rotation: Rotation::DAILY,
1065 prefix: Some("app"),
1066 suffix: Some("log"),
1067 },
1068 TestCase {
1069 expected: "app.log",
1070 rotation: Rotation::NEVER,
1071 prefix: Some("app"),
1072 suffix: Some("log"),
1073 },
1074 TestCase {
1076 expected: "2020-02-01-10-01.log",
1077 rotation: Rotation::MINUTELY,
1078 prefix: None,
1079 suffix: Some("log"),
1080 },
1081 TestCase {
1082 expected: "2020-02-01-10.log",
1083 rotation: Rotation::HOURLY,
1084 prefix: None,
1085 suffix: Some("log"),
1086 },
1087 TestCase {
1088 expected: "2020-02-01.log",
1089 rotation: Rotation::DAILY,
1090 prefix: None,
1091 suffix: Some("log"),
1092 },
1093 TestCase {
1094 expected: "log",
1095 rotation: Rotation::NEVER,
1096 prefix: None,
1097 suffix: Some("log"),
1098 },
1099 ];
1100 for test_case in test_cases {
1101 test(test_case)
1102 }
1103 }
1104
1105 #[test]
1106 fn test_make_writer() {
1107 use std::sync::{Arc, Mutex};
1108 use tracing_subscriber::prelude::*;
1109
1110 let format = format_description::parse(
1111 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1112 sign:mandatory]:[offset_minute]:[offset_second]",
1113 )
1114 .unwrap();
1115
1116 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1117 let directory = tempfile::tempdir().expect("failed to create tempdir");
1118 let (state, writer) = Inner::new(
1119 now,
1120 Rotation::HOURLY,
1121 directory.path(),
1122 Some("test_make_writer".to_string()),
1123 None,
1124 None,
1125 )
1126 .unwrap();
1127
1128 let clock = Arc::new(Mutex::new(now));
1129 let now = {
1130 let clock = clock.clone();
1131 Box::new(move || *clock.lock().unwrap())
1132 };
1133 let appender = RollingFileAppender { state, writer, now };
1134 let default = tracing_subscriber::fmt()
1135 .without_time()
1136 .with_level(false)
1137 .with_target(false)
1138 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1139 .with_writer(appender)
1140 .finish()
1141 .set_default();
1142
1143 tracing::info!("file 1");
1144
1145 (*clock.lock().unwrap()) += Duration::seconds(1);
1147
1148 tracing::info!("file 1");
1149
1150 (*clock.lock().unwrap()) += Duration::hours(1);
1152
1153 tracing::info!("file 2");
1154
1155 (*clock.lock().unwrap()) += Duration::seconds(1);
1157
1158 tracing::info!("file 2");
1159
1160 drop(default);
1161
1162 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1163 println!("dir={:?}", dir_contents);
1164 for entry in dir_contents {
1165 println!("entry={:?}", entry);
1166 let path = entry.expect("Expected dir entry").path();
1167 let file = fs::read_to_string(&path).expect("Failed to read file");
1168 println!("path={}\nfile={:?}", path.display(), file);
1169
1170 match path
1171 .extension()
1172 .expect("found a file without a date!")
1173 .to_str()
1174 .expect("extension should be UTF8")
1175 {
1176 "2020-02-01-10" => {
1177 assert_eq!("file 1\nfile 1\n", file);
1178 }
1179 "2020-02-01-11" => {
1180 assert_eq!("file 2\nfile 2\n", file);
1181 }
1182 x => panic!("unexpected date {}", x),
1183 }
1184 }
1185 }
1186
1187 #[test]
1188 fn test_max_log_files() {
1189 use std::sync::{Arc, Mutex};
1190 use tracing_subscriber::prelude::*;
1191
1192 let format = format_description::parse(
1193 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1194 sign:mandatory]:[offset_minute]:[offset_second]",
1195 )
1196 .unwrap();
1197
1198 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1199 let directory = tempfile::tempdir().expect("failed to create tempdir");
1200 let (state, writer) = Inner::new(
1201 now,
1202 Rotation::HOURLY,
1203 directory.path(),
1204 Some("test_max_log_files".to_string()),
1205 None,
1206 Some(2),
1207 )
1208 .unwrap();
1209
1210 let clock = Arc::new(Mutex::new(now));
1211 let now = {
1212 let clock = clock.clone();
1213 Box::new(move || *clock.lock().unwrap())
1214 };
1215 let appender = RollingFileAppender { state, writer, now };
1216 let default = tracing_subscriber::fmt()
1217 .without_time()
1218 .with_level(false)
1219 .with_target(false)
1220 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1221 .with_writer(appender)
1222 .finish()
1223 .set_default();
1224
1225 tracing::info!("file 1");
1226
1227 (*clock.lock().unwrap()) += Duration::seconds(1);
1229
1230 tracing::info!("file 1");
1231
1232 (*clock.lock().unwrap()) += Duration::hours(1);
1234
1235 std::thread::sleep(std::time::Duration::from_secs(1));
1239
1240 tracing::info!("file 2");
1241
1242 (*clock.lock().unwrap()) += Duration::seconds(1);
1244
1245 tracing::info!("file 2");
1246
1247 (*clock.lock().unwrap()) += Duration::hours(1);
1249
1250 std::thread::sleep(std::time::Duration::from_secs(1));
1252
1253 tracing::info!("file 3");
1254
1255 (*clock.lock().unwrap()) += Duration::seconds(1);
1257
1258 tracing::info!("file 3");
1259
1260 drop(default);
1261
1262 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1263 println!("dir={:?}", dir_contents);
1264
1265 for entry in dir_contents {
1266 println!("entry={:?}", entry);
1267 let path = entry.expect("Expected dir entry").path();
1268 let file = fs::read_to_string(&path).expect("Failed to read file");
1269 println!("path={}\nfile={:?}", path.display(), file);
1270
1271 match path
1272 .extension()
1273 .expect("found a file without a date!")
1274 .to_str()
1275 .expect("extension should be UTF8")
1276 {
1277 "2020-02-01-10" => {
1278 panic!("this file should have been pruned already!");
1279 }
1280 "2020-02-01-11" => {
1281 assert_eq!("file 2\nfile 2\n", file);
1282 }
1283 "2020-02-01-12" => {
1284 assert_eq!("file 3\nfile 3\n", file);
1285 }
1286 x => panic!("unexpected date {}", x),
1287 }
1288 }
1289 }
1290}