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.