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 time::SystemTime,
37};
38use time::{format_description, Date, Duration, OffsetDateTime, PrimitiveDateTime, Time};
39
40mod builder;
41pub use builder::{Builder, InitError};
42
43pub struct RollingFileAppender {
88 state: Inner,
89 writer: RwLock<File>,
90 #[cfg(test)]
91 now: Box<dyn Fn() -> OffsetDateTime + Send + Sync>,
92}
93
94#[derive(Debug)]
101pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>);
102
103#[derive(Debug)]
104struct Inner {
105 log_directory: PathBuf,
106 log_filename_prefix: Option<String>,
107 log_filename_suffix: Option<String>,
108 log_latest_symlink_name: Option<String>,
109 date_format: Vec<format_description::FormatItem<'static>>,
110 rotation: Rotation,
111 next_date: AtomicUsize,
112 max_files: Option<usize>,
113}
114
115impl RollingFileAppender {
118 pub fn new(
144 rotation: Rotation,
145 directory: impl AsRef<Path>,
146 filename_prefix: impl AsRef<Path>,
147 ) -> RollingFileAppender {
148 let filename_prefix = filename_prefix
149 .as_ref()
150 .to_str()
151 .expect("filename prefix must be a valid UTF-8 string");
152 Self::builder()
153 .rotation(rotation)
154 .filename_prefix(filename_prefix)
155 .build(directory)
156 .expect("initializing rolling file appender failed")
157 }
158
159 #[must_use]
185 pub fn builder() -> Builder {
186 Builder::new()
187 }
188
189 fn from_builder(builder: &Builder, directory: impl AsRef<Path>) -> Result<Self, InitError> {
190 let Builder {
191 ref rotation,
192 ref prefix,
193 ref suffix,
194 ref latest_symlink,
195 ref max_files,
196 } = builder;
197 let directory = directory.as_ref().to_path_buf();
198 let now = OffsetDateTime::now_utc();
199 let (state, writer) = Inner::new(
200 now,
201 rotation.clone(),
202 directory,
203 prefix.clone(),
204 suffix.clone(),
205 latest_symlink.clone(),
206 *max_files,
207 )?;
208 Ok(Self {
209 state,
210 writer,
211 #[cfg(test)]
212 now: Box::new(OffsetDateTime::now_utc),
213 })
214 }
215
216 #[inline]
217 fn now(&self) -> OffsetDateTime {
218 #[cfg(test)]
219 return (self.now)();
220
221 #[cfg(not(test))]
222 OffsetDateTime::now_utc()
223 }
224}
225
226impl io::Write for RollingFileAppender {
227 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
228 let now = self.now();
229 let writer = self.writer.get_mut();
230 if let Some(current_time) = self.state.should_rollover(now) {
231 let _did_cas = self.state.advance_date(now, current_time);
232 debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
233 self.state.refresh_writer(now, writer);
234 }
235 writer.write(buf)
236 }
237
238 fn flush(&mut self) -> io::Result<()> {
239 self.writer.get_mut().flush()
240 }
241}
242
243impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender {
244 type Writer = RollingWriter<'a>;
245 fn make_writer(&'a self) -> Self::Writer {
246 let now = self.now();
247
248 if let Some(current_time) = self.state.should_rollover(now) {
250 if self.state.advance_date(now, current_time) {
253 self.state.refresh_writer(now, &mut self.writer.write());
254 }
255 }
256 RollingWriter(self.writer.read())
257 }
258}
259
260impl fmt::Debug for RollingFileAppender {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264 f.debug_struct("RollingFileAppender")
265 .field("state", &self.state)
266 .field("writer", &self.writer)
267 .finish()
268 }
269}
270
271pub fn minutely(
300 directory: impl AsRef<Path>,
301 file_name_prefix: impl AsRef<Path>,
302) -> RollingFileAppender {
303 RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix)
304}
305
306pub fn hourly(
335 directory: impl AsRef<Path>,
336 file_name_prefix: impl AsRef<Path>,
337) -> RollingFileAppender {
338 RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix)
339}
340
341pub fn daily(
371 directory: impl AsRef<Path>,
372 file_name_prefix: impl AsRef<Path>,
373) -> RollingFileAppender {
374 RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
375}
376
377pub fn weekly(
407 directory: impl AsRef<Path>,
408 file_name_prefix: impl AsRef<Path>,
409) -> RollingFileAppender {
410 RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
411}
412
413pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> RollingFileAppender {
441 RollingFileAppender::new(Rotation::NEVER, directory, file_name)
442}
443
444#[derive(Clone, Eq, PartialEq, Debug)]
488pub struct Rotation(RotationKind);
489
490#[derive(Clone, Eq, PartialEq, Debug)]
491enum RotationKind {
492 Minutely,
493 Hourly,
494 Daily,
495 Weekly,
496 Never,
497}
498
499impl Rotation {
500 pub const MINUTELY: Self = Self(RotationKind::Minutely);
502 pub const HOURLY: Self = Self(RotationKind::Hourly);
504 pub const DAILY: Self = Self(RotationKind::Daily);
506 pub const WEEKLY: Self = Self(RotationKind::Weekly);
508 pub const NEVER: Self = Self(RotationKind::Never);
510
511 pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
513 let unrounded_next_date = match *self {
514 Rotation::MINUTELY => *current_date + Duration::minutes(1),
515 Rotation::HOURLY => *current_date + Duration::hours(1),
516 Rotation::DAILY => *current_date + Duration::days(1),
517 Rotation::WEEKLY => *current_date + Duration::weeks(1),
518 Rotation::NEVER => return None,
519 };
520 Some(self.round_date(unrounded_next_date))
521 }
522
523 pub(crate) fn round_date(&self, date: OffsetDateTime) -> OffsetDateTime {
529 match *self {
530 Rotation::MINUTELY => {
531 let time = Time::from_hms(date.hour(), date.minute(), 0)
532 .expect("Invalid time; this is a bug in tracing-appender");
533 date.replace_time(time)
534 }
535 Rotation::HOURLY => {
536 let time = Time::from_hms(date.hour(), 0, 0)
537 .expect("Invalid time; this is a bug in tracing-appender");
538 date.replace_time(time)
539 }
540 Rotation::DAILY => {
541 let time = Time::from_hms(0, 0, 0)
542 .expect("Invalid time; this is a bug in tracing-appender");
543 date.replace_time(time)
544 }
545 Rotation::WEEKLY => {
546 let zero_time = Time::from_hms(0, 0, 0)
547 .expect("Invalid time; this is a bug in tracing-appender");
548
549 let days_since_sunday = date.weekday().number_days_from_sunday();
550 let date = date - Duration::days(days_since_sunday.into());
551 date.replace_time(zero_time)
552 }
553 Rotation::NEVER => {
555 unreachable!("Rotation::NEVER is impossible to round.")
556 }
557 }
558 }
559
560 fn date_format(&self) -> Vec<format_description::FormatItem<'static>> {
561 match *self {
562 Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
563 Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
564 Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
565 Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
566 Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
567 }
568 .expect("Unable to create a formatter; this is a bug in tracing-appender")
569 }
570}
571
572impl io::Write for RollingWriter<'_> {
575 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
576 (&*self.0).write(buf)
577 }
578
579 fn flush(&mut self) -> io::Result<()> {
580 (&*self.0).flush()
581 }
582}
583
584impl Inner {
587 fn new(
588 now: OffsetDateTime,
589 rotation: Rotation,
590 directory: impl AsRef<Path>,
591 log_filename_prefix: Option<String>,
592 log_filename_suffix: Option<String>,
593 log_latest_symlink_name: Option<String>,
594 max_files: Option<usize>,
595 ) -> Result<(Self, RwLock<File>), builder::InitError> {
596 let log_directory = directory.as_ref().to_path_buf();
597 let date_format = rotation.date_format();
598 let next_date = rotation.next_date(&now);
599
600 let inner = Inner {
601 log_directory,
602 log_filename_prefix,
603 log_filename_suffix,
604 log_latest_symlink_name,
605 date_format,
606 next_date: AtomicUsize::new(
607 next_date
608 .map(|date| date.unix_timestamp() as usize)
609 .unwrap_or(0),
610 ),
611 rotation,
612 max_files,
613 };
614
615 if let Some(max_files) = max_files {
616 inner.prune_old_logs(max_files);
617 }
618
619 let filename = inner.join_date(&now);
620 let writer = RwLock::new(create_writer(
621 inner.log_directory.as_ref(),
622 &filename,
623 inner.log_latest_symlink_name.as_deref(),
624 )?);
625 Ok((inner, writer))
626 }
627
628 pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
630 let date = if let Rotation::NEVER = self.rotation {
631 date.format(&self.date_format)
632 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
633 } else {
634 self.rotation
635 .round_date(*date)
636 .format(&self.date_format)
637 .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
638 };
639
640 match (
641 &self.rotation,
642 &self.log_filename_prefix,
643 &self.log_filename_suffix,
644 ) {
645 (&Rotation::NEVER, Some(filename), None) => filename.to_string(),
646 (&Rotation::NEVER, Some(filename), Some(suffix)) => format!("{}.{}", filename, suffix),
647 (&Rotation::NEVER, None, Some(suffix)) => suffix.to_string(),
648 (_, Some(filename), Some(suffix)) => format!("{}.{}.{}", filename, date, suffix),
649 (_, Some(filename), None) => format!("{}.{}", filename, date),
650 (_, None, Some(suffix)) => format!("{}.{}", date, suffix),
651 (_, None, None) => date,
652 }
653 }
654
655 fn prune_old_logs(&self, max_files: usize) {
656 let files = fs::read_dir(&self.log_directory).map(|dir| {
657 dir.filter_map(|entry| {
658 let entry = entry.ok()?;
659 let metadata = entry.metadata().ok()?;
660
661 if !metadata.is_file() {
664 return None;
665 }
666
667 let filename = entry.file_name();
668 let filename = filename.to_str()?;
670 if let Some(prefix) = &self.log_filename_prefix {
671 if !filename.starts_with(prefix) {
672 return None;
673 }
674 }
675
676 if let Some(suffix) = &self.log_filename_suffix {
677 if !filename.ends_with(suffix) {
678 return None;
679 }
680 }
681
682 if self.log_filename_prefix.is_none()
683 && self.log_filename_suffix.is_none()
684 && Date::parse(filename, &self.date_format).is_err()
685 {
686 return None;
687 }
688
689 let created = metadata.created().ok().or_else(|| {
690 parse_date_from_filename(
691 filename,
692 &self.date_format,
693 self.log_filename_prefix.as_deref(),
694 self.log_filename_suffix.as_deref(),
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(
736 &self.log_directory,
737 &filename,
738 self.log_latest_symlink_name.as_deref(),
739 ) {
740 Ok(new_file) => {
741 if let Err(err) = file.flush() {
742 eprintln!("Couldn't flush previous writer: {}", err);
743 }
744 *file = new_file;
745 }
746 Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
747 }
748 }
749
750 fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
759 let next_date = self.next_date.load(Ordering::Acquire);
760 if next_date == 0 {
762 return None;
763 }
764
765 if date.unix_timestamp() as usize >= next_date {
766 return Some(next_date);
767 }
768
769 None
770 }
771
772 fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
773 let next_date = self
774 .rotation
775 .next_date(&now)
776 .map(|date| date.unix_timestamp() as usize)
777 .unwrap_or(0);
778 self.next_date
779 .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
780 .is_ok()
781 }
782}
783
784fn create_writer(
785 directory: &Path,
786 filename: &str,
787 latest_symlink_name: Option<&str>,
788) -> Result<File, InitError> {
789 let path = directory.join(filename);
790 let mut open_options = OpenOptions::new();
791 open_options.append(true).create(true);
792
793 let new_file = open_options.open(&path).or_else(|_| {
794 if let Some(parent) = path.parent() {
795 fs::create_dir_all(parent).map_err(InitError::ctx("failed to create log directory"))?;
796 }
797 open_options
798 .open(&path)
799 .map_err(InitError::ctx("failed to create log file"))
800 })?;
801
802 if let Some(symlink_name) = latest_symlink_name {
803 let symlink_path = directory.join(symlink_name);
804 let _ = symlink::remove_symlink_file(&symlink_path);
805 symlink::symlink_file(path, symlink_path).map_err(InitError::ctx(
806 "failed to create symlink to latest log file",
807 ))?;
808 }
809
810 Ok(new_file)
811}
812
813fn parse_date_from_filename(
814 filename: &str,
815 date_format: &Vec<format_description::FormatItem<'static>>,
816 prefix: Option<&str>,
817 suffix: Option<&str>,
818) -> Option<SystemTime> {
819 let mut datetime = filename;
820 if let Some(prefix) = prefix {
821 datetime = datetime.strip_prefix(prefix)?;
822 datetime = datetime.strip_prefix('.')?;
823 }
824 if let Some(suffix) = suffix {
825 datetime = datetime.strip_suffix(suffix)?;
826 datetime = datetime.strip_suffix('.')?;
827 }
828
829 PrimitiveDateTime::parse(datetime, date_format)
830 .or_else(|_| Date::parse(datetime, date_format).map(|d| d.with_time(Time::MIDNIGHT)))
831 .ok()
832 .map(|dt| dt.assume_utc().into())
833}
834
835#[cfg(test)]
836mod test {
837 use super::*;
838 use std::fs;
839 use std::io::Write;
840
841 fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool {
842 let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory");
843
844 for entry in dir_contents {
845 let path = entry.expect("Expected dir entry").path();
846 let file = fs::read_to_string(&path).expect("Failed to read file");
847 println!("path={}\nfile={:?}", path.display(), file);
848
849 if file.as_str() == expected_value {
850 return true;
851 }
852 }
853
854 false
855 }
856
857 fn write_to_log(appender: &mut RollingFileAppender, msg: &str) {
858 appender
859 .write_all(msg.as_bytes())
860 .expect("Failed to write to appender");
861 appender.flush().expect("Failed to flush!");
862 }
863
864 fn test_appender(rotation: Rotation, file_prefix: &str) {
865 let directory = tempfile::tempdir().expect("failed to create tempdir");
866 let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix);
867
868 let expected_value = "Hello";
869 write_to_log(&mut appender, expected_value);
870 assert!(find_str_in_log(directory.path(), expected_value));
871
872 directory
873 .close()
874 .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.")
875 }
876
877 #[test]
878 fn write_minutely_log() {
879 test_appender(Rotation::MINUTELY, "minutely.log");
880 }
881
882 #[test]
883 fn write_hourly_log() {
884 test_appender(Rotation::HOURLY, "hourly.log");
885 }
886
887 #[test]
888 fn write_daily_log() {
889 test_appender(Rotation::DAILY, "daily.log");
890 }
891
892 #[test]
893 fn write_weekly_log() {
894 test_appender(Rotation::WEEKLY, "weekly.log");
895 }
896
897 #[test]
898 fn write_never_log() {
899 test_appender(Rotation::NEVER, "never.log");
900 }
901
902 #[test]
903 fn test_rotations() {
904 let now = OffsetDateTime::now_utc();
906 let next = Rotation::MINUTELY.next_date(&now).unwrap();
907 assert_eq!((now + Duration::MINUTE).minute(), next.minute());
908
909 let now = OffsetDateTime::now_utc();
911 let next = Rotation::HOURLY.next_date(&now).unwrap();
912 assert_eq!((now + Duration::HOUR).hour(), next.hour());
913
914 let now = OffsetDateTime::now_utc();
916 let next = Rotation::DAILY.next_date(&now).unwrap();
917 assert_eq!((now + Duration::DAY).day(), next.day());
918
919 let now = OffsetDateTime::now_utc();
921 let now_rounded = Rotation::WEEKLY.round_date(now);
922 let next = Rotation::WEEKLY.next_date(&now).unwrap();
923 assert!(now_rounded < next);
924
925 let now = OffsetDateTime::now_utc();
927 let next = Rotation::NEVER.next_date(&now);
928 assert!(next.is_none());
929 }
930
931 #[test]
932 fn test_join_date() {
933 struct TestCase {
934 expected: &'static str,
935 rotation: Rotation,
936 prefix: Option<&'static str>,
937 suffix: Option<&'static str>,
938 now: OffsetDateTime,
939 }
940
941 let format = format_description::parse(
942 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
943 sign:mandatory]:[offset_minute]:[offset_second]",
944 )
945 .unwrap();
946 let directory = tempfile::tempdir().expect("failed to create tempdir");
947
948 let test_cases = vec![
949 TestCase {
950 expected: "my_prefix.2025-02-16.log",
951 rotation: Rotation::WEEKLY,
952 prefix: Some("my_prefix"),
953 suffix: Some("log"),
954 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
955 },
956 TestCase {
958 expected: "my_prefix.2024-12-29.log",
959 rotation: Rotation::WEEKLY,
960 prefix: Some("my_prefix"),
961 suffix: Some("log"),
962 now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
963 },
964 TestCase {
965 expected: "my_prefix.2025-02-17.log",
966 rotation: Rotation::DAILY,
967 prefix: Some("my_prefix"),
968 suffix: Some("log"),
969 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
970 },
971 TestCase {
972 expected: "my_prefix.2025-02-17-10.log",
973 rotation: Rotation::HOURLY,
974 prefix: Some("my_prefix"),
975 suffix: Some("log"),
976 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
977 },
978 TestCase {
979 expected: "my_prefix.2025-02-17-10-01.log",
980 rotation: Rotation::MINUTELY,
981 prefix: Some("my_prefix"),
982 suffix: Some("log"),
983 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
984 },
985 TestCase {
986 expected: "my_prefix.log",
987 rotation: Rotation::NEVER,
988 prefix: Some("my_prefix"),
989 suffix: Some("log"),
990 now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
991 },
992 ];
993
994 for test_case in test_cases {
995 let (inner, _) = Inner::new(
996 test_case.now,
997 test_case.rotation.clone(),
998 directory.path(),
999 test_case.prefix.map(ToString::to_string),
1000 test_case.suffix.map(ToString::to_string),
1001 None,
1002 None,
1003 )
1004 .unwrap();
1005 let path = inner.join_date(&test_case.now);
1006
1007 assert_eq!(path, test_case.expected);
1008 }
1009 }
1010
1011 #[test]
1012 #[should_panic(
1013 expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
1014 )]
1015 fn test_never_date_rounding() {
1016 let now = OffsetDateTime::now_utc();
1017 let _ = Rotation::NEVER.round_date(now);
1018 }
1019
1020 #[test]
1021 fn test_path_concatenation() {
1022 let format = format_description::parse(
1023 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1024 sign:mandatory]:[offset_minute]:[offset_second]",
1025 )
1026 .unwrap();
1027 let directory = tempfile::tempdir().expect("failed to create tempdir");
1028
1029 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1030
1031 struct TestCase {
1032 expected: &'static str,
1033 rotation: Rotation,
1034 prefix: Option<&'static str>,
1035 suffix: Option<&'static str>,
1036 }
1037
1038 let test = |TestCase {
1039 expected,
1040 rotation,
1041 prefix,
1042 suffix,
1043 }| {
1044 let (inner, _) = Inner::new(
1045 now,
1046 rotation.clone(),
1047 directory.path(),
1048 prefix.map(ToString::to_string),
1049 suffix.map(ToString::to_string),
1050 None,
1051 None,
1052 )
1053 .unwrap();
1054 let path = inner.join_date(&now);
1055 assert_eq!(
1056 expected, path,
1057 "rotation = {:?}, prefix = {:?}, suffix = {:?}",
1058 rotation, prefix, suffix
1059 );
1060 };
1061
1062 let test_cases = vec![
1063 TestCase {
1065 expected: "app.log.2020-02-01-10-01",
1066 rotation: Rotation::MINUTELY,
1067 prefix: Some("app.log"),
1068 suffix: None,
1069 },
1070 TestCase {
1071 expected: "app.log.2020-02-01-10",
1072 rotation: Rotation::HOURLY,
1073 prefix: Some("app.log"),
1074 suffix: None,
1075 },
1076 TestCase {
1077 expected: "app.log.2020-02-01",
1078 rotation: Rotation::DAILY,
1079 prefix: Some("app.log"),
1080 suffix: None,
1081 },
1082 TestCase {
1083 expected: "app.log",
1084 rotation: Rotation::NEVER,
1085 prefix: Some("app.log"),
1086 suffix: None,
1087 },
1088 TestCase {
1090 expected: "app.2020-02-01-10-01.log",
1091 rotation: Rotation::MINUTELY,
1092 prefix: Some("app"),
1093 suffix: Some("log"),
1094 },
1095 TestCase {
1096 expected: "app.2020-02-01-10.log",
1097 rotation: Rotation::HOURLY,
1098 prefix: Some("app"),
1099 suffix: Some("log"),
1100 },
1101 TestCase {
1102 expected: "app.2020-02-01.log",
1103 rotation: Rotation::DAILY,
1104 prefix: Some("app"),
1105 suffix: Some("log"),
1106 },
1107 TestCase {
1108 expected: "app.log",
1109 rotation: Rotation::NEVER,
1110 prefix: Some("app"),
1111 suffix: Some("log"),
1112 },
1113 TestCase {
1115 expected: "2020-02-01-10-01.log",
1116 rotation: Rotation::MINUTELY,
1117 prefix: None,
1118 suffix: Some("log"),
1119 },
1120 TestCase {
1121 expected: "2020-02-01-10.log",
1122 rotation: Rotation::HOURLY,
1123 prefix: None,
1124 suffix: Some("log"),
1125 },
1126 TestCase {
1127 expected: "2020-02-01.log",
1128 rotation: Rotation::DAILY,
1129 prefix: None,
1130 suffix: Some("log"),
1131 },
1132 TestCase {
1133 expected: "log",
1134 rotation: Rotation::NEVER,
1135 prefix: None,
1136 suffix: Some("log"),
1137 },
1138 ];
1139 for test_case in test_cases {
1140 test(test_case)
1141 }
1142 }
1143
1144 #[test]
1145 fn test_make_writer() {
1146 use std::sync::{Arc, Mutex};
1147 use tracing_subscriber::prelude::*;
1148
1149 let format = format_description::parse(
1150 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1151 sign:mandatory]:[offset_minute]:[offset_second]",
1152 )
1153 .unwrap();
1154
1155 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1156 let directory = tempfile::tempdir().expect("failed to create tempdir");
1157 let (state, writer) = Inner::new(
1158 now,
1159 Rotation::HOURLY,
1160 directory.path(),
1161 Some("test_make_writer".to_string()),
1162 None,
1163 None,
1164 None,
1165 )
1166 .unwrap();
1167
1168 let clock = Arc::new(Mutex::new(now));
1169 let now = {
1170 let clock = clock.clone();
1171 Box::new(move || *clock.lock().unwrap())
1172 };
1173 let appender = RollingFileAppender { state, writer, now };
1174 let default = tracing_subscriber::fmt()
1175 .without_time()
1176 .with_level(false)
1177 .with_target(false)
1178 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1179 .with_writer(appender)
1180 .finish()
1181 .set_default();
1182
1183 tracing::info!("file 1");
1184
1185 (*clock.lock().unwrap()) += Duration::seconds(1);
1187
1188 tracing::info!("file 1");
1189
1190 (*clock.lock().unwrap()) += Duration::hours(1);
1192
1193 tracing::info!("file 2");
1194
1195 (*clock.lock().unwrap()) += Duration::seconds(1);
1197
1198 tracing::info!("file 2");
1199
1200 drop(default);
1201
1202 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1203 println!("dir={:?}", dir_contents);
1204 for entry in dir_contents {
1205 println!("entry={:?}", entry);
1206 let path = entry.expect("Expected dir entry").path();
1207 let file = fs::read_to_string(&path).expect("Failed to read file");
1208 println!("path={}\nfile={:?}", path.display(), file);
1209
1210 match path
1211 .extension()
1212 .expect("found a file without a date!")
1213 .to_str()
1214 .expect("extension should be UTF8")
1215 {
1216 "2020-02-01-10" => {
1217 assert_eq!("file 1\nfile 1\n", file);
1218 }
1219 "2020-02-01-11" => {
1220 assert_eq!("file 2\nfile 2\n", file);
1221 }
1222 x => panic!("unexpected date {}", x),
1223 }
1224 }
1225 }
1226
1227 #[test]
1228 fn test_max_log_files() {
1229 use std::sync::{Arc, Mutex};
1230 use tracing_subscriber::prelude::*;
1231
1232 let format = format_description::parse(
1233 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1234 sign:mandatory]:[offset_minute]:[offset_second]",
1235 )
1236 .unwrap();
1237
1238 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1239 let directory = tempfile::tempdir().expect("failed to create tempdir");
1240 let (state, writer) = Inner::new(
1241 now,
1242 Rotation::HOURLY,
1243 directory.path(),
1244 Some("test_max_log_files".to_string()),
1245 None,
1246 None,
1247 Some(2),
1248 )
1249 .unwrap();
1250
1251 let clock = Arc::new(Mutex::new(now));
1252 let now = {
1253 let clock = clock.clone();
1254 Box::new(move || *clock.lock().unwrap())
1255 };
1256 let appender = RollingFileAppender { state, writer, now };
1257 let default = tracing_subscriber::fmt()
1258 .without_time()
1259 .with_level(false)
1260 .with_target(false)
1261 .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE)
1262 .with_writer(appender)
1263 .finish()
1264 .set_default();
1265
1266 tracing::info!("file 1");
1267
1268 (*clock.lock().unwrap()) += Duration::seconds(1);
1270
1271 tracing::info!("file 1");
1272
1273 (*clock.lock().unwrap()) += Duration::hours(1);
1275
1276 std::thread::sleep(std::time::Duration::from_secs(1));
1280
1281 tracing::info!("file 2");
1282
1283 (*clock.lock().unwrap()) += Duration::seconds(1);
1285
1286 tracing::info!("file 2");
1287
1288 (*clock.lock().unwrap()) += Duration::hours(1);
1290
1291 std::thread::sleep(std::time::Duration::from_secs(1));
1293
1294 tracing::info!("file 3");
1295
1296 (*clock.lock().unwrap()) += Duration::seconds(1);
1298
1299 tracing::info!("file 3");
1300
1301 drop(default);
1302
1303 let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory");
1304 println!("dir={:?}", dir_contents);
1305
1306 for entry in dir_contents {
1307 println!("entry={:?}", entry);
1308 let path = entry.expect("Expected dir entry").path();
1309 let file = fs::read_to_string(&path).expect("Failed to read file");
1310 println!("path={}\nfile={:?}", path.display(), file);
1311
1312 match path
1313 .extension()
1314 .expect("found a file without a date!")
1315 .to_str()
1316 .expect("extension should be UTF8")
1317 {
1318 "2020-02-01-10" => {
1319 panic!("this file should have been pruned already!");
1320 }
1321 "2020-02-01-11" => {
1322 assert_eq!("file 2\nfile 2\n", file);
1323 }
1324 "2020-02-01-12" => {
1325 assert_eq!("file 3\nfile 3\n", file);
1326 }
1327 x => panic!("unexpected date {}", x),
1328 }
1329 }
1330 }
1331
1332 #[test]
1333 fn test_parse_date_from_filename_daily() {
1334 let date_format = Rotation::DAILY.date_format();
1335 let filename = "app.2020-02-01.log";
1336 let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
1337 assert_eq!(
1338 created,
1339 Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580515200))
1340 );
1341 }
1342
1343 #[test]
1344 fn test_parse_date_from_filename_hourly() {
1345 let date_format = Rotation::HOURLY.date_format();
1346 let filename = "app.2020-02-01-10.log";
1347 let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
1348 assert_eq!(
1349 created,
1350 Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580551200))
1351 );
1352 }
1353
1354 #[test]
1355 fn test_parse_date_from_filename_minutely() {
1356 let date_format = Rotation::MINUTELY.date_format();
1357 let filename = "app.2020-02-01-10-01.log";
1358 let created = parse_date_from_filename(filename, &date_format, Some("app"), Some("log"));
1359 assert_eq!(
1360 created,
1361 Some(SystemTime::UNIX_EPOCH + Duration::seconds(1580551260))
1362 );
1363 }
1364
1365 #[test]
1366 fn test_latest_symlink() {
1367 use std::sync::{Arc, Mutex};
1368
1369 let format = format_description::parse(
1370 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1371 sign:mandatory]:[offset_minute]:[offset_second]",
1372 )
1373 .unwrap();
1374
1375 let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap();
1376 let directory = tempfile::tempdir().expect("failed to create tempdir");
1377 let (state, writer) = Inner::new(
1378 now,
1379 Rotation::HOURLY,
1380 directory.path(),
1381 Some("test_latest_symlink".to_string()),
1382 None,
1383 Some("latest.log".to_string()),
1384 None,
1385 )
1386 .unwrap();
1387
1388 let symlink_path = directory.path().join("latest.log");
1390 assert!(symlink_path.is_symlink(), "latest.log should be a symlink");
1391 let target = fs::read_link(&symlink_path).expect("failed to read symlink");
1392 assert!(
1393 target.to_string_lossy().contains("2020-02-01-10"),
1394 "symlink should point to file with date 2020-02-01-10, but points to {:?}",
1395 target
1396 );
1397
1398 let clock = Arc::new(Mutex::new(now));
1400 let now_fn = {
1401 let clock = clock.clone();
1402 Box::new(move || *clock.lock().unwrap())
1403 };
1404 let mut appender = RollingFileAppender {
1405 state,
1406 writer,
1407 now: now_fn,
1408 };
1409
1410 *clock.lock().unwrap() += Duration::hours(1);
1412 appender.write_all(b"test\n").expect("failed to write");
1413 appender.flush().expect("failed to flush");
1414
1415 let target = fs::read_link(&symlink_path).expect("failed to read symlink");
1417 assert!(
1418 target.to_string_lossy().contains("2020-02-01-11"),
1419 "symlink should point to file with date 2020-02-01-11, but points to {:?}",
1420 target
1421 );
1422
1423 let content = fs::read_to_string(&symlink_path).expect("failed to read through symlink");
1425 assert_eq!("test\n", content);
1426 }
1427}