diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 709eebf554..9b64d8104c 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -42,7 +42,24 @@ jobs: uses: actions-rs/cargo@v1 with: command: check - args: --all + args: --all --exclude=tracing-appender + + # TODO: remove this once tracing's MSRV is bumped. + check-msrv-appender: + # Run `cargo check` on our minimum supported Rust version (1.51.0). + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@main + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.51.0 + profile: minimal + override: true + - name: Check + uses: actions-rs/cargo@v1 + with: + command: check + args: --lib=tracing-appender check: # Run `cargo check` first to ensure that the pushed code at least compiles. diff --git a/tracing-appender/Cargo.toml b/tracing-appender/Cargo.toml index e96fe443fa..a4276ab23d 100644 --- a/tracing-appender/Cargo.toml +++ b/tracing-appender/Cargo.toml @@ -21,14 +21,15 @@ edition = "2018" [dependencies] crossbeam-channel = "0.5.0" -chrono = { version = "0.4.16", default-features = false, features = ["clock", "std"] } +time = { version = "0.3", default-features = false, features = ["formatting"] } [dependencies.tracing-subscriber] path = "../tracing-subscriber" version = "0.2.7" default-features = false -features = ["fmt"] +features = ["fmt", "std"] [dev-dependencies] tracing = { path = "../tracing", version = "0.1" } +time = { version = "0.3", default-features = false, features = ["formatting", "parsing"] } tempfile = "3" diff --git a/tracing-appender/README.md b/tracing-appender/README.md index 9e881201d4..2bd0d348e8 100644 --- a/tracing-appender/README.md +++ b/tracing-appender/README.md @@ -36,7 +36,7 @@ allows events and spans to be recorded in a non-blocking manner through a dedicated logging thread. It also provides a [`RollingFileAppender`][file_appender] that can be used with _or_ without the non-blocking writer. -*Compiler support: [requires `rustc` 1.42+][msrv]* +*Compiler support: [requires `rustc` 1.51+][msrv]* [msrv]: #supported-rust-versions @@ -145,17 +145,17 @@ fn main() { ## Supported Rust Versions -Tracing is built against the latest stable release. The minimum supported -version is 1.42. The current Tracing version is not guaranteed to build on Rust -versions earlier than the minimum supported version. +`tracing-appender` is built against the latest stable release. The minimum supported +version is 1.51. The current `tracing-appender` version is not guaranteed to build on +Rust versions earlier than the minimum supported version. Tracing follows the same compiler support policies as the rest of the Tokio project. The current stable Rust compiler and the three most recent minor -versions before it will always be supported. For example, if the current stable -compiler version is 1.45, the minimum supported version will not be increased -past 1.42, three minor versions prior. Increasing the minimum supported compiler -version is not considered a semver breaking change as long as doing so complies -with this policy. +versions before it will always be supported. For example, if the current +stable compiler version is 1.45, the minimum supported version will not be +increased past 1.42, three minor versions prior. Increasing the minimum +supported compiler version is not considered a semver breaking change as +long as doing so complies with this policy. ## License diff --git a/tracing-appender/src/inner.rs b/tracing-appender/src/inner.rs index 409918b159..9c9a74c45f 100644 --- a/tracing-appender/src/inner.rs +++ b/tracing-appender/src/inner.rs @@ -2,23 +2,23 @@ use std::io::{BufWriter, Write}; use std::{fs, io}; use crate::rolling::Rotation; -use chrono::prelude::*; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::path::Path; +use time::OffsetDateTime; #[derive(Debug)] pub(crate) struct InnerAppender { log_directory: String, log_filename_prefix: String, writer: BufWriter, - next_date: DateTime, + next_date: Option, rotation: Rotation, } impl io::Write for InnerAppender { fn write(&mut self, buf: &[u8]) -> io::Result { - let now = Utc::now(); + let now = OffsetDateTime::now_utc(); self.write_timestamped(buf, now) } @@ -32,7 +32,7 @@ impl InnerAppender { log_directory: &Path, log_filename_prefix: &Path, rotation: Rotation, - now: DateTime, + now: OffsetDateTime, ) -> io::Result { let log_directory = log_directory.to_str().unwrap(); let log_filename_prefix = log_filename_prefix.to_str().unwrap(); @@ -49,7 +49,7 @@ impl InnerAppender { }) } - fn write_timestamped(&mut self, buf: &[u8], date: DateTime) -> io::Result { + fn write_timestamped(&mut self, buf: &[u8], date: OffsetDateTime) -> io::Result { // Even if refresh_writer fails, we still have the original writer. Ignore errors // and proceed with the write. let buf_len = buf.len(); @@ -57,7 +57,7 @@ impl InnerAppender { self.writer.write_all(buf).map(|_| buf_len) } - fn refresh_writer(&mut self, now: DateTime) { + fn refresh_writer(&mut self, now: OffsetDateTime) { if self.should_rollover(now) { let filename = self.rotation.join_date(&self.log_filename_prefix, &now); @@ -75,8 +75,12 @@ impl InnerAppender { } } - fn should_rollover(&self, date: DateTime) -> bool { - date >= self.next_date + fn should_rollover(&self, date: OffsetDateTime) -> bool { + // the `None` case means that the `InnerAppender` *never* rorates log files. + match self.next_date { + None => false, + Some(next_date) => date >= next_date, + } } } diff --git a/tracing-appender/src/lib.rs b/tracing-appender/src/lib.rs index 2c9c86a6fd..0dbb2c428b 100644 --- a/tracing-appender/src/lib.rs +++ b/tracing-appender/src/lib.rs @@ -7,7 +7,7 @@ //! a dedicated logging thread. It also provides a [`RollingFileAppender`][file_appender] that can //! be used with _or_ without the non-blocking writer. //! -//! *Compiler support: [requires `rustc` 1.42+][msrv]* +//! *Compiler support: [requires `rustc` 1.51+][msrv]* //! //! [msrv]: #supported-rust-versions //! [file_appender]: ./rolling/struct.RollingFileAppender.html @@ -110,8 +110,8 @@ //! //! ## Supported Rust Versions //! -//! Tracing is built against the latest stable release. The minimum supported -//! version is 1.42. The current Tracing version is not guaranteed to build on +//! `tracing-appender` is built against the latest stable release. The minimum supported +//! version is 1.51. The current `tracing-appender` version is not guaranteed to build on //! Rust versions earlier than the minimum supported version. //! //! Tracing follows the same compiler support policies as the rest of the Tokio diff --git a/tracing-appender/src/rolling.rs b/tracing-appender/src/rolling.rs index 6f3ff0f259..1a9bdac85b 100644 --- a/tracing-appender/src/rolling.rs +++ b/tracing-appender/src/rolling.rs @@ -31,10 +31,9 @@ //! # } //! ``` use crate::inner::InnerAppender; -use chrono::{DateTime, Datelike, TimeZone, Timelike, Utc}; -use std::fmt::Debug; use std::io; use std::path::Path; +use time::{format_description, Duration, OffsetDateTime, Time}; /// A file appender with the ability to rotate log files at a fixed schedule. /// @@ -95,7 +94,7 @@ impl RollingFileAppender { directory.as_ref(), file_name_prefix.as_ref(), rotation, - Utc::now(), + OffsetDateTime::now_utc(), ) .expect("Failed to create appender"), } @@ -305,42 +304,69 @@ impl Rotation { /// Provides a rotation that never rotates. pub const NEVER: Self = Self(RotationKind::Never); - pub(crate) fn next_date(&self, current_date: &DateTime) -> DateTime { + pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option { let unrounded_next_date = match *self { - Rotation::MINUTELY => *current_date + chrono::Duration::minutes(1), - Rotation::HOURLY => *current_date + chrono::Duration::hours(1), - Rotation::DAILY => *current_date + chrono::Duration::days(1), - Rotation::NEVER => Utc.ymd(9999, 1, 1).and_hms(1, 0, 0), + Rotation::MINUTELY => *current_date + Duration::minutes(1), + Rotation::HOURLY => *current_date + Duration::hours(1), + Rotation::DAILY => *current_date + Duration::days(1), + Rotation::NEVER => return None, }; - self.round_date(&unrounded_next_date) + Some(self.round_date(&unrounded_next_date)) } - pub(crate) fn round_date(&self, date: &DateTime) -> DateTime { + // note that this method will panic if passed a `Rotation::NEVER`. + pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime { match *self { - Rotation::MINUTELY => Utc.ymd(date.year(), date.month(), date.day()).and_hms( - date.hour(), - date.minute(), - 0, - ), + Rotation::MINUTELY => { + let time = Time::from_hms(date.hour(), date.minute(), 0) + .expect("Invalid time; this is a bug in tracing-appender"); + date.replace_time(time) + } Rotation::HOURLY => { - Utc.ymd(date.year(), date.month(), date.day()) - .and_hms(date.hour(), 0, 0) + let time = Time::from_hms(date.hour(), 0, 0) + .expect("Invalid time; this is a bug in tracing-appender"); + date.replace_time(time) + } + Rotation::DAILY => { + let time = Time::from_hms(0, 0, 0) + .expect("Invalid time; this is a bug in tracing-appender"); + date.replace_time(time) } - Rotation::DAILY => Utc - .ymd(date.year(), date.month(), date.day()) - .and_hms(0, 0, 0), + // Rotation::NEVER is impossible to round. Rotation::NEVER => { - Utc.ymd(date.year(), date.month(), date.day()) - .and_hms(date.hour(), 0, 0) + unreachable!("Rotation::NEVER is impossible to round.") } } } - pub(crate) fn join_date(&self, filename: &str, date: &DateTime) -> String { + pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String { match *self { - Rotation::MINUTELY => format!("{}.{}", filename, date.format("%F-%H-%M")), - Rotation::HOURLY => format!("{}.{}", filename, date.format("%F-%H")), - Rotation::DAILY => format!("{}.{}", filename, date.format("%F")), + Rotation::MINUTELY => { + let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]") + .expect("Unable to create a formatter; this is a bug in tracing-appender"); + + let date = date + .format(&format) + .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); + format!("{}.{}", filename, date) + } + Rotation::HOURLY => { + let format = format_description::parse("[year]-[month]-[day]-[hour]") + .expect("Unable to create a formatter; this is a bug in tracing-appender"); + + let date = date + .format(&format) + .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); + format!("{}.{}", filename, date) + } + Rotation::DAILY => { + let format = format_description::parse("[year]-[month]-[day]") + .expect("Unable to create a formatter; this is a bug in tracing-appender"); + let date = date + .format(&format) + .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); + format!("{}.{}", filename, date) + } Rotation::NEVER => filename.to_string(), } } @@ -408,172 +434,61 @@ mod test { } #[test] - fn test_next_date_minutely() { - let r = Rotation::MINUTELY; - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_minute(1).unwrap(), next); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 20, 30); - let next = r.next_date(&mock_now); - assert_eq!( - mock_now - .with_hour(0) - .unwrap() - .with_minute(21) - .unwrap() - .with_second(0) - .unwrap(), - next - ); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 59, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_hour(1).unwrap().with_minute(0).unwrap(), next); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(23, 59, 0); - let next = r.next_date(&mock_now); - assert_eq!( - mock_now - .with_day(2) - .unwrap() - .with_hour(0) - .unwrap() - .with_minute(0) - .unwrap(), - next - ); - - let mock_now = Utc.ymd(2020, 12, 31).and_hms(23, 59, 0); - let next = r.next_date(&mock_now); - assert_eq!(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), next); - } - - #[test] - fn test_next_date_hourly() { - let r = Rotation::HOURLY; - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_hour(1).unwrap(), next); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 20, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_hour(1).unwrap().with_minute(0).unwrap(), next); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(1, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_hour(2).unwrap(), next); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(23, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_day(2).unwrap().with_hour(0).unwrap(), next); - - let mock_now = Utc.ymd(2020, 12, 31).and_hms(23, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), next); - } - - #[test] - fn test_next_date_daily() { - let r = Rotation::DAILY; - - let mock_now = Utc.ymd(2020, 8, 1).and_hms(0, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(mock_now.with_day(2).unwrap().with_hour(0).unwrap(), next); - - let mock_now = Utc.ymd(2020, 8, 1).and_hms(0, 20, 5); - let next = r.next_date(&mock_now); - assert_eq!(Utc.ymd(2020, 8, 2).and_hms(0, 0, 0), next); - - let mock_now = Utc.ymd(2020, 8, 31).and_hms(11, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(Utc.ymd(2020, 9, 1).and_hms(0, 0, 0), next); - - let mock_now = Utc.ymd(2020, 12, 31).and_hms(23, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(Utc.ymd(2021, 1, 1).and_hms(0, 0, 0), next); - } - - #[test] - fn test_round_date_minutely() { - let r = Rotation::MINUTELY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - assert_eq!( - Utc.ymd(2020, 2, 1).and_hms(10, 3, 0), - r.round_date(&mock_now) - ); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 0); - assert_eq!(mock_now, r.round_date(&mock_now)); + fn test_rotations() { + // per-minute basis + let now = OffsetDateTime::now_utc(); + let next = Rotation::MINUTELY.next_date(&now).unwrap(); + assert_eq!((now + Duration::MINUTE).minute(), next.minute()); + + // per-hour basis + let now = OffsetDateTime::now_utc(); + let next = Rotation::HOURLY.next_date(&now).unwrap(); + assert_eq!((now + Duration::HOUR).hour(), next.hour()); + + // daily-basis + let now = OffsetDateTime::now_utc(); + let next = Rotation::DAILY.next_date(&now).unwrap(); + assert_eq!((now + Duration::DAY).day(), next.day()); + + // never + let now = OffsetDateTime::now_utc(); + let next = Rotation::NEVER.next_date(&now); + assert!(next.is_none()); } #[test] - fn test_round_date_hourly() { - let r = Rotation::HOURLY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - assert_eq!( - Utc.ymd(2020, 2, 1).and_hms(10, 0, 0), - r.round_date(&mock_now) - ); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 0, 0); - assert_eq!(mock_now, r.round_date(&mock_now)); + #[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); } #[test] - fn test_rotation_path_minutely() { - let r = Rotation::MINUTELY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - let path = r.join_date("MyApplication.log", &mock_now); - assert_eq!("MyApplication.log.2020-02-01-10-03", path); - } - - #[test] - fn test_rotation_path_hourly() { - let r = Rotation::HOURLY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - let path = r.join_date("MyApplication.log", &mock_now); - assert_eq!("MyApplication.log.2020-02-01-10", path); - } - - #[test] - fn test_rotation_path_daily() { - let r = Rotation::DAILY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - let path = r.join_date("MyApplication.log", &mock_now); - assert_eq!("MyApplication.log.2020-02-01", path); - } - - #[test] - fn test_round_date_daily() { - let r = Rotation::DAILY; - let mock_now = Utc.ymd(2020, 2, 1).and_hms(10, 3, 1); - assert_eq!( - Utc.ymd(2020, 2, 1).and_hms(0, 0, 0), - r.round_date(&mock_now) - ); - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 0, 0); - assert_eq!(mock_now, r.round_date(&mock_now)); - } - - #[test] - fn test_next_date_never() { - let r = Rotation::NEVER; - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 0, 0); - let next = r.next_date(&mock_now); - assert_eq!(next, Utc.ymd(9999, 1, 1).and_hms(1, 0, 0)); - } - - #[test] - fn test_join_date_never() { - let r = Rotation::NEVER; - - let mock_now = Utc.ymd(2020, 2, 1).and_hms(0, 0, 0); - let joined_date = r.join_date("Hello.log", &mock_now); - assert_eq!(joined_date, "Hello.log"); + fn test_path_concatination() { + let format = format_description::parse( + "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ + sign:mandatory]:[offset_minute]:[offset_second]", + ) + .unwrap(); + + let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); + + // per-minute + let path = Rotation::MINUTELY.join_date("app.log", &now); + assert_eq!("app.log.2020-02-01-10-01", path); + + // per-hour + let path = Rotation::HOURLY.join_date("app.log", &now); + assert_eq!("app.log.2020-02-01-10", path); + + // per-day + let path = Rotation::DAILY.join_date("app.log", &now); + assert_eq!("app.log.2020-02-01", path); + + // never + let path = Rotation::NEVER.join_date("app.log", &now); + assert_eq!("app.log", path); } }