From 8018168cf14d46e32a9674df97a8db64730575d3 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 20:26:36 -0600 Subject: [PATCH 01/28] refactor(fmt): Remove glob mods --- src/fmt/mod.rs | 7 ++----- src/fmt/writer/buffer/plain.rs | 2 +- src/fmt/writer/buffer/termcolor.rs | 2 +- src/fmt/writer/mod.rs | 4 ---- src/lib.rs | 2 +- 5 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 5f1d5c8f..95feb782 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -50,14 +50,11 @@ pub use style::{Color, Style, StyledValue}; #[cfg(feature = "humantime")] pub use self::humantime::Timestamp; -pub use self::writer::glob::*; +pub use self::writer::Target; +pub use self::writer::WriteStyle; use self::writer::{Buffer, Writer}; -pub(crate) mod glob { - pub use super::{Target, TimestampPrecision, WriteStyle}; -} - /// Formatting precision of timestamps. /// /// Seconds give precision of full seconds, milliseconds give thousands of a diff --git a/src/fmt/writer/buffer/plain.rs b/src/fmt/writer/buffer/plain.rs index e6809b06..99056783 100644 --- a/src/fmt/writer/buffer/plain.rs +++ b/src/fmt/writer/buffer/plain.rs @@ -1,6 +1,6 @@ use std::{io, sync::Mutex}; -use crate::fmt::{WritableTarget, WriteStyle}; +use crate::fmt::writer::{WritableTarget, WriteStyle}; pub(in crate::fmt::writer) struct BufferWriter { target: WritableTarget, diff --git a/src/fmt/writer/buffer/termcolor.rs b/src/fmt/writer/buffer/termcolor.rs index d3090a17..648ed16f 100644 --- a/src/fmt/writer/buffer/termcolor.rs +++ b/src/fmt/writer/buffer/termcolor.rs @@ -3,7 +3,7 @@ use std::sync::Mutex; use termcolor::{self, ColorSpec, WriteColor}; -use crate::fmt::{WritableTarget, WriteStyle}; +use crate::fmt::writer::{WritableTarget, WriteStyle}; pub(in crate::fmt::writer) struct BufferWriter { inner: termcolor::BufferWriter, diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 41466b92..986b75c6 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -5,10 +5,6 @@ use self::atty::{is_stderr, is_stdout}; use self::buffer::BufferWriter; use std::{fmt, io, mem, sync::Mutex}; -pub(super) mod glob { - pub use super::*; -} - pub(super) use self::buffer::Buffer; /// Log target, either `stdout`, `stderr` or a custom pipe. diff --git a/src/lib.rs b/src/lib.rs index cdc2badc..93eba6ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -280,5 +280,5 @@ mod logger; pub mod filter; pub mod fmt; -pub use self::fmt::glob::*; +pub use self::fmt::{Target, TimestampPrecision, WriteStyle}; pub use self::logger::*; From f4808e46e8adf6f4d0182538070ce98f11723bb1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 20:29:23 -0600 Subject: [PATCH 02/28] refactor(fmt): Pull out target mod --- src/fmt/writer/mod.rs | 98 ++-------------------------------------- src/fmt/writer/target.rs | 96 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 95 deletions(-) create mode 100644 src/fmt/writer/target.rs diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 986b75c6..c8753e0f 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -1,5 +1,6 @@ mod atty; mod buffer; +mod target; use self::atty::{is_stderr, is_stdout}; use self::buffer::BufferWriter; @@ -7,102 +8,9 @@ use std::{fmt, io, mem, sync::Mutex}; pub(super) use self::buffer::Buffer; -/// Log target, either `stdout`, `stderr` or a custom pipe. -#[non_exhaustive] -pub enum Target { - /// Logs will be sent to standard output. - Stdout, - /// Logs will be sent to standard error. - Stderr, - /// Logs will be sent to a custom pipe. - Pipe(Box), -} - -impl Default for Target { - fn default() -> Self { - Target::Stderr - } -} - -impl fmt::Debug for Target { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Stdout => "stdout", - Self::Stderr => "stderr", - Self::Pipe(_) => "pipe", - } - ) - } -} - -/// Log target, either `stdout`, `stderr` or a custom pipe. -/// -/// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. -pub(super) enum WritableTarget { - /// Logs will be written to standard output. - #[allow(dead_code)] - WriteStdout, - /// Logs will be printed to standard output. - PrintStdout, - /// Logs will be written to standard error. - #[allow(dead_code)] - WriteStderr, - /// Logs will be printed to standard error. - PrintStderr, - /// Logs will be sent to a custom pipe. - Pipe(Box>), -} - -impl WritableTarget { - fn print(&self, buf: &Buffer) -> io::Result<()> { - use std::io::Write as _; +pub use target::Target; +use target::WritableTarget; - let buf = buf.as_bytes(); - match self { - WritableTarget::WriteStdout => { - let stream = std::io::stdout(); - let mut stream = stream.lock(); - stream.write_all(buf)?; - stream.flush()?; - } - WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), - WritableTarget::WriteStderr => { - let stream = std::io::stderr(); - let mut stream = stream.lock(); - stream.write_all(buf)?; - stream.flush()?; - } - WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), - // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. - WritableTarget::Pipe(pipe) => { - let mut stream = pipe.lock().unwrap(); - stream.write_all(buf)?; - stream.flush()?; - } - } - - Ok(()) - } -} - -impl fmt::Debug for WritableTarget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::WriteStdout => "stdout", - Self::PrintStdout => "stdout", - Self::WriteStderr => "stderr", - Self::PrintStderr => "stderr", - Self::Pipe(_) => "pipe", - } - ) - } -} /// Whether or not to print styles to the target. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum WriteStyle { diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs new file mode 100644 index 00000000..3d908665 --- /dev/null +++ b/src/fmt/writer/target.rs @@ -0,0 +1,96 @@ +/// Log target, either `stdout`, `stderr` or a custom pipe. +#[non_exhaustive] +pub enum Target { + /// Logs will be sent to standard output. + Stdout, + /// Logs will be sent to standard error. + Stderr, + /// Logs will be sent to a custom pipe. + Pipe(Box), +} + +impl Default for Target { + fn default() -> Self { + Target::Stderr + } +} + +impl std::fmt::Debug for Target { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Stdout => "stdout", + Self::Stderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} + +/// Log target, either `stdout`, `stderr` or a custom pipe. +/// +/// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. +pub(super) enum WritableTarget { + /// Logs will be written to standard output. + #[allow(dead_code)] + WriteStdout, + /// Logs will be printed to standard output. + PrintStdout, + /// Logs will be written to standard error. + #[allow(dead_code)] + WriteStderr, + /// Logs will be printed to standard error. + PrintStderr, + /// Logs will be sent to a custom pipe. + Pipe(Box>), +} + +impl WritableTarget { + pub(super) fn print(&self, buf: &super::Buffer) -> std::io::Result<()> { + use std::io::Write as _; + + let buf = buf.as_bytes(); + match self { + WritableTarget::WriteStdout => { + let stream = std::io::stdout(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), + WritableTarget::WriteStderr => { + let stream = std::io::stderr(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), + // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + WritableTarget::Pipe(pipe) => { + let mut stream = pipe.lock().unwrap(); + stream.write_all(buf)?; + stream.flush()?; + } + } + + Ok(()) + } +} + +impl std::fmt::Debug for WritableTarget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::WriteStdout => "stdout", + Self::PrintStdout => "stdout", + Self::WriteStderr => "stderr", + Self::PrintStderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} From a77a76138dc28723d07ad8816b5f524d71778bd0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 16 Jan 2024 14:35:21 -0600 Subject: [PATCH 03/28] chore: Bump MSRV to 1.71 - 1.70.0 is needed for `anstream` to be added - 1.71.0 is needed for changes to how cargo treats features --- .clippy.toml | 2 +- .github/workflows/ci.yml | 6 +++--- Cargo.toml | 2 +- src/fmt/writer/mod.rs | 8 +++----- src/fmt/writer/target.rs | 8 +++----- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.clippy.toml b/.clippy.toml index 146c2e97..18554b15 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,4 +1,4 @@ -msrv = "1.60.0" # MSRV +msrv = "1.71" # MSRV warn-on-all-wildcard-imports = true disallowed-methods = [ { path = "std::option::Option::map_or", reason = "prefer `map(..).unwrap_or(..)` for legibility" }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de41ed3a..49e64dfc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Run crate example run: cargo run --example default msrv: - name: "Check MSRV: 1.60.0" + name: "Check MSRV: 1.71" runs-on: ubuntu-latest steps: - name: Checkout repository @@ -56,7 +56,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.60" # MSRV + toolchain: "1.71" # MSRV - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@cargo-hack - name: Check @@ -115,7 +115,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: "1.60" # MSRV + toolchain: "1.71" # MSRV components: clippy - uses: Swatinem/rust-cache@v2 - name: Install SARIF tools diff --git a/Cargo.toml b/Cargo.toml index 08da79ce..3d19de74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ categories = ["development-tools::debugging"] keywords = ["logging", "log", "logger"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.60.0" # MSRV +rust-version = "1.71" # MSRV include = [ "build.rs", "src/**/*", diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index c8753e0f..4195e788 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -13,8 +13,10 @@ use target::WritableTarget; /// Whether or not to print styles to the target. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Default)] pub enum WriteStyle { /// Try to print styles, but don't force the issue. + #[default] Auto, /// Try very hard to print styles. Always, @@ -22,11 +24,7 @@ pub enum WriteStyle { Never, } -impl Default for WriteStyle { - fn default() -> Self { - WriteStyle::Auto - } -} + #[cfg(feature = "color")] impl WriteStyle { diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs index 3d908665..c3fe4825 100644 --- a/src/fmt/writer/target.rs +++ b/src/fmt/writer/target.rs @@ -1,19 +1,17 @@ /// Log target, either `stdout`, `stderr` or a custom pipe. #[non_exhaustive] +#[derive(Default)] pub enum Target { /// Logs will be sent to standard output. Stdout, /// Logs will be sent to standard error. + #[default] Stderr, /// Logs will be sent to a custom pipe. Pipe(Box), } -impl Default for Target { - fn default() -> Self { - Target::Stderr - } -} + impl std::fmt::Debug for Target { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From fb603ba2e711b743398ba54c263396d797ead2c0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 16 Jan 2024 14:36:37 -0600 Subject: [PATCH 04/28] perf: Remove is-terminal dependency Fixes #276 --- Cargo.lock | 148 ----------------------------------------- Cargo.toml | 3 +- src/fmt/writer/atty.rs | 26 ++------ 3 files changed, 6 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b0e0086..cca961ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,18 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "cc" -version = "1.0.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" - [[package]] name = "cfg-if" version = "1.0.0" @@ -34,82 +22,17 @@ name = "env_logger" version = "0.10.2" dependencies = [ "humantime", - "is-terminal", "log", "regex", "termcolor", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" -[[package]] -name = "io-lifetimes" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d367024b3f3414d8e01f437f704f41a9f64ab36f9067fa73e526ad4c763c87" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "is-terminal" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d" -dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys", -] - -[[package]] -name = "libc" -version = "0.2.137" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" - -[[package]] -name = "linux-raw-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" - [[package]] name = "log" version = "0.4.17" @@ -142,20 +65,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "rustix" -version = "0.36.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -195,60 +104,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 3d19de74..2f831016 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ pre-release-replacements = [ [features] default = ["auto-color", "humantime", "regex"] color = ["dep:termcolor"] -auto-color = ["dep:is-terminal", "color"] +auto-color = ["color"] humantime = ["dep:humantime"] regex = ["dep:regex"] @@ -48,7 +48,6 @@ log = { version = "0.4.8", features = ["std"] } regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } termcolor = { version = "1.1.1", optional = true } humantime = { version = "2.0.0", optional = true } -is-terminal = { version = "0.4.0", optional = true } [[test]] name = "regexp_filter" diff --git a/src/fmt/writer/atty.rs b/src/fmt/writer/atty.rs index 1a133eef..13c994f5 100644 --- a/src/fmt/writer/atty.rs +++ b/src/fmt/writer/atty.rs @@ -6,28 +6,12 @@ Otherwise, assume we're not attached to anything. This effectively prevents styl printed. */ -#[cfg(feature = "auto-color")] -mod imp { - use is_terminal::IsTerminal; +use std::io::IsTerminal; - pub(in crate::fmt) fn is_stdout() -> bool { - std::io::stdout().is_terminal() - } - - pub(in crate::fmt) fn is_stderr() -> bool { - std::io::stderr().is_terminal() - } +pub(in crate::fmt) fn is_stdout() -> bool { + cfg!(feature = "auto-color") && std::io::stdout().is_terminal() } -#[cfg(not(feature = "auto-color"))] -mod imp { - pub(in crate::fmt) fn is_stdout() -> bool { - false - } - - pub(in crate::fmt) fn is_stderr() -> bool { - false - } +pub(in crate::fmt) fn is_stderr() -> bool { + cfg!(feature = "auto-color") && std::io::stderr().is_terminal() } - -pub(in crate::fmt) use self::imp::*; From 5cbbff926fafac2cefd301d6f57c51aaa7b5d7d1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 20:05:05 -0600 Subject: [PATCH 05/28] fix(fmt)!: Remove color support --- Cargo.lock | 41 ---- Cargo.toml | 3 +- examples/custom_format.rs | 12 +- src/fmt/mod.rs | 81 ------- src/fmt/style.rs | 351 ----------------------------- src/fmt/writer/buffer/mod.rs | 13 -- src/fmt/writer/buffer/termcolor.rs | 108 --------- src/fmt/writer/mod.rs | 11 - src/fmt/writer/target.rs | 2 - 9 files changed, 3 insertions(+), 619 deletions(-) delete mode 100644 src/fmt/style.rs delete mode 100644 src/fmt/writer/buffer/termcolor.rs diff --git a/Cargo.lock b/Cargo.lock index cca961ea..80a265e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,6 @@ dependencies = [ "humantime", "log", "regex", - "termcolor", ] [[package]] @@ -64,43 +63,3 @@ name = "regex-syntax" version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 2f831016..499fbbb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ pre-release-replacements = [ [features] default = ["auto-color", "humantime", "regex"] -color = ["dep:termcolor"] +color = [] auto-color = ["color"] humantime = ["dep:humantime"] regex = ["dep:regex"] @@ -46,7 +46,6 @@ regex = ["dep:regex"] [dependencies] log = { version = "0.4.8", features = ["std"] } regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } -termcolor = { version = "1.1.1", optional = true } humantime = { version = "2.0.0", optional = true } [[test]] diff --git a/examples/custom_format.rs b/examples/custom_format.rs index cc16b336..1cc4bdec 100644 --- a/examples/custom_format.rs +++ b/examples/custom_format.rs @@ -19,7 +19,7 @@ If you want to control the logging output completely, see the `custom_logger` ex #[cfg(all(feature = "color", feature = "humantime"))] fn main() { - use env_logger::{fmt::Color, Builder, Env}; + use env_logger::{Builder, Env}; use std::io::Write; @@ -30,17 +30,9 @@ fn main() { Builder::from_env(env) .format(|buf, record| { - let mut style = buf.style(); - style.set_bg(Color::Yellow).set_bold(true); - let timestamp = buf.timestamp(); - writeln!( - buf, - "My formatted log ({}): {}", - timestamp, - style.value(record.args()) - ) + writeln!(buf, "My formatted log ({}): {}", timestamp, record.args()) }) .init(); } diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 95feb782..dd78402f 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -35,19 +35,12 @@ use std::io::prelude::*; use std::rc::Rc; use std::{fmt, io, mem}; -#[cfg(feature = "color")] -use log::Level; use log::Record; #[cfg(feature = "humantime")] mod humantime; pub(crate) mod writer; -#[cfg(feature = "color")] -mod style; -#[cfg(feature = "color")] -pub use style::{Color, Style, StyledValue}; - #[cfg(feature = "humantime")] pub use self::humantime::Timestamp; pub use self::writer::Target; @@ -126,62 +119,6 @@ impl Formatter { } } -#[cfg(feature = "color")] -impl Formatter { - /// Begin a new [`Style`]. - /// - /// # Examples - /// - /// Create a bold, red colored style and use it to print the log level: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::fmt::Color; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut level_style = buf.style(); - /// - /// level_style.set_color(Color::Red).set_bold(true); - /// - /// writeln!(buf, "{}: {}", - /// level_style.value(record.level()), - /// record.args()) - /// }); - /// ``` - /// - /// [`Style`]: struct.Style.html - pub fn style(&self) -> Style { - Style { - buf: self.buf.clone(), - spec: termcolor::ColorSpec::new(), - } - } - - /// Get the default [`Style`] for the given level. - /// - /// The style can be used to print other values besides the level. - pub fn default_level_style(&self, level: Level) -> Style { - let mut level_style = self.style(); - match level { - Level::Trace => level_style.set_color(Color::Cyan), - Level::Debug => level_style.set_color(Color::Blue), - Level::Info => level_style.set_color(Color::Green), - Level::Warn => level_style.set_color(Color::Yellow), - Level::Error => level_style.set_color(Color::Red).set_bold(true), - }; - level_style - } - - /// Get a printable [`Style`] for the given level. - /// - /// The style can only be used to print the level. - pub fn default_styled_level(&self, level: Level) -> StyledValue<'static, Level> { - self.default_level_style(level).into_value(level) - } -} - impl Write for Formatter { fn write(&mut self, buf: &[u8]) -> io::Result { self.buf.borrow_mut().write(buf) @@ -264,9 +201,6 @@ impl Default for Builder { } } -#[cfg(feature = "color")] -type SubtleStyle = StyledValue<'static, &'static str>; -#[cfg(not(feature = "color"))] type SubtleStyle = &'static str; /// The default format. @@ -295,16 +229,6 @@ impl<'a> DefaultFormat<'a> { } fn subtle_style(&self, text: &'static str) -> SubtleStyle { - #[cfg(feature = "color")] - { - self.buf - .style() - .set_color(Color::Black) - .set_intense(true) - .clone() - .into_value(text) - } - #[cfg(not(feature = "color"))] { text } @@ -330,11 +254,6 @@ impl<'a> DefaultFormat<'a> { } let level = { - #[cfg(feature = "color")] - { - self.buf.default_styled_level(record.level()) - } - #[cfg(not(feature = "color"))] { record.level() } diff --git a/src/fmt/style.rs b/src/fmt/style.rs deleted file mode 100644 index dd4463ac..00000000 --- a/src/fmt/style.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::borrow::Cow; -use std::cell::RefCell; -use std::fmt; -use std::rc::Rc; - -use super::Buffer; - -/// A set of styles to apply to the terminal output. -/// -/// Call [`Formatter::style`] to get a `Style` and use the builder methods to -/// set styling properties, like [color] and [weight]. -/// To print a value using the style, wrap it in a call to [`value`] when the log -/// record is formatted. -/// -/// # Examples -/// -/// Create a bold, red colored style and use it to print the log level: -/// -/// ``` -/// use std::io::Write; -/// use env_logger::fmt::Color; -/// -/// let mut builder = env_logger::Builder::new(); -/// -/// builder.format(|buf, record| { -/// let mut level_style = buf.style(); -/// -/// level_style.set_color(Color::Red).set_bold(true); -/// -/// writeln!(buf, "{}: {}", -/// level_style.value(record.level()), -/// record.args()) -/// }); -/// ``` -/// -/// Styles can be re-used to output multiple values: -/// -/// ``` -/// use std::io::Write; -/// use env_logger::fmt::Color; -/// -/// let mut builder = env_logger::Builder::new(); -/// -/// builder.format(|buf, record| { -/// let mut bold = buf.style(); -/// -/// bold.set_bold(true); -/// -/// writeln!(buf, "{}: {} {}", -/// bold.value(record.level()), -/// bold.value("some bold text"), -/// record.args()) -/// }); -/// ``` -/// -/// [`Formatter::style`]: struct.Formatter.html#method.style -/// [color]: #method.set_color -/// [weight]: #method.set_bold -/// [`value`]: #method.value -#[derive(Clone)] -pub struct Style { - pub(in crate::fmt) buf: Rc>, - pub(in crate::fmt) spec: termcolor::ColorSpec, -} - -impl Style { - /// Set the text color. - /// - /// # Examples - /// - /// Create a style with red text: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::fmt::Color; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_color(Color::Red); - /// - /// writeln!(buf, "{}", style.value(record.args())) - /// }); - /// ``` - pub fn set_color(&mut self, color: Color) -> &mut Style { - self.spec.set_fg(Some(color.into_termcolor())); - self - } - - /// Set the text weight. - /// - /// If `yes` is true then text will be written in bold. - /// If `yes` is false then text will be written in the default weight. - /// - /// # Examples - /// - /// Create a style with bold text: - /// - /// ``` - /// use std::io::Write; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_bold(true); - /// - /// writeln!(buf, "{}", style.value(record.args())) - /// }); - /// ``` - pub fn set_bold(&mut self, yes: bool) -> &mut Style { - self.spec.set_bold(yes); - self - } - - /// Set the text intensity. - /// - /// If `yes` is true then text will be written in a brighter color. - /// If `yes` is false then text will be written in the default color. - /// - /// # Examples - /// - /// Create a style with intense text: - /// - /// ``` - /// use std::io::Write; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_intense(true); - /// - /// writeln!(buf, "{}", style.value(record.args())) - /// }); - /// ``` - pub fn set_intense(&mut self, yes: bool) -> &mut Style { - self.spec.set_intense(yes); - self - } - - /// Set whether the text is dimmed. - /// - /// If `yes` is true then text will be written in a dimmer color. - /// If `yes` is false then text will be written in the default color. - /// - /// # Examples - /// - /// Create a style with dimmed text: - /// - /// ``` - /// use std::io::Write; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_dimmed(true); - /// - /// writeln!(buf, "{}", style.value(record.args())) - /// }); - /// ``` - pub fn set_dimmed(&mut self, yes: bool) -> &mut Style { - self.spec.set_dimmed(yes); - self - } - - /// Set the background color. - /// - /// # Examples - /// - /// Create a style with a yellow background: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::fmt::Color; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_bg(Color::Yellow); - /// - /// writeln!(buf, "{}", style.value(record.args())) - /// }); - /// ``` - pub fn set_bg(&mut self, color: Color) -> &mut Style { - self.spec.set_bg(Some(color.into_termcolor())); - self - } - - /// Wrap a value in the style. - /// - /// The same `Style` can be used to print multiple different values. - /// - /// # Examples - /// - /// Create a bold, red colored style and use it to print the log level: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::fmt::Color; - /// - /// let mut builder = env_logger::Builder::new(); - /// - /// builder.format(|buf, record| { - /// let mut style = buf.style(); - /// - /// style.set_color(Color::Red).set_bold(true); - /// - /// writeln!(buf, "{}: {}", - /// style.value(record.level()), - /// record.args()) - /// }); - /// ``` - pub fn value(&self, value: T) -> StyledValue { - StyledValue { - style: Cow::Borrowed(self), - value, - } - } - - /// Wrap a value in the style by taking ownership of it. - pub(crate) fn into_value(self, value: T) -> StyledValue<'static, T> { - StyledValue { - style: Cow::Owned(self), - value, - } - } -} - -impl fmt::Debug for Style { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Style").field("spec", &self.spec).finish() - } -} - -/// A value that can be printed using the given styles. -/// -/// It is the result of calling [`Style::value`]. -/// -/// [`Style::value`]: struct.Style.html#method.value -pub struct StyledValue<'a, T> { - style: Cow<'a, Style>, - value: T, -} - -impl<'a, T> StyledValue<'a, T> { - fn write_fmt(&self, f: F) -> fmt::Result - where - F: FnOnce() -> fmt::Result, - { - self.style - .buf - .borrow_mut() - .set_color(&self.style.spec) - .map_err(|_| fmt::Error)?; - - // Always try to reset the terminal style, even if writing failed - let write = f(); - let reset = self.style.buf.borrow_mut().reset().map_err(|_| fmt::Error); - - write.and(reset) - } -} - -macro_rules! impl_styled_value_fmt { - ($($fmt_trait:path),*) => { - $( - impl<'a, T: $fmt_trait> $fmt_trait for StyledValue<'a, T> { - fn fmt(&self, f: &mut fmt::Formatter)->fmt::Result { - self.write_fmt(|| T::fmt(&self.value, f)) - } - } - )* - }; -} - -impl_styled_value_fmt!( - fmt::Debug, - fmt::Display, - fmt::Pointer, - fmt::Octal, - fmt::Binary, - fmt::UpperHex, - fmt::LowerHex, - fmt::UpperExp, - fmt::LowerExp -); - -// The `Color` type is copied from https://github.com/BurntSushi/termcolor - -/// The set of available colors for the terminal foreground/background. -/// -/// The `Ansi256` and `Rgb` colors will only output the correct codes when -/// paired with the `Ansi` `WriteColor` implementation. -/// -/// The `Ansi256` and `Rgb` color types are not supported when writing colors -/// on Windows using the console. If they are used on Windows, then they are -/// silently ignored and no colors will be emitted. -/// -/// This set may expand over time. -/// -/// This type has a `FromStr` impl that can parse colors from their human -/// readable form. The format is as follows: -/// -/// 1. Any of the explicitly listed colors in English. They are matched -/// case insensitively. -/// 2. A single 8-bit integer, in either decimal or hexadecimal format. -/// 3. A triple of 8-bit integers separated by a comma, where each integer is -/// in decimal or hexadecimal format. -/// -/// Hexadecimal numbers are written with a `0x` prefix. -#[allow(missing_docs)] -#[non_exhaustive] -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Color { - Black, - Blue, - Green, - Red, - Cyan, - Magenta, - Yellow, - White, - Ansi256(u8), - Rgb(u8, u8, u8), -} - -impl Color { - fn into_termcolor(self) -> termcolor::Color { - match self { - Color::Black => termcolor::Color::Black, - Color::Blue => termcolor::Color::Blue, - Color::Green => termcolor::Color::Green, - Color::Red => termcolor::Color::Red, - Color::Cyan => termcolor::Color::Cyan, - Color::Magenta => termcolor::Color::Magenta, - Color::Yellow => termcolor::Color::Yellow, - Color::White => termcolor::Color::White, - Color::Ansi256(value) => termcolor::Color::Ansi256(value), - Color::Rgb(r, g, b) => termcolor::Color::Rgb(r, g, b), - } - } -} diff --git a/src/fmt/writer/buffer/mod.rs b/src/fmt/writer/buffer/mod.rs index 4e678b2d..d0dbb5f5 100644 --- a/src/fmt/writer/buffer/mod.rs +++ b/src/fmt/writer/buffer/mod.rs @@ -1,15 +1,2 @@ -/* -This internal module contains the style and terminal writing implementation. - -Its public API is available when the `termcolor` crate is available. -The terminal printing is shimmed when the `termcolor` crate is not available. -*/ - -#[cfg(feature = "color")] -mod termcolor; -#[cfg(feature = "color")] -pub(in crate::fmt) use self::termcolor::*; -#[cfg(not(feature = "color"))] mod plain; -#[cfg(not(feature = "color"))] pub(in crate::fmt) use plain::*; diff --git a/src/fmt/writer/buffer/termcolor.rs b/src/fmt/writer/buffer/termcolor.rs deleted file mode 100644 index 648ed16f..00000000 --- a/src/fmt/writer/buffer/termcolor.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::io::{self, Write}; -use std::sync::Mutex; - -use termcolor::{self, ColorSpec, WriteColor}; - -use crate::fmt::writer::{WritableTarget, WriteStyle}; - -pub(in crate::fmt::writer) struct BufferWriter { - inner: termcolor::BufferWriter, - uncolored_target: Option, - write_style: WriteStyle, -} - -impl BufferWriter { - pub(in crate::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { - BufferWriter { - inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), - uncolored_target: if is_test { - Some(WritableTarget::PrintStderr) - } else { - None - }, - write_style, - } - } - - pub(in crate::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { - BufferWriter { - inner: termcolor::BufferWriter::stdout(write_style.into_color_choice()), - uncolored_target: if is_test { - Some(WritableTarget::PrintStdout) - } else { - None - }, - write_style, - } - } - - pub(in crate::fmt::writer) fn pipe(pipe: Box>) -> Self { - let write_style = WriteStyle::Never; - BufferWriter { - // The inner Buffer is never printed from, but it is still needed to handle coloring and other formatting - inner: termcolor::BufferWriter::stderr(write_style.into_color_choice()), - uncolored_target: Some(WritableTarget::Pipe(pipe)), - write_style, - } - } - - pub(in crate::fmt::writer) fn write_style(&self) -> WriteStyle { - self.write_style - } - - pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { - Buffer { - inner: self.inner.buffer(), - has_uncolored_target: self.uncolored_target.is_some(), - } - } - - pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - if let Some(target) = &self.uncolored_target { - target.print(buf) - } else { - self.inner.print(&buf.inner) - } - } -} - -pub(in crate::fmt) struct Buffer { - inner: termcolor::Buffer, - has_uncolored_target: bool, -} - -impl Buffer { - pub(in crate::fmt) fn clear(&mut self) { - self.inner.clear() - } - - pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { - self.inner.write(buf) - } - - pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { - self.inner.flush() - } - - pub(in crate::fmt) fn as_bytes(&self) -> &[u8] { - self.inner.as_slice() - } - - pub(in crate::fmt) fn set_color(&mut self, spec: &ColorSpec) -> io::Result<()> { - // Ignore styles for test captured logs because they can't be printed - if !self.has_uncolored_target { - self.inner.set_color(spec) - } else { - Ok(()) - } - } - - pub(in crate::fmt) fn reset(&mut self) -> io::Result<()> { - // Ignore styles for test captured logs because they can't be printed - if !self.has_uncolored_target { - self.inner.reset() - } else { - Ok(()) - } - } -} diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 4195e788..74ab560d 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -26,17 +26,6 @@ pub enum WriteStyle { -#[cfg(feature = "color")] -impl WriteStyle { - fn into_color_choice(self) -> ::termcolor::ColorChoice { - match self { - WriteStyle::Always => ::termcolor::ColorChoice::Always, - WriteStyle::Auto => ::termcolor::ColorChoice::Auto, - WriteStyle::Never => ::termcolor::ColorChoice::Never, - } - } -} - /// A terminal target with color awareness. pub(crate) struct Writer { inner: BufferWriter, diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs index c3fe4825..0b806f9e 100644 --- a/src/fmt/writer/target.rs +++ b/src/fmt/writer/target.rs @@ -32,12 +32,10 @@ impl std::fmt::Debug for Target { /// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. pub(super) enum WritableTarget { /// Logs will be written to standard output. - #[allow(dead_code)] WriteStdout, /// Logs will be printed to standard output. PrintStdout, /// Logs will be written to standard error. - #[allow(dead_code)] WriteStderr, /// Logs will be printed to standard error. PrintStderr, From 0016e2c51212ea192bec1beaeb5e01fef5285eb9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 20:21:11 -0600 Subject: [PATCH 06/28] refactor(fmt): Flatten mods --- src/fmt/writer/{buffer/plain.rs => buffer.rs} | 0 src/fmt/writer/buffer/mod.rs | 2 -- 2 files changed, 2 deletions(-) rename src/fmt/writer/{buffer/plain.rs => buffer.rs} (100%) delete mode 100644 src/fmt/writer/buffer/mod.rs diff --git a/src/fmt/writer/buffer/plain.rs b/src/fmt/writer/buffer.rs similarity index 100% rename from src/fmt/writer/buffer/plain.rs rename to src/fmt/writer/buffer.rs diff --git a/src/fmt/writer/buffer/mod.rs b/src/fmt/writer/buffer/mod.rs deleted file mode 100644 index d0dbb5f5..00000000 --- a/src/fmt/writer/buffer/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod plain; -pub(in crate::fmt) use plain::*; From 1c50358db72afd46bf38922279ed7d492f472b29 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 21:12:39 -0600 Subject: [PATCH 07/28] refactor(fmt): Inline the print call --- src/fmt/writer/buffer.rs | 28 +++++++++++++++++++++++++++- src/fmt/writer/target.rs | 32 -------------------------------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index 99056783..64f9f087 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -42,7 +42,33 @@ impl BufferWriter { } pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - self.target.print(buf) + use std::io::Write as _; + + let buf = buf.as_bytes(); + match &self.target { + WritableTarget::WriteStdout => { + let stream = std::io::stdout(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), + WritableTarget::WriteStderr => { + let stream = std::io::stderr(); + let mut stream = stream.lock(); + stream.write_all(buf)?; + stream.flush()?; + } + WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), + // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + WritableTarget::Pipe(pipe) => { + let mut stream = pipe.lock().unwrap(); + stream.write_all(buf)?; + stream.flush()?; + } + } + + Ok(()) } } diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs index 0b806f9e..02f69c75 100644 --- a/src/fmt/writer/target.rs +++ b/src/fmt/writer/target.rs @@ -43,38 +43,6 @@ pub(super) enum WritableTarget { Pipe(Box>), } -impl WritableTarget { - pub(super) fn print(&self, buf: &super::Buffer) -> std::io::Result<()> { - use std::io::Write as _; - - let buf = buf.as_bytes(); - match self { - WritableTarget::WriteStdout => { - let stream = std::io::stdout(); - let mut stream = stream.lock(); - stream.write_all(buf)?; - stream.flush()?; - } - WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), - WritableTarget::WriteStderr => { - let stream = std::io::stderr(); - let mut stream = stream.lock(); - stream.write_all(buf)?; - stream.flush()?; - } - WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), - // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. - WritableTarget::Pipe(pipe) => { - let mut stream = pipe.lock().unwrap(); - stream.write_all(buf)?; - stream.flush()?; - } - } - - Ok(()) - } -} - impl std::fmt::Debug for WritableTarget { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( From f3de55b9eb44a0da90242961e581bac58817cc9c Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 21:40:58 -0600 Subject: [PATCH 08/28] refactor(fmt): Pull in Target to its only use --- src/fmt/writer/buffer.rs | 34 +++++++++++++++++++++++++++++++++- src/fmt/writer/mod.rs | 1 - src/fmt/writer/target.rs | 34 ---------------------------------- 3 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index 64f9f087..9b7cbf3c 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -1,6 +1,6 @@ use std::{io, sync::Mutex}; -use crate::fmt::writer::{WritableTarget, WriteStyle}; +use crate::fmt::writer::WriteStyle; pub(in crate::fmt::writer) struct BufferWriter { target: WritableTarget, @@ -92,3 +92,35 @@ impl Buffer { &self.0 } } + +/// Log target, either `stdout`, `stderr` or a custom pipe. +/// +/// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. +pub(super) enum WritableTarget { + /// Logs will be written to standard output. + WriteStdout, + /// Logs will be printed to standard output. + PrintStdout, + /// Logs will be written to standard error. + WriteStderr, + /// Logs will be printed to standard error. + PrintStderr, + /// Logs will be sent to a custom pipe. + Pipe(Box>), +} + +impl std::fmt::Debug for WritableTarget { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::WriteStdout => "stdout", + Self::PrintStdout => "stdout", + Self::WriteStderr => "stderr", + Self::PrintStderr => "stderr", + Self::Pipe(_) => "pipe", + } + ) + } +} diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 74ab560d..f5f7de32 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -9,7 +9,6 @@ use std::{fmt, io, mem, sync::Mutex}; pub(super) use self::buffer::Buffer; pub use target::Target; -use target::WritableTarget; /// Whether or not to print styles to the target. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs index 02f69c75..a1220ffe 100644 --- a/src/fmt/writer/target.rs +++ b/src/fmt/writer/target.rs @@ -11,8 +11,6 @@ pub enum Target { Pipe(Box), } - - impl std::fmt::Debug for Target { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -26,35 +24,3 @@ impl std::fmt::Debug for Target { ) } } - -/// Log target, either `stdout`, `stderr` or a custom pipe. -/// -/// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. -pub(super) enum WritableTarget { - /// Logs will be written to standard output. - WriteStdout, - /// Logs will be printed to standard output. - PrintStdout, - /// Logs will be written to standard error. - WriteStderr, - /// Logs will be printed to standard error. - PrintStderr, - /// Logs will be sent to a custom pipe. - Pipe(Box>), -} - -impl std::fmt::Debug for WritableTarget { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::WriteStdout => "stdout", - Self::PrintStdout => "stdout", - Self::WriteStderr => "stderr", - Self::PrintStderr => "stderr", - Self::Pipe(_) => "pipe", - } - ) - } -} From bce8463804c027d2e2aca8a07ef6c4cd29ab98e9 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Wed, 17 Jan 2024 20:30:39 -0600 Subject: [PATCH 09/28] feat(fmt): Add back color support This also adds support for `NO_COLOR` and `CLICOLOR_FORCE`, see https://bixense.com/clicolors/ --- Cargo.lock | 127 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 +- src/fmt/writer/atty.rs | 17 ------ src/fmt/writer/buffer.rs | 44 ++++++++++++-- src/fmt/writer/mod.rs | 59 ++++++++++++------ src/logger.rs | 3 + 6 files changed, 213 insertions(+), 42 deletions(-) delete mode 100644 src/fmt/writer/atty.rs diff --git a/Cargo.lock b/Cargo.lock index 80a265e7..ac42c663 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,16 +11,71 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "env_logger" version = "0.10.2" dependencies = [ + "anstream", "humantime", "log", "regex", @@ -63,3 +118,75 @@ name = "regex-syntax" version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index 499fbbb9..06871c62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,8 +38,8 @@ pre-release-replacements = [ [features] default = ["auto-color", "humantime", "regex"] -color = [] -auto-color = ["color"] +color = ["dep:anstream"] +auto-color = ["color", "anstream/auto"] humantime = ["dep:humantime"] regex = ["dep:regex"] @@ -47,6 +47,7 @@ regex = ["dep:regex"] log = { version = "0.4.8", features = ["std"] } regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } humantime = { version = "2.0.0", optional = true } +anstream = { version = "0.6.11", default-features = false, features = ["wincon"], optional = true } [[test]] name = "regexp_filter" diff --git a/src/fmt/writer/atty.rs b/src/fmt/writer/atty.rs deleted file mode 100644 index 13c994f5..00000000 --- a/src/fmt/writer/atty.rs +++ /dev/null @@ -1,17 +0,0 @@ -/* -This internal module contains the terminal detection implementation. - -If the `auto-color` feature is enabled then we detect whether we're attached to a particular TTY. -Otherwise, assume we're not attached to anything. This effectively prevents styles from being -printed. -*/ - -use std::io::IsTerminal; - -pub(in crate::fmt) fn is_stdout() -> bool { - cfg!(feature = "auto-color") && std::io::stdout().is_terminal() -} - -pub(in crate::fmt) fn is_stderr() -> bool { - cfg!(feature = "auto-color") && std::io::stderr().is_terminal() -} diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index 9b7cbf3c..aeffde05 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -4,37 +4,41 @@ use crate::fmt::writer::WriteStyle; pub(in crate::fmt::writer) struct BufferWriter { target: WritableTarget, + write_style: WriteStyle, } impl BufferWriter { - pub(in crate::fmt::writer) fn stderr(is_test: bool, _write_style: WriteStyle) -> Self { + pub(in crate::fmt::writer) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { BufferWriter { target: if is_test { WritableTarget::PrintStderr } else { WritableTarget::WriteStderr }, + write_style, } } - pub(in crate::fmt::writer) fn stdout(is_test: bool, _write_style: WriteStyle) -> Self { + pub(in crate::fmt::writer) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { BufferWriter { target: if is_test { WritableTarget::PrintStdout } else { WritableTarget::WriteStdout }, + write_style, } } pub(in crate::fmt::writer) fn pipe(pipe: Box>) -> Self { BufferWriter { target: WritableTarget::Pipe(pipe), + write_style: WriteStyle::Never, } } pub(in crate::fmt::writer) fn write_style(&self) -> WriteStyle { - WriteStyle::Never + self.write_style } pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { @@ -48,19 +52,36 @@ impl BufferWriter { match &self.target { WritableTarget::WriteStdout => { let stream = std::io::stdout(); + #[cfg(feature = "color")] + let stream = anstream::AutoStream::new(stream, self.write_style.into()); let mut stream = stream.lock(); stream.write_all(buf)?; stream.flush()?; } - WritableTarget::PrintStdout => print!("{}", String::from_utf8_lossy(buf)), + WritableTarget::PrintStdout => { + #[cfg(feature = "color")] + let buf = adapt(buf, self.write_style)?; + #[cfg(feature = "color")] + let buf = &buf; + let buf = String::from_utf8_lossy(buf); + print!("{}", buf); + } WritableTarget::WriteStderr => { let stream = std::io::stderr(); + #[cfg(feature = "color")] + let stream = anstream::AutoStream::new(stream, self.write_style.into()); let mut stream = stream.lock(); stream.write_all(buf)?; stream.flush()?; } - WritableTarget::PrintStderr => eprint!("{}", String::from_utf8_lossy(buf)), - // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. + WritableTarget::PrintStderr => { + #[cfg(feature = "color")] + let buf = adapt(buf, self.write_style)?; + #[cfg(feature = "color")] + let buf = &buf; + let buf = String::from_utf8_lossy(buf); + eprint!("{}", buf); + } WritableTarget::Pipe(pipe) => { let mut stream = pipe.lock().unwrap(); stream.write_all(buf)?; @@ -72,6 +93,17 @@ impl BufferWriter { } } +#[cfg(feature = "color")] +fn adapt(buf: &[u8], _write_style: WriteStyle) -> std::io::Result> { + use std::io::Write as _; + + let adapted = Vec::with_capacity(buf.len()); + let mut stream = anstream::StripStream::new(adapted); + stream.write_all(buf)?; + let adapted = stream.into_inner(); + Ok(adapted) +} + pub(in crate::fmt) struct Buffer(Vec); impl Buffer { diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index f5f7de32..65f3445e 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -1,8 +1,6 @@ -mod atty; mod buffer; mod target; -use self::atty::{is_stderr, is_stdout}; use self::buffer::BufferWriter; use std::{fmt, io, mem, sync::Mutex}; @@ -11,8 +9,7 @@ pub(super) use self::buffer::Buffer; pub use target::Target; /// Whether or not to print styles to the target. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -#[derive(Default)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)] pub enum WriteStyle { /// Try to print styles, but don't force the issue. #[default] @@ -23,7 +20,28 @@ pub enum WriteStyle { Never, } +#[cfg(feature = "color")] +impl From for WriteStyle { + fn from(choice: anstream::ColorChoice) -> Self { + match choice { + anstream::ColorChoice::Auto => Self::Auto, + anstream::ColorChoice::Always => Self::Always, + anstream::ColorChoice::AlwaysAnsi => Self::Always, + anstream::ColorChoice::Never => Self::Never, + } + } +} +#[cfg(feature = "color")] +impl From for anstream::ColorChoice { + fn from(choice: WriteStyle) -> Self { + match choice { + WriteStyle::Auto => anstream::ColorChoice::Auto, + WriteStyle::Always => anstream::ColorChoice::Always, + WriteStyle::Never => anstream::ColorChoice::Never, + } + } +} /// A terminal target with color awareness. pub(crate) struct Writer { @@ -105,29 +123,36 @@ impl Builder { assert!(!self.built, "attempt to re-use consumed builder"); self.built = true; - let color_choice = match self.write_style { - WriteStyle::Auto => { - if match &self.target { - Target::Stderr => is_stderr(), - Target::Stdout => is_stdout(), - Target::Pipe(_) => false, - } { - WriteStyle::Auto - } else { - WriteStyle::Never - } + let color_choice = self.write_style; + #[cfg(feature = "auto-color")] + let color_choice = if color_choice == WriteStyle::Auto { + match &self.target { + Target::Stdout => anstream::AutoStream::choice(&std::io::stdout()).into(), + Target::Stderr => anstream::AutoStream::choice(&std::io::stderr()).into(), + Target::Pipe(_) => WriteStyle::Never, } - color_choice => color_choice, + } else { + color_choice + }; + let color_choice = match &self.target { + Target::Stdout => color_choice, + Target::Stderr => color_choice, + Target::Pipe(_) => WriteStyle::Never, }; let color_choice = if self.is_test { WriteStyle::Never } else { color_choice }; + let color_choice = if color_choice == WriteStyle::Auto { + WriteStyle::Never + } else { + color_choice + }; let writer = match mem::take(&mut self.target) { - Target::Stderr => BufferWriter::stderr(self.is_test, color_choice), Target::Stdout => BufferWriter::stdout(self.is_test, color_choice), + Target::Stderr => BufferWriter::stderr(self.is_test, color_choice), Target::Pipe(pipe) => BufferWriter::pipe(Box::new(Mutex::new(pipe))), }; diff --git a/src/logger.rs b/src/logger.rs index 6c8a00d4..1e7d5894 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -223,6 +223,9 @@ impl Builder { /// to format and output without intermediate heap allocations. The default /// `env_logger` formatter takes advantage of this. /// + /// When the `color` feature is enabled, styling via ANSI escape codes is supported and the + /// output will automatically respect [`Builder::write_style`]. + /// /// # Examples /// /// Use a custom format to write only the log message: From 7d5b72a76e62616a4d3328bde084a95af052ba8b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 10:29:49 -0600 Subject: [PATCH 10/28] feat(fmt): Add back in Formatter::default_level_style --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/fmt/mod.rs | 27 +++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ac42c663..69452d28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,7 @@ name = "env_logger" version = "0.10.2" dependencies = [ "anstream", + "anstyle", "humantime", "log", "regex", diff --git a/Cargo.toml b/Cargo.toml index 06871c62..a4ccec85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ pre-release-replacements = [ [features] default = ["auto-color", "humantime", "regex"] -color = ["dep:anstream"] +color = ["dep:anstream", "dep:anstyle"] auto-color = ["color", "anstream/auto"] humantime = ["dep:humantime"] regex = ["dep:regex"] @@ -48,6 +48,7 @@ log = { version = "0.4.8", features = ["std"] } regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } humantime = { version = "2.0.0", optional = true } anstream = { version = "0.6.11", default-features = false, features = ["wincon"], optional = true } +anstyle = { version = "1.0.4", optional = true } [[test]] name = "regexp_filter" diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index dd78402f..bc5e6134 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -35,12 +35,17 @@ use std::io::prelude::*; use std::rc::Rc; use std::{fmt, io, mem}; +#[cfg(feature = "color")] +use log::Level; use log::Record; #[cfg(feature = "humantime")] mod humantime; pub(crate) mod writer; +#[cfg(feature = "color")] +pub use anstyle as style; + #[cfg(feature = "humantime")] pub use self::humantime::Timestamp; pub use self::writer::Target; @@ -119,6 +124,28 @@ impl Formatter { } } +#[cfg(feature = "color")] +impl Formatter { + /// Get the default [`style::Style`] for the given level. + /// + /// The style can be used to print other values besides the level. + pub fn default_level_style(&self, level: Level) -> style::Style { + if self.write_style == WriteStyle::Never { + style::Style::new() + } else { + match level { + Level::Trace => style::AnsiColor::Cyan.on_default(), + Level::Debug => style::AnsiColor::Blue.on_default(), + Level::Info => style::AnsiColor::Green.on_default(), + Level::Warn => style::AnsiColor::Yellow.on_default(), + Level::Error => style::AnsiColor::Red + .on_default() + .effects(style::Effects::BOLD), + } + } + } +} + impl Write for Formatter { fn write(&mut self, buf: &[u8]) -> io::Result { self.buf.borrow_mut().write(buf) From 2f3ca7d6956fa014927bf1a7316e6981e3829309 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 10:36:03 -0600 Subject: [PATCH 11/28] docs: Add back in custom colors --- examples/custom_format.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/examples/custom_format.rs b/examples/custom_format.rs index 1cc4bdec..8f575fd4 100644 --- a/examples/custom_format.rs +++ b/examples/custom_format.rs @@ -30,9 +30,18 @@ fn main() { Builder::from_env(env) .format(|buf, record| { + // We are reusing `anstyle` but there are `anstyle-*` crates to adapt it to your + // preferred styling crate. + let warn_style = buf.default_level_style(log::Level::Warn); + let reset = warn_style.render_reset(); + let warn_style = warn_style.render(); let timestamp = buf.timestamp(); - writeln!(buf, "My formatted log ({}): {}", timestamp, record.args()) + writeln!( + buf, + "My formatted log ({timestamp}): {warn_style}{}{reset}", + record.args() + ) }) .init(); } From 303a9c0ed288c5c7c15b6a74e29bfc1467bf9eab Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 10:56:55 -0600 Subject: [PATCH 12/28] feat(fmt): Add back in level styling --- src/fmt/mod.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index bc5e6134..771fd5cd 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -228,8 +228,37 @@ impl Default for Builder { } } +#[cfg(feature = "color")] +type SubtleStyle = StyledValue<&'static str>; +#[cfg(not(feature = "color"))] type SubtleStyle = &'static str; +/// A value that can be printed using the given styles. +/// +/// It is the result of calling [`Style::value`]. +/// +/// [`Style::value`]: struct.Style.html#method.value +#[cfg(feature = "color")] +struct StyledValue { + style: style::Style, + value: T, +} + +#[cfg(feature = "color")] +impl std::fmt::Display for StyledValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let style = self.style.render(); + let reset = self.style.render_reset(); + + // We need to make sure `f`s settings don't get passed onto the styling but do get passed + // to the value + write!(f, "{style}")?; + self.value.fmt(f)?; + write!(f, "{reset}")?; + Ok(()) + } +} + /// The default format. /// /// This format needs to work with any combination of crate features. @@ -256,6 +285,18 @@ impl<'a> DefaultFormat<'a> { } fn subtle_style(&self, text: &'static str) -> SubtleStyle { + #[cfg(feature = "color")] + { + StyledValue { + style: if self.buf.write_style == WriteStyle::Never { + style::Style::new() + } else { + style::AnsiColor::BrightBlack.on_default() + }, + value: text, + } + } + #[cfg(not(feature = "color"))] { text } @@ -281,8 +322,17 @@ impl<'a> DefaultFormat<'a> { } let level = { + let level = record.level(); + #[cfg(feature = "color")] + { + StyledValue { + style: self.buf.default_level_style(level), + value: level, + } + } + #[cfg(not(feature = "color"))] { - record.level() + level } }; From 4276f5fa7efc08408e1c2f6dcd9cc8b042afd6c1 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 11:14:10 -0600 Subject: [PATCH 13/28] fix(fmt): Print colors for tests This was discussed in #225 but no dedicated issue for it. --- src/fmt/writer/buffer.rs | 4 ++-- src/fmt/writer/mod.rs | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index aeffde05..fe983a00 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -94,11 +94,11 @@ impl BufferWriter { } #[cfg(feature = "color")] -fn adapt(buf: &[u8], _write_style: WriteStyle) -> std::io::Result> { +fn adapt(buf: &[u8], write_style: WriteStyle) -> std::io::Result> { use std::io::Write as _; let adapted = Vec::with_capacity(buf.len()); - let mut stream = anstream::StripStream::new(adapted); + let mut stream = anstream::AutoStream::new(adapted, write_style.into()); stream.write_all(buf)?; let adapted = stream.into_inner(); Ok(adapted) diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 65f3445e..545d642d 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -139,11 +139,6 @@ impl Builder { Target::Stderr => color_choice, Target::Pipe(_) => WriteStyle::Never, }; - let color_choice = if self.is_test { - WriteStyle::Never - } else { - color_choice - }; let color_choice = if color_choice == WriteStyle::Auto { WriteStyle::Never } else { From 730b0a096b121e4732f9e69a0132f1ccd4dbd9ed Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 11:15:32 -0600 Subject: [PATCH 14/28] fix(fmt): Allow styling on the `Pipe` Fixes #274 --- src/fmt/writer/buffer.rs | 4 ++++ src/fmt/writer/mod.rs | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index fe983a00..89ad9125 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -83,6 +83,10 @@ impl BufferWriter { eprint!("{}", buf); } WritableTarget::Pipe(pipe) => { + #[cfg(feature = "color")] + let buf = adapt(buf, self.write_style)?; + #[cfg(feature = "color")] + let buf = &buf; let mut stream = pipe.lock().unwrap(); stream.write_all(buf)?; stream.flush()?; diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index 545d642d..a3e66065 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -129,16 +129,11 @@ impl Builder { match &self.target { Target::Stdout => anstream::AutoStream::choice(&std::io::stdout()).into(), Target::Stderr => anstream::AutoStream::choice(&std::io::stderr()).into(), - Target::Pipe(_) => WriteStyle::Never, + Target::Pipe(_) => color_choice, } } else { color_choice }; - let color_choice = match &self.target { - Target::Stdout => color_choice, - Target::Stderr => color_choice, - Target::Pipe(_) => WriteStyle::Never, - }; let color_choice = if color_choice == WriteStyle::Auto { WriteStyle::Never } else { From ea464099b79fd5038d77e318bb46237868473b3d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Thu, 18 Jan 2024 12:21:39 -0600 Subject: [PATCH 15/28] chore(fmt): Add debug impls --- src/fmt/mod.rs | 6 +++++- src/fmt/writer/buffer.rs | 7 +++++++ src/fmt/writer/mod.rs | 9 ++------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 771fd5cd..faee69bb 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -158,7 +158,11 @@ impl Write for Formatter { impl fmt::Debug for Formatter { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Formatter").finish() + let buf = self.buf.borrow(); + f.debug_struct("Formatter") + .field("buf", &buf) + .field("write_style", &self.write_style) + .finish() } } diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs index 89ad9125..a7ea25bf 100644 --- a/src/fmt/writer/buffer.rs +++ b/src/fmt/writer/buffer.rs @@ -2,6 +2,7 @@ use std::{io, sync::Mutex}; use crate::fmt::writer::WriteStyle; +#[derive(Debug)] pub(in crate::fmt::writer) struct BufferWriter { target: WritableTarget, write_style: WriteStyle, @@ -129,6 +130,12 @@ impl Buffer { } } +impl std::fmt::Debug for Buffer { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + String::from_utf8_lossy(self.as_bytes()).fmt(f) + } +} + /// Log target, either `stdout`, `stderr` or a custom pipe. /// /// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. diff --git a/src/fmt/writer/mod.rs b/src/fmt/writer/mod.rs index a3e66065..dbcf45b6 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -2,7 +2,7 @@ mod buffer; mod target; use self::buffer::BufferWriter; -use std::{fmt, io, mem, sync::Mutex}; +use std::{io, mem, sync::Mutex}; pub(super) use self::buffer::Buffer; @@ -44,6 +44,7 @@ impl From for anstream::ColorChoice { } /// A terminal target with color awareness. +#[derive(Debug)] pub(crate) struct Writer { inner: BufferWriter, } @@ -62,12 +63,6 @@ impl Writer { } } -impl fmt::Debug for Writer { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Writer").finish() - } -} - /// A builder for a terminal writer. /// /// The target and style choice can be configured before building. From b4a2c304c16d1db4a2998f24c00e00c0f776113b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 10:56:23 -0600 Subject: [PATCH 16/28] chore: Add a workspace --- Cargo.toml | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4ccec85..9c17f7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,7 @@ -[package] -name = "env_logger" -version = "0.10.2" -description = """ -A logging implementation for `log` which is configured via an environment -variable. -""" -repository = "https://github.com/rust-cli/env_logger" -categories = ["development-tools::debugging"] -keywords = ["logging", "log", "logger"] +[workspace] +resolver = "2" + +[workspace.package] license = "MIT OR Apache-2.0" edition = "2021" rust-version = "1.71" # MSRV @@ -23,6 +17,21 @@ include = [ "tests/**/*", ] +[package] +name = "env_logger" +version = "0.10.2" +description = """ +A logging implementation for `log` which is configured via an environment +variable. +""" +repository = "https://github.com/rust-cli/env_logger" +categories = ["development-tools::debugging"] +keywords = ["logging", "log", "logger"] +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] From f1877824da51e8b79bcd8747e168b610b4a64cb3 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:12:17 -0600 Subject: [PATCH 17/28] refactor: Split out env_filter package Fixes #193 --- Cargo.lock | 10 +++++- Cargo.toml | 5 +-- crates/env_filter/CHANGELOG.md | 11 ++++++ crates/env_filter/Cargo.toml | 34 +++++++++++++++++++ crates/env_filter/LICENSE-APACHE | 1 + crates/env_filter/LICENSE-MIT | 1 + crates/env_filter/README.md | 6 ++++ .../mod.rs => crates/env_filter/src/lib.rs | 27 +++++---------- .../filter => crates/env_filter/src}/regex.rs | 4 +-- .../env_filter/src}/string.rs | 0 src/lib.rs | 3 +- 11 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 crates/env_filter/CHANGELOG.md create mode 100644 crates/env_filter/Cargo.toml create mode 120000 crates/env_filter/LICENSE-APACHE create mode 120000 crates/env_filter/LICENSE-MIT create mode 100644 crates/env_filter/README.md rename src/filter/mod.rs => crates/env_filter/src/lib.rs (97%) rename {src/filter => crates/env_filter/src}/regex.rs (91%) rename {src/filter => crates/env_filter/src}/string.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 69452d28..776d3400 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,15 +71,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "env_filter" +version = "0.1.0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" dependencies = [ "anstream", "anstyle", + "env_filter", "humantime", "log", - "regex", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9c17f7c7..b669cef6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] resolver = "2" +members = ["crates/*"] [workspace.package] license = "MIT OR Apache-2.0" @@ -50,11 +51,11 @@ default = ["auto-color", "humantime", "regex"] color = ["dep:anstream", "dep:anstyle"] auto-color = ["color", "anstream/auto"] humantime = ["dep:humantime"] -regex = ["dep:regex"] +regex = ["env_filter/regex"] [dependencies] log = { version = "0.4.8", features = ["std"] } -regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } +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 } anstyle = { version = "1.0.4", optional = true } diff --git a/crates/env_filter/CHANGELOG.md b/crates/env_filter/CHANGELOG.md new file mode 100644 index 00000000..78e2e120 --- /dev/null +++ b/crates/env_filter/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + + +## [Unreleased] - ReleaseDate + + +[Unreleased]: https://github.com/rust-cli/env_logger/compare/b4a2c304c16d1db4a2998f24c00e00c0f776113b...HEAD diff --git a/crates/env_filter/Cargo.toml b/crates/env_filter/Cargo.toml new file mode 100644 index 00000000..9ac0fa47 --- /dev/null +++ b/crates/env_filter/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "env_filter" +version = "0.1.0" +description = """ +Filter log events using environment variables +""" +repository = "https://github.com/rust-cli/env_logger" +categories = ["development-tools::debugging"] +keywords = ["logging", "log", "logger"] +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[package.metadata.release] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-cli/env_logger/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[features] +default = ["regex"] +regex = ["dep:regex"] + +[dependencies] +log = { version = "0.4.8", features = ["std"] } +regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } diff --git a/crates/env_filter/LICENSE-APACHE b/crates/env_filter/LICENSE-APACHE new file mode 120000 index 00000000..1cd601d0 --- /dev/null +++ b/crates/env_filter/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/env_filter/LICENSE-MIT b/crates/env_filter/LICENSE-MIT new file mode 120000 index 00000000..b2cfbdc7 --- /dev/null +++ b/crates/env_filter/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/env_filter/README.md b/crates/env_filter/README.md new file mode 100644 index 00000000..9e5164fb --- /dev/null +++ b/crates/env_filter/README.md @@ -0,0 +1,6 @@ +# env_filter + +[![crates.io](https://img.shields.io/crates/v/env_filter.svg)](https://crates.io/crates/env_filter) +[![Documentation](https://docs.rs/env_filter/badge.svg)](https://docs.rs/env_filter) + +> Filter log events using environment variables diff --git a/src/filter/mod.rs b/crates/env_filter/src/lib.rs similarity index 97% rename from src/filter/mod.rs rename to crates/env_filter/src/lib.rs index d4acf632..5d7d60b0 100644 --- a/src/filter/mod.rs +++ b/crates/env_filter/src/lib.rs @@ -1,21 +1,17 @@ //! Filtering for log records. //! -//! This module contains the log filtering used by `env_logger` to match records. -//! You can use the `Filter` type in your own logger implementation to use the same -//! filter parsing and matching as `env_logger`. For more details about the format -//! for directive strings see [Enabling Logging]. +//! You can use the [`Filter`] type in your own logger implementation to use the same +//! filter parsing and matching as `env_logger`. //! -//! ## Using `env_logger` in your own logger +//! ## Using `env_filter` in your own logger //! -//! You can use `env_logger`'s filtering functionality with your own logger. +//! You can use `env_filter`'s filtering functionality with your own logger. //! Call [`Builder::parse`] to parse directives from a string when constructing //! your logger. Call [`Filter::matches`] to check whether a record should be //! logged based on the parsed filters when log records are received. //! //! ``` -//! extern crate log; -//! extern crate env_logger; -//! use env_logger::filter::Filter; +//! use env_filter::Filter; //! use log::{Log, Metadata, Record}; //! //! struct MyLogger { @@ -24,7 +20,7 @@ //! //! impl MyLogger { //! fn new() -> MyLogger { -//! use env_logger::filter::Builder; +//! use env_filter::Builder; //! let mut builder = Builder::new(); //! //! // Parse a directives string from an environment variable @@ -53,10 +49,6 @@ //! fn flush(&self) {} //! } //! ``` -//! -//! [Enabling Logging]: ../index.html#enabling-logging -//! [`Builder::parse`]: struct.Builder.html#method.parse -//! [`Filter::matches`]: struct.Filter.html#method.matches use log::{Level, LevelFilter, Metadata, Record}; use std::env; @@ -79,9 +71,8 @@ mod inner; /// ## Example /// /// ``` -/// # #[macro_use] extern crate log; /// # use std::env; -/// use env_logger::filter::Builder; +/// use env_filter::Builder; /// /// let mut builder = Builder::new(); /// @@ -92,8 +83,6 @@ mod inner; /// /// let filter = builder.build(); /// ``` -/// -/// [`Filter`]: struct.Filter.html pub struct Builder { directives: Vec, filter: Option, @@ -248,7 +237,7 @@ impl Filter { /// /// ```rust /// use log::LevelFilter; - /// use env_logger::filter::Builder; + /// use env_filter::Builder; /// /// let mut builder = Builder::new(); /// builder.filter(Some("module1"), LevelFilter::Info); diff --git a/src/filter/regex.rs b/crates/env_filter/src/regex.rs similarity index 91% rename from src/filter/regex.rs rename to crates/env_filter/src/regex.rs index fb21528a..d56355af 100644 --- a/src/filter/regex.rs +++ b/crates/env_filter/src/regex.rs @@ -1,8 +1,6 @@ -extern crate regex; - use std::fmt; -use self::regex::Regex; +use regex::Regex; #[derive(Debug)] pub struct Filter { diff --git a/src/filter/string.rs b/crates/env_filter/src/string.rs similarity index 100% rename from src/filter/string.rs rename to crates/env_filter/src/string.rs diff --git a/src/lib.rs b/src/lib.rs index 93eba6ce..76cbf3a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,7 +277,8 @@ mod logger; -pub mod filter; +#[doc(inline)] +pub use ::env_filter as filter; pub mod fmt; pub use self::fmt::{Target, TimestampPrecision, WriteStyle}; From 57d938c7c75f07f927e8969a8013c65873f8fce0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:28:41 -0600 Subject: [PATCH 18/28] refactor(filter): Clean up op mod --- crates/env_filter/src/lib.rs | 16 ++++--------- crates/env_filter/src/op.rs | 42 +++++++++++++++++++++++++++++++++ crates/env_filter/src/regex.rs | 27 --------------------- crates/env_filter/src/string.rs | 24 ------------------- 4 files changed, 47 insertions(+), 62 deletions(-) create mode 100644 crates/env_filter/src/op.rs delete mode 100644 crates/env_filter/src/regex.rs delete mode 100644 crates/env_filter/src/string.rs diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index 5d7d60b0..609654a4 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -55,13 +55,7 @@ use std::env; use std::fmt; use std::mem; -#[cfg(feature = "regex")] -#[path = "regex.rs"] -mod inner; - -#[cfg(not(feature = "regex"))] -#[path = "string.rs"] -mod inner; +mod op; /// A builder for a log filter. /// @@ -85,7 +79,7 @@ mod inner; /// ``` pub struct Builder { directives: Vec, - filter: Option, + filter: Option, built: bool, } @@ -226,7 +220,7 @@ struct Directive { /// [`Builder`]: struct.Builder.html pub struct Filter { directives: Vec, - filter: Option, + filter: Option, } impl Filter { @@ -289,7 +283,7 @@ impl fmt::Debug for Filter { /// Parse a logging specification string (e.g: "crate1,crate2::mod3,crate3::x=error/foo") /// and return a vector with log directives. -fn parse_spec(spec: &str) -> (Vec, Option) { +fn parse_spec(spec: &str) -> (Vec, Option) { let mut dirs = Vec::new(); let mut parts = spec.split('/'); @@ -347,7 +341,7 @@ fn parse_spec(spec: &str) -> (Vec, Option) { } } - let filter = filter.and_then(|filter| match inner::Filter::new(filter) { + let filter = filter.and_then(|filter| match op::FilterOp::new(filter) { Ok(re) => Some(re), Err(e) => { eprintln!("warning: invalid regex filter - {}", e); diff --git a/crates/env_filter/src/op.rs b/crates/env_filter/src/op.rs new file mode 100644 index 00000000..e018e540 --- /dev/null +++ b/crates/env_filter/src/op.rs @@ -0,0 +1,42 @@ +use std::fmt; + +#[derive(Debug)] +pub struct FilterOp { + #[cfg(feature = "regex")] + inner: regex::Regex, + #[cfg(not(feature = "regex"))] + inner: String, +} + +#[cfg(feature = "regex")] +impl FilterOp { + pub fn new(spec: &str) -> Result { + match regex::Regex::new(spec) { + Ok(r) => Ok(Self { inner: r }), + Err(e) => Err(e.to_string()), + } + } + + pub fn is_match(&self, s: &str) -> bool { + self.inner.is_match(s) + } +} + +#[cfg(not(feature = "regex"))] +impl FilterOp { + pub fn new(spec: &str) -> Result { + Ok(Self { + inner: spec.to_string(), + }) + } + + pub fn is_match(&self, s: &str) -> bool { + s.contains(&self.inner) + } +} + +impl fmt::Display for FilterOp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(f) + } +} diff --git a/crates/env_filter/src/regex.rs b/crates/env_filter/src/regex.rs deleted file mode 100644 index d56355af..00000000 --- a/crates/env_filter/src/regex.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::fmt; - -use regex::Regex; - -#[derive(Debug)] -pub struct Filter { - inner: Regex, -} - -impl Filter { - pub fn new(spec: &str) -> Result { - match Regex::new(spec) { - Ok(r) => Ok(Filter { inner: r }), - Err(e) => Err(e.to_string()), - } - } - - pub fn is_match(&self, s: &str) -> bool { - self.inner.is_match(s) - } -} - -impl fmt::Display for Filter { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} diff --git a/crates/env_filter/src/string.rs b/crates/env_filter/src/string.rs deleted file mode 100644 index ea476e42..00000000 --- a/crates/env_filter/src/string.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::fmt; - -#[derive(Debug)] -pub struct Filter { - inner: String, -} - -impl Filter { - pub fn new(spec: &str) -> Result { - Ok(Filter { - inner: spec.to_string(), - }) - } - - pub fn is_match(&self, s: &str) -> bool { - s.contains(&self.inner) - } -} - -impl fmt::Display for Filter { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.inner.fmt(f) - } -} From 3b45c6ffc11d0952dc67ebc9aadf8a990905d44b Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:32:01 -0600 Subject: [PATCH 19/28] refactor(filter): Pull out parser mod --- crates/env_filter/src/lib.rs | 255 +------------------------------ crates/env_filter/src/parser.rs | 261 ++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 252 deletions(-) create mode 100644 crates/env_filter/src/parser.rs diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index 609654a4..a90342e7 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -56,6 +56,7 @@ use std::fmt; use std::mem; mod op; +mod parser; /// A builder for a log filter. /// @@ -145,7 +146,7 @@ impl Builder { /// /// [Enabling Logging]: ../index.html#enabling-logging pub fn parse(&mut self, filters: &str) -> &mut Self { - let (directives, filter) = parse_spec(filters); + let (directives, filter) = parser::parse_spec(filters); self.filter = filter; @@ -281,77 +282,6 @@ impl fmt::Debug for Filter { } } -/// Parse a logging specification string (e.g: "crate1,crate2::mod3,crate3::x=error/foo") -/// and return a vector with log directives. -fn parse_spec(spec: &str) -> (Vec, Option) { - let mut dirs = Vec::new(); - - let mut parts = spec.split('/'); - let mods = parts.next(); - let filter = parts.next(); - if parts.next().is_some() { - eprintln!( - "warning: invalid logging spec '{}', \ - ignoring it (too many '/'s)", - spec - ); - return (dirs, None); - } - if let Some(m) = mods { - for s in m.split(',').map(|ss| ss.trim()) { - if s.is_empty() { - continue; - } - let mut parts = s.split('='); - let (log_level, name) = - match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) { - (Some(part0), None, None) => { - // if the single argument is a log-level string or number, - // treat that as a global fallback - match part0.parse() { - Ok(num) => (num, None), - Err(_) => (LevelFilter::max(), Some(part0)), - } - } - (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)), - (Some(part0), Some(part1), None) => match part1.parse() { - Ok(num) => (num, Some(part0)), - _ => { - eprintln!( - "warning: invalid logging spec '{}', \ - ignoring it", - part1 - ); - continue; - } - }, - _ => { - eprintln!( - "warning: invalid logging spec '{}', \ - ignoring it", - s - ); - continue; - } - }; - dirs.push(Directive { - name: name.map(|s| s.to_string()), - level: log_level, - }); - } - } - - let filter = filter.and_then(|filter| match op::FilterOp::new(filter) { - Ok(re) => Some(re), - Err(e) => { - eprintln!("warning: invalid regex filter - {}", e); - None - } - }); - - (dirs, filter) -} - // Check whether a level and target are enabled by the set of directives. fn enabled(directives: &[Directive], level: Level, target: &str) -> bool { // Search for the longest match, the vector is assumed to be pre-sorted. @@ -368,7 +298,7 @@ fn enabled(directives: &[Directive], level: Level, target: &str) -> bool { mod tests { use log::{Level, LevelFilter}; - use super::{enabled, parse_spec, Builder, Directive, Filter}; + use super::{enabled, Builder, Directive, Filter}; fn make_logger_filter(dirs: Vec) -> Filter { let mut logger = Builder::new().build(); @@ -680,183 +610,4 @@ mod tests { assert!(!enabled(&logger.directives, Level::Error, "crate1::mod1")); assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); } - - #[test] - fn parse_spec_valid() { - let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug"); - assert_eq!(dirs.len(), 3); - assert_eq!(dirs[0].name, Some("crate1::mod1".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Error); - - assert_eq!(dirs[1].name, Some("crate1::mod2".to_string())); - assert_eq!(dirs[1].level, LevelFilter::max()); - - assert_eq!(dirs[2].name, Some("crate2".to_string())); - assert_eq!(dirs[2].level, LevelFilter::Debug); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_invalid_crate() { - // test parse_spec with multiple = in specification - let (dirs, filter) = parse_spec("crate1::mod1=warn=info,crate2=debug"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Debug); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_invalid_level() { - // test parse_spec with 'noNumber' as log level - let (dirs, filter) = parse_spec("crate1::mod1=noNumber,crate2=debug"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Debug); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_string_level() { - // test parse_spec with 'warn' as log level - let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2=warn"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Warn); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_empty_level() { - // test parse_spec with '' as log level - let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2="); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_string())); - assert_eq!(dirs[0].level, LevelFilter::max()); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_empty_level_isolated() { - // test parse_spec with "" as log level (and the entire spec str) - let (dirs, filter) = parse_spec(""); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_blank_level_isolated() { - // test parse_spec with a white-space-only string specified as the log - // level (and the entire spec str) - let (dirs, filter) = parse_spec(" "); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_blank_level_isolated_comma_only() { - // The spec should contain zero or more comma-separated string slices, - // so a comma-only string should be interpreted as two empty strings - // (which should both be treated as invalid, so ignored). - let (dirs, filter) = parse_spec(","); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_blank_level_isolated_comma_blank() { - // The spec should contain zero or more comma-separated string slices, - // so this bogus spec should be interpreted as containing one empty - // string and one blank string. Both should both be treated as - // invalid, so ignored. - let (dirs, filter) = parse_spec(", "); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_blank_level_isolated_blank_comma() { - // The spec should contain zero or more comma-separated string slices, - // so this bogus spec should be interpreted as containing one blank - // string and one empty string. Both should both be treated as - // invalid, so ignored. - let (dirs, filter) = parse_spec(" ,"); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_global() { - // test parse_spec with no crate - let (dirs, filter) = parse_spec("warn,crate2=debug"); - assert_eq!(dirs.len(), 2); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::Warn); - assert_eq!(dirs[1].name, Some("crate2".to_string())); - assert_eq!(dirs[1].level, LevelFilter::Debug); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_global_bare_warn_lc() { - // test parse_spec with no crate, in isolation, all lowercase - let (dirs, filter) = parse_spec("warn"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::Warn); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_global_bare_warn_uc() { - // test parse_spec with no crate, in isolation, all uppercase - let (dirs, filter) = parse_spec("WARN"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::Warn); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_global_bare_warn_mixed() { - // test parse_spec with no crate, in isolation, mixed case - let (dirs, filter) = parse_spec("wArN"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::Warn); - assert!(filter.is_none()); - } - - #[test] - fn parse_spec_valid_filter() { - let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc"); - assert_eq!(dirs.len(), 3); - assert_eq!(dirs[0].name, Some("crate1::mod1".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Error); - - assert_eq!(dirs[1].name, Some("crate1::mod2".to_string())); - assert_eq!(dirs[1].level, LevelFilter::max()); - - assert_eq!(dirs[2].name, Some("crate2".to_string())); - assert_eq!(dirs[2].level, LevelFilter::Debug); - assert!(filter.is_some() && filter.unwrap().to_string() == "abc"); - } - - #[test] - fn parse_spec_invalid_crate_filter() { - let (dirs, filter) = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_string())); - assert_eq!(dirs[0].level, LevelFilter::Debug); - assert!(filter.is_some() && filter.unwrap().to_string() == "a.c"); - } - - #[test] - fn parse_spec_empty_with_filter() { - let (dirs, filter) = parse_spec("crate1/a*c"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate1".to_string())); - assert_eq!(dirs[0].level, LevelFilter::max()); - assert!(filter.is_some() && filter.unwrap().to_string() == "a*c"); - } } diff --git a/crates/env_filter/src/parser.rs b/crates/env_filter/src/parser.rs new file mode 100644 index 00000000..d9158fab --- /dev/null +++ b/crates/env_filter/src/parser.rs @@ -0,0 +1,261 @@ +use log::LevelFilter; + +use crate::op; +use crate::Directive; + +/// Parse a logging specification string (e.g: "crate1,crate2::mod3,crate3::x=error/foo") +/// and return a vector with log directives. +pub(crate) fn parse_spec(spec: &str) -> (Vec, Option) { + let mut dirs = Vec::new(); + + let mut parts = spec.split('/'); + let mods = parts.next(); + let filter = parts.next(); + if parts.next().is_some() { + eprintln!( + "warning: invalid logging spec '{}', \ + ignoring it (too many '/'s)", + spec + ); + return (dirs, None); + } + if let Some(m) = mods { + for s in m.split(',').map(|ss| ss.trim()) { + if s.is_empty() { + continue; + } + let mut parts = s.split('='); + let (log_level, name) = + match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) { + (Some(part0), None, None) => { + // if the single argument is a log-level string or number, + // treat that as a global fallback + match part0.parse() { + Ok(num) => (num, None), + Err(_) => (LevelFilter::max(), Some(part0)), + } + } + (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)), + (Some(part0), Some(part1), None) => match part1.parse() { + Ok(num) => (num, Some(part0)), + _ => { + eprintln!( + "warning: invalid logging spec '{}', \ + ignoring it", + part1 + ); + continue; + } + }, + _ => { + eprintln!( + "warning: invalid logging spec '{}', \ + ignoring it", + s + ); + continue; + } + }; + dirs.push(Directive { + name: name.map(|s| s.to_string()), + level: log_level, + }); + } + } + + let filter = filter.and_then(|filter| match op::FilterOp::new(filter) { + Ok(re) => Some(re), + Err(e) => { + eprintln!("warning: invalid regex filter - {}", e); + None + } + }); + + (dirs, filter) +} + +#[cfg(test)] +mod tests { + use log::LevelFilter; + + use super::parse_spec; + + #[test] + fn parse_spec_valid() { + let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug"); + assert_eq!(dirs.len(), 3); + assert_eq!(dirs[0].name, Some("crate1::mod1".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Error); + + assert_eq!(dirs[1].name, Some("crate1::mod2".to_string())); + assert_eq!(dirs[1].level, LevelFilter::max()); + + assert_eq!(dirs[2].name, Some("crate2".to_string())); + assert_eq!(dirs[2].level, LevelFilter::Debug); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_invalid_crate() { + // test parse_spec with multiple = in specification + let (dirs, filter) = parse_spec("crate1::mod1=warn=info,crate2=debug"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Debug); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_invalid_level() { + // test parse_spec with 'noNumber' as log level + let (dirs, filter) = parse_spec("crate1::mod1=noNumber,crate2=debug"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Debug); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_string_level() { + // test parse_spec with 'warn' as log level + let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2=warn"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Warn); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_empty_level() { + // test parse_spec with '' as log level + let (dirs, filter) = parse_spec("crate1::mod1=wrong,crate2="); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_string())); + assert_eq!(dirs[0].level, LevelFilter::max()); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_empty_level_isolated() { + // test parse_spec with "" as log level (and the entire spec str) + let (dirs, filter) = parse_spec(""); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_blank_level_isolated() { + // test parse_spec with a white-space-only string specified as the log + // level (and the entire spec str) + let (dirs, filter) = parse_spec(" "); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_blank_level_isolated_comma_only() { + // The spec should contain zero or more comma-separated string slices, + // so a comma-only string should be interpreted as two empty strings + // (which should both be treated as invalid, so ignored). + let (dirs, filter) = parse_spec(","); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_blank_level_isolated_comma_blank() { + // The spec should contain zero or more comma-separated string slices, + // so this bogus spec should be interpreted as containing one empty + // string and one blank string. Both should both be treated as + // invalid, so ignored. + let (dirs, filter) = parse_spec(", "); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_blank_level_isolated_blank_comma() { + // The spec should contain zero or more comma-separated string slices, + // so this bogus spec should be interpreted as containing one blank + // string and one empty string. Both should both be treated as + // invalid, so ignored. + let (dirs, filter) = parse_spec(" ,"); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_global() { + // test parse_spec with no crate + let (dirs, filter) = parse_spec("warn,crate2=debug"); + assert_eq!(dirs.len(), 2); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::Warn); + assert_eq!(dirs[1].name, Some("crate2".to_string())); + assert_eq!(dirs[1].level, LevelFilter::Debug); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_global_bare_warn_lc() { + // test parse_spec with no crate, in isolation, all lowercase + let (dirs, filter) = parse_spec("warn"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::Warn); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_global_bare_warn_uc() { + // test parse_spec with no crate, in isolation, all uppercase + let (dirs, filter) = parse_spec("WARN"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::Warn); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_global_bare_warn_mixed() { + // test parse_spec with no crate, in isolation, mixed case + let (dirs, filter) = parse_spec("wArN"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::Warn); + assert!(filter.is_none()); + } + + #[test] + fn parse_spec_valid_filter() { + let (dirs, filter) = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc"); + assert_eq!(dirs.len(), 3); + assert_eq!(dirs[0].name, Some("crate1::mod1".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Error); + + assert_eq!(dirs[1].name, Some("crate1::mod2".to_string())); + assert_eq!(dirs[1].level, LevelFilter::max()); + + assert_eq!(dirs[2].name, Some("crate2".to_string())); + assert_eq!(dirs[2].level, LevelFilter::Debug); + assert!(filter.is_some() && filter.unwrap().to_string() == "abc"); + } + + #[test] + fn parse_spec_invalid_crate_filter() { + let (dirs, filter) = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_string())); + assert_eq!(dirs[0].level, LevelFilter::Debug); + assert!(filter.is_some() && filter.unwrap().to_string() == "a.c"); + } + + #[test] + fn parse_spec_empty_with_filter() { + let (dirs, filter) = parse_spec("crate1/a*c"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate1".to_string())); + assert_eq!(dirs[0].level, LevelFilter::max()); + assert!(filter.is_some() && filter.unwrap().to_string() == "a*c"); + } +} From c769e03f40e03e83b972c334195014eddf8b2c9a Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:34:45 -0600 Subject: [PATCH 20/28] refactor(filter): Flatten the mod --- crates/env_filter/src/lib.rs | 16 ++++++++++------ crates/env_filter/src/parser.rs | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index a90342e7..3d97d387 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -50,13 +50,17 @@ //! } //! ``` -use log::{Level, LevelFilter, Metadata, Record}; +mod op; +mod parser; + use std::env; use std::fmt; use std::mem; -mod op; -mod parser; +use log::{Level, LevelFilter, Metadata, Record}; + +use op::FilterOp; +use parser::parse_spec; /// A builder for a log filter. /// @@ -80,7 +84,7 @@ mod parser; /// ``` pub struct Builder { directives: Vec, - filter: Option, + filter: Option, built: bool, } @@ -146,7 +150,7 @@ impl Builder { /// /// [Enabling Logging]: ../index.html#enabling-logging pub fn parse(&mut self, filters: &str) -> &mut Self { - let (directives, filter) = parser::parse_spec(filters); + let (directives, filter) = parse_spec(filters); self.filter = filter; @@ -221,7 +225,7 @@ struct Directive { /// [`Builder`]: struct.Builder.html pub struct Filter { directives: Vec, - filter: Option, + filter: Option, } impl Filter { diff --git a/crates/env_filter/src/parser.rs b/crates/env_filter/src/parser.rs index d9158fab..2d0f90b0 100644 --- a/crates/env_filter/src/parser.rs +++ b/crates/env_filter/src/parser.rs @@ -1,11 +1,11 @@ use log::LevelFilter; -use crate::op; use crate::Directive; +use crate::FilterOp; /// Parse a logging specification string (e.g: "crate1,crate2::mod3,crate3::x=error/foo") /// and return a vector with log directives. -pub(crate) fn parse_spec(spec: &str) -> (Vec, Option) { +pub(crate) fn parse_spec(spec: &str) -> (Vec, Option) { let mut dirs = Vec::new(); let mut parts = spec.split('/'); @@ -63,7 +63,7 @@ pub(crate) fn parse_spec(spec: &str) -> (Vec, Option) { } } - let filter = filter.and_then(|filter| match op::FilterOp::new(filter) { + let filter = filter.and_then(|filter| match FilterOp::new(filter) { Ok(re) => Some(re), Err(e) => { eprintln!("warning: invalid regex filter - {}", e); From 98c450f85b95779b60be37d847e176856305b6fd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:36:37 -0600 Subject: [PATCH 21/28] refactor(filter): Pull out directive mod --- crates/env_filter/src/directive.rs | 20 ++++++++++++++++++++ crates/env_filter/src/lib.rs | 23 ++++------------------- 2 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 crates/env_filter/src/directive.rs diff --git a/crates/env_filter/src/directive.rs b/crates/env_filter/src/directive.rs new file mode 100644 index 00000000..3721ef7f --- /dev/null +++ b/crates/env_filter/src/directive.rs @@ -0,0 +1,20 @@ +use log::Level; +use log::LevelFilter; + +#[derive(Debug)] +pub(crate) struct Directive { + pub(crate) name: Option, + pub(crate) level: LevelFilter, +} + +// Check whether a level and target are enabled by the set of directives. +pub(crate) fn enabled(directives: &[Directive], level: Level, target: &str) -> bool { + // Search for the longest match, the vector is assumed to be pre-sorted. + for directive in directives.iter().rev() { + match directive.name { + Some(ref name) if !target.starts_with(&**name) => {} + Some(..) | None => return level <= directive.level, + } + } + false +} diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index 3d97d387..41b71f8c 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -50,6 +50,7 @@ //! } //! ``` +mod directive; mod op; mod parser; @@ -57,8 +58,10 @@ use std::env; use std::fmt; use std::mem; -use log::{Level, LevelFilter, Metadata, Record}; +use log::{LevelFilter, Metadata, Record}; +use directive::enabled; +use directive::Directive; use op::FilterOp; use parser::parse_spec; @@ -210,12 +213,6 @@ impl fmt::Debug for Builder { } } -#[derive(Debug)] -struct Directive { - name: Option, - level: LevelFilter, -} - /// A log filter. /// /// This struct can be used to determine whether or not a log record @@ -286,18 +283,6 @@ impl fmt::Debug for Filter { } } -// Check whether a level and target are enabled by the set of directives. -fn enabled(directives: &[Directive], level: Level, target: &str) -> bool { - // Search for the longest match, the vector is assumed to be pre-sorted. - for directive in directives.iter().rev() { - match directive.name { - Some(ref name) if !target.starts_with(&**name) => {} - Some(..) | None => return level <= directive.level, - } - } - false -} - #[cfg(test)] mod tests { use log::{Level, LevelFilter}; From 841eba41feb44317facc586745b28707590d11fd Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:38:22 -0600 Subject: [PATCH 22/28] refactor(filter): Pull out filter mod --- crates/env_filter/src/filter.rs | 546 ++++++++++++++++++++++++++++++++ crates/env_filter/src/lib.rs | 544 +------------------------------ 2 files changed, 549 insertions(+), 541 deletions(-) create mode 100644 crates/env_filter/src/filter.rs diff --git a/crates/env_filter/src/filter.rs b/crates/env_filter/src/filter.rs new file mode 100644 index 00000000..7a052720 --- /dev/null +++ b/crates/env_filter/src/filter.rs @@ -0,0 +1,546 @@ +use std::env; +use std::fmt; +use std::mem; + +use log::{LevelFilter, Metadata, Record}; + +use crate::enabled; +use crate::Directive; +use crate::FilterOp; +use crate::parse_spec; + +/// A builder for a log filter. +/// +/// It can be used to parse a set of directives from a string before building +/// a [`Filter`] instance. +/// +/// ## Example +/// +/// ``` +/// # use std::env; +/// use env_filter::Builder; +/// +/// let mut builder = Builder::new(); +/// +/// // Parse a logging filter from an environment variable. +/// if let Ok(rust_log) = env::var("RUST_LOG") { +/// builder.parse(&rust_log); +/// } +/// +/// let filter = builder.build(); +/// ``` +pub struct Builder { + directives: Vec, + filter: Option, + built: bool, +} + +impl Builder { + /// Initializes the filter builder with defaults. + pub fn new() -> Builder { + Builder { + directives: Vec::new(), + filter: None, + built: false, + } + } + + /// Initializes the filter builder from an environment. + pub fn from_env(env: &str) -> Builder { + let mut builder = Builder::new(); + + if let Ok(s) = env::var(env) { + builder.parse(&s); + } + + builder + } + + /// Insert the directive replacing any directive with the same name. + fn insert_directive(&mut self, mut directive: Directive) { + if let Some(pos) = self + .directives + .iter() + .position(|d| d.name == directive.name) + { + mem::swap(&mut self.directives[pos], &mut directive); + } else { + self.directives.push(directive); + } + } + + /// Adds a directive to the filter for a specific module. + pub fn filter_module(&mut self, module: &str, level: LevelFilter) -> &mut Self { + self.filter(Some(module), level) + } + + /// Adds a directive to the filter for all modules. + pub fn filter_level(&mut self, level: LevelFilter) -> &mut Self { + self.filter(None, level) + } + + /// Adds a directive to the filter. + /// + /// The given module (if any) will log at most the specified level provided. + /// If no module is provided then the filter will apply to all log messages. + pub fn filter(&mut self, module: Option<&str>, level: LevelFilter) -> &mut Self { + self.insert_directive(Directive { + name: module.map(|s| s.to_string()), + level, + }); + self + } + + /// Parses the directives string. + /// + /// See the [Enabling Logging] section for more details. + /// + /// [Enabling Logging]: ../index.html#enabling-logging + pub fn parse(&mut self, filters: &str) -> &mut Self { + let (directives, filter) = parse_spec(filters); + + self.filter = filter; + + for directive in directives { + self.insert_directive(directive); + } + self + } + + /// Build a log filter. + pub fn build(&mut self) -> Filter { + assert!(!self.built, "attempt to re-use consumed builder"); + self.built = true; + + let mut directives = Vec::new(); + if self.directives.is_empty() { + // Adds the default filter if none exist + directives.push(Directive { + name: None, + level: LevelFilter::Error, + }); + } else { + // Consume directives. + directives = mem::take(&mut self.directives); + // Sort the directives by length of their name, this allows a + // little more efficient lookup at runtime. + directives.sort_by(|a, b| { + let alen = a.name.as_ref().map(|a| a.len()).unwrap_or(0); + let blen = b.name.as_ref().map(|b| b.len()).unwrap_or(0); + alen.cmp(&blen) + }); + } + + Filter { + directives: mem::take(&mut directives), + filter: mem::take(&mut self.filter), + } + } +} + +impl Default for Builder { + fn default() -> Self { + Builder::new() + } +} + +impl fmt::Debug for Builder { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.built { + f.debug_struct("Filter").field("built", &true).finish() + } else { + f.debug_struct("Filter") + .field("filter", &self.filter) + .field("directives", &self.directives) + .finish() + } + } +} + +/// A log filter. +/// +/// This struct can be used to determine whether or not a log record +/// should be written to the output. +/// Use the [`Builder`] type to parse and construct a `Filter`. +/// +/// [`Builder`]: struct.Builder.html +pub struct Filter { + directives: Vec, + filter: Option, +} + +impl Filter { + /// Returns the maximum `LevelFilter` that this filter instance is + /// configured to output. + /// + /// # Example + /// + /// ```rust + /// use log::LevelFilter; + /// use env_filter::Builder; + /// + /// let mut builder = Builder::new(); + /// builder.filter(Some("module1"), LevelFilter::Info); + /// builder.filter(Some("module2"), LevelFilter::Error); + /// + /// let filter = builder.build(); + /// assert_eq!(filter.filter(), LevelFilter::Info); + /// ``` + pub fn filter(&self) -> LevelFilter { + self.directives + .iter() + .map(|d| d.level) + .max() + .unwrap_or(LevelFilter::Off) + } + + /// Checks if this record matches the configured filter. + pub fn matches(&self, record: &Record) -> bool { + if !self.enabled(record.metadata()) { + return false; + } + + if let Some(filter) = self.filter.as_ref() { + if !filter.is_match(&record.args().to_string()) { + return false; + } + } + + true + } + + /// Determines if a log message with the specified metadata would be logged. + pub fn enabled(&self, metadata: &Metadata) -> bool { + let level = metadata.level(); + let target = metadata.target(); + + enabled(&self.directives, level, target) + } +} + +impl fmt::Debug for Filter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Filter") + .field("filter", &self.filter) + .field("directives", &self.directives) + .finish() + } +} + +#[cfg(test)] +mod tests { + use log::{Level, LevelFilter}; + + use super::{enabled, Builder, Directive, Filter}; + + fn make_logger_filter(dirs: Vec) -> Filter { + let mut logger = Builder::new().build(); + logger.directives = dirs; + logger + } + + #[test] + fn filter_info() { + let logger = Builder::new().filter(None, LevelFilter::Info).build(); + assert!(enabled(&logger.directives, Level::Info, "crate1")); + assert!(!enabled(&logger.directives, Level::Debug, "crate1")); + } + + #[test] + fn filter_beginning_longest_match() { + let logger = Builder::new() + .filter(Some("crate2"), LevelFilter::Info) + .filter(Some("crate2::mod"), LevelFilter::Debug) + .filter(Some("crate1::mod1"), LevelFilter::Warn) + .build(); + assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); + assert!(!enabled(&logger.directives, Level::Debug, "crate2")); + } + + // Some of our tests are only correct or complete when they cover the full + // universe of variants for log::Level. In the unlikely event that a new + // variant is added in the future, this test will detect the scenario and + // alert us to the need to review and update the tests. In such a + // situation, this test will fail to compile, and the error message will + // look something like this: + // + // error[E0004]: non-exhaustive patterns: `NewVariant` not covered + // --> src/filter/mod.rs:413:15 + // | + // 413 | match level_universe { + // | ^^^^^^^^^^^^^^ pattern `NewVariant` not covered + #[test] + fn ensure_tests_cover_level_universe() { + let level_universe: Level = Level::Trace; // use of trace variant is arbitrary + match level_universe { + Level::Error | Level::Warn | Level::Info | Level::Debug | Level::Trace => (), + } + } + + #[test] + fn parse_default() { + let logger = Builder::new().parse("info,crate1::mod1=warn").build(); + assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); + assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); + } + + #[test] + fn parse_default_bare_level_off_lc() { + let logger = Builder::new().parse("off").build(); + assert!(!enabled(&logger.directives, Level::Error, "")); + assert!(!enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_off_uc() { + let logger = Builder::new().parse("OFF").build(); + assert!(!enabled(&logger.directives, Level::Error, "")); + assert!(!enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_error_lc() { + let logger = Builder::new().parse("error").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(!enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_error_uc() { + let logger = Builder::new().parse("ERROR").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(!enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_warn_lc() { + let logger = Builder::new().parse("warn").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_warn_uc() { + let logger = Builder::new().parse("WARN").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(!enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_info_lc() { + let logger = Builder::new().parse("info").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_info_uc() { + let logger = Builder::new().parse("INFO").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(!enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_debug_lc() { + let logger = Builder::new().parse("debug").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_debug_uc() { + let logger = Builder::new().parse("DEBUG").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_trace_lc() { + let logger = Builder::new().parse("trace").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(enabled(&logger.directives, Level::Trace, "")); + } + + #[test] + fn parse_default_bare_level_trace_uc() { + let logger = Builder::new().parse("TRACE").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(enabled(&logger.directives, Level::Trace, "")); + } + + // In practice, the desired log level is typically specified by a token + // that is either all lowercase (e.g., 'trace') or all uppercase (.e.g, + // 'TRACE'), but this tests serves as a reminder that + // log::Level::from_str() ignores all case variants. + #[test] + fn parse_default_bare_level_debug_mixed() { + { + let logger = Builder::new().parse("Debug").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + { + let logger = Builder::new().parse("debuG").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + { + let logger = Builder::new().parse("deBug").build(); + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + { + let logger = Builder::new().parse("DeBuG").build(); // LaTeX flavor! + assert!(enabled(&logger.directives, Level::Error, "")); + assert!(enabled(&logger.directives, Level::Warn, "")); + assert!(enabled(&logger.directives, Level::Info, "")); + assert!(enabled(&logger.directives, Level::Debug, "")); + assert!(!enabled(&logger.directives, Level::Trace, "")); + } + } + + #[test] + fn match_full_path() { + let logger = make_logger_filter(vec![ + Directive { + name: Some("crate2".to_string()), + level: LevelFilter::Info, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Warn, + }, + ]); + assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); + assert!(!enabled(&logger.directives, Level::Info, "crate1::mod1")); + assert!(enabled(&logger.directives, Level::Info, "crate2")); + assert!(!enabled(&logger.directives, Level::Debug, "crate2")); + } + + #[test] + fn no_match() { + let logger = make_logger_filter(vec![ + Directive { + name: Some("crate2".to_string()), + level: LevelFilter::Info, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Warn, + }, + ]); + assert!(!enabled(&logger.directives, Level::Warn, "crate3")); + } + + #[test] + fn match_beginning() { + let logger = make_logger_filter(vec![ + Directive { + name: Some("crate2".to_string()), + level: LevelFilter::Info, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Warn, + }, + ]); + assert!(enabled(&logger.directives, Level::Info, "crate2::mod1")); + } + + #[test] + fn match_beginning_longest_match() { + let logger = make_logger_filter(vec![ + Directive { + name: Some("crate2".to_string()), + level: LevelFilter::Info, + }, + Directive { + name: Some("crate2::mod".to_string()), + level: LevelFilter::Debug, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Warn, + }, + ]); + assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); + assert!(!enabled(&logger.directives, Level::Debug, "crate2")); + } + + #[test] + fn match_default() { + let logger = make_logger_filter(vec![ + Directive { + name: None, + level: LevelFilter::Info, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Warn, + }, + ]); + assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); + assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); + } + + #[test] + fn zero_level() { + let logger = make_logger_filter(vec![ + Directive { + name: None, + level: LevelFilter::Info, + }, + Directive { + name: Some("crate1::mod1".to_string()), + level: LevelFilter::Off, + }, + ]); + assert!(!enabled(&logger.directives, Level::Error, "crate1::mod1")); + assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); + } +} diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index 41b71f8c..494b82da 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -51,552 +51,14 @@ //! ``` mod directive; +mod filter; mod op; mod parser; -use std::env; -use std::fmt; -use std::mem; - -use log::{LevelFilter, Metadata, Record}; - use directive::enabled; use directive::Directive; use op::FilterOp; use parser::parse_spec; -/// A builder for a log filter. -/// -/// It can be used to parse a set of directives from a string before building -/// a [`Filter`] instance. -/// -/// ## Example -/// -/// ``` -/// # use std::env; -/// use env_filter::Builder; -/// -/// let mut builder = Builder::new(); -/// -/// // Parse a logging filter from an environment variable. -/// if let Ok(rust_log) = env::var("RUST_LOG") { -/// builder.parse(&rust_log); -/// } -/// -/// let filter = builder.build(); -/// ``` -pub struct Builder { - directives: Vec, - filter: Option, - built: bool, -} - -impl Builder { - /// Initializes the filter builder with defaults. - pub fn new() -> Builder { - Builder { - directives: Vec::new(), - filter: None, - built: false, - } - } - - /// Initializes the filter builder from an environment. - pub fn from_env(env: &str) -> Builder { - let mut builder = Builder::new(); - - if let Ok(s) = env::var(env) { - builder.parse(&s); - } - - builder - } - - /// Insert the directive replacing any directive with the same name. - fn insert_directive(&mut self, mut directive: Directive) { - if let Some(pos) = self - .directives - .iter() - .position(|d| d.name == directive.name) - { - mem::swap(&mut self.directives[pos], &mut directive); - } else { - self.directives.push(directive); - } - } - - /// Adds a directive to the filter for a specific module. - pub fn filter_module(&mut self, module: &str, level: LevelFilter) -> &mut Self { - self.filter(Some(module), level) - } - - /// Adds a directive to the filter for all modules. - pub fn filter_level(&mut self, level: LevelFilter) -> &mut Self { - self.filter(None, level) - } - - /// Adds a directive to the filter. - /// - /// The given module (if any) will log at most the specified level provided. - /// If no module is provided then the filter will apply to all log messages. - pub fn filter(&mut self, module: Option<&str>, level: LevelFilter) -> &mut Self { - self.insert_directive(Directive { - name: module.map(|s| s.to_string()), - level, - }); - self - } - - /// Parses the directives string. - /// - /// See the [Enabling Logging] section for more details. - /// - /// [Enabling Logging]: ../index.html#enabling-logging - pub fn parse(&mut self, filters: &str) -> &mut Self { - let (directives, filter) = parse_spec(filters); - - self.filter = filter; - - for directive in directives { - self.insert_directive(directive); - } - self - } - - /// Build a log filter. - pub fn build(&mut self) -> Filter { - assert!(!self.built, "attempt to re-use consumed builder"); - self.built = true; - - let mut directives = Vec::new(); - if self.directives.is_empty() { - // Adds the default filter if none exist - directives.push(Directive { - name: None, - level: LevelFilter::Error, - }); - } else { - // Consume directives. - directives = mem::take(&mut self.directives); - // Sort the directives by length of their name, this allows a - // little more efficient lookup at runtime. - directives.sort_by(|a, b| { - let alen = a.name.as_ref().map(|a| a.len()).unwrap_or(0); - let blen = b.name.as_ref().map(|b| b.len()).unwrap_or(0); - alen.cmp(&blen) - }); - } - - Filter { - directives: mem::take(&mut directives), - filter: mem::take(&mut self.filter), - } - } -} - -impl Default for Builder { - fn default() -> Self { - Builder::new() - } -} - -impl fmt::Debug for Builder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.built { - f.debug_struct("Filter").field("built", &true).finish() - } else { - f.debug_struct("Filter") - .field("filter", &self.filter) - .field("directives", &self.directives) - .finish() - } - } -} - -/// A log filter. -/// -/// This struct can be used to determine whether or not a log record -/// should be written to the output. -/// Use the [`Builder`] type to parse and construct a `Filter`. -/// -/// [`Builder`]: struct.Builder.html -pub struct Filter { - directives: Vec, - filter: Option, -} - -impl Filter { - /// Returns the maximum `LevelFilter` that this filter instance is - /// configured to output. - /// - /// # Example - /// - /// ```rust - /// use log::LevelFilter; - /// use env_filter::Builder; - /// - /// let mut builder = Builder::new(); - /// builder.filter(Some("module1"), LevelFilter::Info); - /// builder.filter(Some("module2"), LevelFilter::Error); - /// - /// let filter = builder.build(); - /// assert_eq!(filter.filter(), LevelFilter::Info); - /// ``` - pub fn filter(&self) -> LevelFilter { - self.directives - .iter() - .map(|d| d.level) - .max() - .unwrap_or(LevelFilter::Off) - } - - /// Checks if this record matches the configured filter. - pub fn matches(&self, record: &Record) -> bool { - if !self.enabled(record.metadata()) { - return false; - } - - if let Some(filter) = self.filter.as_ref() { - if !filter.is_match(&record.args().to_string()) { - return false; - } - } - - true - } - - /// Determines if a log message with the specified metadata would be logged. - pub fn enabled(&self, metadata: &Metadata) -> bool { - let level = metadata.level(); - let target = metadata.target(); - - enabled(&self.directives, level, target) - } -} - -impl fmt::Debug for Filter { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Filter") - .field("filter", &self.filter) - .field("directives", &self.directives) - .finish() - } -} - -#[cfg(test)] -mod tests { - use log::{Level, LevelFilter}; - - use super::{enabled, Builder, Directive, Filter}; - - fn make_logger_filter(dirs: Vec) -> Filter { - let mut logger = Builder::new().build(); - logger.directives = dirs; - logger - } - - #[test] - fn filter_info() { - let logger = Builder::new().filter(None, LevelFilter::Info).build(); - assert!(enabled(&logger.directives, Level::Info, "crate1")); - assert!(!enabled(&logger.directives, Level::Debug, "crate1")); - } - - #[test] - fn filter_beginning_longest_match() { - let logger = Builder::new() - .filter(Some("crate2"), LevelFilter::Info) - .filter(Some("crate2::mod"), LevelFilter::Debug) - .filter(Some("crate1::mod1"), LevelFilter::Warn) - .build(); - assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); - assert!(!enabled(&logger.directives, Level::Debug, "crate2")); - } - - // Some of our tests are only correct or complete when they cover the full - // universe of variants for log::Level. In the unlikely event that a new - // variant is added in the future, this test will detect the scenario and - // alert us to the need to review and update the tests. In such a - // situation, this test will fail to compile, and the error message will - // look something like this: - // - // error[E0004]: non-exhaustive patterns: `NewVariant` not covered - // --> src/filter/mod.rs:413:15 - // | - // 413 | match level_universe { - // | ^^^^^^^^^^^^^^ pattern `NewVariant` not covered - #[test] - fn ensure_tests_cover_level_universe() { - let level_universe: Level = Level::Trace; // use of trace variant is arbitrary - match level_universe { - Level::Error | Level::Warn | Level::Info | Level::Debug | Level::Trace => (), - } - } - - #[test] - fn parse_default() { - let logger = Builder::new().parse("info,crate1::mod1=warn").build(); - assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); - assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); - } - - #[test] - fn parse_default_bare_level_off_lc() { - let logger = Builder::new().parse("off").build(); - assert!(!enabled(&logger.directives, Level::Error, "")); - assert!(!enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_off_uc() { - let logger = Builder::new().parse("OFF").build(); - assert!(!enabled(&logger.directives, Level::Error, "")); - assert!(!enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_error_lc() { - let logger = Builder::new().parse("error").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(!enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_error_uc() { - let logger = Builder::new().parse("ERROR").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(!enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_warn_lc() { - let logger = Builder::new().parse("warn").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_warn_uc() { - let logger = Builder::new().parse("WARN").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(!enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_info_lc() { - let logger = Builder::new().parse("info").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_info_uc() { - let logger = Builder::new().parse("INFO").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(!enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_debug_lc() { - let logger = Builder::new().parse("debug").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_debug_uc() { - let logger = Builder::new().parse("DEBUG").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_trace_lc() { - let logger = Builder::new().parse("trace").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(enabled(&logger.directives, Level::Trace, "")); - } - - #[test] - fn parse_default_bare_level_trace_uc() { - let logger = Builder::new().parse("TRACE").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(enabled(&logger.directives, Level::Trace, "")); - } - - // In practice, the desired log level is typically specified by a token - // that is either all lowercase (e.g., 'trace') or all uppercase (.e.g, - // 'TRACE'), but this tests serves as a reminder that - // log::Level::from_str() ignores all case variants. - #[test] - fn parse_default_bare_level_debug_mixed() { - { - let logger = Builder::new().parse("Debug").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - { - let logger = Builder::new().parse("debuG").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - { - let logger = Builder::new().parse("deBug").build(); - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - { - let logger = Builder::new().parse("DeBuG").build(); // LaTeX flavor! - assert!(enabled(&logger.directives, Level::Error, "")); - assert!(enabled(&logger.directives, Level::Warn, "")); - assert!(enabled(&logger.directives, Level::Info, "")); - assert!(enabled(&logger.directives, Level::Debug, "")); - assert!(!enabled(&logger.directives, Level::Trace, "")); - } - } - - #[test] - fn match_full_path() { - let logger = make_logger_filter(vec![ - Directive { - name: Some("crate2".to_string()), - level: LevelFilter::Info, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Warn, - }, - ]); - assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); - assert!(!enabled(&logger.directives, Level::Info, "crate1::mod1")); - assert!(enabled(&logger.directives, Level::Info, "crate2")); - assert!(!enabled(&logger.directives, Level::Debug, "crate2")); - } - - #[test] - fn no_match() { - let logger = make_logger_filter(vec![ - Directive { - name: Some("crate2".to_string()), - level: LevelFilter::Info, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Warn, - }, - ]); - assert!(!enabled(&logger.directives, Level::Warn, "crate3")); - } - - #[test] - fn match_beginning() { - let logger = make_logger_filter(vec![ - Directive { - name: Some("crate2".to_string()), - level: LevelFilter::Info, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Warn, - }, - ]); - assert!(enabled(&logger.directives, Level::Info, "crate2::mod1")); - } - - #[test] - fn match_beginning_longest_match() { - let logger = make_logger_filter(vec![ - Directive { - name: Some("crate2".to_string()), - level: LevelFilter::Info, - }, - Directive { - name: Some("crate2::mod".to_string()), - level: LevelFilter::Debug, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Warn, - }, - ]); - assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); - assert!(!enabled(&logger.directives, Level::Debug, "crate2")); - } - - #[test] - fn match_default() { - let logger = make_logger_filter(vec![ - Directive { - name: None, - level: LevelFilter::Info, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Warn, - }, - ]); - assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); - assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); - } - - #[test] - fn zero_level() { - let logger = make_logger_filter(vec![ - Directive { - name: None, - level: LevelFilter::Info, - }, - Directive { - name: Some("crate1::mod1".to_string()), - level: LevelFilter::Off, - }, - ]); - assert!(!enabled(&logger.directives, Level::Error, "crate1::mod1")); - assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); - } -} +pub use filter::Builder; +pub use filter::Filter; From e6e2b633688a56a53ad718b3b498243cb3893d52 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:44:39 -0600 Subject: [PATCH 23/28] fix(log)!: Dont re-export env_filter This lets us shrink our public API --- examples/custom_logger.rs | 59 --------------------------------------- src/lib.rs | 2 -- src/logger.rs | 6 ++-- 3 files changed, 2 insertions(+), 65 deletions(-) delete mode 100644 examples/custom_logger.rs diff --git a/examples/custom_logger.rs b/examples/custom_logger.rs deleted file mode 100644 index e924faee..00000000 --- a/examples/custom_logger.rs +++ /dev/null @@ -1,59 +0,0 @@ -/*! -Using `env_logger` to drive a custom logger. - -Before running this example, try setting the `MY_LOG_LEVEL` environment variable to `info`: - -```no_run,shell -$ export MY_LOG_LEVEL='info' -``` - -If you only want to change the way logs are formatted, look at the `custom_format` example. -*/ - -use env_logger::filter::{Builder, Filter}; - -use log::{info, Log, Metadata, Record, SetLoggerError}; - -const FILTER_ENV: &str = "MY_LOG_LEVEL"; - -struct MyLogger { - inner: Filter, -} - -impl MyLogger { - fn new() -> MyLogger { - let mut builder = Builder::from_env(FILTER_ENV); - - MyLogger { - inner: builder.build(), - } - } - - fn init() -> Result<(), SetLoggerError> { - let logger = Self::new(); - - log::set_max_level(logger.inner.filter()); - log::set_boxed_logger(Box::new(logger)) - } -} - -impl Log for MyLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - self.inner.enabled(metadata) - } - - fn log(&self, record: &Record) { - // Check if the record is matched by the logger before logging - if self.inner.matches(record) { - println!("{} - {}", record.level(), record.args()); - } - } - - fn flush(&self) {} -} - -fn main() { - MyLogger::init().unwrap(); - - info!("a log from `MyLogger`"); -} diff --git a/src/lib.rs b/src/lib.rs index 76cbf3a5..26f25b85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,8 +277,6 @@ mod logger; -#[doc(inline)] -pub use ::env_filter as filter; pub mod fmt; pub use self::fmt::{Target, TimestampPrecision, WriteStyle}; diff --git a/src/logger.rs b/src/logger.rs index 1e7d5894..18bfe9fa 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -2,8 +2,6 @@ use std::{borrow::Cow, cell::RefCell, env, io}; use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; -use crate::filter; -use crate::filter::Filter; use crate::fmt; use crate::fmt::writer::{self, Writer}; use crate::fmt::{FormatFn, Formatter}; @@ -38,7 +36,7 @@ pub const DEFAULT_WRITE_STYLE_ENV: &str = "RUST_LOG_STYLE"; /// ``` #[derive(Default)] pub struct Builder { - filter: filter::Builder, + filter: env_filter::Builder, writer: writer::Builder, format: fmt::Builder, built: bool, @@ -532,7 +530,7 @@ impl std::fmt::Debug for Builder { /// [`Builder`]: struct.Builder.html pub struct Logger { writer: Writer, - filter: Filter, + filter: env_filter::Filter, format: FormatFn, } From 2d3526001061bacbf4a4c47767a318986c2c61b0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:48:20 -0600 Subject: [PATCH 24/28] feat(filter): Add a Logger decorator --- crates/env_filter/src/filtered_log.rs | 45 +++++++++++++++++++++++++++ crates/env_filter/src/lib.rs | 39 +++++++++-------------- 2 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 crates/env_filter/src/filtered_log.rs diff --git a/crates/env_filter/src/filtered_log.rs b/crates/env_filter/src/filtered_log.rs new file mode 100644 index 00000000..f707bcc3 --- /dev/null +++ b/crates/env_filter/src/filtered_log.rs @@ -0,0 +1,45 @@ +use log::Log; + +use crate::Filter; + +/// Decorate a [`log::Log`] with record [`Filter`]ing. +/// +/// Records that match the filter will be forwarded to the wrapped log. +/// Other records will be ignored. +#[derive(Debug)] +pub struct FilteredLog { + log: T, + filter: Filter, +} + +impl FilteredLog { + /// Create a new filtered log. + pub fn new(log: T, filter: Filter) -> Self { + Self { log, filter } + } +} + +impl Log for FilteredLog { + /// Determines if a log message with the specified metadata would be logged. + /// + /// For the wrapped log, this returns `true` only if both the filter and the wrapped log return `true`. + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.filter.enabled(metadata) && self.log.enabled(metadata) + } + + /// Logs the record. + /// + /// Forwards the record to the wrapped log, but only if the record matches the filter. + fn log(&self, record: &log::Record) { + if self.filter.matches(record) { + self.log.log(record) + } + } + + /// Flushes any buffered records. + /// + /// Forwards directly to the wrapped log. + fn flush(&self) { + self.log.flush() + } +} diff --git a/crates/env_filter/src/lib.rs b/crates/env_filter/src/lib.rs index 494b82da..dad06817 100644 --- a/crates/env_filter/src/lib.rs +++ b/crates/env_filter/src/lib.rs @@ -14,44 +14,32 @@ //! use env_filter::Filter; //! use log::{Log, Metadata, Record}; //! -//! struct MyLogger { -//! filter: Filter -//! } -//! -//! impl MyLogger { -//! fn new() -> MyLogger { -//! use env_filter::Builder; -//! let mut builder = Builder::new(); -//! -//! // Parse a directives string from an environment variable -//! if let Ok(ref filter) = std::env::var("MY_LOG_LEVEL") { -//! builder.parse(filter); -//! } +//! struct PrintLogger; //! -//! MyLogger { -//! filter: builder.build() -//! } -//! } -//! } -//! -//! impl Log for MyLogger { +//! impl Log for PrintLogger { //! fn enabled(&self, metadata: &Metadata) -> bool { -//! self.filter.enabled(metadata) +//! true //! } //! //! fn log(&self, record: &Record) { -//! // Check if the record is matched by the filter -//! if self.filter.matches(record) { -//! println!("{:?}", record); -//! } +//! println!("{:?}", record); //! } //! //! fn flush(&self) {} //! } +//! +//! let mut builder = env_filter::Builder::new(); +//! // Parse a directives string from an environment variable +//! if let Ok(ref filter) = std::env::var("MY_LOG_LEVEL") { +//! builder.parse(filter); +//! } +//! +//! let logger = env_filter::FilteredLog::new(PrintLogger, builder.build()); //! ``` mod directive; mod filter; +mod filtered_log; mod op; mod parser; @@ -62,3 +50,4 @@ use parser::parse_spec; pub use filter::Builder; pub use filter::Filter; +pub use filtered_log::FilteredLog; From 6c2ea8028236fe80c1da0a354b19808bf440858d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:50:52 -0600 Subject: [PATCH 25/28] style(filter): Clean up --- crates/env_filter/src/filter.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/env_filter/src/filter.rs b/crates/env_filter/src/filter.rs index 7a052720..c5d107da 100644 --- a/crates/env_filter/src/filter.rs +++ b/crates/env_filter/src/filter.rs @@ -5,9 +5,9 @@ use std::mem; use log::{LevelFilter, Metadata, Record}; use crate::enabled; +use crate::parse_spec; use crate::Directive; use crate::FilterOp; -use crate::parse_spec; /// A builder for a log filter. /// From 5e226cb2b73d6c9f1b21886a4b504afdea1ebfcf Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 11:59:56 -0600 Subject: [PATCH 26/28] chore: Release --- crates/env_filter/CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/env_filter/CHANGELOG.md b/crates/env_filter/CHANGELOG.md index 78e2e120..b9a276ca 100644 --- a/crates/env_filter/CHANGELOG.md +++ b/crates/env_filter/CHANGELOG.md @@ -7,5 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [0.1.0] - 2024-01-19 + -[Unreleased]: https://github.com/rust-cli/env_logger/compare/b4a2c304c16d1db4a2998f24c00e00c0f776113b...HEAD +[Unreleased]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.0...HEAD +[0.1.0]: https://github.com/rust-cli/env_logger/compare/b4a2c304c16d1db4a2998f24c00e00c0f776113b...env_filter-v0.1.0 From ba41ebb6d2d726403560cd987b1c5b3c6797f817 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 12:07:14 -0600 Subject: [PATCH 27/28] docs: Update changelog --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b911e5e..93b9369e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +### Breaking Change + +- Removed bespoke styling API + - `env_logger::fmt::Formatter::style` + - `env_logger::fmt::Formatter::default_styled_level` + - `env_logger::fmt::Style` + - `env_logger::fmt::Color` + - `env_logger::fmt::StyledValue` +- Removed `env_logger::filter` in favor of `env_filter` + +### Compatibility + +MSRV changed to 1.71 + +### Features + +- Automatically adapt ANSI escape codes in logged messages to the current terminal's capabilities +- Add support for `NO_COLOR` and `CLICOLOR_FORCE`, see https://bixense.com/clicolors/ + +### Fixes + +- Print colors when `is_test(true)` +- Allow styling with `Target::Pipe` + ## [0.10.2] - 2024-01-18 ### Performance From 8f4361ba4439acb69068be0e181d2d1300b7218d Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Jan 2024 12:08:36 -0600 Subject: [PATCH 28/28] chore: Release --- CHANGELOG.md | 5 ++++- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b9369e..e3f20a27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [0.11.0] - 2024-01-19 + ### Breaking Change - Removed bespoke styling API @@ -85,7 +87,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.10.2...HEAD +[Unreleased]: https://github.com/rust-cli/env_logger/compare/v0.11.0...HEAD +[0.11.0]: https://github.com/rust-cli/env_logger/compare/v0.10.2...v0.11.0 [0.10.2]: https://github.com/rust-cli/env_logger/compare/v0.10.1...v0.10.2 [0.10.1]: https://github.com/rust-cli/env_logger/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/rust-cli/env_logger/compare/v0.9.3...v0.10.0 diff --git a/Cargo.lock b/Cargo.lock index 776d3400..204d2c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,7 +81,7 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.10.2" +version = "0.11.0" dependencies = [ "anstream", "anstyle", diff --git a/Cargo.toml b/Cargo.toml index b669cef6..224c6659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ include = [ [package] name = "env_logger" -version = "0.10.2" +version = "0.11.0" description = """ A logging implementation for `log` which is configured via an environment variable. 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