mirror of
https://github.com/tokio-rs/tracing.git
synced 2026-01-24 20:06:16 +00:00
appender: introduce weekly rotation (#3218)
## Motivation While configuring tracing-appender, I wanted to specify a weekly log rotation interval. I was unable to do so, as the largest rotation interval was daily. ## Solution Before my introduction of weekly log rotation, rounding the current `OffsetDateTime` was straightforward: we could simply keep the current date and truncate part or all of the time component. However, we cannot simply truncate the time with weekly rotation; the date must now be modified. To round the date, we roll logs at 00:00 UTC on Sunday. This gives us consistent date-times that only change weekly.
This commit is contained in:
@@ -362,7 +362,7 @@ pub fn hourly(
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`.
|
||||
/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
|
||||
pub fn daily(
|
||||
directory: impl AsRef<Path>,
|
||||
file_name_prefix: impl AsRef<Path>,
|
||||
@@ -370,6 +370,42 @@ pub fn daily(
|
||||
RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix)
|
||||
}
|
||||
|
||||
/// Creates a weekly-rotating file appender. The logs will rotate every Sunday at midnight UTC.
|
||||
///
|
||||
/// The appender returned by `rolling::weekly` can be used with `non_blocking` to create
|
||||
/// a non-blocking, weekly file appender.
|
||||
///
|
||||
/// A `RollingFileAppender` has a fixed rotation whose frequency is
|
||||
/// defined by [`Rotation`][self::Rotation]. The `directory` and
|
||||
/// `file_name_prefix` arguments determine the location and file name's _prefix_
|
||||
/// of the log file. `RollingFileAppender` automatically appends the current date in UTC.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ``` rust
|
||||
/// # #[clippy::allow(needless_doctest_main)]
|
||||
/// fn main () {
|
||||
/// # fn doc() {
|
||||
/// let appender = tracing_appender::rolling::weekly("/some/path", "rolling.log");
|
||||
/// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender);
|
||||
///
|
||||
/// let collector = tracing_subscriber::fmt().with_writer(non_blocking_appender);
|
||||
///
|
||||
/// tracing::collect::with_default(collector.finish(), || {
|
||||
/// tracing::event!(tracing::Level::INFO, "Hello");
|
||||
/// });
|
||||
/// # }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd`.
|
||||
pub fn weekly(
|
||||
directory: impl AsRef<Path>,
|
||||
file_name_prefix: impl AsRef<Path>,
|
||||
) -> RollingFileAppender {
|
||||
RollingFileAppender::new(Rotation::WEEKLY, directory, file_name_prefix)
|
||||
}
|
||||
|
||||
/// Creates a non-rolling file appender.
|
||||
///
|
||||
/// The appender returned by `rolling::never` can be used with `non_blocking` to create
|
||||
@@ -429,6 +465,14 @@ pub fn never(directory: impl AsRef<Path>, file_name: impl AsRef<Path>) -> Rollin
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ### Weekly Rotation
|
||||
/// ```rust
|
||||
/// # fn docs() {
|
||||
/// use tracing_appender::rolling::Rotation;
|
||||
/// let rotation = tracing_appender::rolling::Rotation::WEEKLY;
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ### No Rotation
|
||||
/// ```rust
|
||||
/// # fn docs() {
|
||||
@@ -444,31 +488,40 @@ enum RotationKind {
|
||||
Minutely,
|
||||
Hourly,
|
||||
Daily,
|
||||
Weekly,
|
||||
Never,
|
||||
}
|
||||
|
||||
impl Rotation {
|
||||
/// Provides an minutely rotation
|
||||
/// Provides a minutely rotation.
|
||||
pub const MINUTELY: Self = Self(RotationKind::Minutely);
|
||||
/// Provides an hourly rotation
|
||||
/// Provides an hourly rotation.
|
||||
pub const HOURLY: Self = Self(RotationKind::Hourly);
|
||||
/// Provides a daily rotation
|
||||
/// Provides a daily rotation.
|
||||
pub const DAILY: Self = Self(RotationKind::Daily);
|
||||
/// Provides a weekly rotation that rotates every Sunday at midnight UTC.
|
||||
pub const WEEKLY: Self = Self(RotationKind::Weekly);
|
||||
/// Provides a rotation that never rotates.
|
||||
pub const NEVER: Self = Self(RotationKind::Never);
|
||||
|
||||
/// Determines the next date that we should round to or `None` if `self` uses [`Rotation::NEVER`].
|
||||
pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
|
||||
let unrounded_next_date = match *self {
|
||||
Rotation::MINUTELY => *current_date + Duration::minutes(1),
|
||||
Rotation::HOURLY => *current_date + Duration::hours(1),
|
||||
Rotation::DAILY => *current_date + Duration::days(1),
|
||||
Rotation::WEEKLY => *current_date + Duration::weeks(1),
|
||||
Rotation::NEVER => return None,
|
||||
};
|
||||
Some(self.round_date(&unrounded_next_date))
|
||||
Some(self.round_date(unrounded_next_date))
|
||||
}
|
||||
|
||||
// note that this method will panic if passed a `Rotation::NEVER`.
|
||||
pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime {
|
||||
/// Rounds the date towards the past using the [`Rotation`] interval.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method will panic if `self`` uses [`Rotation::NEVER`].
|
||||
pub(crate) fn round_date(&self, date: OffsetDateTime) -> OffsetDateTime {
|
||||
match *self {
|
||||
Rotation::MINUTELY => {
|
||||
let time = Time::from_hms(date.hour(), date.minute(), 0)
|
||||
@@ -485,6 +538,14 @@ impl Rotation {
|
||||
.expect("Invalid time; this is a bug in tracing-appender");
|
||||
date.replace_time(time)
|
||||
}
|
||||
Rotation::WEEKLY => {
|
||||
let zero_time = Time::from_hms(0, 0, 0)
|
||||
.expect("Invalid time; this is a bug in tracing-appender");
|
||||
|
||||
let days_since_sunday = date.weekday().number_days_from_sunday();
|
||||
let date = date - Duration::days(days_since_sunday.into());
|
||||
date.replace_time(zero_time)
|
||||
}
|
||||
// Rotation::NEVER is impossible to round.
|
||||
Rotation::NEVER => {
|
||||
unreachable!("Rotation::NEVER is impossible to round.")
|
||||
@@ -497,6 +558,7 @@ impl Rotation {
|
||||
Rotation::MINUTELY => format_description::parse("[year]-[month]-[day]-[hour]-[minute]"),
|
||||
Rotation::HOURLY => format_description::parse("[year]-[month]-[day]-[hour]"),
|
||||
Rotation::DAILY => format_description::parse("[year]-[month]-[day]"),
|
||||
Rotation::WEEKLY => format_description::parse("[year]-[month]-[day]"),
|
||||
Rotation::NEVER => format_description::parse("[year]-[month]-[day]"),
|
||||
}
|
||||
.expect("Unable to create a formatter; this is a bug in tracing-appender")
|
||||
@@ -548,10 +610,17 @@ impl Inner {
|
||||
Ok((inner, writer))
|
||||
}
|
||||
|
||||
/// Returns the full filename for the provided date, using [`Rotation`] to round accordingly.
|
||||
pub(crate) fn join_date(&self, date: &OffsetDateTime) -> String {
|
||||
let date = date
|
||||
.format(&self.date_format)
|
||||
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
|
||||
let date = if let Rotation::NEVER = self.rotation {
|
||||
date.format(&self.date_format)
|
||||
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
|
||||
} else {
|
||||
self.rotation
|
||||
.round_date(*date)
|
||||
.format(&self.date_format)
|
||||
.expect("Unable to format OffsetDateTime; this is a bug in tracing-appender")
|
||||
};
|
||||
|
||||
match (
|
||||
&self.rotation,
|
||||
@@ -748,7 +817,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn write_minutely_log() {
|
||||
test_appender(Rotation::HOURLY, "minutely.log");
|
||||
test_appender(Rotation::MINUTELY, "minutely.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -761,6 +830,11 @@ mod test {
|
||||
test_appender(Rotation::DAILY, "daily.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_weekly_log() {
|
||||
test_appender(Rotation::WEEKLY, "weekly.log");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_never_log() {
|
||||
test_appender(Rotation::NEVER, "never.log");
|
||||
@@ -778,24 +852,109 @@ mod test {
|
||||
let next = Rotation::HOURLY.next_date(&now).unwrap();
|
||||
assert_eq!((now + Duration::HOUR).hour(), next.hour());
|
||||
|
||||
// daily-basis
|
||||
// per-day basis
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let next = Rotation::DAILY.next_date(&now).unwrap();
|
||||
assert_eq!((now + Duration::DAY).day(), next.day());
|
||||
|
||||
// per-week basis
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let now_rounded = Rotation::WEEKLY.round_date(now);
|
||||
let next = Rotation::WEEKLY.next_date(&now).unwrap();
|
||||
assert!(now_rounded < next);
|
||||
|
||||
// never
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let next = Rotation::NEVER.next_date(&now);
|
||||
assert!(next.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_date() {
|
||||
struct TestCase {
|
||||
expected: &'static str,
|
||||
rotation: Rotation,
|
||||
prefix: Option<&'static str>,
|
||||
suffix: Option<&'static str>,
|
||||
now: OffsetDateTime,
|
||||
}
|
||||
|
||||
let format = format_description::parse(
|
||||
"[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \
|
||||
sign:mandatory]:[offset_minute]:[offset_second]",
|
||||
)
|
||||
.unwrap();
|
||||
let directory = tempfile::tempdir().expect("failed to create tempdir");
|
||||
|
||||
let test_cases = vec![
|
||||
TestCase {
|
||||
expected: "my_prefix.2025-02-16.log",
|
||||
rotation: Rotation::WEEKLY,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
// Make sure weekly rotation rounds to the preceding year when appropriate
|
||||
TestCase {
|
||||
expected: "my_prefix.2024-12-29.log",
|
||||
rotation: Rotation::WEEKLY,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-01-01 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
TestCase {
|
||||
expected: "my_prefix.2025-02-17.log",
|
||||
rotation: Rotation::DAILY,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
TestCase {
|
||||
expected: "my_prefix.2025-02-17-10.log",
|
||||
rotation: Rotation::HOURLY,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
TestCase {
|
||||
expected: "my_prefix.2025-02-17-10-01.log",
|
||||
rotation: Rotation::MINUTELY,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
TestCase {
|
||||
expected: "my_prefix.log",
|
||||
rotation: Rotation::NEVER,
|
||||
prefix: Some("my_prefix"),
|
||||
suffix: Some("log"),
|
||||
now: OffsetDateTime::parse("2025-02-17 10:01:00 +00:00:00", &format).unwrap(),
|
||||
},
|
||||
];
|
||||
|
||||
for test_case in test_cases {
|
||||
let (inner, _) = Inner::new(
|
||||
test_case.now,
|
||||
test_case.rotation.clone(),
|
||||
directory.path(),
|
||||
test_case.prefix.map(ToString::to_string),
|
||||
test_case.suffix.map(ToString::to_string),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let path = inner.join_date(&test_case.now);
|
||||
|
||||
assert_eq!(path, test_case.expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round."
|
||||
)]
|
||||
fn test_never_date_rounding() {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let _ = Rotation::NEVER.round_date(&now);
|
||||
let _ = Rotation::NEVER.round_date(now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user