@@ -362,14 +362,50 @@ pub fn hourly(
362
362
/// }
363
363
/// ```
364
364
///
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`.
366
366
pub fn daily (
367
367
directory : impl AsRef < Path > ,
368
368
file_name_prefix : impl AsRef < Path > ,
369
369
) -> RollingFileAppender {
370
370
RollingFileAppender :: new ( Rotation :: DAILY , directory, file_name_prefix)
371
371
}
372
372
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
+
373
409
/// Creates a non-rolling file appender.
374
410
///
375
411
/// 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
429
465
/// # }
430
466
/// ```
431
467
///
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
+ ///
432
476
/// ### No Rotation
433
477
/// ```rust
434
478
/// # fn docs() {
@@ -444,31 +488,40 @@ enum RotationKind {
444
488
Minutely ,
445
489
Hourly ,
446
490
Daily ,
491
+ Weekly ,
447
492
Never ,
448
493
}
449
494
450
495
impl Rotation {
451
- /// Provides an minutely rotation
496
+ /// Provides a minutely rotation.
452
497
pub const MINUTELY : Self = Self ( RotationKind :: Minutely ) ;
453
- /// Provides an hourly rotation
498
+ /// Provides an hourly rotation.
454
499
pub const HOURLY : Self = Self ( RotationKind :: Hourly ) ;
455
- /// Provides a daily rotation
500
+ /// Provides a daily rotation.
456
501
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 ) ;
457
504
/// Provides a rotation that never rotates.
458
505
pub const NEVER : Self = Self ( RotationKind :: Never ) ;
459
506
507
+ /// Determines the next date that we should round to or `None` if `self` uses [`Rotation::NEVER`].
460
508
pub ( crate ) fn next_date ( & self , current_date : & OffsetDateTime ) -> Option < OffsetDateTime > {
461
509
let unrounded_next_date = match * self {
462
510
Rotation :: MINUTELY => * current_date + Duration :: minutes ( 1 ) ,
463
511
Rotation :: HOURLY => * current_date + Duration :: hours ( 1 ) ,
464
512
Rotation :: DAILY => * current_date + Duration :: days ( 1 ) ,
513
+ Rotation :: WEEKLY => * current_date + Duration :: weeks ( 1 ) ,
465
514
Rotation :: NEVER => return None ,
466
515
} ;
467
- Some ( self . round_date ( & unrounded_next_date) )
516
+ Some ( self . round_date ( unrounded_next_date) )
468
517
}
469
518
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 {
472
525
match * self {
473
526
Rotation :: MINUTELY => {
474
527
let time = Time :: from_hms ( date. hour ( ) , date. minute ( ) , 0 )
@@ -485,6 +538,14 @@ impl Rotation {
485
538
. expect ( "Invalid time; this is a bug in tracing-appender" ) ;
486
539
date. replace_time ( time)
487
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
+ }
488
549
// Rotation::NEVER is impossible to round.
489
550
Rotation :: NEVER => {
490
551
unreachable ! ( "Rotation::NEVER is impossible to round." )
@@ -497,6 +558,7 @@ impl Rotation {
497
558
Rotation :: MINUTELY => format_description:: parse ( "[year]-[month]-[day]-[hour]-[minute]" ) ,
498
559
Rotation :: HOURLY => format_description:: parse ( "[year]-[month]-[day]-[hour]" ) ,
499
560
Rotation :: DAILY => format_description:: parse ( "[year]-[month]-[day]" ) ,
561
+ Rotation :: WEEKLY => format_description:: parse ( "[year]-[month]-[day]" ) ,
500
562
Rotation :: NEVER => format_description:: parse ( "[year]-[month]-[day]" ) ,
501
563
}
502
564
. expect ( "Unable to create a formatter; this is a bug in tracing-appender" )
@@ -548,10 +610,17 @@ impl Inner {
548
610
Ok ( ( inner, writer) )
549
611
}
550
612
613
+ /// Returns the full filename for the provided date, using [`Rotation`] to round accordingly.
551
614
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
+ } ;
555
624
556
625
match (
557
626
& self . rotation ,
@@ -748,7 +817,7 @@ mod test {
748
817
749
818
#[ test]
750
819
fn write_minutely_log ( ) {
751
- test_appender ( Rotation :: HOURLY , "minutely.log" ) ;
820
+ test_appender ( Rotation :: MINUTELY , "minutely.log" ) ;
752
821
}
753
822
754
823
#[ test]
@@ -761,6 +830,11 @@ mod test {
761
830
test_appender ( Rotation :: DAILY , "daily.log" ) ;
762
831
}
763
832
833
+ #[ test]
834
+ fn write_weekly_log ( ) {
835
+ test_appender ( Rotation :: WEEKLY , "weekly.log" ) ;
836
+ }
837
+
764
838
#[ test]
765
839
fn write_never_log ( ) {
766
840
test_appender ( Rotation :: NEVER , "never.log" ) ;
@@ -778,24 +852,109 @@ mod test {
778
852
let next = Rotation :: HOURLY . next_date ( & now) . unwrap ( ) ;
779
853
assert_eq ! ( ( now + Duration :: HOUR ) . hour( ) , next. hour( ) ) ;
780
854
781
- // daily- basis
855
+ // per-day basis
782
856
let now = OffsetDateTime :: now_utc ( ) ;
783
857
let next = Rotation :: DAILY . next_date ( & now) . unwrap ( ) ;
784
858
assert_eq ! ( ( now + Duration :: DAY ) . day( ) , next. day( ) ) ;
785
859
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
+
786
866
// never
787
867
let now = OffsetDateTime :: now_utc ( ) ;
788
868
let next = Rotation :: NEVER . next_date ( & now) ;
789
869
assert ! ( next. is_none( ) ) ;
790
870
}
791
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
+ // 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
+
792
951
#[ test]
793
952
#[ should_panic(
794
953
expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
795
954
) ]
796
955
fn test_never_date_rounding ( ) {
797
956
let now = OffsetDateTime :: now_utc ( ) ;
798
- let _ = Rotation :: NEVER . round_date ( & now) ;
957
+ let _ = Rotation :: NEVER . round_date ( now) ;
799
958
}
800
959
801
960
#[ test]
0 commit comments