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/renovate.json5 b/.github/renovate.json5 index 00e7caf0..3fdb4a41 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -29,7 +29,7 @@ { commitMessageTopic: 'MSRV', matchManagers: [ - 'regex', + 'custom.regex', ], matchPackageNames: [ 'rust', diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ecff712..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 @@ -130,7 +130,7 @@ jobs: | sarif-fmt continue-on-error: true - name: Upload - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: clippy-results.sarif wait-for-processing: true diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 80447507..95514075 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -19,5 +19,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d9e40fd..68db968e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-yaml stages: [commit] @@ -15,7 +15,7 @@ repos: - id: detect-private-key stages: [commit] - repo: https://github.com/crate-ci/typos - rev: v1.16.3 + rev: v1.16.20 hooks: - id: typos stages: [commit] diff --git a/CHANGELOG.md b/CHANGELOG.md index cb73af76..c6b4ef76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,58 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - ReleaseDate +## [0.11.1] - 2024-01-27 + +### Fixes + +- Allow styling with `Target::Pipe` + +## [0.11.0] - 2024-01-19 + +### 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)` + +## [0.10.2] - 2024-01-18 + +### Performance + +- Avoid extra UTF-8 validation performed in some cases + +### Fixes + +- Ensure custom pipes/stdout get flushed +- Don't panic on broken pipes when `color` is disabled + ## [0.10.1] - 2023-11-10 +### Performance + +- Avoid hashing directives and accessing RNG on startup + +### Documentation + +- Tweak `RUST_LOG` documentation + ## [0.10.0] - 2022-11-24 MSRV changed to 1.60 to hide optional dependencies @@ -42,7 +92,10 @@ 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.1...HEAD +[Unreleased]: https://github.com/rust-cli/env_logger/compare/v0.11.1...HEAD +[0.11.1]: https://github.com/rust-cli/env_logger/compare/v0.11.0...v0.11.1 +[0.11.0]: https://github.com/rust-cli/env_logger/compare/v0.10.2...v0.11.0 +[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 [0.9.3]: https://github.com/rust-cli/env_logger/compare/v0.9.2...v0.9.3 diff --git a/Cargo.lock b/Cargo.lock index 2bb97a6a..bb0de810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,103 +12,89 @@ dependencies = [ ] [[package]] -name = "bitflags" -version = "1.3.2" +name = "anstream" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] [[package]] -name = "cc" -version = "1.0.77" +name = "anstyle" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "anstyle-parse" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "env_logger" -version = "0.10.1" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", + "utf8parse", ] [[package]] -name = "errno" -version = "0.2.8" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "errno-dragonfly", - "libc", - "winapi", + "windows-sys", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "anstyle-wincon" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ - "cc", - "libc", + "anstyle", + "windows-sys", ] [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "humantime" -version = "2.1.0" +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] -name = "io-lifetimes" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7d367024b3f3414d8e01f437f704f41a9f64ab36f9067fa73e526ad4c763c87" +name = "env_filter" +version = "0.1.0" dependencies = [ - "libc", - "windows-sys", + "log", + "regex", ] [[package]] -name = "is-terminal" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae5bc6e2eb41c9def29a3e0f1306382807764b9b53112030eff57435667352d" +name = "env_logger" +version = "0.11.1" dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys", + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[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" +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "log" @@ -143,64 +129,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] -name = "rustix" -version = "0.36.3" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1fbb4dfc4eb1d390c02df47760bb19a84bb80b301ecc947ab5406394d8223e" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys", -] +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[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" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "winapi", + "windows-targets", ] [[package]] -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" +name = "windows-targets" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -213,42 +160,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/Cargo.toml b/Cargo.toml index 7a1ae457..97ffdd69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,11 @@ -[package] -name = "env_logger" -version = "0.10.1" -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" +members = ["crates/*"] + +[workspace.package] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.60.0" # MSRV +rust-version = "1.71" # MSRV include = [ "build.rs", "src/**/*", @@ -23,6 +18,21 @@ include = [ "tests/**/*", ] +[package] +name = "env_logger" +version = "0.11.1" +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"] @@ -38,17 +48,17 @@ pre-release-replacements = [ [features] default = ["auto-color", "humantime", "regex"] -color = ["dep:termcolor"] -auto-color = ["dep:is-terminal", "color"] +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"] } -termcolor = { version = "1.1.1", optional = true } +env_filter = { version = "0.1.0", path = "crates/env_filter", default-features = false } humantime = { version = "2.0.0", optional = true } -is-terminal = { version = "0.4.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/crates/env_filter/CHANGELOG.md b/crates/env_filter/CHANGELOG.md new file mode 100644 index 00000000..b9a276ca --- /dev/null +++ b/crates/env_filter/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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 + +## [0.1.0] - 2024-01-19 + + +[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 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/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/src/filter/mod.rs b/crates/env_filter/src/filter.rs similarity index 60% rename from src/filter/mod.rs rename to crates/env_filter/src/filter.rs index 81bb017a..c5d107da 100644 --- a/src/filter/mod.rs +++ b/crates/env_filter/src/filter.rs @@ -1,87 +1,13 @@ -//! 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]. -//! -//! ## Using `env_logger` in your own logger -//! -//! You can use `env_logger`'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 log::{Log, Metadata, Record}; -//! -//! struct MyLogger { -//! filter: Filter -//! } -//! -//! impl MyLogger { -//! fn new() -> MyLogger { -//! use env_logger::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); -//! } -//! -//! MyLogger { -//! filter: builder.build() -//! } -//! } -//! } -//! -//! impl Log for MyLogger { -//! fn enabled(&self, metadata: &Metadata) -> bool { -//! self.filter.enabled(metadata) -//! } -//! -//! fn log(&self, record: &Record) { -//! // Check if the record is matched by the filter -//! if self.filter.matches(record) { -//! println!("{:?}", record); -//! } -//! } -//! -//! 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; use std::fmt; use std::mem; -#[cfg(feature = "regex")] -#[path = "regex.rs"] -mod inner; +use log::{LevelFilter, Metadata, Record}; -#[cfg(not(feature = "regex"))] -#[path = "string.rs"] -mod inner; - -/// 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, -} +use crate::enabled; +use crate::parse_spec; +use crate::Directive; +use crate::FilterOp; /// A builder for a log filter. /// @@ -91,9 +17,8 @@ pub struct Filter { /// ## Example /// /// ``` -/// # #[macro_use] extern crate log; /// # use std::env; -/// use env_logger::filter::Builder; +/// use env_filter::Builder; /// /// let mut builder = Builder::new(); /// @@ -104,69 +29,12 @@ pub struct Filter { /// /// let filter = builder.build(); /// ``` -/// -/// [`Filter`]: struct.Filter.html pub struct Builder { directives: Vec, - filter: Option, + filter: Option, built: bool, } -#[derive(Debug)] -struct Directive { - name: Option, - level: LevelFilter, -} - -impl Filter { - /// Returns the maximum `LevelFilter` that this filter instance is - /// configured to output. - /// - /// # Example - /// - /// ```rust - /// use log::LevelFilter; - /// use env_logger::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 Builder { /// Initializes the filter builder with defaults. pub fn new() -> Builder { @@ -265,7 +133,7 @@ impl Builder { Filter { directives: mem::take(&mut directives), - filter: mem::replace(&mut self.filter, None), + filter: mem::take(&mut self.filter), } } } @@ -276,15 +144,6 @@ impl Default for Builder { } } -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() - } -} - impl fmt::Debug for Builder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.built { @@ -298,94 +157,81 @@ impl fmt::Debug for Builder { } } -/// 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; +/// 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; } - 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, - }); } + + true } - let filter = filter.and_then(|filter| match inner::Filter::new(filter) { - Ok(re) => Some(re), - Err(e) => { - eprintln!("warning: invalid regex filter - {}", e); - None - } - }); + /// 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(); - (dirs, filter) + enabled(&self.directives, level, target) + } } -// 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, - } +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() } - false } #[cfg(test)] 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(); @@ -697,183 +543,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/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 new file mode 100644 index 00000000..dad06817 --- /dev/null +++ b/crates/env_filter/src/lib.rs @@ -0,0 +1,53 @@ +//! Filtering for log records. +//! +//! You can use the [`Filter`] type in your own logger implementation to use the same +//! filter parsing and matching as `env_logger`. +//! +//! ## Using `env_filter` in 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. +//! +//! ``` +//! use env_filter::Filter; +//! use log::{Log, Metadata, Record}; +//! +//! struct PrintLogger; +//! +//! impl Log for PrintLogger { +//! fn enabled(&self, metadata: &Metadata) -> bool { +//! true +//! } +//! +//! fn log(&self, record: &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; + +use directive::enabled; +use directive::Directive; +use op::FilterOp; +use parser::parse_spec; + +pub use filter::Builder; +pub use filter::Filter; +pub use filtered_log::FilteredLog; 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/parser.rs b/crates/env_filter/src/parser.rs new file mode 100644 index 00000000..2d0f90b0 --- /dev/null +++ b/crates/env_filter/src/parser.rs @@ -0,0 +1,261 @@ +use log::LevelFilter; + +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) { + 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 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"); + } +} diff --git a/examples/custom_format.rs b/examples/custom_format.rs index cc16b336..8f575fd4 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,16 +30,17 @@ fn main() { Builder::from_env(env) .format(|buf, record| { - let mut style = buf.style(); - style.set_bg(Color::Yellow).set_bold(true); - + // 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, - style.value(record.args()) + "My formatted log ({timestamp}): {warn_style}{}{reset}", + record.args() ) }) .init(); 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/filter/regex.rs b/src/filter/regex.rs deleted file mode 100644 index fb21528a..00000000 --- a/src/filter/regex.rs +++ /dev/null @@ -1,29 +0,0 @@ -extern crate regex; - -use std::fmt; - -use self::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/src/filter/string.rs b/src/filter/string.rs deleted file mode 100644 index ea476e42..00000000 --- a/src/filter/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) - } -} diff --git a/src/fmt/humantime/extern_impl.rs b/src/fmt/humantime.rs similarity index 98% rename from src/fmt/humantime/extern_impl.rs rename to src/fmt/humantime.rs index bdf165c4..582a6318 100644 --- a/src/fmt/humantime/extern_impl.rs +++ b/src/fmt/humantime.rs @@ -7,10 +7,6 @@ use humantime::{ use crate::fmt::{Formatter, TimestampPrecision}; -pub(in crate::fmt) mod glob { - pub use super::*; -} - impl Formatter { /// Get a [`Timestamp`] for the current date and time in UTC. /// diff --git a/src/fmt/humantime/mod.rs b/src/fmt/humantime/mod.rs deleted file mode 100644 index ac23ae24..00000000 --- a/src/fmt/humantime/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -/* -This internal module contains the timestamp implementation. - -Its public API is available when the `humantime` crate is available. -*/ - -#[cfg_attr(feature = "humantime", path = "extern_impl.rs")] -#[cfg_attr(not(feature = "humantime"), path = "shim_impl.rs")] -mod imp; - -pub(in crate::fmt) use self::imp::*; diff --git a/src/fmt/humantime/shim_impl.rs b/src/fmt/humantime/shim_impl.rs deleted file mode 100644 index 906bf9e4..00000000 --- a/src/fmt/humantime/shim_impl.rs +++ /dev/null @@ -1,5 +0,0 @@ -/* -Timestamps aren't available when we don't have a `humantime` dependency. -*/ - -pub(in crate::fmt) mod glob {} diff --git a/src/fmt/mod.rs b/src/fmt/mod.rs index 86c093f0..faee69bb 100644 --- a/src/fmt/mod.rs +++ b/src/fmt/mod.rs @@ -35,19 +35,23 @@ 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; -pub use self::humantime::glob::*; -pub use self::writer::glob::*; +#[cfg(feature = "color")] +pub use anstyle as style; -use self::writer::{Buffer, Writer}; +#[cfg(feature = "humantime")] +pub use self::humantime::Timestamp; +pub use self::writer::Target; +pub use self::writer::WriteStyle; -pub(crate) mod glob { - pub use super::{Target, TimestampPrecision, WriteStyle}; -} +use self::writer::{Buffer, Writer}; /// Formatting precision of timestamps. /// @@ -120,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) @@ -132,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() } } @@ -149,21 +179,6 @@ pub(crate) struct Builder { built: bool, } -impl Default for Builder { - fn default() -> Self { - Builder { - format_timestamp: Some(Default::default()), - format_module_path: false, - format_target: true, - format_level: true, - format_indent: Some(4), - custom_format: None, - format_suffix: "\n", - built: false, - } - } -} - impl Builder { /// Convert the format into a callable function. /// @@ -202,11 +217,52 @@ impl Builder { } } +impl Default for Builder { + fn default() -> Self { + Builder { + format_timestamp: Some(Default::default()), + format_module_path: false, + format_target: true, + format_level: true, + format_indent: Some(4), + custom_format: None, + format_suffix: "\n", + built: false, + } + } +} + #[cfg(feature = "color")] -type SubtleStyle = StyledValue<'static, &'static str>; +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. @@ -235,12 +291,14 @@ 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) + StyledValue { + style: if self.buf.write_style == WriteStyle::Never { + style::Style::new() + } else { + style::AnsiColor::BrightBlack.on_default() + }, + value: text, + } } #[cfg(not(feature = "color"))] { @@ -268,13 +326,17 @@ impl<'a> DefaultFormat<'a> { } let level = { + let level = record.level(); #[cfg(feature = "color")] { - self.buf.default_styled_level(record.level()) + StyledValue { + style: self.buf.default_level_style(level), + value: level, + } } #[cfg(not(feature = "color"))] { - record.level() + level } }; @@ -403,7 +465,7 @@ mod tests { fmt.write(&record).expect("failed to write record"); let buf = buf.borrow(); - String::from_utf8(buf.bytes().to_vec()).expect("failed to read record") + String::from_utf8(buf.as_bytes().to_vec()).expect("failed to read record") } fn write_target(target: &str, fmt: DefaultFormat) -> String { diff --git a/src/fmt/writer/atty.rs b/src/fmt/writer/atty.rs deleted file mode 100644 index 1a133eef..00000000 --- a/src/fmt/writer/atty.rs +++ /dev/null @@ -1,33 +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. -*/ - -#[cfg(feature = "auto-color")] -mod imp { - use is_terminal::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() - } -} - -#[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) use self::imp::*; diff --git a/src/fmt/writer/buffer.rs b/src/fmt/writer/buffer.rs new file mode 100644 index 00000000..b720a988 --- /dev/null +++ b/src/fmt/writer/buffer.rs @@ -0,0 +1,172 @@ +use std::{io, sync::Mutex}; + +use crate::fmt::writer::WriteStyle; + +#[derive(Debug)] +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 { + 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 { + BufferWriter { + target: if is_test { + WritableTarget::PrintStdout + } else { + WritableTarget::WriteStdout + }, + write_style, + } + } + + pub(in crate::fmt::writer) fn pipe( + pipe: Box>, + write_style: WriteStyle, + ) -> Self { + BufferWriter { + target: 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(Vec::new()) + } + + pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { + use std::io::Write as _; + + let buf = buf.as_bytes(); + 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 => { + #[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 => { + #[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) => { + #[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()?; + } + } + + Ok(()) + } +} + +#[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::AutoStream::new(adapted, write_style.into()); + stream.write_all(buf)?; + let adapted = stream.into_inner(); + Ok(adapted) +} + +pub(in crate::fmt) struct Buffer(Vec); + +impl Buffer { + pub(in crate::fmt) fn clear(&mut self) { + self.0.clear(); + } + + pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.extend(buf); + Ok(buf.len()) + } + + pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + + pub(in crate::fmt) fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +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. +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 7f4b6f94..688fda16 100644 --- a/src/fmt/writer/mod.rs +++ b/src/fmt/writer/mod.rs @@ -1,93 +1,18 @@ -mod atty; -mod termcolor; +mod buffer; +mod target; -use self::atty::{is_stderr, is_stdout}; -use self::termcolor::BufferWriter; -use std::{fmt, io, mem, sync::Mutex}; +use self::buffer::BufferWriter; +use std::{io, mem, sync::Mutex}; -pub(super) mod glob { - pub use super::termcolor::glob::*; - pub use super::*; -} - -pub(super) use self::termcolor::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 - } -} +pub(super) use self::buffer::Buffer; -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", - } - ) - } -} +pub use target::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 sent to standard output. - Stdout, - /// Logs will be sent to standard error. - Stderr, - /// Logs will be sent to a custom pipe. - Pipe(Box>), -} - -impl From for WritableTarget { - fn from(target: Target) -> Self { - match target { - Target::Stdout => Self::Stdout, - Target::Stderr => Self::Stderr, - Target::Pipe(pipe) => Self::Pipe(Box::new(Mutex::new(pipe))), - } - } -} - -impl Default for WritableTarget { - fn default() -> Self { - Self::from(Target::default()) - } -} - -impl fmt::Debug for WritableTarget { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Stdout => "stdout", - Self::Stderr => "stderr", - Self::Pipe(_) => "pipe", - } - ) - } -} /// Whether or not to print styles to the target. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)] pub enum WriteStyle { /// Try to print styles, but don't force the issue. + #[default] Auto, /// Try very hard to print styles. Always, @@ -95,21 +20,38 @@ pub enum WriteStyle { Never, } -impl Default for WriteStyle { - fn default() -> Self { - WriteStyle::Auto +#[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. +#[derive(Debug)] pub(crate) struct Writer { inner: BufferWriter, - write_style: WriteStyle, } impl Writer { pub fn write_style(&self) -> WriteStyle { - self.write_style + self.inner.write_style() } pub(super) fn buffer(&self) -> Buffer { @@ -126,7 +68,7 @@ impl Writer { /// The target and style choice can be configured before building. #[derive(Debug)] pub(crate) struct Builder { - target: WritableTarget, + target: Target, write_style: WriteStyle, is_test: bool, built: bool, @@ -145,7 +87,7 @@ impl Builder { /// Set the target to write to. pub(crate) fn target(&mut self, target: Target) -> &mut Self { - self.target = target.into(); + self.target = target; self } @@ -176,31 +118,30 @@ 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 { - WritableTarget::Stderr => is_stderr(), - WritableTarget::Stdout => is_stdout(), - WritableTarget::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(_) => color_choice, } - color_choice => color_choice, + } else { + color_choice + }; + let color_choice = if color_choice == WriteStyle::Auto { + WriteStyle::Never + } else { + color_choice }; let writer = match mem::take(&mut self.target) { - WritableTarget::Stderr => BufferWriter::stderr(self.is_test, color_choice), - WritableTarget::Stdout => BufferWriter::stdout(self.is_test, color_choice), - WritableTarget::Pipe(pipe) => BufferWriter::pipe(color_choice, pipe), + 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)), color_choice), }; - Writer { - inner: writer, - write_style: self.write_style, - } + Writer { inner: writer } } } @@ -210,12 +151,6 @@ impl Default for Builder { } } -impl fmt::Debug for Writer { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Writer").finish() - } -} - fn parse_write_style(spec: &str) -> WriteStyle { match spec { "auto" => WriteStyle::Auto, diff --git a/src/fmt/writer/target.rs b/src/fmt/writer/target.rs new file mode 100644 index 00000000..a1220ffe --- /dev/null +++ b/src/fmt/writer/target.rs @@ -0,0 +1,26 @@ +/// 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 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", + } + ) + } +} diff --git a/src/fmt/writer/termcolor/extern_impl.rs b/src/fmt/writer/termcolor/extern_impl.rs deleted file mode 100644 index 89c38223..00000000 --- a/src/fmt/writer/termcolor/extern_impl.rs +++ /dev/null @@ -1,532 +0,0 @@ -use std::borrow::Cow; -use std::cell::RefCell; -use std::fmt; -use std::io::{self, Write}; -use std::rc::Rc; -use std::sync::Mutex; - -use log::Level; -use termcolor::{self, ColorChoice, ColorSpec, WriteColor}; - -use crate::fmt::{Formatter, WritableTarget, WriteStyle}; - -pub(in crate::fmt::writer) mod glob { - pub use super::*; -} - -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: 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) - } -} - -pub(in crate::fmt::writer) struct BufferWriter { - inner: termcolor::BufferWriter, - uncolored_target: Option, -} - -pub(in crate::fmt) struct Buffer { - inner: termcolor::Buffer, - has_uncolored_target: bool, -} - -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::Stderr) - } else { - None - }, - } - } - - 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::Stdout) - } else { - None - }, - } - } - - pub(in crate::fmt::writer) fn pipe( - write_style: WriteStyle, - pipe: Box>, - ) -> Self { - 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)), - } - } - - 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 { - // This impl uses the `eprint` and `print` macros - // instead of `termcolor`'s buffer. - // This is so their output can be captured by `cargo test` - let log = String::from_utf8_lossy(buf.bytes()); - - match target { - WritableTarget::Stderr => eprint!("{}", log), - WritableTarget::Stdout => print!("{}", log), - WritableTarget::Pipe(pipe) => write!(pipe.lock().unwrap(), "{}", log)?, - } - - Ok(()) - } else { - self.inner.print(&buf.inner) - } - } -} - -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 bytes(&self) -> &[u8] { - self.inner.as_slice() - } - - 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(()) - } - } - - 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(()) - } - } -} - -impl WriteStyle { - fn into_color_choice(self) -> ColorChoice { - match self { - WriteStyle::Always => ColorChoice::Always, - WriteStyle::Auto => ColorChoice::Auto, - WriteStyle::Never => ColorChoice::Never, - } - } -} - -/// 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 { - buf: Rc>, - spec: ColorSpec, -} - -/// 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 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<'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) - } -} - -impl fmt::Debug for Style { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Style").field("spec", &self.spec).finish() - } -} - -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/termcolor/mod.rs b/src/fmt/writer/termcolor/mod.rs deleted file mode 100644 index 20f01979..00000000 --- a/src/fmt/writer/termcolor/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -/* -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_attr(feature = "color", path = "extern_impl.rs")] -#[cfg_attr(not(feature = "color"), path = "shim_impl.rs")] -mod imp; - -pub(in crate::fmt) use self::imp::*; diff --git a/src/fmt/writer/termcolor/shim_impl.rs b/src/fmt/writer/termcolor/shim_impl.rs deleted file mode 100644 index 0705770c..00000000 --- a/src/fmt/writer/termcolor/shim_impl.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::{io, sync::Mutex}; - -use crate::fmt::{WritableTarget, WriteStyle}; - -pub(in crate::fmt::writer) mod glob {} - -pub(in crate::fmt::writer) struct BufferWriter { - target: WritableTarget, -} - -pub(in crate::fmt) struct Buffer(Vec); - -impl BufferWriter { - pub(in crate::fmt::writer) fn stderr(_is_test: bool, _write_style: WriteStyle) -> Self { - BufferWriter { - target: WritableTarget::Stderr, - } - } - - pub(in crate::fmt::writer) fn stdout(_is_test: bool, _write_style: WriteStyle) -> Self { - BufferWriter { - target: WritableTarget::Stdout, - } - } - - pub(in crate::fmt::writer) fn pipe( - _write_style: WriteStyle, - pipe: Box>, - ) -> Self { - BufferWriter { - target: WritableTarget::Pipe(pipe), - } - } - - pub(in crate::fmt::writer) fn buffer(&self) -> Buffer { - Buffer(Vec::new()) - } - - pub(in crate::fmt::writer) fn print(&self, buf: &Buffer) -> io::Result<()> { - // This impl uses the `eprint` and `print` macros - // instead of using the streams directly. - // This is so their output can be captured by `cargo test`. - match &self.target { - // Safety: If the target type is `Pipe`, `target_pipe` will always be non-empty. - WritableTarget::Pipe(pipe) => pipe.lock().unwrap().write_all(&buf.0)?, - WritableTarget::Stdout => print!("{}", String::from_utf8_lossy(&buf.0)), - WritableTarget::Stderr => eprint!("{}", String::from_utf8_lossy(&buf.0)), - } - - Ok(()) - } -} - -impl Buffer { - pub(in crate::fmt) fn clear(&mut self) { - self.0.clear(); - } - - pub(in crate::fmt) fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend(buf); - Ok(buf.len()) - } - - pub(in crate::fmt) fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - - #[cfg(test)] - pub(in crate::fmt) fn bytes(&self) -> &[u8] { - &self.0 - } -} diff --git a/src/lib.rs b/src/lib.rs index 4e5d5428..26f25b85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,7 +229,7 @@ //! ### Using a custom format //! //! Custom formats can be provided as closures to the [`Builder`]. -//! These closures take a [`Formatter`] and `log::Record` as arguments: +//! These closures take a [`Formatter`][crate::fmt::Formatter] and `log::Record` as arguments: //! //! ``` //! use std::io::Write; @@ -275,1027 +275,9 @@ #![cfg_attr(rustbuild, unstable(feature = "rustc_private", issue = "27812"))] #![deny(missing_debug_implementations, missing_docs)] -use std::{borrow::Cow, cell::RefCell, env, io}; +mod logger; -use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; - -pub mod filter; pub mod fmt; -pub use self::fmt::glob::*; - -use self::filter::Filter; -use self::fmt::writer::{self, Writer}; -use self::fmt::{FormatFn, Formatter}; - -/// The default name for the environment variable to read filters from. -pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; - -/// The default name for the environment variable to read style preferences from. -pub const DEFAULT_WRITE_STYLE_ENV: &str = "RUST_LOG_STYLE"; - -/// Set of environment variables to configure from. -/// -/// # Default environment variables -/// -/// By default, the `Env` will read the following environment variables: -/// -/// - `RUST_LOG`: the level filter -/// - `RUST_LOG_STYLE`: whether or not to print styles with records. -/// -/// These sources can be configured using the builder methods on `Env`. -#[derive(Debug)] -pub struct Env<'a> { - filter: Var<'a>, - write_style: Var<'a>, -} - -#[derive(Debug)] -struct Var<'a> { - name: Cow<'a, str>, - default: Option>, -} - -/// The env logger. -/// -/// This struct implements the `Log` trait from the [`log` crate][log-crate-url], -/// which allows it to act as a logger. -/// -/// The [`init()`], [`try_init()`], [`Builder::init()`] and [`Builder::try_init()`] -/// methods will each construct a `Logger` and immediately initialize it as the -/// default global logger. -/// -/// If you'd instead need access to the constructed `Logger`, you can use -/// the associated [`Builder`] and install it with the -/// [`log` crate][log-crate-url] directly. -/// -/// [log-crate-url]: https://docs.rs/log -/// [`init()`]: fn.init.html -/// [`try_init()`]: fn.try_init.html -/// [`Builder::init()`]: struct.Builder.html#method.init -/// [`Builder::try_init()`]: struct.Builder.html#method.try_init -/// [`Builder`]: struct.Builder.html -pub struct Logger { - writer: Writer, - filter: Filter, - format: FormatFn, -} - -/// `Builder` acts as builder for initializing a `Logger`. -/// -/// It can be used to customize the log format, change the environment variable used -/// to provide the logging directives and also set the default log level filter. -/// -/// # Examples -/// -/// ``` -/// # use std::io::Write; -/// use env_logger::Builder; -/// use log::{LevelFilter, error, info}; -/// -/// let mut builder = Builder::from_default_env(); -/// -/// builder -/// .format(|buf, record| writeln!(buf, "{} - {}", record.level(), record.args())) -/// .filter(None, LevelFilter::Info) -/// .init(); -/// -/// error!("error message"); -/// info!("info message"); -/// ``` -#[derive(Default)] -pub struct Builder { - filter: filter::Builder, - writer: writer::Builder, - format: fmt::Builder, - built: bool, -} - -impl Builder { - /// Initializes the log builder with defaults. - /// - /// **NOTE:** This method won't read from any environment variables. - /// Use the [`filter`] and [`write_style`] methods to configure the builder - /// or use [`from_env`] or [`from_default_env`] instead. - /// - /// # Examples - /// - /// Create a new builder and configure filters and style: - /// - /// ``` - /// use log::LevelFilter; - /// use env_logger::{Builder, WriteStyle}; - /// - /// let mut builder = Builder::new(); - /// - /// builder - /// .filter(None, LevelFilter::Info) - /// .write_style(WriteStyle::Always) - /// .init(); - /// ``` - /// - /// [`filter`]: #method.filter - /// [`write_style`]: #method.write_style - /// [`from_env`]: #method.from_env - /// [`from_default_env`]: #method.from_default_env - pub fn new() -> Builder { - Default::default() - } - - /// Initializes the log builder from the environment. - /// - /// The variables used to read configuration from can be tweaked before - /// passing in. - /// - /// # Examples - /// - /// Initialise a logger reading the log filter from an environment variable - /// called `MY_LOG`: - /// - /// ``` - /// use env_logger::Builder; - /// - /// let mut builder = Builder::from_env("MY_LOG"); - /// builder.init(); - /// ``` - /// - /// Initialise a logger using the `MY_LOG` variable for filtering and - /// `MY_LOG_STYLE` for whether or not to write styles: - /// - /// ``` - /// use env_logger::{Builder, Env}; - /// - /// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); - /// - /// let mut builder = Builder::from_env(env); - /// builder.init(); - /// ``` - pub fn from_env<'a, E>(env: E) -> Self - where - E: Into>, - { - let mut builder = Builder::new(); - builder.parse_env(env); - builder - } - - /// Applies the configuration from the environment. - /// - /// This function allows a builder to be configured with default parameters, - /// to be then overridden by the environment. - /// - /// # Examples - /// - /// Initialise a logger with filter level `Off`, then override the log - /// filter from an environment variable called `MY_LOG`: - /// - /// ``` - /// use log::LevelFilter; - /// use env_logger::Builder; - /// - /// let mut builder = Builder::new(); - /// - /// builder.filter_level(LevelFilter::Off); - /// builder.parse_env("MY_LOG"); - /// builder.init(); - /// ``` - /// - /// Initialise a logger with filter level `Off`, then use the `MY_LOG` - /// variable to override filtering and `MY_LOG_STYLE` to override whether - /// or not to write styles: - /// - /// ``` - /// use log::LevelFilter; - /// use env_logger::{Builder, Env}; - /// - /// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); - /// - /// let mut builder = Builder::new(); - /// builder.filter_level(LevelFilter::Off); - /// builder.parse_env(env); - /// builder.init(); - /// ``` - pub fn parse_env<'a, E>(&mut self, env: E) -> &mut Self - where - E: Into>, - { - let env = env.into(); - - if let Some(s) = env.get_filter() { - self.parse_filters(&s); - } - - if let Some(s) = env.get_write_style() { - self.parse_write_style(&s); - } - - self - } - - /// Initializes the log builder from the environment using default variable names. - /// - /// This method is a convenient way to call `from_env(Env::default())` without - /// having to use the `Env` type explicitly. The builder will use the - /// [default environment variables]. - /// - /// # Examples - /// - /// Initialise a logger using the default environment variables: - /// - /// ``` - /// use env_logger::Builder; - /// - /// let mut builder = Builder::from_default_env(); - /// builder.init(); - /// ``` - /// - /// [default environment variables]: struct.Env.html#default-environment-variables - pub fn from_default_env() -> Self { - Self::from_env(Env::default()) - } - - /// Applies the configuration from the environment using default variable names. - /// - /// This method is a convenient way to call `parse_env(Env::default())` without - /// having to use the `Env` type explicitly. The builder will use the - /// [default environment variables]. - /// - /// # Examples - /// - /// Initialise a logger with filter level `Off`, then configure it using the - /// default environment variables: - /// - /// ``` - /// use log::LevelFilter; - /// use env_logger::Builder; - /// - /// let mut builder = Builder::new(); - /// builder.filter_level(LevelFilter::Off); - /// builder.parse_default_env(); - /// builder.init(); - /// ``` - /// - /// [default environment variables]: struct.Env.html#default-environment-variables - pub fn parse_default_env(&mut self) -> &mut Self { - self.parse_env(Env::default()) - } - - /// Sets the format function for formatting the log output. - /// - /// This function is called on each record logged and should format the - /// log record and output it to the given [`Formatter`]. - /// - /// The format function is expected to output the string directly to the - /// `Formatter` so that implementations can use the [`std::fmt`] macros - /// to format and output without intermediate heap allocations. The default - /// `env_logger` formatter takes advantage of this. - /// - /// # Examples - /// - /// Use a custom format to write only the log message: - /// - /// ``` - /// use std::io::Write; - /// use env_logger::Builder; - /// - /// let mut builder = Builder::new(); - /// - /// builder.format(|buf, record| writeln!(buf, "{}", record.args())); - /// ``` - /// - /// [`Formatter`]: fmt/struct.Formatter.html - /// [`String`]: https://doc.rust-lang.org/stable/std/string/struct.String.html - /// [`std::fmt`]: https://doc.rust-lang.org/std/fmt/index.html - pub fn format(&mut self, format: F) -> &mut Self - where - F: Fn(&mut Formatter, &Record) -> io::Result<()> + Sync + Send, - { - self.format.custom_format = Some(Box::new(format)); - self - } - - /// Use the default format. - /// - /// This method will clear any custom format set on the builder. - pub fn default_format(&mut self) -> &mut Self { - self.format = Default::default(); - self - } - - /// Whether or not to write the level in the default format. - pub fn format_level(&mut self, write: bool) -> &mut Self { - self.format.format_level = write; - self - } - - /// Whether or not to write the module path in the default format. - pub fn format_module_path(&mut self, write: bool) -> &mut Self { - self.format.format_module_path = write; - self - } - - /// Whether or not to write the target in the default format. - pub fn format_target(&mut self, write: bool) -> &mut Self { - self.format.format_target = write; - self - } - - /// Configures the amount of spaces to use to indent multiline log records. - /// A value of `None` disables any kind of indentation. - pub fn format_indent(&mut self, indent: Option) -> &mut Self { - self.format.format_indent = indent; - self - } - - /// Configures if timestamp should be included and in what precision. - pub fn format_timestamp(&mut self, timestamp: Option) -> &mut Self { - self.format.format_timestamp = timestamp; - self - } - - /// Configures the timestamp to use second precision. - pub fn format_timestamp_secs(&mut self) -> &mut Self { - self.format_timestamp(Some(fmt::TimestampPrecision::Seconds)) - } - - /// Configures the timestamp to use millisecond precision. - pub fn format_timestamp_millis(&mut self) -> &mut Self { - self.format_timestamp(Some(fmt::TimestampPrecision::Millis)) - } - - /// Configures the timestamp to use microsecond precision. - pub fn format_timestamp_micros(&mut self) -> &mut Self { - self.format_timestamp(Some(fmt::TimestampPrecision::Micros)) - } - - /// Configures the timestamp to use nanosecond precision. - pub fn format_timestamp_nanos(&mut self) -> &mut Self { - self.format_timestamp(Some(fmt::TimestampPrecision::Nanos)) - } - - /// Configures the end of line suffix. - pub fn format_suffix(&mut self, suffix: &'static str) -> &mut Self { - self.format.format_suffix = suffix; - self - } - - /// Adds a directive to the filter for a specific module. - /// - /// # Examples - /// - /// Only include messages for info and above for logs in `path::to::module`: - /// - /// ``` - /// use env_logger::Builder; - /// use log::LevelFilter; - /// - /// let mut builder = Builder::new(); - /// - /// builder.filter_module("path::to::module", LevelFilter::Info); - /// ``` - pub fn filter_module(&mut self, module: &str, level: LevelFilter) -> &mut Self { - self.filter.filter_module(module, level); - self - } - - /// Adds a directive to the filter for all modules. - /// - /// # Examples - /// - /// Only include messages for info and above for logs globally: - /// - /// ``` - /// use env_logger::Builder; - /// use log::LevelFilter; - /// - /// let mut builder = Builder::new(); - /// - /// builder.filter_level(LevelFilter::Info); - /// ``` - pub fn filter_level(&mut self, level: LevelFilter) -> &mut Self { - self.filter.filter_level(level); - self - } - - /// Adds filters to the logger. - /// - /// 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. - /// - /// # Examples - /// - /// Only include messages for info and above for logs in `path::to::module`: - /// - /// ``` - /// use env_logger::Builder; - /// use log::LevelFilter; - /// - /// let mut builder = Builder::new(); - /// - /// builder.filter(Some("path::to::module"), LevelFilter::Info); - /// ``` - pub fn filter(&mut self, module: Option<&str>, level: LevelFilter) -> &mut Self { - self.filter.filter(module, level); - self - } - - /// Parses the directives string in the same form as the `RUST_LOG` - /// environment variable. - /// - /// See the module documentation for more details. - pub fn parse_filters(&mut self, filters: &str) -> &mut Self { - self.filter.parse(filters); - self - } - - /// Sets the target for the log output. - /// - /// Env logger can log to either stdout, stderr or a custom pipe. The default is stderr. - /// - /// The custom pipe can be used to send the log messages to a custom sink (for example a file). - /// Do note that direct writes to a file can become a bottleneck due to IO operation times. - /// - /// # Examples - /// - /// Write log message to `stdout`: - /// - /// ``` - /// use env_logger::{Builder, Target}; - /// - /// let mut builder = Builder::new(); - /// - /// builder.target(Target::Stdout); - /// ``` - pub fn target(&mut self, target: fmt::Target) -> &mut Self { - self.writer.target(target); - self - } - - /// Sets whether or not styles will be written. - /// - /// This can be useful in environments that don't support control characters - /// for setting colors. - /// - /// # Examples - /// - /// Never attempt to write styles: - /// - /// ``` - /// use env_logger::{Builder, WriteStyle}; - /// - /// let mut builder = Builder::new(); - /// - /// builder.write_style(WriteStyle::Never); - /// ``` - pub fn write_style(&mut self, write_style: fmt::WriteStyle) -> &mut Self { - self.writer.write_style(write_style); - self - } - - /// Parses whether or not to write styles in the same form as the `RUST_LOG_STYLE` - /// environment variable. - /// - /// See the module documentation for more details. - pub fn parse_write_style(&mut self, write_style: &str) -> &mut Self { - self.writer.parse_write_style(write_style); - self - } - - /// Sets whether or not the logger will be used in unit tests. - /// - /// If `is_test` is `true` then the logger will allow the testing framework to - /// capture log records rather than printing them to the terminal directly. - pub fn is_test(&mut self, is_test: bool) -> &mut Self { - self.writer.is_test(is_test); - self - } - - /// Initializes the global logger with the built env logger. - /// - /// This should be called early in the execution of a Rust program. Any log - /// events that occur before initialization will be ignored. - /// - /// # Errors - /// - /// This function will fail if it is called more than once, or if another - /// library has already initialized a global logger. - pub fn try_init(&mut self) -> Result<(), SetLoggerError> { - let logger = self.build(); - - let max_level = logger.filter(); - let r = log::set_boxed_logger(Box::new(logger)); - - if r.is_ok() { - log::set_max_level(max_level); - } - - r - } - - /// Initializes the global logger with the built env logger. - /// - /// This should be called early in the execution of a Rust program. Any log - /// events that occur before initialization will be ignored. - /// - /// # Panics - /// - /// This function will panic if it is called more than once, or if another - /// library has already initialized a global logger. - pub fn init(&mut self) { - self.try_init() - .expect("Builder::init should not be called after logger initialized"); - } - - /// Build an env logger. - /// - /// The returned logger implements the `Log` trait and can be installed manually - /// or nested within another logger. - pub fn build(&mut self) -> Logger { - assert!(!self.built, "attempt to re-use consumed builder"); - self.built = true; - - Logger { - writer: self.writer.build(), - filter: self.filter.build(), - format: self.format.build(), - } - } -} - -impl Logger { - /// Creates the logger from the environment. - /// - /// The variables used to read configuration from can be tweaked before - /// passing in. - /// - /// # Examples - /// - /// Create a logger reading the log filter from an environment variable - /// called `MY_LOG`: - /// - /// ``` - /// use env_logger::Logger; - /// - /// let logger = Logger::from_env("MY_LOG"); - /// ``` - /// - /// Create a logger using the `MY_LOG` variable for filtering and - /// `MY_LOG_STYLE` for whether or not to write styles: - /// - /// ``` - /// use env_logger::{Logger, Env}; - /// - /// let env = Env::new().filter_or("MY_LOG", "info").write_style_or("MY_LOG_STYLE", "always"); - /// - /// let logger = Logger::from_env(env); - /// ``` - pub fn from_env<'a, E>(env: E) -> Self - where - E: Into>, - { - Builder::from_env(env).build() - } - - /// Creates the logger from the environment using default variable names. - /// - /// This method is a convenient way to call `from_env(Env::default())` without - /// having to use the `Env` type explicitly. The logger will use the - /// [default environment variables]. - /// - /// # Examples - /// - /// Creates a logger using the default environment variables: - /// - /// ``` - /// use env_logger::Logger; - /// - /// let logger = Logger::from_default_env(); - /// ``` - /// - /// [default environment variables]: struct.Env.html#default-environment-variables - pub fn from_default_env() -> Self { - Builder::from_default_env().build() - } - - /// Returns the maximum `LevelFilter` that this env logger instance is - /// configured to output. - pub fn filter(&self) -> LevelFilter { - self.filter.filter() - } - - /// Checks if this record matches the configured filter. - pub fn matches(&self, record: &Record) -> bool { - self.filter.matches(record) - } -} - -impl Log for Logger { - fn enabled(&self, metadata: &Metadata) -> bool { - self.filter.enabled(metadata) - } - - fn log(&self, record: &Record) { - if self.matches(record) { - // Log records are written to a thread-local buffer before being printed - // to the terminal. We clear these buffers afterwards, but they aren't shrunk - // so will always at least have capacity for the largest log record formatted - // on that thread. - // - // If multiple `Logger`s are used by the same threads then the thread-local - // formatter might have different color support. If this is the case the - // formatter and its buffer are discarded and recreated. - - thread_local! { - static FORMATTER: RefCell> = RefCell::new(None); - } - - let print = |formatter: &mut Formatter, record: &Record| { - let _ = - (self.format)(formatter, record).and_then(|_| formatter.print(&self.writer)); - - // Always clear the buffer afterwards - formatter.clear(); - }; - - let printed = FORMATTER - .try_with(|tl_buf| { - match tl_buf.try_borrow_mut() { - // There are no active borrows of the buffer - Ok(mut tl_buf) => match *tl_buf { - // We have a previously set formatter - Some(ref mut formatter) => { - // Check the buffer style. If it's different from the logger's - // style then drop the buffer and recreate it. - if formatter.write_style() != self.writer.write_style() { - *formatter = Formatter::new(&self.writer); - } - - print(formatter, record); - } - // We don't have a previously set formatter - None => { - let mut formatter = Formatter::new(&self.writer); - print(&mut formatter, record); - - *tl_buf = Some(formatter); - } - }, - // There's already an active borrow of the buffer (due to re-entrancy) - Err(_) => { - print(&mut Formatter::new(&self.writer), record); - } - } - }) - .is_ok(); - - if !printed { - // The thread-local storage was not available (because its - // destructor has already run). Create a new single-use - // Formatter on the stack for this call. - print(&mut Formatter::new(&self.writer), record); - } - } - } - - fn flush(&self) {} -} - -impl<'a> Env<'a> { - /// Get a default set of environment variables. - pub fn new() -> Self { - Self::default() - } - - /// Specify an environment variable to read the filter from. - pub fn filter(mut self, filter_env: E) -> Self - where - E: Into>, - { - self.filter = Var::new(filter_env); - - self - } - - /// Specify an environment variable to read the filter from. - /// - /// If the variable is not set, the default value will be used. - pub fn filter_or(mut self, filter_env: E, default: V) -> Self - where - E: Into>, - V: Into>, - { - self.filter = Var::new_with_default(filter_env, default); - - self - } - - /// Use the default environment variable to read the filter from. - /// - /// If the variable is not set, the default value will be used. - pub fn default_filter_or(mut self, default: V) -> Self - where - V: Into>, - { - self.filter = Var::new_with_default(DEFAULT_FILTER_ENV, default); - - self - } - - fn get_filter(&self) -> Option { - self.filter.get() - } - - /// Specify an environment variable to read the style from. - pub fn write_style(mut self, write_style_env: E) -> Self - where - E: Into>, - { - self.write_style = Var::new(write_style_env); - - self - } - - /// Specify an environment variable to read the style from. - /// - /// If the variable is not set, the default value will be used. - pub fn write_style_or(mut self, write_style_env: E, default: V) -> Self - where - E: Into>, - V: Into>, - { - self.write_style = Var::new_with_default(write_style_env, default); - - self - } - - /// Use the default environment variable to read the style from. - /// - /// If the variable is not set, the default value will be used. - pub fn default_write_style_or(mut self, default: V) -> Self - where - V: Into>, - { - self.write_style = Var::new_with_default(DEFAULT_WRITE_STYLE_ENV, default); - - self - } - - fn get_write_style(&self) -> Option { - self.write_style.get() - } -} - -impl<'a> Var<'a> { - fn new(name: E) -> Self - where - E: Into>, - { - Var { - name: name.into(), - default: None, - } - } - - fn new_with_default(name: E, default: V) -> Self - where - E: Into>, - V: Into>, - { - Var { - name: name.into(), - default: Some(default.into()), - } - } - - fn get(&self) -> Option { - env::var(&*self.name) - .ok() - .or_else(|| self.default.to_owned().map(|v| v.into_owned())) - } -} - -impl<'a, T> From for Env<'a> -where - T: Into>, -{ - fn from(filter_env: T) -> Self { - Env::default().filter(filter_env.into()) - } -} - -impl<'a> Default for Env<'a> { - fn default() -> Self { - Env { - filter: Var::new(DEFAULT_FILTER_ENV), - write_style: Var::new(DEFAULT_WRITE_STYLE_ENV), - } - } -} - -mod std_fmt_impls { - use super::*; - use std::fmt; - - impl fmt::Debug for Logger { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Logger") - .field("filter", &self.filter) - .finish() - } - } - - impl fmt::Debug for Builder { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.built { - f.debug_struct("Logger").field("built", &true).finish() - } else { - f.debug_struct("Logger") - .field("filter", &self.filter) - .field("writer", &self.writer) - .finish() - } - } - } -} - -/// Attempts to initialize the global logger with an env logger. -/// -/// This should be called early in the execution of a Rust program. Any log -/// events that occur before initialization will be ignored. -/// -/// # Errors -/// -/// This function will fail if it is called more than once, or if another -/// library has already initialized a global logger. -pub fn try_init() -> Result<(), SetLoggerError> { - try_init_from_env(Env::default()) -} - -/// Initializes the global logger with an env logger. -/// -/// This should be called early in the execution of a Rust program. Any log -/// events that occur before initialization will be ignored. -/// -/// # Panics -/// -/// This function will panic if it is called more than once, or if another -/// library has already initialized a global logger. -pub fn init() { - try_init().expect("env_logger::init should not be called after logger initialized"); -} - -/// Attempts to initialize the global logger with an env logger from the given -/// environment variables. -/// -/// This should be called early in the execution of a Rust program. Any log -/// events that occur before initialization will be ignored. -/// -/// # Examples -/// -/// Initialise a logger using the `MY_LOG` environment variable for filters -/// and `MY_LOG_STYLE` for writing colors: -/// -/// ``` -/// use env_logger::{Builder, Env}; -/// -/// # fn run() -> Result<(), Box> { -/// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); -/// -/// env_logger::try_init_from_env(env)?; -/// -/// Ok(()) -/// # } -/// # run().unwrap(); -/// ``` -/// -/// # Errors -/// -/// This function will fail if it is called more than once, or if another -/// library has already initialized a global logger. -pub fn try_init_from_env<'a, E>(env: E) -> Result<(), SetLoggerError> -where - E: Into>, -{ - let mut builder = Builder::from_env(env); - - builder.try_init() -} - -/// Initializes the global logger with an env logger from the given environment -/// variables. -/// -/// This should be called early in the execution of a Rust program. Any log -/// events that occur before initialization will be ignored. -/// -/// # Examples -/// -/// Initialise a logger using the `MY_LOG` environment variable for filters -/// and `MY_LOG_STYLE` for writing colors: -/// -/// ``` -/// use env_logger::{Builder, Env}; -/// -/// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); -/// -/// env_logger::init_from_env(env); -/// ``` -/// -/// # Panics -/// -/// This function will panic if it is called more than once, or if another -/// library has already initialized a global logger. -pub fn init_from_env<'a, E>(env: E) -where - E: Into>, -{ - try_init_from_env(env) - .expect("env_logger::init_from_env should not be called after logger initialized"); -} - -/// Create a new builder with the default environment variables. -/// -/// The builder can be configured before being initialized. -/// This is a convenient way of calling [`Builder::from_default_env`]. -/// -/// [`Builder::from_default_env`]: struct.Builder.html#method.from_default_env -pub fn builder() -> Builder { - Builder::from_default_env() -} - -/// Create a builder from the given environment variables. -/// -/// The builder can be configured before being initialized. -#[deprecated( - since = "0.8.0", - note = "Prefer `env_logger::Builder::from_env()` instead." -)] -pub fn from_env<'a, E>(env: E) -> Builder -where - E: Into>, -{ - Builder::from_env(env) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn env_get_filter_reads_from_var_if_set() { - env::set_var("env_get_filter_reads_from_var_if_set", "from var"); - - let env = Env::new().filter_or("env_get_filter_reads_from_var_if_set", "from default"); - - assert_eq!(Some("from var".to_owned()), env.get_filter()); - } - - #[test] - fn env_get_filter_reads_from_default_if_var_not_set() { - env::remove_var("env_get_filter_reads_from_default_if_var_not_set"); - - let env = Env::new().filter_or( - "env_get_filter_reads_from_default_if_var_not_set", - "from default", - ); - - assert_eq!(Some("from default".to_owned()), env.get_filter()); - } - - #[test] - fn env_get_write_style_reads_from_var_if_set() { - env::set_var("env_get_write_style_reads_from_var_if_set", "from var"); - - let env = - Env::new().write_style_or("env_get_write_style_reads_from_var_if_set", "from default"); - - assert_eq!(Some("from var".to_owned()), env.get_write_style()); - } - - #[test] - fn env_get_write_style_reads_from_default_if_var_not_set() { - env::remove_var("env_get_write_style_reads_from_default_if_var_not_set"); - - let env = Env::new().write_style_or( - "env_get_write_style_reads_from_default_if_var_not_set", - "from default", - ); - - assert_eq!(Some("from default".to_owned()), env.get_write_style()); - } - - #[test] - fn builder_parse_env_overrides_existing_filters() { - env::set_var( - "builder_parse_default_env_overrides_existing_filters", - "debug", - ); - let env = Env::new().filter("builder_parse_default_env_overrides_existing_filters"); - - let mut builder = Builder::new(); - builder.filter_level(LevelFilter::Trace); - // Overrides global level to debug - builder.parse_env(env); - - assert_eq!(builder.filter.build().filter(), LevelFilter::Debug); - } -} +pub use self::fmt::{Target, TimestampPrecision, WriteStyle}; +pub use self::logger::*; diff --git a/src/logger.rs b/src/logger.rs new file mode 100644 index 00000000..18bfe9fa --- /dev/null +++ b/src/logger.rs @@ -0,0 +1,1017 @@ +use std::{borrow::Cow, cell::RefCell, env, io}; + +use log::{LevelFilter, Log, Metadata, Record, SetLoggerError}; + +use crate::fmt; +use crate::fmt::writer::{self, Writer}; +use crate::fmt::{FormatFn, Formatter}; + +/// The default name for the environment variable to read filters from. +pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; + +/// The default name for the environment variable to read style preferences from. +pub const DEFAULT_WRITE_STYLE_ENV: &str = "RUST_LOG_STYLE"; + +/// `Builder` acts as builder for initializing a `Logger`. +/// +/// It can be used to customize the log format, change the environment variable used +/// to provide the logging directives and also set the default log level filter. +/// +/// # Examples +/// +/// ``` +/// # use std::io::Write; +/// use env_logger::Builder; +/// use log::{LevelFilter, error, info}; +/// +/// let mut builder = Builder::from_default_env(); +/// +/// builder +/// .format(|buf, record| writeln!(buf, "{} - {}", record.level(), record.args())) +/// .filter(None, LevelFilter::Info) +/// .init(); +/// +/// error!("error message"); +/// info!("info message"); +/// ``` +#[derive(Default)] +pub struct Builder { + filter: env_filter::Builder, + writer: writer::Builder, + format: fmt::Builder, + built: bool, +} + +impl Builder { + /// Initializes the log builder with defaults. + /// + /// **NOTE:** This method won't read from any environment variables. + /// Use the [`filter`] and [`write_style`] methods to configure the builder + /// or use [`from_env`] or [`from_default_env`] instead. + /// + /// # Examples + /// + /// Create a new builder and configure filters and style: + /// + /// ``` + /// use log::LevelFilter; + /// use env_logger::{Builder, WriteStyle}; + /// + /// let mut builder = Builder::new(); + /// + /// builder + /// .filter(None, LevelFilter::Info) + /// .write_style(WriteStyle::Always) + /// .init(); + /// ``` + /// + /// [`filter`]: #method.filter + /// [`write_style`]: #method.write_style + /// [`from_env`]: #method.from_env + /// [`from_default_env`]: #method.from_default_env + pub fn new() -> Builder { + Default::default() + } + + /// Initializes the log builder from the environment. + /// + /// The variables used to read configuration from can be tweaked before + /// passing in. + /// + /// # Examples + /// + /// Initialise a logger reading the log filter from an environment variable + /// called `MY_LOG`: + /// + /// ``` + /// use env_logger::Builder; + /// + /// let mut builder = Builder::from_env("MY_LOG"); + /// builder.init(); + /// ``` + /// + /// Initialise a logger using the `MY_LOG` variable for filtering and + /// `MY_LOG_STYLE` for whether or not to write styles: + /// + /// ``` + /// use env_logger::{Builder, Env}; + /// + /// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); + /// + /// let mut builder = Builder::from_env(env); + /// builder.init(); + /// ``` + pub fn from_env<'a, E>(env: E) -> Self + where + E: Into>, + { + let mut builder = Builder::new(); + builder.parse_env(env); + builder + } + + /// Applies the configuration from the environment. + /// + /// This function allows a builder to be configured with default parameters, + /// to be then overridden by the environment. + /// + /// # Examples + /// + /// Initialise a logger with filter level `Off`, then override the log + /// filter from an environment variable called `MY_LOG`: + /// + /// ``` + /// use log::LevelFilter; + /// use env_logger::Builder; + /// + /// let mut builder = Builder::new(); + /// + /// builder.filter_level(LevelFilter::Off); + /// builder.parse_env("MY_LOG"); + /// builder.init(); + /// ``` + /// + /// Initialise a logger with filter level `Off`, then use the `MY_LOG` + /// variable to override filtering and `MY_LOG_STYLE` to override whether + /// or not to write styles: + /// + /// ``` + /// use log::LevelFilter; + /// use env_logger::{Builder, Env}; + /// + /// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); + /// + /// let mut builder = Builder::new(); + /// builder.filter_level(LevelFilter::Off); + /// builder.parse_env(env); + /// builder.init(); + /// ``` + pub fn parse_env<'a, E>(&mut self, env: E) -> &mut Self + where + E: Into>, + { + let env = env.into(); + + if let Some(s) = env.get_filter() { + self.parse_filters(&s); + } + + if let Some(s) = env.get_write_style() { + self.parse_write_style(&s); + } + + self + } + + /// Initializes the log builder from the environment using default variable names. + /// + /// This method is a convenient way to call `from_env(Env::default())` without + /// having to use the `Env` type explicitly. The builder will use the + /// [default environment variables]. + /// + /// # Examples + /// + /// Initialise a logger using the default environment variables: + /// + /// ``` + /// use env_logger::Builder; + /// + /// let mut builder = Builder::from_default_env(); + /// builder.init(); + /// ``` + /// + /// [default environment variables]: struct.Env.html#default-environment-variables + pub fn from_default_env() -> Self { + Self::from_env(Env::default()) + } + + /// Applies the configuration from the environment using default variable names. + /// + /// This method is a convenient way to call `parse_env(Env::default())` without + /// having to use the `Env` type explicitly. The builder will use the + /// [default environment variables]. + /// + /// # Examples + /// + /// Initialise a logger with filter level `Off`, then configure it using the + /// default environment variables: + /// + /// ``` + /// use log::LevelFilter; + /// use env_logger::Builder; + /// + /// let mut builder = Builder::new(); + /// builder.filter_level(LevelFilter::Off); + /// builder.parse_default_env(); + /// builder.init(); + /// ``` + /// + /// [default environment variables]: struct.Env.html#default-environment-variables + pub fn parse_default_env(&mut self) -> &mut Self { + self.parse_env(Env::default()) + } + + /// Sets the format function for formatting the log output. + /// + /// This function is called on each record logged and should format the + /// log record and output it to the given [`Formatter`]. + /// + /// The format function is expected to output the string directly to the + /// `Formatter` so that implementations can use the [`std::fmt`] macros + /// 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: + /// + /// ``` + /// use std::io::Write; + /// use env_logger::Builder; + /// + /// let mut builder = Builder::new(); + /// + /// builder.format(|buf, record| writeln!(buf, "{}", record.args())); + /// ``` + /// + /// [`Formatter`]: fmt/struct.Formatter.html + /// [`String`]: https://doc.rust-lang.org/stable/std/string/struct.String.html + /// [`std::fmt`]: https://doc.rust-lang.org/std/fmt/index.html + pub fn format(&mut self, format: F) -> &mut Self + where + F: Fn(&mut Formatter, &Record) -> io::Result<()> + Sync + Send, + { + self.format.custom_format = Some(Box::new(format)); + self + } + + /// Use the default format. + /// + /// This method will clear any custom format set on the builder. + pub fn default_format(&mut self) -> &mut Self { + self.format = Default::default(); + self + } + + /// Whether or not to write the level in the default format. + pub fn format_level(&mut self, write: bool) -> &mut Self { + self.format.format_level = write; + self + } + + /// Whether or not to write the module path in the default format. + pub fn format_module_path(&mut self, write: bool) -> &mut Self { + self.format.format_module_path = write; + self + } + + /// Whether or not to write the target in the default format. + pub fn format_target(&mut self, write: bool) -> &mut Self { + self.format.format_target = write; + self + } + + /// Configures the amount of spaces to use to indent multiline log records. + /// A value of `None` disables any kind of indentation. + pub fn format_indent(&mut self, indent: Option) -> &mut Self { + self.format.format_indent = indent; + self + } + + /// Configures if timestamp should be included and in what precision. + pub fn format_timestamp(&mut self, timestamp: Option) -> &mut Self { + self.format.format_timestamp = timestamp; + self + } + + /// Configures the timestamp to use second precision. + pub fn format_timestamp_secs(&mut self) -> &mut Self { + self.format_timestamp(Some(fmt::TimestampPrecision::Seconds)) + } + + /// Configures the timestamp to use millisecond precision. + pub fn format_timestamp_millis(&mut self) -> &mut Self { + self.format_timestamp(Some(fmt::TimestampPrecision::Millis)) + } + + /// Configures the timestamp to use microsecond precision. + pub fn format_timestamp_micros(&mut self) -> &mut Self { + self.format_timestamp(Some(fmt::TimestampPrecision::Micros)) + } + + /// Configures the timestamp to use nanosecond precision. + pub fn format_timestamp_nanos(&mut self) -> &mut Self { + self.format_timestamp(Some(fmt::TimestampPrecision::Nanos)) + } + + /// Configures the end of line suffix. + pub fn format_suffix(&mut self, suffix: &'static str) -> &mut Self { + self.format.format_suffix = suffix; + self + } + + /// Adds a directive to the filter for a specific module. + /// + /// # Examples + /// + /// Only include messages for info and above for logs in `path::to::module`: + /// + /// ``` + /// use env_logger::Builder; + /// use log::LevelFilter; + /// + /// let mut builder = Builder::new(); + /// + /// builder.filter_module("path::to::module", LevelFilter::Info); + /// ``` + pub fn filter_module(&mut self, module: &str, level: LevelFilter) -> &mut Self { + self.filter.filter_module(module, level); + self + } + + /// Adds a directive to the filter for all modules. + /// + /// # Examples + /// + /// Only include messages for info and above for logs globally: + /// + /// ``` + /// use env_logger::Builder; + /// use log::LevelFilter; + /// + /// let mut builder = Builder::new(); + /// + /// builder.filter_level(LevelFilter::Info); + /// ``` + pub fn filter_level(&mut self, level: LevelFilter) -> &mut Self { + self.filter.filter_level(level); + self + } + + /// Adds filters to the logger. + /// + /// 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. + /// + /// # Examples + /// + /// Only include messages for info and above for logs in `path::to::module`: + /// + /// ``` + /// use env_logger::Builder; + /// use log::LevelFilter; + /// + /// let mut builder = Builder::new(); + /// + /// builder.filter(Some("path::to::module"), LevelFilter::Info); + /// ``` + pub fn filter(&mut self, module: Option<&str>, level: LevelFilter) -> &mut Self { + self.filter.filter(module, level); + self + } + + /// Parses the directives string in the same form as the `RUST_LOG` + /// environment variable. + /// + /// See the module documentation for more details. + pub fn parse_filters(&mut self, filters: &str) -> &mut Self { + self.filter.parse(filters); + self + } + + /// Sets the target for the log output. + /// + /// Env logger can log to either stdout, stderr or a custom pipe. The default is stderr. + /// + /// The custom pipe can be used to send the log messages to a custom sink (for example a file). + /// Do note that direct writes to a file can become a bottleneck due to IO operation times. + /// + /// # Examples + /// + /// Write log message to `stdout`: + /// + /// ``` + /// use env_logger::{Builder, Target}; + /// + /// let mut builder = Builder::new(); + /// + /// builder.target(Target::Stdout); + /// ``` + pub fn target(&mut self, target: fmt::Target) -> &mut Self { + self.writer.target(target); + self + } + + /// Sets whether or not styles will be written. + /// + /// This can be useful in environments that don't support control characters + /// for setting colors. + /// + /// # Examples + /// + /// Never attempt to write styles: + /// + /// ``` + /// use env_logger::{Builder, WriteStyle}; + /// + /// let mut builder = Builder::new(); + /// + /// builder.write_style(WriteStyle::Never); + /// ``` + pub fn write_style(&mut self, write_style: fmt::WriteStyle) -> &mut Self { + self.writer.write_style(write_style); + self + } + + /// Parses whether or not to write styles in the same form as the `RUST_LOG_STYLE` + /// environment variable. + /// + /// See the module documentation for more details. + pub fn parse_write_style(&mut self, write_style: &str) -> &mut Self { + self.writer.parse_write_style(write_style); + self + } + + /// Sets whether or not the logger will be used in unit tests. + /// + /// If `is_test` is `true` then the logger will allow the testing framework to + /// capture log records rather than printing them to the terminal directly. + pub fn is_test(&mut self, is_test: bool) -> &mut Self { + self.writer.is_test(is_test); + self + } + + /// Initializes the global logger with the built env logger. + /// + /// This should be called early in the execution of a Rust program. Any log + /// events that occur before initialization will be ignored. + /// + /// # Errors + /// + /// This function will fail if it is called more than once, or if another + /// library has already initialized a global logger. + pub fn try_init(&mut self) -> Result<(), SetLoggerError> { + let logger = self.build(); + + let max_level = logger.filter(); + let r = log::set_boxed_logger(Box::new(logger)); + + if r.is_ok() { + log::set_max_level(max_level); + } + + r + } + + /// Initializes the global logger with the built env logger. + /// + /// This should be called early in the execution of a Rust program. Any log + /// events that occur before initialization will be ignored. + /// + /// # Panics + /// + /// This function will panic if it is called more than once, or if another + /// library has already initialized a global logger. + pub fn init(&mut self) { + self.try_init() + .expect("Builder::init should not be called after logger initialized"); + } + + /// Build an env logger. + /// + /// The returned logger implements the `Log` trait and can be installed manually + /// or nested within another logger. + pub fn build(&mut self) -> Logger { + assert!(!self.built, "attempt to re-use consumed builder"); + self.built = true; + + Logger { + writer: self.writer.build(), + filter: self.filter.build(), + format: self.format.build(), + } + } +} + +impl std::fmt::Debug for Builder { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.built { + f.debug_struct("Logger").field("built", &true).finish() + } else { + f.debug_struct("Logger") + .field("filter", &self.filter) + .field("writer", &self.writer) + .finish() + } + } +} + +/// The env logger. +/// +/// This struct implements the `Log` trait from the [`log` crate][log-crate-url], +/// which allows it to act as a logger. +/// +/// The [`init()`], [`try_init()`], [`Builder::init()`] and [`Builder::try_init()`] +/// methods will each construct a `Logger` and immediately initialize it as the +/// default global logger. +/// +/// If you'd instead need access to the constructed `Logger`, you can use +/// the associated [`Builder`] and install it with the +/// [`log` crate][log-crate-url] directly. +/// +/// [log-crate-url]: https://docs.rs/log +/// [`init()`]: fn.init.html +/// [`try_init()`]: fn.try_init.html +/// [`Builder::init()`]: struct.Builder.html#method.init +/// [`Builder::try_init()`]: struct.Builder.html#method.try_init +/// [`Builder`]: struct.Builder.html +pub struct Logger { + writer: Writer, + filter: env_filter::Filter, + format: FormatFn, +} + +impl Logger { + /// Creates the logger from the environment. + /// + /// The variables used to read configuration from can be tweaked before + /// passing in. + /// + /// # Examples + /// + /// Create a logger reading the log filter from an environment variable + /// called `MY_LOG`: + /// + /// ``` + /// use env_logger::Logger; + /// + /// let logger = Logger::from_env("MY_LOG"); + /// ``` + /// + /// Create a logger using the `MY_LOG` variable for filtering and + /// `MY_LOG_STYLE` for whether or not to write styles: + /// + /// ``` + /// use env_logger::{Logger, Env}; + /// + /// let env = Env::new().filter_or("MY_LOG", "info").write_style_or("MY_LOG_STYLE", "always"); + /// + /// let logger = Logger::from_env(env); + /// ``` + pub fn from_env<'a, E>(env: E) -> Self + where + E: Into>, + { + Builder::from_env(env).build() + } + + /// Creates the logger from the environment using default variable names. + /// + /// This method is a convenient way to call `from_env(Env::default())` without + /// having to use the `Env` type explicitly. The logger will use the + /// [default environment variables]. + /// + /// # Examples + /// + /// Creates a logger using the default environment variables: + /// + /// ``` + /// use env_logger::Logger; + /// + /// let logger = Logger::from_default_env(); + /// ``` + /// + /// [default environment variables]: struct.Env.html#default-environment-variables + pub fn from_default_env() -> Self { + Builder::from_default_env().build() + } + + /// Returns the maximum `LevelFilter` that this env logger instance is + /// configured to output. + pub fn filter(&self) -> LevelFilter { + self.filter.filter() + } + + /// Checks if this record matches the configured filter. + pub fn matches(&self, record: &Record) -> bool { + self.filter.matches(record) + } +} + +impl Log for Logger { + fn enabled(&self, metadata: &Metadata) -> bool { + self.filter.enabled(metadata) + } + + fn log(&self, record: &Record) { + if self.matches(record) { + // Log records are written to a thread-local buffer before being printed + // to the terminal. We clear these buffers afterwards, but they aren't shrunk + // so will always at least have capacity for the largest log record formatted + // on that thread. + // + // If multiple `Logger`s are used by the same threads then the thread-local + // formatter might have different color support. If this is the case the + // formatter and its buffer are discarded and recreated. + + thread_local! { + static FORMATTER: RefCell> = RefCell::new(None); + } + + let print = |formatter: &mut Formatter, record: &Record| { + let _ = + (self.format)(formatter, record).and_then(|_| formatter.print(&self.writer)); + + // Always clear the buffer afterwards + formatter.clear(); + }; + + let printed = FORMATTER + .try_with(|tl_buf| { + match tl_buf.try_borrow_mut() { + // There are no active borrows of the buffer + Ok(mut tl_buf) => match *tl_buf { + // We have a previously set formatter + Some(ref mut formatter) => { + // Check the buffer style. If it's different from the logger's + // style then drop the buffer and recreate it. + if formatter.write_style() != self.writer.write_style() { + *formatter = Formatter::new(&self.writer); + } + + print(formatter, record); + } + // We don't have a previously set formatter + None => { + let mut formatter = Formatter::new(&self.writer); + print(&mut formatter, record); + + *tl_buf = Some(formatter); + } + }, + // There's already an active borrow of the buffer (due to re-entrancy) + Err(_) => { + print(&mut Formatter::new(&self.writer), record); + } + } + }) + .is_ok(); + + if !printed { + // The thread-local storage was not available (because its + // destructor has already run). Create a new single-use + // Formatter on the stack for this call. + print(&mut Formatter::new(&self.writer), record); + } + } + } + + fn flush(&self) {} +} + +impl std::fmt::Debug for Logger { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("Logger") + .field("filter", &self.filter) + .finish() + } +} + +/// Set of environment variables to configure from. +/// +/// # Default environment variables +/// +/// By default, the `Env` will read the following environment variables: +/// +/// - `RUST_LOG`: the level filter +/// - `RUST_LOG_STYLE`: whether or not to print styles with records. +/// +/// These sources can be configured using the builder methods on `Env`. +#[derive(Debug)] +pub struct Env<'a> { + filter: Var<'a>, + write_style: Var<'a>, +} + +impl<'a> Env<'a> { + /// Get a default set of environment variables. + pub fn new() -> Self { + Self::default() + } + + /// Specify an environment variable to read the filter from. + pub fn filter(mut self, filter_env: E) -> Self + where + E: Into>, + { + self.filter = Var::new(filter_env); + + self + } + + /// Specify an environment variable to read the filter from. + /// + /// If the variable is not set, the default value will be used. + pub fn filter_or(mut self, filter_env: E, default: V) -> Self + where + E: Into>, + V: Into>, + { + self.filter = Var::new_with_default(filter_env, default); + + self + } + + /// Use the default environment variable to read the filter from. + /// + /// If the variable is not set, the default value will be used. + pub fn default_filter_or(mut self, default: V) -> Self + where + V: Into>, + { + self.filter = Var::new_with_default(DEFAULT_FILTER_ENV, default); + + self + } + + fn get_filter(&self) -> Option { + self.filter.get() + } + + /// Specify an environment variable to read the style from. + pub fn write_style(mut self, write_style_env: E) -> Self + where + E: Into>, + { + self.write_style = Var::new(write_style_env); + + self + } + + /// Specify an environment variable to read the style from. + /// + /// If the variable is not set, the default value will be used. + pub fn write_style_or(mut self, write_style_env: E, default: V) -> Self + where + E: Into>, + V: Into>, + { + self.write_style = Var::new_with_default(write_style_env, default); + + self + } + + /// Use the default environment variable to read the style from. + /// + /// If the variable is not set, the default value will be used. + pub fn default_write_style_or(mut self, default: V) -> Self + where + V: Into>, + { + self.write_style = Var::new_with_default(DEFAULT_WRITE_STYLE_ENV, default); + + self + } + + fn get_write_style(&self) -> Option { + self.write_style.get() + } +} + +impl<'a, T> From for Env<'a> +where + T: Into>, +{ + fn from(filter_env: T) -> Self { + Env::default().filter(filter_env.into()) + } +} + +impl<'a> Default for Env<'a> { + fn default() -> Self { + Env { + filter: Var::new(DEFAULT_FILTER_ENV), + write_style: Var::new(DEFAULT_WRITE_STYLE_ENV), + } + } +} + +#[derive(Debug)] +struct Var<'a> { + name: Cow<'a, str>, + default: Option>, +} + +impl<'a> Var<'a> { + fn new(name: E) -> Self + where + E: Into>, + { + Var { + name: name.into(), + default: None, + } + } + + fn new_with_default(name: E, default: V) -> Self + where + E: Into>, + V: Into>, + { + Var { + name: name.into(), + default: Some(default.into()), + } + } + + fn get(&self) -> Option { + env::var(&*self.name) + .ok() + .or_else(|| self.default.to_owned().map(|v| v.into_owned())) + } +} + +/// Attempts to initialize the global logger with an env logger. +/// +/// This should be called early in the execution of a Rust program. Any log +/// events that occur before initialization will be ignored. +/// +/// # Errors +/// +/// This function will fail if it is called more than once, or if another +/// library has already initialized a global logger. +pub fn try_init() -> Result<(), SetLoggerError> { + try_init_from_env(Env::default()) +} + +/// Initializes the global logger with an env logger. +/// +/// This should be called early in the execution of a Rust program. Any log +/// events that occur before initialization will be ignored. +/// +/// # Panics +/// +/// This function will panic if it is called more than once, or if another +/// library has already initialized a global logger. +pub fn init() { + try_init().expect("env_logger::init should not be called after logger initialized"); +} + +/// Attempts to initialize the global logger with an env logger from the given +/// environment variables. +/// +/// This should be called early in the execution of a Rust program. Any log +/// events that occur before initialization will be ignored. +/// +/// # Examples +/// +/// Initialise a logger using the `MY_LOG` environment variable for filters +/// and `MY_LOG_STYLE` for writing colors: +/// +/// ``` +/// use env_logger::{Builder, Env}; +/// +/// # fn run() -> Result<(), Box> { +/// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); +/// +/// env_logger::try_init_from_env(env)?; +/// +/// Ok(()) +/// # } +/// # run().unwrap(); +/// ``` +/// +/// # Errors +/// +/// This function will fail if it is called more than once, or if another +/// library has already initialized a global logger. +pub fn try_init_from_env<'a, E>(env: E) -> Result<(), SetLoggerError> +where + E: Into>, +{ + let mut builder = Builder::from_env(env); + + builder.try_init() +} + +/// Initializes the global logger with an env logger from the given environment +/// variables. +/// +/// This should be called early in the execution of a Rust program. Any log +/// events that occur before initialization will be ignored. +/// +/// # Examples +/// +/// Initialise a logger using the `MY_LOG` environment variable for filters +/// and `MY_LOG_STYLE` for writing colors: +/// +/// ``` +/// use env_logger::{Builder, Env}; +/// +/// let env = Env::new().filter("MY_LOG").write_style("MY_LOG_STYLE"); +/// +/// env_logger::init_from_env(env); +/// ``` +/// +/// # Panics +/// +/// This function will panic if it is called more than once, or if another +/// library has already initialized a global logger. +pub fn init_from_env<'a, E>(env: E) +where + E: Into>, +{ + try_init_from_env(env) + .expect("env_logger::init_from_env should not be called after logger initialized"); +} + +/// Create a new builder with the default environment variables. +/// +/// The builder can be configured before being initialized. +/// This is a convenient way of calling [`Builder::from_default_env`]. +/// +/// [`Builder::from_default_env`]: struct.Builder.html#method.from_default_env +pub fn builder() -> Builder { + Builder::from_default_env() +} + +/// Create a builder from the given environment variables. +/// +/// The builder can be configured before being initialized. +#[deprecated( + since = "0.8.0", + note = "Prefer `env_logger::Builder::from_env()` instead." +)] +pub fn from_env<'a, E>(env: E) -> Builder +where + E: Into>, +{ + Builder::from_env(env) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn env_get_filter_reads_from_var_if_set() { + env::set_var("env_get_filter_reads_from_var_if_set", "from var"); + + let env = Env::new().filter_or("env_get_filter_reads_from_var_if_set", "from default"); + + assert_eq!(Some("from var".to_owned()), env.get_filter()); + } + + #[test] + fn env_get_filter_reads_from_default_if_var_not_set() { + env::remove_var("env_get_filter_reads_from_default_if_var_not_set"); + + let env = Env::new().filter_or( + "env_get_filter_reads_from_default_if_var_not_set", + "from default", + ); + + assert_eq!(Some("from default".to_owned()), env.get_filter()); + } + + #[test] + fn env_get_write_style_reads_from_var_if_set() { + env::set_var("env_get_write_style_reads_from_var_if_set", "from var"); + + let env = + Env::new().write_style_or("env_get_write_style_reads_from_var_if_set", "from default"); + + assert_eq!(Some("from var".to_owned()), env.get_write_style()); + } + + #[test] + fn env_get_write_style_reads_from_default_if_var_not_set() { + env::remove_var("env_get_write_style_reads_from_default_if_var_not_set"); + + let env = Env::new().write_style_or( + "env_get_write_style_reads_from_default_if_var_not_set", + "from default", + ); + + assert_eq!(Some("from default".to_owned()), env.get_write_style()); + } + + #[test] + fn builder_parse_env_overrides_existing_filters() { + env::set_var( + "builder_parse_default_env_overrides_existing_filters", + "debug", + ); + let env = Env::new().filter("builder_parse_default_env_overrides_existing_filters"); + + let mut builder = Builder::new(); + builder.filter_level(LevelFilter::Trace); + // Overrides global level to debug + builder.parse_env(env); + + assert_eq!(builder.filter.build().filter(), LevelFilter::Debug); + } +} pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy