From c6d96fd0ae43a5c44e2ad5e2c57c581e20187b0a Mon Sep 17 00:00:00 2001 From: Shayne Fletcher Date: Mon, 25 Sep 2023 13:51:03 -0400 Subject: [PATCH] [tracing-subscriber]: add chrono crate implementations of FormatTime (#2690) Issue https://github.com/tokio-rs/tracing/issues/2080 explains that it's not possible to soundly use [`tracing_subscriber::fmt::time::LocalTime`](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/time/struct.LocalTime.html) in a multithreaded context. It proposes adding alternative time formatters that use the [chrono crate](https://docs.rs/chrono/latest/chrono/) to workaround which is what this PR offers. A new source file 'chrono_crate.rs' is added to the 'tracing-subscriber' package implementing `mod chrono_crate` providing two new tag types `LocalTime` and `Utc` with associated `time::FormatTime` trait implementations that call `chrono::Local::now().to_rfc3339()` and `chrono::Utc::now().to_rfc3339()` respectively. Simple unit-tests of the new functionality accompany the additions. --------- Co-authored-by: David Barsky Co-authored-by: Shayne Fletcher --- tracing-subscriber/Cargo.toml | 1 + .../src/fmt/time/chrono_crate.rs | 177 ++++++++++++++++++ tracing-subscriber/src/fmt/time/mod.rs | 13 ++ 3 files changed, 191 insertions(+) create mode 100644 tracing-subscriber/src/fmt/time/chrono_crate.rs diff --git a/tracing-subscriber/Cargo.toml b/tracing-subscriber/Cargo.toml index 3fbeb2feab..3ee5a99e41 100644 --- a/tracing-subscriber/Cargo.toml +++ b/tracing-subscriber/Cargo.toml @@ -59,6 +59,7 @@ tracing-serde = { path = "../tracing-serde", version = "0.1.3", optional = true # opt-in deps parking_lot = { version = "0.12.1", optional = true } +chrono = { version = "0.4.26", default-features = false, features = ["clock", "std"], optional = true } # registry sharded-slab = { version = "0.1.4", optional = true } diff --git a/tracing-subscriber/src/fmt/time/chrono_crate.rs b/tracing-subscriber/src/fmt/time/chrono_crate.rs new file mode 100644 index 0000000000..1a831efa1b --- /dev/null +++ b/tracing-subscriber/src/fmt/time/chrono_crate.rs @@ -0,0 +1,177 @@ +use crate::fmt::format::Writer; +use crate::fmt::time::FormatTime; + +use std::sync::Arc; + +/// Formats [local time]s and [UTC time]s with `FormatTime` implementations +/// that use the [`chrono` crate]. +/// +/// [local time]: [`chrono::offset::Local`] +/// [UTC time]: [`chrono::offset::Utc`] +/// [`chrono` crate]: [`chrono`] + +/// Formats the current [local time] using a [formatter] from the [`chrono`] crate. +/// +/// [local time]: chrono::Local::now() +/// [formatter]: chrono::format +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct ChronoLocal { + format: Arc, +} + +impl ChronoLocal { + /// Format the time using the [`RFC 3339`] format + /// (a subset of [`ISO 8601`]). + /// + /// [`RFC 3339`]: https://tools.ietf.org/html/rfc3339 + /// [`ISO 8601`]: https://en.wikipedia.org/wiki/ISO_8601 + pub fn rfc_3339() -> Self { + Self { + format: Arc::new(ChronoFmtType::Rfc3339), + } + } + + /// Format the time using the given format string. + /// + /// See [`chrono::format::strftime`] for details on the supported syntax. + pub fn new(format_string: String) -> Self { + Self { + format: Arc::new(ChronoFmtType::Custom(format_string)), + } + } +} + +impl FormatTime for ChronoLocal { + fn format_time(&self, w: &mut Writer<'_>) -> alloc::fmt::Result { + let t = chrono::Local::now(); + match self.format.as_ref() { + ChronoFmtType::Rfc3339 => { + use chrono::format::{Fixed, Item}; + write!( + w, + "{}", + t.format_with_items(core::iter::once(Item::Fixed(Fixed::RFC3339))) + ) + } + ChronoFmtType::Custom(fmt) => { + write!(w, "{}", t.format(fmt)) + } + } + } +} + +/// Formats the current [UTC time] using a [formatter] from the [`chrono`] crate. +/// +/// [UTC time]: chrono::Utc::now() +/// [formatter]: chrono::format +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub struct ChronoUtc { + format: Arc, +} + +impl ChronoUtc { + /// Format the time using the [`RFC 3339`] format + /// (a subset of [`ISO 8601`]). + /// + /// [`RFC 3339`]: https://tools.ietf.org/html/rfc3339 + /// [`ISO 8601`]: https://en.wikipedia.org/wiki/ISO_8601 + pub fn rfc_3339() -> Self { + Self { + format: Arc::new(ChronoFmtType::Rfc3339), + } + } + + /// Format the time using the given format string. + /// + /// See [`chrono::format::strftime`] for details on the supported syntax. + pub fn new(format_string: String) -> Self { + Self { + format: Arc::new(ChronoFmtType::Custom(format_string)), + } + } +} + +impl FormatTime for ChronoUtc { + fn format_time(&self, w: &mut Writer<'_>) -> alloc::fmt::Result { + let t = chrono::Utc::now(); + match self.format.as_ref() { + ChronoFmtType::Rfc3339 => w.write_str(&t.to_rfc3339()), + ChronoFmtType::Custom(fmt) => w.write_str(&format!("{}", t.format(fmt))), + } + } +} + +/// The RFC 3339 format is used by default but a custom format string +/// can be used. See [`chrono::format::strftime`]for details on +/// the supported syntax. +/// +/// [`chrono::format::strftime`]: https://docs.rs/chrono/0.4.9/chrono/format/strftime/index.html +#[derive(Debug, Clone, Eq, PartialEq)] +enum ChronoFmtType { + /// Format according to the RFC 3339 convention. + Rfc3339, + /// Format according to a custom format string. + Custom(String), +} + +impl Default for ChronoFmtType { + fn default() -> Self { + ChronoFmtType::Rfc3339 + } +} + +#[cfg(test)] +mod tests { + use crate::fmt::format::Writer; + use crate::fmt::time::FormatTime; + + use std::sync::Arc; + + use super::ChronoFmtType; + use super::ChronoLocal; + use super::ChronoUtc; + + #[test] + fn test_chrono_format_time_utc_default() { + let mut buf = String::new(); + let mut dst: Writer<'_> = Writer::new(&mut buf); + assert!(FormatTime::format_time(&ChronoUtc::default(), &mut dst).is_ok()); + // e.g. `buf` contains "2023-08-18T19:05:08.662499+00:00" + assert!(chrono::DateTime::parse_from_str(&buf, "%FT%H:%M:%S%.6f%z").is_ok()); + } + + #[test] + fn test_chrono_format_time_utc_custom() { + let fmt = ChronoUtc { + format: Arc::new(ChronoFmtType::Custom("%a %b %e %T %Y".to_owned())), + }; + let mut buf = String::new(); + let mut dst: Writer<'_> = Writer::new(&mut buf); + assert!(FormatTime::format_time(&fmt, &mut dst).is_ok()); + // e.g. `buf` contains "Wed Aug 23 15:53:23 2023" + assert!(chrono::NaiveDateTime::parse_from_str(&buf, "%a %b %e %T %Y").is_ok()); + } + + #[test] + fn test_chrono_format_time_local_default() { + let mut buf = String::new(); + let mut dst: Writer<'_> = Writer::new(&mut buf); + assert!(FormatTime::format_time(&ChronoLocal::default(), &mut dst).is_ok()); + // e.g. `buf` contains "2023-08-18T14:59:08.662499-04:00". + assert!(chrono::DateTime::parse_from_str(&buf, "%FT%H:%M:%S%.6f%z").is_ok()); + } + + #[test] + fn test_chrono_format_time_local_custom() { + let fmt = ChronoLocal { + format: Arc::new(ChronoFmtType::Custom("%a %b %e %T %Y".to_owned())), + }; + let mut buf = String::new(); + let mut dst: Writer<'_> = Writer::new(&mut buf); + assert!(FormatTime::format_time(&fmt, &mut dst).is_ok()); + // e.g. `buf` contains "Wed Aug 23 15:55:46 2023". + assert!(chrono::NaiveDateTime::parse_from_str(&buf, "%a %b %e %T %Y").is_ok()); + } +} diff --git a/tracing-subscriber/src/fmt/time/mod.rs b/tracing-subscriber/src/fmt/time/mod.rs index 5ba6698194..48f34d18d2 100644 --- a/tracing-subscriber/src/fmt/time/mod.rs +++ b/tracing-subscriber/src/fmt/time/mod.rs @@ -7,6 +7,7 @@ mod datetime; #[cfg(feature = "time")] mod time_crate; + #[cfg(feature = "time")] #[cfg_attr(docsrs, doc(cfg(feature = "time")))] pub use time_crate::UtcTime; @@ -19,6 +20,18 @@ pub use time_crate::LocalTime; #[cfg_attr(docsrs, doc(cfg(feature = "time")))] pub use time_crate::OffsetTime; +/// [`chrono`]-based implementation for [`FormatTime`]. +#[cfg(feature = "chrono")] +mod chrono_crate; + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +pub use chrono_crate::ChronoLocal; + +#[cfg(feature = "chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))] +pub use chrono_crate::ChronoUtc; + /// A type that can measure and format the current time. /// /// This trait is used by `Format` to include a timestamp with each `Event` when it is logged.