@@ -362,14 +362,50 @@ pub fn hourly(
362362/// }
363363/// ```
364364///
365- /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH `.
365+ /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
366366pub fn daily (
367367 directory : impl AsRef < Path > ,
368368 file_name_prefix : impl AsRef < Path > ,
369369) -> RollingFileAppender {
370370 RollingFileAppender :: new ( Rotation :: DAILY , directory, file_name_prefix)
371371}
372372
373+ /// Creates a weekly-rotating file appender. The logs will rotate every Sunday at midnight UTC.
374+ ///
375+ /// The appender returned by `rolling::weekly` can be used with `non_blocking` to create
376+ /// a non-blocking, weekly file appender.
377+ ///
378+ /// A `RollingFileAppender` has a fixed rotation whose frequency is
379+ /// defined by [`Rotation`][self::Rotation]. The `directory` and
380+ /// `file_name_prefix` arguments determine the location and file name's _prefix_
381+ /// of the log file. `RollingFileAppender` automatically appends the current date in UTC.
382+ ///
383+ /// # Examples
384+ ///
385+ /// ``` rust
386+ /// # #[clippy::allow(needless_doctest_main)]
387+ /// fn main () {
388+ /// # fn doc() {
389+ /// let appender = tracing_appender::rolling::weekly("/some/path", "rolling.log");
390+ /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
391+ ///
392+ /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender);
393+ ///
394+ /// tracing::subscriber::with_default(subscriber.finish(), || {
395+ /// tracing::event!(tracing::Level::INFO, "Hello");
396+ /// });
397+ /// # }
398+ /// }
399+ /// ```
400+ ///
401+ /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
402+ pub 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+
373409/// Creates a non-rolling file appender.
374410///
375411/// The appender returned by `rolling::never` can be used with `non_blocking` to create
@@ -429,6 +465,14 @@ pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> Rollin
429465/// # }
430466/// ```
431467///
468+ /// ### Weekly Rotation
469+ /// ```rust
470+ /// # fn docs() {
471+ /// use tracing_appender::rolling::Rotation;
472+ /// let rotation = tracing_appender::rolling::Rotation::WEEKLY;
473+ /// # }
474+ /// ```
475+ ///
432476/// ### No Rotation
433477/// ```rust
434478/// # fn docs() {
@@ -444,31 +488,40 @@ enum RotationKind {
444488 Minutely ,
445489 Hourly ,
446490 Daily ,
491+ Weekly ,
447492 Never ,
448493}
449494
450495impl Rotation {
451- /// Provides an minutely rotation
496+ /// Provides a minutely rotation.
452497 pub const MINUTELY : Self = Self ( RotationKind :: Minutely ) ;
453- /// Provides an hourly rotation
498+ /// Provides an hourly rotation.
454499 pub const HOURLY : Self = Self ( RotationKind :: Hourly ) ;
455- /// Provides a daily rotation
500+ /// Provides a daily rotation.
456501 pub const DAILY : Self = Self ( RotationKind :: Daily ) ;
502+ /// Provides a weekly rotation that rotates every Sunday at midnight UTC.
503+ pub const WEEKLY : Self = Self ( RotationKind :: Weekly ) ;
457504 /// Provides a rotation that never rotates.
458505 pub const NEVER : Self = Self ( RotationKind :: Never ) ;
459506
507+ /// Determines the next date that we should round to or `None` if `self` uses [`Rotation::NEVER`].
460508 pub ( crate ) fn next_date ( & self , current_date : & OffsetDateTime ) -> Option < OffsetDateTime > {
461509 let unrounded_next_date = match * self {
462510 Rotation :: MINUTELY => * current_date + Duration :: minutes ( 1 ) ,
463511 Rotation :: HOURLY => * current_date + Duration :: hours ( 1 ) ,
464512 Rotation :: DAILY => * current_date + Duration :: days ( 1 ) ,
513+ Rotation :: WEEKLY => * current_date + Duration :: weeks ( 1 ) ,
465514 Rotation :: NEVER => return None ,
466515 } ;
467- Some ( self . round_date ( & unrounded_next_date) )
516+ Some ( self . round_date ( unrounded_next_date) )
468517 }
469518
470- // note that this method will panic if passed a `Rotation::NEVER`.
471- pub ( crate ) fn round_date ( & self , date : & OffsetDateTime ) -> OffsetDateTime {
519+ /// Rounds the date towards the past using the [`Rotation`] interval.
520+ ///
521+ /// # Panics
522+ ///
523+ /// This method will panic if `self`` uses [`Rotation::NEVER`].
524+ pub ( crate ) fn round_date ( & self , date : OffsetDateTime ) -> OffsetDateTime {
472525 match * self {
473526 Rotation :: MINUTELY => {
474527 let time = Time :: from_hms ( date. hour ( ) , date. minute ( ) , 0 )
@@ -485,6 +538,14 @@ impl Rotation {
485538 . expect ( "Invalid time; this is a bug in tracing-appender" ) ;
486539 date. replace_time ( time)
487540 }
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+ }
488549 // Rotation::NEVER is impossible to round.
489550 Rotation :: NEVER => {
490551 unreachable ! ( "Rotation::NEVER is impossible to round." )
@@ -497,6 +558,7 @@ impl Rotation {
497558 Rotation :: MINUTELY => format_description:: parse ( "[year]-[month]-[day]-[hour]-[minute]" ) ,
498559 Rotation :: HOURLY => format_description:: parse ( "[year]-[month]-[day]-[hour]" ) ,
499560 Rotation :: DAILY => format_description:: parse ( "[year]-[month]-[day]" ) ,
561+ Rotation :: WEEKLY => format_description:: parse ( "[year]-[month]-[day]" ) ,
500562 Rotation :: NEVER => format_description:: parse ( "[year]-[month]-[day]" ) ,
501563 }
502564 . expect ( "Unable to create a formatter; this is a bug in tracing-appender" )
@@ -548,10 +610,17 @@ impl Inner {
548610 Ok ( ( inner, writer) )
549611 }
550612
613+ /// Returns the full filename for the provided date, using [`Rotation`] to round accordingly.
551614 pub ( crate ) fn join_date ( & self , date : & OffsetDateTime ) -> String {
552- let date = date
553- . format ( & self . date_format )
554- . expect ( "Unable to format OffsetDateTime; this is a bug in tracing-appender" ) ;
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+ } ;
555624
556625 match (
557626 & self . rotation ,
@@ -748,7 +817,7 @@ mod test {
748817
749818 #[ test]
750819 fn write_minutely_log ( ) {
751- test_appender ( Rotation :: HOURLY , "minutely.log" ) ;
820+ test_appender ( Rotation :: MINUTELY , "minutely.log" ) ;
752821 }
753822
754823 #[ test]
@@ -761,6 +830,11 @@ mod test {
761830 test_appender ( Rotation :: DAILY , "daily.log" ) ;
762831 }
763832
833+ #[ test]
834+ fn write_weekly_log ( ) {
835+ test_appender ( Rotation :: WEEKLY , "weekly.log" ) ;
836+ }
837+
764838 #[ test]
765839 fn write_never_log ( ) {
766840 test_appender ( Rotation :: NEVER , "never.log" ) ;
@@ -778,24 +852,109 @@ mod test {
778852 let next = Rotation :: HOURLY . next_date ( & now) . unwrap ( ) ;
779853 assert_eq ! ( ( now + Duration :: HOUR ) . hour( ) , next. hour( ) ) ;
780854
781- // daily- basis
855+ // per-day basis
782856 let now = OffsetDateTime :: now_utc ( ) ;
783857 let next = Rotation :: DAILY . next_date ( & now) . unwrap ( ) ;
784858 assert_eq ! ( ( now + Duration :: DAY ) . day( ) , next. day( ) ) ;
785859
860+ // per-week basis
861+ 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+
786866 // never
787867 let now = OffsetDateTime :: now_utc ( ) ;
788868 let next = Rotation :: NEVER . next_date ( & now) ;
789869 assert ! ( next. is_none( ) ) ;
790870 }
791871
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+ // Make sure weekly rotation rounds to the preceding year when appropriate
898+ 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+
792951 #[ test]
793952 #[ should_panic(
794953 expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
795954 ) ]
796955 fn test_never_date_rounding ( ) {
797956 let now = OffsetDateTime :: now_utc ( ) ;
798- let _ = Rotation :: NEVER . round_date ( & now) ;
957+ let _ = Rotation :: NEVER . round_date ( now) ;
799958 }
800959
801960 #[ test]
0 commit comments