diff --git a/Cargo.lock b/Cargo.lock index 204d2c4..74f3a1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "env_filter" version = "0.1.0" @@ -103,6 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", + "value-bag", ] [[package]] @@ -111,6 +122,24 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + [[package]] name = "regex" version = "1.7.0" @@ -128,12 +157,45 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 224c665..1d56730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ color = ["dep:anstream", "dep:anstyle"] auto-color = ["color", "anstream/auto"] humantime = ["dep:humantime"] regex = ["env_filter/regex"] +unstable-kv = ["log/kv_unstable"] [dependencies] log = { version = "0.4.8", features = ["std"] } diff --git a/src/fmt/kv.rs b/src/fmt/kv.rs new file mode 100644 index 0000000..4f94b47 --- /dev/null +++ b/src/fmt/kv.rs @@ -0,0 +1,45 @@ +use std::io::{self, Write}; + +use super::Formatter; +use log::kv::{source::Source, Error, Key, Value, Visitor}; + +/// Format function for serializing key/value pairs +/// +/// This trait determines how key/value pairs for structured logs are serialized within the default +/// format. +pub(crate) type KvFormatFn = dyn Fn(&mut Formatter, &dyn Source) -> io::Result<()> + Sync + Send; + +/// Null Key Value Format +/// +/// This function is intended to be passed to [`env_logger::Builder::format_key_values`]. +/// +/// This key value format simply ignores any key/value fields and doesn't include them in the +/// output. +pub fn hidden_kv_format(_formatter: &mut Formatter, _fields: &dyn Source) -> io::Result<()> { + Ok(()) +} + +/// Defualt Key Value Format +/// +/// This function is intended to be passed to [`env_logger::Builder::format_key_values`]. +/// +/// This is the default key/value format. Which uses an "=" as the separator between the key and +/// value and a " " between each pair. +/// +/// For example: `ip=127.0.0.1 port=123456 path=/example` +pub fn default_kv_format(formatter: &mut Formatter, fields: &dyn Source) -> io::Result<()> { + fields + .visit(&mut DefaultVisitor(formatter)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) +} + +struct DefaultVisitor<'a>(&'a mut Formatter); + +impl<'a, 'kvs> Visitor<'kvs> for DefaultVisitor<'a> { + fn visit_pair(&mut self, key: Key, value: Value<'kvs>) -> Result<(), Error> { + // TODO: add styling + // tracing-subscriber uses italic for the key and dimmed for the = + write!(self.0, " {}={}", key, value)?; + Ok(()) + } +} diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index faee69b..5d99dec 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -24,10 +24,27 @@ //! }); //! ``` //! +//! # Key Value arguments +//! +//! If the `unstable-kv` feature is enabled, then the default format will include key values from +//! the log by default, but this can be disabled by calling [`Builder::format_key_value_style`] +//! with [`KVStyle::Hidden`]. +//! +//! ``` +//! use log::info; +//! env_logger::init(); +//! info!(x="45"; "Some message"); +//! info!(x="12"; "Another message {x}", x="12"); +//! ``` +//! +//! See . +//! //! [`Formatter`]: struct.Formatter.html //! [`Style`]: struct.Style.html //! [`Builder::format`]: ../struct.Builder.html#method.format //! [`Write`]: https://doc.rust-lang.org/stable/std/io/trait.Write.html +//! [`Builder::format_key_value_style`]: ../struct.Builder.html#method.format_key_value_style +//! [`KVStyle::Hidden`]: ../enum.KVStyle.html#variant.Hidden use std::cell::RefCell; use std::fmt::Display; @@ -41,6 +58,8 @@ use log::Record; #[cfg(feature = "humantime")] mod humantime; +#[cfg(feature = "unstable-kv")] +mod kv; pub(crate) mod writer; #[cfg(feature = "color")] @@ -50,6 +69,8 @@ pub use anstyle as style; pub use self::humantime::Timestamp; pub use self::writer::Target; pub use self::writer::WriteStyle; +#[cfg(feature = "unstable-kv")] +pub use self::kv::*; use self::writer::{Buffer, Writer}; @@ -70,6 +91,38 @@ pub enum TimestampPrecision { Nanos, } +/// Style for displaying key/value structured data +/// +/// This determines how key/value pairs are displayed in the log output. +/// The values can be suppressed, included in a trailer at the end of the line, or +/// included as multiple lines after the message. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum KVStyle { + /// Key/value pairs are not included in the output + Hidden, + /// Display the KV values at the end of the line + /// + /// After the message, any KV data is printed within curly braces on the same line. An "=" + /// is used to delimite the key from a value. + /// + /// For example: + /// ```txt + /// [INFO] log message { a=1 b=2 } + /// ``` + Inline, + /// Display the KV values on additional lines + /// + /// Each key/value pair is printed on its own line, with key and value delimited by ": ". + /// + /// For example: + /// ```txt + /// [INFO] log message + /// a: 1 + /// b: 2 + /// ``` + Multiline, +} + /// The default timestamp precision is seconds. impl Default for TimestampPrecision { fn default() -> Self { @@ -176,6 +229,8 @@ pub(crate) struct Builder { pub format_indent: Option, pub custom_format: Option, pub format_suffix: &'static str, + #[cfg(feature = "unstable-kv")] + pub kv_format: Option>, built: bool, } @@ -208,6 +263,8 @@ impl Builder { written_header_value: false, indent: built.format_indent, suffix: built.format_suffix, + #[cfg(feature = "unstable-kv")] + kv_format: built.kv_format.as_deref().unwrap_or(&default_kv_format), buf, }; @@ -227,6 +284,8 @@ impl Default for Builder { format_indent: Some(4), custom_format: None, format_suffix: "\n", + #[cfg(feature = "unstable-kv")] + kv_format: None, built: false, } } @@ -275,6 +334,8 @@ struct DefaultFormat<'a> { indent: Option, buf: &'a mut Formatter, suffix: &'a str, + #[cfg(feature = "unstable-kv")] + kv_format: &'a KvFormatFn, } impl<'a> DefaultFormat<'a> { @@ -285,7 +346,10 @@ impl<'a> DefaultFormat<'a> { self.write_target(record)?; self.finish_header()?; - self.write_args(record) + self.write_args(record)?; + #[cfg(feature = "unstable-kv")] + self.write_kv(record)?; + write!(self.buf, "{}", self.suffix) } fn subtle_style(&self, text: &'static str) -> SubtleStyle { @@ -401,7 +465,7 @@ impl<'a> DefaultFormat<'a> { fn write_args(&mut self, record: &Record) -> io::Result<()> { match self.indent { // Fast path for no indentation - None => write!(self.buf, "{}{}", record.args(), self.suffix), + None => write!(self.buf, "{}", record.args()), Some(indent_count) => { // Create a wrapper around the buffer only if we have to actually indent the message @@ -445,12 +509,16 @@ impl<'a> DefaultFormat<'a> { write!(wrapper, "{}", record.args())?; } - write!(self.buf, "{}", self.suffix)?; - Ok(()) } } } + + #[cfg(feature = "unstable-kv")] + fn write_kv(&mut self, record: &Record) -> io::Result<()> { + let format = self.kv_format; + format(self.buf, record.key_values()) + } } #[cfg(test)] @@ -486,19 +554,25 @@ mod tests { write_target("", fmt) } - #[test] - fn format_with_header() { + fn formatter() -> Formatter { let writer = writer::Builder::new() .write_style(WriteStyle::Never) .build(); - let mut f = Formatter::new(&writer); + Formatter::new(&writer) + } + + #[test] + fn format_with_header() { + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: true, target: false, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n", @@ -510,17 +584,15 @@ mod tests { #[test] fn format_no_header() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: false, target: false, level: false, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n", @@ -532,17 +604,15 @@ mod tests { #[test] fn format_indent_spaces() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: true, target: false, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: Some(4), suffix: "\n", @@ -554,17 +624,15 @@ mod tests { #[test] fn format_indent_zero_spaces() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: true, target: false, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: Some(0), suffix: "\n", @@ -576,17 +644,15 @@ mod tests { #[test] fn format_indent_spaces_no_header() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: false, target: false, level: false, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: Some(4), suffix: "\n", @@ -598,17 +664,15 @@ mod tests { #[test] fn format_suffix() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: false, target: false, level: false, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n\n", @@ -620,17 +684,15 @@ mod tests { #[test] fn format_suffix_with_indent() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: false, target: false, level: false, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: Some(4), suffix: "\n\n", @@ -642,11 +704,7 @@ mod tests { #[test] fn format_target() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write_target( "target", @@ -655,6 +713,8 @@ mod tests { module_path: true, target: true, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n", @@ -667,17 +727,15 @@ mod tests { #[test] fn format_empty_target() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write(DefaultFormat { timestamp: None, module_path: true, target: true, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n", @@ -689,11 +747,7 @@ mod tests { #[test] fn format_no_target() { - let writer = writer::Builder::new() - .write_style(WriteStyle::Never) - .build(); - - let mut f = Formatter::new(&writer); + let mut f = formatter(); let written = write_target( "target", @@ -702,6 +756,8 @@ mod tests { module_path: true, target: false, level: true, + #[cfg(feature = "unstable-kv")] + kv_format: &hidden_kv_format, written_header_value: false, indent: None, suffix: "\n", @@ -711,4 +767,67 @@ mod tests { assert_eq!("[INFO test::path] log\nmessage\n", written); } + + #[cfg(feature = "unstable-kv")] + #[test] + fn format_kv_default() { + let kvs = &[("a", 1u32), ("b", 2u32)][..]; + let mut f = formatter(); + let record = Record::builder() + .args(format_args!("log message")) + .level(Level::Info) + .module_path(Some("test::path")) + .key_values(&kvs) + .build(); + + let written = write_record( + record, + DefaultFormat { + timestamp: None, + module_path: false, + target: false, + level: true, + kv_format: &default_kv_format, + written_header_value: false, + indent: None, + suffix: "\n", + buf: &mut f, + }, + ); + + assert_eq!("[INFO ] log message a=1 b=2\n", written); + } + + #[cfg(feature = "unstable-kv")] + #[test] + fn format_kv_default_full() { + let kvs = &[("a", 1u32), ("b", 2u32)][..]; + let mut f = formatter(); + let record = Record::builder() + .args(format_args!("log\nmessage")) + .level(Level::Info) + .module_path(Some("test::path")) + .target("target") + .file(Some("test.rs")) + .line(Some(42)) + .key_values(&kvs) + .build(); + + let written = write_record( + record, + DefaultFormat { + timestamp: None, + module_path: true, + target: true, + level: true, + kv_format: &default_kv_format, + written_header_value: false, + indent: None, + suffix: "\n", + buf: &mut f, + }, + ); + + assert_eq!("[INFO test::path target] log\nmessage a=1 b=2\n", written); + } } diff --git a/src/logger.rs b/src/logger.rs index 18bfe9f..d0e6440 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -313,6 +313,16 @@ impl Builder { self } + /// Configure the style for writing key/value pairs + #[cfg(feature = "unstable-kv")] + pub fn format_key_values(&mut self, format: F) -> &mut Self + where + F: Fn(&mut Formatter, &dyn log::kv::source::Source) -> io::Result<()> + Sync + Send, + { + self.format.kv_format = Some(Box::new(format)); + self + } + /// Adds a directive to the filter for a specific module. /// /// # Examples