2828//! ```
2929use crate :: sync:: { RwLock , RwLockReadGuard } ;
3030use std:: {
31- fmt:: Debug ,
31+ fmt:: { self , Debug } ,
3232 fs:: { self , File , OpenOptions } ,
3333 io:: { self , Write } ,
3434 path:: Path ,
@@ -80,10 +80,11 @@ use time::{format_description, Duration, OffsetDateTime, Time};
8080/// ```
8181///
8282/// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter
83- #[ derive( Debug ) ]
8483pub struct RollingFileAppender {
8584 state : Inner ,
8685 writer : RwLock < File > ,
86+ #[ cfg( test) ]
87+ now : Box < dyn Fn ( ) -> OffsetDateTime + Send + Sync > ,
8788}
8889
8990/// A [writer] that writes to a rolling log file.
@@ -135,36 +136,31 @@ impl RollingFileAppender {
135136 file_name_prefix : impl AsRef < Path > ,
136137 ) -> RollingFileAppender {
137138 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) ;
146140 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,
157142 writer,
143+ #[ cfg( test) ]
144+ now : Box :: new ( OffsetDateTime :: now_utc) ,
158145 }
159146 }
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+ }
160156}
161157
162158impl io:: Write for RollingFileAppender {
163159 fn write ( & mut self , buf : & [ u8 ] ) -> io:: Result < usize > {
164- let now = OffsetDateTime :: now_utc ( ) ;
160+ let now = self . now ( ) ;
165161 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 ) ;
168164 debug_assert ! ( _did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..." ) ;
169165 self . state . refresh_writer ( now, writer) ;
170166 }
@@ -179,20 +175,31 @@ impl io::Write for RollingFileAppender {
179175impl < ' a > tracing_subscriber:: fmt:: writer:: MakeWriter < ' a > for RollingFileAppender {
180176 type Writer = RollingWriter < ' a > ;
181177 fn make_writer ( & ' a self ) -> Self :: Writer {
182- let now = OffsetDateTime :: now_utc ( ) ;
178+ let now = self . now ( ) ;
183179
184180 // 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) {
186182 // Did we get the right to lock the file? If not, another thread
187183 // 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 ) {
189185 self . state . refresh_writer ( now, & mut * self . writer . write ( ) ) ;
190186 }
191187 }
192188 RollingWriter ( self . writer . read ( ) )
193189 }
194190}
195191
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+
196203/// Creates a minutely, rolling file appender. This will rotate the log file once per minute.
197204///
198205/// The appender returned by `rolling::minutely` can be used with `non_blocking` to create
@@ -469,9 +476,35 @@ impl io::Write for RollingWriter<'_> {
469476// === impl Inner ===
470477
471478impl 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 ( ) ;
474487
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 ) {
475508 let filename = self . rotation . join_date ( & self . log_filename_prefix , & now) ;
476509
477510 match create_writer ( & self . log_directory , & filename) {
@@ -485,28 +518,36 @@ impl Inner {
485518 }
486519 }
487520
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 > {
490530 let next_date = self . next_date . load ( Ordering :: Acquire ) ;
531+ // if the next date is 0, this appender *never* rotates log files.
491532 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) ;
493538 }
494- date. unix_timestamp ( ) as usize >= next_date
539+
540+ None
495541 }
496542
497- fn advance_date ( & self , now : OffsetDateTime ) -> bool {
543+ fn advance_date ( & self , now : OffsetDateTime , current : usize ) -> bool {
498544 let next_date = self
499545 . rotation
500546 . next_date ( & now)
501547 . map ( |date| date. unix_timestamp ( ) as usize )
502548 . unwrap_or ( 0 ) ;
503549 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 )
510551 . is_ok ( )
511552 }
512553}
@@ -538,9 +579,10 @@ mod test {
538579
539580 for entry in dir_contents {
540581 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) ;
542584
543- if result . as_str ( ) == expected_value {
585+ if file . as_str ( ) == expected_value {
544586 return true ;
545587 }
546588 }
@@ -646,4 +688,79 @@ mod test {
646688 let path = Rotation :: NEVER . join_date ( "app.log" , & now) ;
647689 assert_eq ! ( "app.log" , path) ;
648690 }
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+ }
649766}
0 commit comments