diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9551407..6eb6ca1 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -20,4 +20,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1be3a..2ca13e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [0.11.3] - 2024-03-05 + +### Features + +- Experimental support for key-value logging behind `unstable-kv` + ## [0.11.2] - 2024-02-13 ## [0.11.1] - 2024-01-27 @@ -109,7 +115,8 @@ To open room for changing dependencies: - Added a method to print the module instead of the target -[Unreleased]: https://github.com/rust-cli/env_logger/compare/v0.11.2...HEAD +[Unreleased]: https://github.com/rust-cli/env_logger/compare/v0.11.3...HEAD +[0.11.3]: https://github.com/rust-cli/env_logger/compare/v0.11.2...v0.11.3 [0.11.2]: https://github.com/rust-cli/env_logger/compare/v0.11.1...v0.11.2 [0.11.1]: https://github.com/rust-cli/env_logger/compare/v0.11.0...v0.11.1 [0.11.0]: https://github.com/rust-cli/env_logger/compare/v0.10.2...v0.11.0 diff --git a/Cargo.lock b/Cargo.lock index eb4cb6a..05bdc0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,12 +59,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - [[package]] name = "colorchoice" version = "1.0.0" @@ -81,7 +75,7 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.3" dependencies = [ "anstream", "anstyle", @@ -98,12 +92,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "log" -version = "0.4.17" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" diff --git a/Cargo.toml b/Cargo.toml index ad6619e..8eb6cd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ include = [ [package] name = "env_logger" -version = "0.11.2" +version = "0.11.3" description = """ A logging implementation for `log` which is configured via an environment variable. @@ -52,9 +52,10 @@ color = ["dep:anstream", "dep:anstyle"] auto-color = ["color", "anstream/auto"] humantime = ["dep:humantime"] regex = ["env_filter/regex"] +unstable-kv = ["log/kv"] [dependencies] -log = { version = "0.4.8", features = ["std"] } +log = { version = "0.4.21", features = ["std"] } env_filter = { version = "0.1.0", path = "crates/env_filter", default-features = false } humantime = { version = "2.0.0", optional = true } anstream = { version = "0.6.11", default-features = false, features = ["wincon"], optional = true } diff --git a/examples/direct_logger.rs b/examples/direct_logger.rs index 4d7f39d..397ccd8 100644 --- a/examples/direct_logger.rs +++ b/examples/direct_logger.rs @@ -8,19 +8,27 @@ use env_logger::{Builder, WriteStyle}; use log::{Level, LevelFilter, Log, MetadataBuilder, Record}; +#[cfg(feature = "unstable-kv")] +static KVS: (&str, &str) = ("test", "something"); + fn record() -> Record<'static> { let error_metadata = MetadataBuilder::new() .target("myApp") .level(Level::Error) .build(); - Record::builder() + let mut builder = Record::builder(); + builder .metadata(error_metadata) .args(format_args!("Error!")) .line(Some(433)) .file(Some("app.rs")) - .module_path(Some("server")) - .build() + .module_path(Some("server")); + #[cfg(feature = "unstable-kv")] + { + builder.key_values(&KVS); + } + builder.build() } fn main() { diff --git a/src/fmt/kv.rs b/src/fmt/kv.rs new file mode 100644 index 0000000..5d8cfca --- /dev/null +++ b/src/fmt/kv.rs @@ -0,0 +1,69 @@ +use std::io::{self, Write}; + +#[cfg(feature = "color")] +use super::WriteStyle; +use super::{Formatter, StyledValue}; +#[cfg(feature = "color")] +use anstyle::Style; +use log::kv::{Error, Key, Source, Value, VisitSource}; + +/// Format function for serializing key/value pairs +/// +/// This function 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 +/// [`Builder::format_key_values`](crate::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(()) +} + +/// Default Key Value Format +/// +/// This function is intended to be passed to +/// [`Builder::format_key_values`](crate::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 DefaultVisitSource(formatter)) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) +} + +struct DefaultVisitSource<'a>(&'a mut Formatter); + +impl<'a, 'kvs> VisitSource<'kvs> for DefaultVisitSource<'a> { + fn visit_pair(&mut self, key: Key, value: Value<'kvs>) -> Result<(), Error> { + write!(self.0, " {}={}", self.style_key(key), value)?; + Ok(()) + } +} + +impl DefaultVisitSource<'_> { + fn style_key<'k>(&self, text: Key<'k>) -> StyledValue> { + #[cfg(feature = "color")] + { + StyledValue { + style: if self.0.write_style == WriteStyle::Never { + Style::new() + } else { + Style::new().italic() + }, + value: text, + } + } + #[cfg(not(feature = "color"))] + { + text + } + } +} diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 883f943..f18940a 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -30,8 +30,30 @@ //! }); //! ``` //! +//! # 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_values`] +//! with [`hidden_kv_format`] as the format function. +//! +//! The way these keys and values are formatted can also be customized with a separate format +//! function that is called by the default format with [`Builder::format_key_values`]. +//! +//! ``` +//! # #[cfg(feature= "unstable-kv")] +//! # { +//! use log::info; +//! env_logger::init(); +//! info!(x="45"; "Some message"); +//! info!(x="12"; "Another message {x}", x="12"); +//! # } +//! ``` +//! +//! See . +//! //! [`Builder::format`]: crate::Builder::format //! [`Write`]: std::io::Write +//! [`Builder::format_key_values`]: crate::Builder::format_key_values use std::cell::RefCell; use std::fmt::Display; @@ -45,6 +67,8 @@ use log::Record; #[cfg(feature = "humantime")] mod humantime; +#[cfg(feature = "unstable-kv")] +mod kv; pub(crate) mod writer; #[cfg(feature = "color")] @@ -52,6 +76,8 @@ pub use anstyle as style; #[cfg(feature = "humantime")] pub use self::humantime::Timestamp; +#[cfg(feature = "unstable-kv")] +pub use self::kv::*; pub use self::writer::Target; pub use self::writer::WriteStyle; @@ -181,6 +207,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, } @@ -213,6 +241,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, }; @@ -232,6 +262,8 @@ impl Default for Builder { format_indent: Some(4), custom_format: None, format_suffix: "\n", + #[cfg(feature = "unstable-kv")] + kv_format: None, built: false, } } @@ -263,6 +295,9 @@ impl std::fmt::Display for StyledValue { } } +#[cfg(not(feature = "color"))] +type StyledValue = T; + /// The default format. /// /// This format needs to work with any combination of crate features. @@ -275,6 +310,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 +322,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 +441,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 +485,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 +530,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 +560,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 +580,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 +600,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 +620,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 +640,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 +660,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 +680,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 +689,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 +703,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 +723,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 +732,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 +743,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..6f41490 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -313,6 +313,25 @@ impl Builder { self } + /// Set the format for structured key/value pairs in the log record + /// + /// With the default format, this function is called for each record and should format + /// the structured key-value pairs as returned by [`log::Record::key_values`]. + /// + /// The format function is expected to output the string directly to the `Formatter` so that + /// implementations can use the [`std::fmt`] macros, similar to the main format function. + /// + /// The default format uses a space to separate each key-value pair, with an "=" between + /// the key and value. + #[cfg(feature = "unstable-kv")] + pub fn format_key_values(&mut self, format: F) -> &mut Self + where + F: Fn(&mut Formatter, &dyn log::kv::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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy