@@ -108,6 +108,7 @@ struct Inner {
108
108
rotation : Rotation ,
109
109
next_date : AtomicUsize ,
110
110
max_files : Option < usize > ,
111
+ max_file_size : Option < u64 > ,
111
112
}
112
113
113
114
// === impl RollingFileAppender ===
@@ -190,6 +191,7 @@ impl RollingFileAppender {
190
191
ref prefix,
191
192
ref suffix,
192
193
ref max_files,
194
+ ref max_file_size,
193
195
} = builder;
194
196
let directory = directory. as_ref ( ) . to_path_buf ( ) ;
195
197
let now = OffsetDateTime :: now_utc ( ) ;
@@ -200,6 +202,7 @@ impl RollingFileAppender {
200
202
prefix. clone ( ) ,
201
203
suffix. clone ( ) ,
202
204
* max_files,
205
+ * max_file_size,
203
206
) ?;
204
207
Ok ( Self {
205
208
state,
@@ -227,6 +230,8 @@ impl io::Write for RollingFileAppender {
227
230
let _did_cas = self . state . advance_date ( now, current_time) ;
228
231
debug_assert ! ( _did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..." ) ;
229
232
self . state . refresh_writer ( now, writer) ;
233
+ } else if self . state . should_rollover_due_to_size ( writer) {
234
+ self . state . refresh_writer ( now, writer) ;
230
235
}
231
236
writer. write ( buf)
232
237
}
@@ -248,6 +253,8 @@ impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender
248
253
if self . state . advance_date ( now, current_time) {
249
254
self . state . refresh_writer ( now, & mut self . writer . write ( ) ) ;
250
255
}
256
+ } else if self . state . should_rollover_due_to_size ( & self . writer . write ( ) ) {
257
+ self . state . refresh_writer ( now, & mut self . writer . write ( ) ) ;
251
258
}
252
259
RollingWriter ( self . writer . read ( ) )
253
260
}
@@ -406,6 +413,38 @@ pub fn weekly(
406
413
RollingFileAppender :: new ( Rotation :: WEEKLY , directory, file_name_prefix)
407
414
}
408
415
416
+ /// Creates a size based rolling file appender.
417
+ ///
418
+ /// The appender returned by `rolling::size` can be used with `non_blocking` to create
419
+ /// a non-blocking, size based rotating appender.
420
+ ///
421
+ /// The location of the log file will be specified by the `directory` passed in.
422
+ /// `file_name` specifies the complete name of the log file.
423
+ /// `RollingFileAppender` automatically appends the current date in UTC.
424
+ ///
425
+ /// # Examples
426
+ ///
427
+ /// ``` rust
428
+ /// # #[clippy::allow(needless_doctest_main)]
429
+ /// fn main () {
430
+ /// # fn doc() {
431
+ /// let appender = tracing_appender::rolling::size("/some/path", "rolling.log");
432
+ /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
433
+ ///
434
+ /// let collector = tracing_subscriber::fmt().with_writer(non_blocking_appender);
435
+ ///
436
+ /// tracing::collect::with_default(collector.finish(), || {
437
+ /// tracing::event!(tracing::Level::INFO, "Hello");
438
+ /// });
439
+ /// # }
440
+ /// }
441
+ /// ```
442
+ ///
443
+ /// This will result in a log file located at `/some/path/rolling.log`.
444
+ pub fn size ( directory : impl AsRef < Path > , file_name : impl AsRef < Path > ) -> RollingFileAppender {
445
+ RollingFileAppender :: new ( Rotation :: SIZE , directory, file_name)
446
+ }
447
+
409
448
/// Creates a non-rolling file appender.
410
449
///
411
450
/// The appender returned by `rolling::never` can be used with `non_blocking` to create
@@ -473,6 +512,14 @@ pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> Rollin
473
512
/// # }
474
513
/// ```
475
514
///
515
+ /// ### Size Rotation
516
+ /// ```rust
517
+ /// # fn docs() {
518
+ /// use tracing_appender::rolling::Rotation;
519
+ /// let rotation = tracing_appender::rolling::Rotation::SIZE;
520
+ /// # }
521
+ /// ```
522
+ ///
476
523
/// ### No Rotation
477
524
/// ```rust
478
525
/// # fn docs() {
@@ -489,6 +536,7 @@ enum RotationKind {
489
536
Hourly ,
490
537
Daily ,
491
538
Weekly ,
539
+ Size ,
492
540
Never ,
493
541
}
494
542
@@ -501,6 +549,8 @@ impl Rotation {
501
549
pub const DAILY : Self = Self ( RotationKind :: Daily ) ;
502
550
/// Provides a weekly rotation that rotates every Sunday at midnight UTC.
503
551
pub const WEEKLY : Self = Self ( RotationKind :: Weekly ) ;
552
+ /// Provides a size based rotation
553
+ pub const SIZE : Self = Self ( RotationKind :: Size ) ;
504
554
/// Provides a rotation that never rotates.
505
555
pub const NEVER : Self = Self ( RotationKind :: Never ) ;
506
556
@@ -511,6 +561,7 @@ impl Rotation {
511
561
Rotation :: HOURLY => * current_date + Duration :: hours ( 1 ) ,
512
562
Rotation :: DAILY => * current_date + Duration :: days ( 1 ) ,
513
563
Rotation :: WEEKLY => * current_date + Duration :: weeks ( 1 ) ,
564
+ Rotation :: SIZE => return None ,
514
565
Rotation :: NEVER => return None ,
515
566
} ;
516
567
Some ( self . round_date ( unrounded_next_date) )
@@ -546,6 +597,10 @@ impl Rotation {
546
597
let date = date - Duration :: days ( days_since_sunday. into ( ) ) ;
547
598
date. replace_time ( zero_time)
548
599
}
600
+ // Rotation::SIZE is impossible to round.
601
+ Rotation :: SIZE => {
602
+ unreachable ! ( "Rotation::SIZE is impossible to round." )
603
+ }
549
604
// Rotation::NEVER is impossible to round.
550
605
Rotation :: NEVER => {
551
606
unreachable ! ( "Rotation::NEVER is impossible to round." )
@@ -559,6 +614,9 @@ impl Rotation {
559
614
Rotation :: HOURLY => format_description:: parse ( "[year]-[month]-[day]-[hour]" ) ,
560
615
Rotation :: DAILY => format_description:: parse ( "[year]-[month]-[day]" ) ,
561
616
Rotation :: WEEKLY => format_description:: parse ( "[year]-[month]-[day]" ) ,
617
+ Rotation :: SIZE => format_description:: parse (
618
+ "[year]-[month]-[day]-[hour]-[minute]-[second]-[subsecond]" ,
619
+ ) ,
562
620
Rotation :: NEVER => format_description:: parse ( "[year]-[month]-[day]" ) ,
563
621
}
564
622
. expect ( "Unable to create a formatter; this is a bug in tracing-appender" )
@@ -587,6 +645,7 @@ impl Inner {
587
645
log_filename_prefix : Option < String > ,
588
646
log_filename_suffix : Option < String > ,
589
647
max_files : Option < usize > ,
648
+ max_file_size : Option < u64 > ,
590
649
) -> Result < ( Self , RwLock < File > ) , builder:: InitError > {
591
650
let log_directory = directory. as_ref ( ) . to_path_buf ( ) ;
592
651
let date_format = rotation. date_format ( ) ;
@@ -604,6 +663,7 @@ impl Inner {
604
663
) ,
605
664
rotation,
606
665
max_files,
666
+ max_file_size,
607
667
} ;
608
668
let filename = inner. join_date ( & now) ;
609
669
let writer = RwLock :: new ( create_writer ( inner. log_directory . as_ref ( ) , & filename) ?) ;
@@ -743,6 +803,23 @@ impl Inner {
743
803
None
744
804
}
745
805
806
+ /// Checks whether or not the file needs to rollover because it reached the size limit.
807
+ ///
808
+ /// If this method returns `true`, we should roll to a new log file.
809
+ /// Otherwise, if this returns `false` we should not rotate the log file.
810
+ fn should_rollover_due_to_size ( & self , current_file : & File ) -> bool {
811
+ current_file. sync_all ( ) . ok ( ) ;
812
+ if let ( Ok ( file_metadata) , Some ( max_file_size) , & Rotation :: SIZE ) =
813
+ ( current_file. metadata ( ) , self . max_file_size , & self . rotation )
814
+ {
815
+ if file_metadata. len ( ) >= max_file_size {
816
+ return true ;
817
+ }
818
+ }
819
+
820
+ false
821
+ }
822
+
746
823
fn advance_date ( & self , now : OffsetDateTime , current : usize ) -> bool {
747
824
let next_date = self
748
825
. rotation
@@ -835,6 +912,11 @@ mod test {
835
912
test_appender ( Rotation :: WEEKLY , "weekly.log" ) ;
836
913
}
837
914
915
+ #[ test]
916
+ fn write_size_log ( ) {
917
+ test_appender ( Rotation :: SIZE , "size.log" ) ;
918
+ }
919
+
838
920
#[ test]
839
921
fn write_never_log ( ) {
840
922
test_appender ( Rotation :: NEVER , "never.log" ) ;
@@ -863,6 +945,11 @@ mod test {
863
945
let next = Rotation :: WEEKLY . next_date ( & now) . unwrap ( ) ;
864
946
assert ! ( now_rounded < next) ;
865
947
948
+ // size
949
+ let now = OffsetDateTime :: now_utc ( ) ;
950
+ let next = Rotation :: SIZE . next_date ( & now) ;
951
+ assert ! ( next. is_none( ) ) ;
952
+
866
953
// never
867
954
let now = OffsetDateTime :: now_utc ( ) ;
868
955
let next = Rotation :: NEVER . next_date ( & now) ;
@@ -940,6 +1027,7 @@ mod test {
940
1027
test_case. prefix . map ( ToString :: to_string) ,
941
1028
test_case. suffix . map ( ToString :: to_string) ,
942
1029
None ,
1030
+ None ,
943
1031
)
944
1032
. unwrap ( ) ;
945
1033
let path = inner. join_date ( & test_case. now ) ;
@@ -988,6 +1076,7 @@ mod test {
988
1076
prefix. map ( ToString :: to_string) ,
989
1077
suffix. map ( ToString :: to_string) ,
990
1078
None ,
1079
+ None ,
991
1080
)
992
1081
. unwrap ( ) ;
993
1082
let path = inner. join_date ( & now) ;
@@ -1018,6 +1107,12 @@ mod test {
1018
1107
prefix: Some ( "app.log" ) ,
1019
1108
suffix: None ,
1020
1109
} ,
1110
+ TestCase {
1111
+ expected: "app.log.2020-02-01-10-01-00-0" ,
1112
+ rotation: Rotation :: SIZE ,
1113
+ prefix: Some ( "app.log" ) ,
1114
+ suffix: None ,
1115
+ } ,
1021
1116
TestCase {
1022
1117
expected: "app.log" ,
1023
1118
rotation: Rotation :: NEVER ,
@@ -1043,6 +1138,12 @@ mod test {
1043
1138
prefix: Some ( "app" ) ,
1044
1139
suffix: Some ( "log" ) ,
1045
1140
} ,
1141
+ TestCase {
1142
+ expected: "app.2020-02-01-10-01-00-0.log" ,
1143
+ rotation: Rotation :: SIZE ,
1144
+ prefix: Some ( "app" ) ,
1145
+ suffix: Some ( "log" ) ,
1146
+ } ,
1046
1147
TestCase {
1047
1148
expected: "app.log" ,
1048
1149
rotation: Rotation :: NEVER ,
@@ -1068,6 +1169,12 @@ mod test {
1068
1169
prefix: None ,
1069
1170
suffix: Some ( "log" ) ,
1070
1171
} ,
1172
+ TestCase {
1173
+ expected: "2020-02-01-10-01-00-0.log" ,
1174
+ rotation: Rotation :: SIZE ,
1175
+ prefix: None ,
1176
+ suffix: Some ( "log" ) ,
1177
+ } ,
1071
1178
TestCase {
1072
1179
expected: "log" ,
1073
1180
rotation: Rotation :: NEVER ,
@@ -1100,6 +1207,7 @@ mod test {
1100
1207
Some ( "test_make_writer" . to_string ( ) ) ,
1101
1208
None ,
1102
1209
None ,
1210
+ None ,
1103
1211
)
1104
1212
. unwrap ( ) ;
1105
1213
@@ -1182,6 +1290,7 @@ mod test {
1182
1290
Some ( "test_max_log_files" . to_string ( ) ) ,
1183
1291
None ,
1184
1292
Some ( 2 ) ,
1293
+ None ,
1185
1294
)
1186
1295
. unwrap ( ) ;
1187
1296
@@ -1265,4 +1374,79 @@ mod test {
1265
1374
}
1266
1375
}
1267
1376
}
1377
+
1378
+ #[ test]
1379
+ fn test_size_based_rolling ( ) {
1380
+ use std:: sync:: { Arc , Mutex } ;
1381
+ use tracing_subscriber:: prelude:: * ;
1382
+
1383
+ let format = format_description:: parse (
1384
+ "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
1385
+ sign:mandatory]:[offset_minute]:[offset_second]",
1386
+ )
1387
+ . unwrap ( ) ;
1388
+
1389
+ const MAX_FILE_SIZE : u64 = 1024 ;
1390
+ let now = OffsetDateTime :: parse ( "2020-02-01 10:01:00 +00:00:00" , & format) . unwrap ( ) ;
1391
+ let directory = tempfile:: tempdir ( ) . expect ( "failed to create tempdir" ) ;
1392
+ let ( state, writer) = Inner :: new (
1393
+ now,
1394
+ Rotation :: SIZE ,
1395
+ directory. path ( ) ,
1396
+ Some ( "test_max_file_size" . to_string ( ) ) ,
1397
+ None ,
1398
+ Some ( 5 ) ,
1399
+ Some ( MAX_FILE_SIZE ) ,
1400
+ )
1401
+ . unwrap ( ) ;
1402
+
1403
+ let clock = Arc :: new ( Mutex :: new ( now) ) ;
1404
+ let now = {
1405
+ let clock = clock. clone ( ) ;
1406
+ Box :: new ( move || * clock. lock ( ) . unwrap ( ) )
1407
+ } ;
1408
+ let appender = RollingFileAppender { state, writer, now } ;
1409
+ let default = tracing_subscriber:: fmt ( )
1410
+ . without_time ( )
1411
+ . with_level ( false )
1412
+ . with_target ( false )
1413
+ . with_max_level ( tracing_subscriber:: filter:: LevelFilter :: TRACE )
1414
+ . with_writer ( appender)
1415
+ . finish ( )
1416
+ . set_default ( ) ;
1417
+
1418
+ for file_num in 0 ..5 {
1419
+ for i in 0 ..58 {
1420
+ tracing:: info!( "file {} content {}" , file_num, i) ;
1421
+ ( * clock. lock ( ) . unwrap ( ) ) += Duration :: milliseconds ( 1 ) ;
1422
+ }
1423
+ }
1424
+
1425
+ drop ( default) ;
1426
+
1427
+ let dir_contents = fs:: read_dir ( directory. path ( ) ) . expect ( "Failed to read directory" ) ;
1428
+ println ! ( "dir={:?}" , dir_contents) ;
1429
+
1430
+ for entry in dir_contents {
1431
+ println ! ( "entry={:?}" , entry) ;
1432
+ let path = entry. expect ( "Expected dir entry" ) . path ( ) ;
1433
+ let file_fd = fs:: File :: open ( & path) . expect ( "Failed to open file" ) ;
1434
+ let file_metadata = file_fd. metadata ( ) . expect ( "Failed to get file metadata" ) ;
1435
+ println ! (
1436
+ "path={}\n file_len={:?}" ,
1437
+ path. display( ) ,
1438
+ file_metadata. len( )
1439
+ ) ;
1440
+ let file = fs:: read_to_string ( & path) . expect ( "Failed to read file" ) ;
1441
+ println ! ( "path={}\n file={:?}" , path. display( ) , file) ;
1442
+
1443
+ assert_eq ! (
1444
+ MAX_FILE_SIZE + 10 ,
1445
+ file_metadata. len( ) ,
1446
+ "expected size = {:?}, file size = {:?}" ,
1447
+ MAX_FILE_SIZE ,
1448
+ file_metadata. len( ) ,
1449
+ ) ;
1450
+ }
1451
+ }
1268
1452
}
0 commit comments