Skip to content

Commit 795e6d8

Browse files
committed
appender: add size based rolling
1 parent e63ef57 commit 795e6d8

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

tracing-appender/src/rolling.rs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}
@@ -406,6 +413,38 @@ pub fn weekly(
406413
RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
407414
}
408415

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+
409448
/// Creates a non-rolling file appender.
410449
///
411450
/// 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
473512
/// # }
474513
/// ```
475514
///
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+
///
476523
/// ### No Rotation
477524
/// ```rust
478525
/// # fn docs() {
@@ -489,6 +536,7 @@ enum RotationKind {
489536
Hourly,
490537
Daily,
491538
Weekly,
539+
Size,
492540
Never,
493541
}
494542

@@ -501,6 +549,8 @@ impl Rotation {
501549
pub const DAILY: Self = Self(RotationKind::Daily);
502550
/// Provides a weekly rotation that rotates every Sunday at midnight UTC.
503551
pub const WEEKLY: Self = Self(RotationKind::Weekly);
552+
/// Provides a size based rotation
553+
pub const SIZE: Self = Self(RotationKind::Size);
504554
/// Provides a rotation that never rotates.
505555
pub const NEVER: Self = Self(RotationKind::Never);
506556

@@ -511,6 +561,7 @@ impl Rotation {
511561
Rotation::HOURLY => *current_date + Duration::hours(1),
512562
Rotation::DAILY => *current_date + Duration::days(1),
513563
Rotation::WEEKLY => *current_date + Duration::weeks(1),
564+
Rotation::SIZE => return None,
514565
Rotation::NEVER => return None,
515566
};
516567
Some(self.round_date(unrounded_next_date))
@@ -546,6 +597,10 @@ impl Rotation {
546597
let date = date - Duration::days(days_since_sunday.into());
547598
date.replace_time(zero_time)
548599
}
600+
// Rotation::SIZE is impossible to round.
601+
Rotation::SIZE => {
602+
unreachable!("Rotation::SIZE is impossible to round.")
603+
}
549604
// Rotation::NEVER is impossible to round.
550605
Rotation::NEVER => {
551606
unreachable!("Rotation::NEVER is impossible to round.")
@@ -559,6 +614,9 @@ impl Rotation {
559614
Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
560615
Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
561616
Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
617+
Rotation::SIZE => format_description::parse(
618+
"[year]-[month]-[day]-[hour]-[minute]-[second]-[subsecond]",
619+
),
562620
Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
563621
}
564622
.expect("Unable to create a formatter; this is a bug in tracing-appender")
@@ -587,6 +645,7 @@ impl Inner {
587645
log_filename_prefix: Option<String>,
588646
log_filename_suffix: Option<String>,
589647
max_files: Option<usize>,
648+
max_file_size: Option<u64>,
590649
) -> Result<(Self, RwLock<File>), builder::InitError> {
591650
let log_directory = directory.as_ref().to_path_buf();
592651
let date_format = rotation.date_format();
@@ -604,6 +663,7 @@ impl Inner {
604663
),
605664
rotation,
606665
max_files,
666+
max_file_size,
607667
};
608668
let filename = inner.join_date(&now);
609669
let writer = RwLock::new(create_writer(inner.log_directory.as_ref(), &filename)?);
@@ -743,6 +803,23 @@ impl Inner {
743803
None
744804
}
745805

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+
746823
fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
747824
let next_date = self
748825
.rotation
@@ -835,6 +912,11 @@ mod test {
835912
test_appender(Rotation::WEEKLY, "weekly.log");
836913
}
837914

915+
#[test]
916+
fn write_size_log() {
917+
test_appender(Rotation::SIZE, "size.log");
918+
}
919+
838920
#[test]
839921
fn write_never_log() {
840922
test_appender(Rotation::NEVER, "never.log");
@@ -863,6 +945,11 @@ mod test {
863945
let next = Rotation::WEEKLY.next_date(&now).unwrap();
864946
assert!(now_rounded < next);
865947

948+
// size
949+
let now = OffsetDateTime::now_utc();
950+
let next = Rotation::SIZE.next_date(&now);
951+
assert!(next.is_none());
952+
866953
// never
867954
let now = OffsetDateTime::now_utc();
868955
let next = Rotation::NEVER.next_date(&now);
@@ -940,6 +1027,7 @@ mod test {
9401027
test_case.prefix.map(ToString::to_string),
9411028
test_case.suffix.map(ToString::to_string),
9421029
None,
1030+
None,
9431031
)
9441032
.unwrap();
9451033
let path = inner.join_date(&test_case.now);
@@ -988,6 +1076,7 @@ mod test {
9881076
prefix.map(ToString::to_string),
9891077
suffix.map(ToString::to_string),
9901078
None,
1079+
None,
9911080
)
9921081
.unwrap();
9931082
let path = inner.join_date(&now);
@@ -1018,6 +1107,12 @@ mod test {
10181107
prefix: Some("app.log"),
10191108
suffix: None,
10201109
},
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+
},
10211116
TestCase {
10221117
expected: "app.log",
10231118
rotation: Rotation::NEVER,
@@ -1043,6 +1138,12 @@ mod test {
10431138
prefix: Some("app"),
10441139
suffix: Some("log"),
10451140
},
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+
},
10461147
TestCase {
10471148
expected: "app.log",
10481149
rotation: Rotation::NEVER,
@@ -1068,6 +1169,12 @@ mod test {
10681169
prefix: None,
10691170
suffix: Some("log"),
10701171
},
1172+
TestCase {
1173+
expected: "2020-02-01-10-01-00-0.log",
1174+
rotation: Rotation::SIZE,
1175+
prefix: None,
1176+
suffix: Some("log"),
1177+
},
10711178
TestCase {
10721179
expected: "log",
10731180
rotation: Rotation::NEVER,
@@ -1100,6 +1207,7 @@ mod test {
11001207
Some("test_make_writer".to_string()),
11011208
None,
11021209
None,
1210+
None,
11031211
)
11041212
.unwrap();
11051213

@@ -1182,6 +1290,7 @@ mod test {
11821290
Some("test_max_log_files".to_string()),
11831291
None,
11841292
Some(2),
1293+
None,
11851294
)
11861295
.unwrap();
11871296

@@ -1265,4 +1374,79 @@ mod test {
12651374
}
12661375
}
12671376
}
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={}\nfile_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={}\nfile={:?}", 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+
}
12681452
}

0 commit comments

Comments
 (0)