Skip to content

Commit 50b6e7b

Browse files
committed
appender: add size based rolling
1 parent d6505ca commit 50b6e7b

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

tracing-appender/src/rolling.rs

Lines changed: 175 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
}
@@ -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={}\nfile_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={}\nfile={:?}", 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
}

tracing-appender/src/rolling/builder.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub struct Builder {
1111
pub(super) prefix: Option<String>,
1212
pub(super) suffix: Option<String>,
1313
pub(super) max_files: Option<usize>,
14+
pub(super) max_file_size: Option<u64>,
1415
}
1516

1617
/// Errors returned by [`Builder::build`].
@@ -42,18 +43,21 @@ impl Builder {
4243
/// | [`filename_prefix`] | `""` | By default, log file names will not have a prefix. |
4344
/// | [`filename_suffix`] | `""` | By default, log file names will not have a suffix. |
4445
/// | [`max_log_files`] | `None` | By default, there is no limit for maximum log file count. |
46+
/// | [`max_file_size`] | `None` | By default, there is no limit for maximum log file size. |
4547
///
4648
/// [`rotation`]: Self::rotation
4749
/// [`filename_prefix`]: Self::filename_prefix
4850
/// [`filename_suffix`]: Self::filename_suffix
4951
/// [`max_log_files`]: Self::max_log_files
52+
/// ['max_file_size`]: Self::max_file_size
5053
#[must_use]
5154
pub const fn new() -> Self {
5255
Self {
5356
rotation: Rotation::NEVER,
5457
prefix: None,
5558
suffix: None,
5659
max_files: None,
60+
max_file_size: None,
5761
}
5862
}
5963

@@ -233,6 +237,32 @@ impl Builder {
233237
}
234238
}
235239

240+
/// Limits the file size to `n` bytes on disk, when using SIZE rotation.
241+
///
242+
/// # Examples
243+
///
244+
/// ```
245+
/// use tracing_appender::rolling::RollingFileAppender;
246+
/// use tracing_appender::rolling::Rotation;
247+
///
248+
/// # fn docs() {
249+
/// let appender = RollingFileAppender::builder()
250+
/// .rotation(Rotation::SIZE) // rotate log files when they reach a certain size
251+
/// .max_file_size(1024) // only the most recent 5 log files will be kept
252+
/// // ...
253+
/// .build("/var/log")
254+
/// .expect("failed to initialize rolling file appender");
255+
/// # drop(appender)
256+
/// # }
257+
/// ```
258+
#[must_use]
259+
pub fn max_file_size(self, n: u64) -> Self {
260+
Self {
261+
max_file_size: Some(n),
262+
..self
263+
}
264+
}
265+
236266
/// Builds a new [`RollingFileAppender`] with the configured parameters,
237267
/// emitting log files to the provided directory.
238268
///

0 commit comments

Comments
 (0)