@@ -108,6 +108,7 @@ struct Inner {
108108 rotation : Rotation ,
109109 next_date : AtomicUsize ,
110110 max_files : Option < usize > ,
111+ max_file_size : Option < u64 > ,
111112}
112113
113114// === impl RollingFileAppender ===
@@ -190,6 +191,7 @@ impl RollingFileAppender {
190191 ref prefix,
191192 ref suffix,
192193 ref max_files,
194+ ref max_file_size,
193195 } = builder;
194196 let directory = directory. as_ref ( ) . to_path_buf ( ) ;
195197 let now = OffsetDateTime :: now_utc ( ) ;
@@ -200,6 +202,7 @@ impl RollingFileAppender {
200202 prefix. clone ( ) ,
201203 suffix. clone ( ) ,
202204 * max_files,
205+ * max_file_size,
203206 ) ?;
204207 Ok ( Self {
205208 state,
@@ -227,6 +230,8 @@ impl io::Write for RollingFileAppender {
227230 let _did_cas = self . state . advance_date ( now, current_time) ;
228231 debug_assert ! ( _did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..." ) ;
229232 self . state . refresh_writer ( now, writer) ;
233+ } else if self . state . should_rollover_due_to_size ( writer) {
234+ self . state . refresh_writer ( now, writer) ;
230235 }
231236 writer. write ( buf)
232237 }
@@ -248,6 +253,8 @@ impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender
248253 if self . state . advance_date ( now, current_time) {
249254 self . state . refresh_writer ( now, & mut self . writer . write ( ) ) ;
250255 }
256+ } else if self . state . should_rollover_due_to_size ( & self . writer . write ( ) ) {
257+ self . state . refresh_writer ( now, & mut self . writer . write ( ) ) ;
251258 }
252259 RollingWriter ( self . writer . read ( ) )
253260 }
@@ -370,6 +377,38 @@ pub fn daily(
370377 RollingFileAppender :: new ( Rotation :: DAILY , directory, file_name_prefix)
371378}
372379
380+ /// Creates a size based rolling file appender.
381+ ///
382+ /// The appender returned by `rolling::size` can be used with `non_blocking` to create
383+ /// a non-blocking, size based rotating appender.
384+ ///
385+ /// The location of the log file will be specified by the `directory` passed in.
386+ /// `file_name` specifies the complete name of the log file.
387+ /// `RollingFileAppender` automatically appends the current date in UTC.
388+ ///
389+ /// # Examples
390+ ///
391+ /// ``` rust
392+ /// # #[clippy::allow(needless_doctest_main)]
393+ /// fn main () {
394+ /// # fn doc() {
395+ /// let appender = tracing_appender::rolling::size("/some/path", "rolling.log");
396+ /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
397+ ///
398+ /// let collector = tracing_subscriber::fmt().with_writer(non_blocking_appender);
399+ ///
400+ /// tracing::collect::with_default(collector.finish(), || {
401+ /// tracing::event!(tracing::Level::INFO, "Hello");
402+ /// });
403+ /// # }
404+ /// }
405+ /// ```
406+ ///
407+ /// This will result in a log file located at `/some/path/rolling.log`.
408+ pub fn size ( directory : impl AsRef < Path > , file_name : impl AsRef < Path > ) -> RollingFileAppender {
409+ RollingFileAppender :: new ( Rotation :: SIZE , directory, file_name)
410+ }
411+
373412/// Creates a non-rolling file appender.
374413///
375414/// The appender returned by `rolling::never` can be used with `non_blocking` to create
@@ -444,6 +483,7 @@ enum RotationKind {
444483 Minutely ,
445484 Hourly ,
446485 Daily ,
486+ Size ,
447487 Never ,
448488}
449489
@@ -454,6 +494,8 @@ impl Rotation {
454494 pub const HOURLY : Self = Self ( RotationKind :: Hourly ) ;
455495 /// Provides a daily rotation
456496 pub const DAILY : Self = Self ( RotationKind :: Daily ) ;
497+ /// Provides a size based rotation
498+ pub const SIZE : Self = Self ( RotationKind :: Size ) ;
457499 /// Provides a rotation that never rotates.
458500 pub const NEVER : Self = Self ( RotationKind :: Never ) ;
459501
@@ -462,6 +504,7 @@ impl Rotation {
462504 Rotation :: MINUTELY => * current_date + Duration :: minutes ( 1 ) ,
463505 Rotation :: HOURLY => * current_date + Duration :: hours ( 1 ) ,
464506 Rotation :: DAILY => * current_date + Duration :: days ( 1 ) ,
507+ Rotation :: SIZE => return None ,
465508 Rotation :: NEVER => return None ,
466509 } ;
467510 Some ( self . round_date ( & unrounded_next_date) )
@@ -485,6 +528,10 @@ impl Rotation {
485528 . expect ( "Invalid time; this is a bug in tracing-appender" ) ;
486529 date. replace_time ( time)
487530 }
531+ // Rotation::SIZE is impossible to round.
532+ Rotation :: SIZE => {
533+ unreachable ! ( "Rotation::SIZE is impossible to round." )
534+ }
488535 // Rotation::NEVER is impossible to round.
489536 Rotation :: NEVER => {
490537 unreachable ! ( "Rotation::NEVER is impossible to round." )
@@ -497,6 +544,9 @@ impl Rotation {
497544 Rotation :: MINUTELY => format_description:: parse ( "[year]-[month]-[day]-[hour]-[minute]" ) ,
498545 Rotation :: HOURLY => format_description:: parse ( "[year]-[month]-[day]-[hour]" ) ,
499546 Rotation :: DAILY => format_description:: parse ( "[year]-[month]-[day]" ) ,
547+ Rotation :: SIZE => format_description:: parse (
548+ "[year]-[month]-[day]-[hour]-[minute]-[second]-[subsecond]" ,
549+ ) ,
500550 Rotation :: NEVER => format_description:: parse ( "[year]-[month]-[day]" ) ,
501551 }
502552 . expect ( "Unable to create a formatter; this is a bug in tracing-appender" )
@@ -525,6 +575,7 @@ impl Inner {
525575 log_filename_prefix : Option < String > ,
526576 log_filename_suffix : Option < String > ,
527577 max_files : Option < usize > ,
578+ max_file_size : Option < u64 > ,
528579 ) -> Result < ( Self , RwLock < File > ) , builder:: InitError > {
529580 let log_directory = directory. as_ref ( ) . to_path_buf ( ) ;
530581 let date_format = rotation. date_format ( ) ;
@@ -542,6 +593,7 @@ impl Inner {
542593 ) ,
543594 rotation,
544595 max_files,
596+ max_file_size,
545597 } ;
546598 let filename = inner. join_date ( & now) ;
547599 let writer = RwLock :: new ( create_writer ( inner. log_directory . as_ref ( ) , & filename) ?) ;
@@ -674,6 +726,23 @@ impl Inner {
674726 None
675727 }
676728
729+ /// Checks whether or not the file needs to rollover because it reached the size limit.
730+ ///
731+ /// If this method returns `true`, we should roll to a new log file.
732+ /// Otherwise, if this returns `false` we should not rotate the log file.
733+ fn should_rollover_due_to_size ( & self , current_file : & File ) -> bool {
734+ current_file. sync_all ( ) . ok ( ) ;
735+ if let ( Ok ( file_metadata) , Some ( max_file_size) , & Rotation :: SIZE ) =
736+ ( current_file. metadata ( ) , self . max_file_size , & self . rotation )
737+ {
738+ if file_metadata. len ( ) >= max_file_size {
739+ return true ;
740+ }
741+ }
742+
743+ false
744+ }
745+
677746 fn advance_date ( & self , now : OffsetDateTime , current : usize ) -> bool {
678747 let next_date = self
679748 . rotation
@@ -761,6 +830,11 @@ mod test {
761830 test_appender ( Rotation :: DAILY , "daily.log" ) ;
762831 }
763832
833+ #[ test]
834+ fn write_size_log ( ) {
835+ test_appender ( Rotation :: SIZE , "size.log" ) ;
836+ }
837+
764838 #[ test]
765839 fn write_never_log ( ) {
766840 test_appender ( Rotation :: NEVER , "never.log" ) ;
@@ -783,6 +857,11 @@ mod test {
783857 let next = Rotation :: DAILY . next_date ( & now) . unwrap ( ) ;
784858 assert_eq ! ( ( now + Duration :: DAY ) . day( ) , next. day( ) ) ;
785859
860+ // size
861+ let now = OffsetDateTime :: now_utc ( ) ;
862+ let next = Rotation :: SIZE . next_date ( & now) ;
863+ assert ! ( next. is_none( ) ) ;
864+
786865 // never
787866 let now = OffsetDateTime :: now_utc ( ) ;
788867 let next = Rotation :: NEVER . next_date ( & now) ;
@@ -829,6 +908,7 @@ mod test {
829908 prefix. map ( ToString :: to_string) ,
830909 suffix. map ( ToString :: to_string) ,
831910 None ,
911+ None ,
832912 )
833913 . unwrap ( ) ;
834914 let path = inner. join_date ( & now) ;
@@ -859,6 +939,12 @@ mod test {
859939 prefix: Some ( "app.log" ) ,
860940 suffix: None ,
861941 } ,
942+ TestCase {
943+ expected: "app.log.2020-02-01-10-01-00-0" ,
944+ rotation: Rotation :: SIZE ,
945+ prefix: Some ( "app.log" ) ,
946+ suffix: None ,
947+ } ,
862948 TestCase {
863949 expected: "app.log" ,
864950 rotation: Rotation :: NEVER ,
@@ -884,6 +970,12 @@ mod test {
884970 prefix: Some ( "app" ) ,
885971 suffix: Some ( "log" ) ,
886972 } ,
973+ TestCase {
974+ expected: "app.2020-02-01-10-01-00-0.log" ,
975+ rotation: Rotation :: SIZE ,
976+ prefix: Some ( "app" ) ,
977+ suffix: Some ( "log" ) ,
978+ } ,
887979 TestCase {
888980 expected: "app.log" ,
889981 rotation: Rotation :: NEVER ,
@@ -909,6 +1001,12 @@ mod test {
9091001 prefix: None ,
9101002 suffix: Some ( "log" ) ,
9111003 } ,
1004+ TestCase {
1005+ expected: "2020-02-01-10-01-00-0.log" ,
1006+ rotation: Rotation :: SIZE ,
1007+ prefix: None ,
1008+ suffix: Some ( "log" ) ,
1009+ } ,
9121010 TestCase {
9131011 expected: "log" ,
9141012 rotation: Rotation :: NEVER ,
@@ -941,6 +1039,7 @@ mod test {
9411039 Some ( "test_make_writer" . to_string ( ) ) ,
9421040 None ,
9431041 None ,
1042+ None ,
9441043 )
9451044 . unwrap ( ) ;
9461045
@@ -1023,6 +1122,7 @@ mod test {
10231122 Some ( "test_max_log_files" . to_string ( ) ) ,
10241123 None ,
10251124 Some ( 2 ) ,
1125+ None ,
10261126 )
10271127 . unwrap ( ) ;
10281128
@@ -1106,4 +1206,79 @@ mod test {
11061206 }
11071207 }
11081208 }
1209+
1210+ #[ test]
1211+ fn test_size_based_rolling ( ) {
1212+ use std:: sync:: { Arc , Mutex } ;
1213+ use tracing_subscriber:: prelude:: * ;
1214+
1215+ let format = format_description:: parse (
1216+ "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1217+ sign:mandatory]:[offset_minute]:[offset_second]",
1218+ )
1219+ . unwrap ( ) ;
1220+
1221+ const MAX_FILE_SIZE : u64 = 1024 ;
1222+ let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
1223+ let directory = tempfile:: tempdir ( ) . expect ( "failed to create tempdir" ) ;
1224+ let ( state, writer) = Inner :: new (
1225+ now,
1226+ Rotation :: SIZE ,
1227+ directory. path ( ) ,
1228+ Some ( "test_max_file_size" . to_string ( ) ) ,
1229+ None ,
1230+ Some ( 5 ) ,
1231+ Some ( MAX_FILE_SIZE ) ,
1232+ )
1233+ . unwrap ( ) ;
1234+
1235+ let clock = Arc :: new ( Mutex :: new ( now) ) ;
1236+ let now = {
1237+ let clock = clock. clone ( ) ;
1238+ Box :: new ( move || * clock. lock ( ) . unwrap ( ) )
1239+ } ;
1240+ let appender = RollingFileAppender { state, writer, now } ;
1241+ let default = tracing_subscriber:: fmt ( )
1242+ . without_time ( )
1243+ . with_level ( false )
1244+ . with_target ( false )
1245+ . with_max_level ( tracing_subscriber:: filter:: LevelFilter :: TRACE )
1246+ . with_writer ( appender)
1247+ . finish ( )
1248+ . set_default ( ) ;
1249+
1250+ for file_num in 0 ..5 {
1251+ for i in 0 ..58 {
1252+ tracing:: info!( "file {} content {}" , file_num, i) ;
1253+ ( * clock. lock ( ) . unwrap ( ) ) += Duration :: milliseconds ( 1 ) ;
1254+ }
1255+ }
1256+
1257+ drop ( default) ;
1258+
1259+ let dir_contents = fs:: read_dir ( directory. path ( ) ) . expect ( "Failed to read directory" ) ;
1260+ println ! ( "dir={:?}" , dir_contents) ;
1261+
1262+ for entry in dir_contents {
1263+ println ! ( "entry={:?}" , entry) ;
1264+ let path = entry. expect ( "Expected dir entry" ) . path ( ) ;
1265+ let file_fd = fs:: File :: open ( & path) . expect ( "Failed to open file" ) ;
1266+ let file_metadata = file_fd. metadata ( ) . expect ( "Failed to get file metadata" ) ;
1267+ println ! (
1268+ "path={}\n file_len={:?}" ,
1269+ path. display( ) ,
1270+ file_metadata. len( )
1271+ ) ;
1272+ let file = fs:: read_to_string ( & path) . expect ( "Failed to read file" ) ;
1273+ println ! ( "path={}\n file={:?}" , path. display( ) , file) ;
1274+
1275+ assert_eq ! (
1276+ MAX_FILE_SIZE + 10 ,
1277+ file_metadata. len( ) ,
1278+ "expected size = {:?}, file size = {:?}" ,
1279+ MAX_FILE_SIZE ,
1280+ file_metadata. len( ) ,
1281+ ) ;
1282+ }
1283+ }
11091284}
0 commit comments