28
28
//! ```
29
29
use crate :: sync:: { RwLock , RwLockReadGuard } ;
30
30
use std:: {
31
- fmt:: Debug ,
31
+ fmt:: { self , Debug } ,
32
32
fs:: { self , File , OpenOptions } ,
33
33
io:: { self , Write } ,
34
34
path:: Path ,
@@ -80,10 +80,11 @@ use time::{format_description, Duration, OffsetDateTime, Time};
80
80
/// ```
81
81
///
82
82
/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
83
- #[ derive( Debug ) ]
84
83
pub struct RollingFileAppender {
85
84
state : Inner ,
86
85
writer : RwLock < File > ,
86
+ #[ cfg( test) ]
87
+ now : Box < dyn Fn ( ) -> OffsetDateTime + Send + Sync > ,
87
88
}
88
89
89
90
/// A [writer] that writes to a rolling log file.
@@ -135,36 +136,31 @@ impl RollingFileAppender {
135
136
file_name_prefix : impl AsRef < Path > ,
136
137
) -> RollingFileAppender {
137
138
let now = OffsetDateTime :: now_utc ( ) ;
138
- let log_directory = directory. as_ref ( ) . to_str ( ) . unwrap ( ) ;
139
- let log_filename_prefix = file_name_prefix. as_ref ( ) . to_str ( ) . unwrap ( ) ;
140
-
141
- let filename = rotation. join_date ( log_filename_prefix, & now) ;
142
- let next_date = rotation. next_date ( & now) ;
143
- let writer = RwLock :: new (
144
- create_writer ( log_directory, & filename) . expect ( "failed to create appender" ) ,
145
- ) ;
139
+ let ( state, writer) = Inner :: new ( now, rotation, directory, file_name_prefix) ;
146
140
Self {
147
- state : Inner {
148
- log_directory : log_directory. to_string ( ) ,
149
- log_filename_prefix : log_filename_prefix. to_string ( ) ,
150
- next_date : AtomicUsize :: new (
151
- next_date
152
- . map ( |date| date. unix_timestamp ( ) as usize )
153
- . unwrap_or ( 0 ) ,
154
- ) ,
155
- rotation,
156
- } ,
141
+ state,
157
142
writer,
143
+ #[ cfg( test) ]
144
+ now : Box :: new ( OffsetDateTime :: now_utc) ,
158
145
}
159
146
}
147
+
148
+ #[ inline]
149
+ fn now ( & self ) -> OffsetDateTime {
150
+ #[ cfg( test) ]
151
+ return ( self . now ) ( ) ;
152
+
153
+ #[ cfg( not( test) ) ]
154
+ OffsetDateTime :: now_utc ( )
155
+ }
160
156
}
161
157
162
158
impl io:: Write for RollingFileAppender {
163
159
fn write ( & mut self , buf : & [ u8 ] ) -> io:: Result < usize > {
164
- let now = OffsetDateTime :: now_utc ( ) ;
160
+ let now = self . now ( ) ;
165
161
let writer = self . writer . get_mut ( ) ;
166
- if self . state . should_rollover ( now) {
167
- let _did_cas = self . state . advance_date ( now) ;
162
+ if let Some ( current_time ) = self . state . should_rollover ( now) {
163
+ let _did_cas = self . state . advance_date ( now, current_time ) ;
168
164
debug_assert ! ( _did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..." ) ;
169
165
self . state . refresh_writer ( now, writer) ;
170
166
}
@@ -179,20 +175,31 @@ impl io::Write for RollingFileAppender {
179
175
impl < ' a > tracing_subscriber:: fmt:: writer:: MakeWriter < ' a > for RollingFileAppender {
180
176
type Writer = RollingWriter < ' a > ;
181
177
fn make_writer ( & ' a self ) -> Self :: Writer {
182
- let now = OffsetDateTime :: now_utc ( ) ;
178
+ let now = self . now ( ) ;
183
179
184
180
// Should we try to roll over the log file?
185
- if self . state . should_rollover ( now) {
181
+ if let Some ( current_time ) = self . state . should_rollover ( now) {
186
182
// Did we get the right to lock the file? If not, another thread
187
183
// did it and we can just make a writer.
188
- if self . state . advance_date ( now) {
184
+ if self . state . advance_date ( now, current_time ) {
189
185
self . state . refresh_writer ( now, & mut * self . writer . write ( ) ) ;
190
186
}
191
187
}
192
188
RollingWriter ( self . writer . read ( ) )
193
189
}
194
190
}
195
191
192
+ impl fmt:: Debug for RollingFileAppender {
193
+ // This manual impl is required because of the `now` field (only present
194
+ // with `cfg(test)`), which is not `Debug`...
195
+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
196
+ f. debug_struct ( "RollingFileAppender" )
197
+ . field ( "state" , & self . state )
198
+ . field ( "writer" , & self . writer )
199
+ . finish ( )
200
+ }
201
+ }
202
+
196
203
/// Creates a minutely, rolling file appender. This will rotate the log file once per minute.
197
204
///
198
205
/// The appender returned by `rolling::minutely` can be used with `non_blocking` to create
@@ -469,9 +476,35 @@ impl io::Write for RollingWriter<'_> {
469
476
// === impl Inner ===
470
477
471
478
impl Inner {
472
- fn refresh_writer ( & self , now : OffsetDateTime , file : & mut File ) {
473
- debug_assert ! ( self . should_rollover( now) ) ;
479
+ fn new (
480
+ now : OffsetDateTime ,
481
+ rotation : Rotation ,
482
+ directory : impl AsRef < Path > ,
483
+ file_name_prefix : impl AsRef < Path > ,
484
+ ) -> ( Self , RwLock < File > ) {
485
+ let log_directory = directory. as_ref ( ) . to_str ( ) . unwrap ( ) ;
486
+ let log_filename_prefix = file_name_prefix. as_ref ( ) . to_str ( ) . unwrap ( ) ;
474
487
488
+ let filename = rotation. join_date ( log_filename_prefix, & now) ;
489
+ let next_date = rotation. next_date ( & now) ;
490
+ let writer = RwLock :: new (
491
+ create_writer ( log_directory, & filename) . expect ( "failed to create appender" ) ,
492
+ ) ;
493
+
494
+ let inner = Inner {
495
+ log_directory : log_directory. to_string ( ) ,
496
+ log_filename_prefix : log_filename_prefix. to_string ( ) ,
497
+ next_date : AtomicUsize :: new (
498
+ next_date
499
+ . map ( |date| date. unix_timestamp ( ) as usize )
500
+ . unwrap_or ( 0 ) ,
501
+ ) ,
502
+ rotation,
503
+ } ;
504
+ ( inner, writer)
505
+ }
506
+
507
+ fn refresh_writer ( & self , now : OffsetDateTime , file : & mut File ) {
475
508
let filename = self . rotation . join_date ( & self . log_filename_prefix , & now) ;
476
509
477
510
match create_writer ( & self . log_directory , & filename) {
@@ -485,28 +518,36 @@ impl Inner {
485
518
}
486
519
}
487
520
488
- fn should_rollover ( & self , date : OffsetDateTime ) -> bool {
489
- // the `None` case means that the `InnerAppender` *never* rotates log files.
521
+ /// Checks whether or not it's time to roll over the log file.
522
+ ///
523
+ /// Rather than returning a `bool`, this returns the current value of
524
+ /// `next_date` so that we can perform a `compare_exchange` operation with
525
+ /// that value when setting the next rollover time.
526
+ ///
527
+ /// If this method returns `Some`, we should roll to a new log file.
528
+ /// Otherwise, if this returns we should not rotate the log file.
529
+ fn should_rollover ( & self , date : OffsetDateTime ) -> Option < usize > {
490
530
let next_date = self . next_date . load ( Ordering :: Acquire ) ;
531
+ // if the next date is 0, this appender *never* rotates log files.
491
532
if next_date == 0 {
492
- return false ;
533
+ return None ;
534
+ }
535
+
536
+ if date. unix_timestamp ( ) as usize >= next_date {
537
+ return Some ( next_date) ;
493
538
}
494
- date. unix_timestamp ( ) as usize >= next_date
539
+
540
+ None
495
541
}
496
542
497
- fn advance_date ( & self , now : OffsetDateTime ) -> bool {
543
+ fn advance_date ( & self , now : OffsetDateTime , current : usize ) -> bool {
498
544
let next_date = self
499
545
. rotation
500
546
. next_date ( & now)
501
547
. map ( |date| date. unix_timestamp ( ) as usize )
502
548
. unwrap_or ( 0 ) ;
503
549
self . next_date
504
- . compare_exchange (
505
- now. unix_timestamp ( ) as usize ,
506
- next_date,
507
- Ordering :: AcqRel ,
508
- Ordering :: Acquire ,
509
- )
550
+ . compare_exchange ( current, next_date, Ordering :: AcqRel , Ordering :: Acquire )
510
551
. is_ok ( )
511
552
}
512
553
}
@@ -538,9 +579,10 @@ mod test {
538
579
539
580
for entry in dir_contents {
540
581
let path = entry. expect ( "Expected dir entry" ) . path ( ) ;
541
- let result = fs:: read_to_string ( path) . expect ( "Failed to read file" ) ;
582
+ let file = fs:: read_to_string ( & path) . expect ( "Failed to read file" ) ;
583
+ println ! ( "path={}\n file={:?}" , path. display( ) , file) ;
542
584
543
- if result . as_str ( ) == expected_value {
585
+ if file . as_str ( ) == expected_value {
544
586
return true ;
545
587
}
546
588
}
@@ -646,4 +688,79 @@ mod test {
646
688
let path = Rotation :: NEVER . join_date ( "app.log" , & now) ;
647
689
assert_eq ! ( "app.log" , path) ;
648
690
}
691
+
692
+ #[ test]
693
+ fn test_make_writer ( ) {
694
+ use std:: sync:: { Arc , Mutex } ;
695
+ use tracing_subscriber:: prelude:: * ;
696
+
697
+ let format = format_description:: parse (
698
+ "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
699
+ sign:mandatory]:[offset_minute]:[offset_second]",
700
+ )
701
+ . unwrap ( ) ;
702
+
703
+ let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
704
+ let directory = tempfile:: tempdir ( ) . expect ( "failed to create tempdir" ) ;
705
+ let ( state, writer) =
706
+ Inner :: new ( now, Rotation :: HOURLY , directory. path ( ) , "test_make_writer" ) ;
707
+
708
+ let clock = Arc :: new ( Mutex :: new ( now) ) ;
709
+ let now = {
710
+ let clock = clock. clone ( ) ;
711
+ Box :: new ( move || * clock. lock ( ) . unwrap ( ) )
712
+ } ;
713
+ let appender = RollingFileAppender { state, writer, now } ;
714
+ let default = tracing_subscriber:: fmt ( )
715
+ . without_time ( )
716
+ . with_level ( false )
717
+ . with_target ( false )
718
+ . with_max_level ( tracing_subscriber:: filter:: LevelFilter :: TRACE )
719
+ . with_writer ( appender)
720
+ . finish ( )
721
+ . set_default ( ) ;
722
+
723
+ tracing:: info!( "file 1" ) ;
724
+
725
+ // advance time by one second
726
+ ( * clock. lock ( ) . unwrap ( ) ) += Duration :: seconds ( 1 ) ;
727
+
728
+ tracing:: info!( "file 1" ) ;
729
+
730
+ // advance time by one hour
731
+ ( * clock. lock ( ) . unwrap ( ) ) += Duration :: hours ( 1 ) ;
732
+
733
+ tracing:: info!( "file 2" ) ;
734
+
735
+ // advance time by one second
736
+ ( * clock. lock ( ) . unwrap ( ) ) += Duration :: seconds ( 1 ) ;
737
+
738
+ tracing:: info!( "file 2" ) ;
739
+
740
+ drop ( default) ;
741
+
742
+ let dir_contents = fs:: read_dir ( directory. path ( ) ) . expect ( "Failed to read directory" ) ;
743
+ println ! ( "dir={:?}" , dir_contents) ;
744
+ for entry in dir_contents {
745
+ println ! ( "entry={:?}" , entry) ;
746
+ let path = entry. expect ( "Expected dir entry" ) . path ( ) ;
747
+ let file = fs:: read_to_string ( & path) . expect ( "Failed to read file" ) ;
748
+ println ! ( "path={}\n file={:?}" , path. display( ) , file) ;
749
+
750
+ match path
751
+ . extension ( )
752
+ . expect ( "found a file without a date!" )
753
+ . to_str ( )
754
+ . expect ( "extension should be UTF8" )
755
+ {
756
+ "2020-02-01-10" => {
757
+ assert_eq ! ( "file 1\n file 1\n " , file) ;
758
+ }
759
+ "2020-02-01-11" => {
760
+ assert_eq ! ( "file 2\n file 2\n " , file) ;
761
+ }
762
+ x => panic ! ( "unexpected date {}" , x) ,
763
+ }
764
+ }
765
+ }
649
766
}
0 commit comments