├── .cargo └── config.toml ├── .clippy.toml ├── .github ├── renovate.json5 ├── settings.yml └── workflows │ ├── audit.yml │ ├── ci.yml │ ├── committed.yml │ ├── post-release.yml │ ├── pre-commit.yml │ ├── release-notes.py │ ├── rust-next.yml │ └── spelling.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── committed.toml ├── crates └── env_filter │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── LICENSE-APACHE │ ├── LICENSE-MIT │ ├── README.md │ └── src │ ├── directive.rs │ ├── filter.rs │ ├── filtered_log.rs │ ├── lib.rs │ ├── op.rs │ └── parser.rs ├── deny.toml ├── examples ├── custom_default_format.rs ├── custom_format.rs ├── default.rs ├── direct_logger.rs ├── filters_from_code.rs ├── in_tests.rs └── syslog_friendly_format.rs ├── release.toml ├── src ├── fmt │ ├── humantime.rs │ ├── kv.rs │ └── mod.rs ├── lib.rs ├── logger.rs └── writer │ ├── buffer.rs │ ├── mod.rs │ └── target.rs └── tests ├── init-twice-retains-filter.rs ├── log-in-log.rs ├── log_tls_dtors.rs └── regexp_filter.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [resolver] 2 | incompatible-rust-versions = "fallback" 3 | -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | allow-print-in-tests = true 2 | allow-expect-in-tests = true 3 | allow-unwrap-in-tests = true 4 | allow-dbg-in-tests = true 5 | disallowed-methods = [ 6 | { path = "std::option::Option::map_or", reason = "prefer `map(..).unwrap_or(..)` for legibility" }, 7 | { path = "std::option::Option::map_or_else", reason = "prefer `map(..).unwrap_or_else(..)` for legibility" }, 8 | { path = "std::result::Result::map_or", reason = "prefer `map(..).unwrap_or(..)` for legibility" }, 9 | { path = "std::result::Result::map_or_else", reason = "prefer `map(..).unwrap_or_else(..)` for legibility" }, 10 | { path = "std::iter::Iterator::for_each", reason = "prefer `for` for side-effects" }, 11 | { path = "std::iter::Iterator::try_for_each", reason = "prefer `for` for side-effects" }, 12 | ] 13 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | schedule: [ 3 | 'before 5am on the first day of the month', 4 | ], 5 | semanticCommits: 'enabled', 6 | commitMessageLowerCase: 'never', 7 | configMigration: true, 8 | dependencyDashboard: true, 9 | customManagers: [ 10 | { 11 | customType: 'regex', 12 | managerFilePatterns: [ 13 | '/^rust-toolchain\\.toml$/', 14 | '/Cargo.toml$/', 15 | '/clippy.toml$/', 16 | '/\\.clippy.toml$/', 17 | '/^\\.github/workflows/ci.yml$/', 18 | '/^\\.github/workflows/rust-next.yml$/', 19 | ], 20 | matchStrings: [ 21 | 'STABLE.*?(?\\d+\\.\\d+(\\.\\d+)?)', 22 | '(?\\d+\\.\\d+(\\.\\d+)?).*?STABLE', 23 | ], 24 | depNameTemplate: 'STABLE', 25 | packageNameTemplate: 'rust-lang/rust', 26 | datasourceTemplate: 'github-releases', 27 | }, 28 | ], 29 | packageRules: [ 30 | { 31 | commitMessageTopic: 'Rust Stable', 32 | matchManagers: [ 33 | 'custom.regex', 34 | ], 35 | matchDepNames: [ 36 | 'STABLE', 37 | ], 38 | extractVersion: '^(?\\d+\\.\\d+)', // Drop the patch version 39 | schedule: [ 40 | '* * * * *', 41 | ], 42 | automerge: true, 43 | }, 44 | // Goals: 45 | // - Keep version reqs low, ignoring compatible normal/build dependencies 46 | // - Take advantage of latest dev-dependencies 47 | // - Rollup safe upgrades to reduce CI runner load 48 | // - Help keep number of versions down by always using latest breaking change 49 | // - Have lockfile and manifest in-sync 50 | { 51 | matchManagers: [ 52 | 'cargo', 53 | ], 54 | matchDepTypes: [ 55 | 'build-dependencies', 56 | 'dependencies', 57 | ], 58 | matchCurrentVersion: '>=0.1.0', 59 | matchUpdateTypes: [ 60 | 'patch', 61 | ], 62 | enabled: false, 63 | }, 64 | { 65 | matchManagers: [ 66 | 'cargo', 67 | ], 68 | matchDepTypes: [ 69 | 'build-dependencies', 70 | 'dependencies', 71 | ], 72 | matchCurrentVersion: '>=1.0.0', 73 | matchUpdateTypes: [ 74 | 'minor', 75 | 'patch', 76 | ], 77 | enabled: false, 78 | }, 79 | { 80 | matchManagers: [ 81 | 'cargo', 82 | ], 83 | matchDepTypes: [ 84 | 'dev-dependencies', 85 | ], 86 | matchCurrentVersion: '>=0.1.0', 87 | matchUpdateTypes: [ 88 | 'patch', 89 | ], 90 | automerge: true, 91 | groupName: 'compatible (dev)', 92 | }, 93 | { 94 | matchManagers: [ 95 | 'cargo', 96 | ], 97 | matchDepTypes: [ 98 | 'dev-dependencies', 99 | ], 100 | matchCurrentVersion: '>=1.0.0', 101 | matchUpdateTypes: [ 102 | 'minor', 103 | 'patch', 104 | ], 105 | automerge: true, 106 | groupName: 'compatible (dev)', 107 | }, 108 | ], 109 | } 110 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # These settings are synced to GitHub by https://probot.github.io/apps/settings/ 2 | 3 | repository: 4 | description: "A logging implementation for `log` which is configured via an environment variable." 5 | homepage: "https://docs.rs/env_logger" 6 | topics: "rust logging" 7 | has_issues: true 8 | has_projects: false 9 | has_wiki: false 10 | has_downloads: false 11 | default_branch: main 12 | 13 | # Preference: people do clean commits 14 | allow_merge_commit: true 15 | # Backup in case we need to clean up commits 16 | allow_squash_merge: true 17 | # Not really needed 18 | allow_rebase_merge: false 19 | 20 | allow_auto_merge: true 21 | delete_branch_on_merge: true 22 | 23 | squash_merge_commit_title: "PR_TITLE" 24 | squash_merge_commit_message: "PR_BODY" 25 | merge_commit_message: "PR_BODY" 26 | 27 | # This serves more as documentation. 28 | # Branch protection API was replaced by rulesets but settings isn't updated. 29 | # See https://github.com/repository-settings/app/issues/825 30 | # 31 | # branches: 32 | # - name: main 33 | # protection: 34 | # required_pull_request_reviews: null 35 | # required_conversation_resolution: true 36 | # required_status_checks: 37 | # # Required. Require branches to be up to date before merging. 38 | # strict: false 39 | # contexts: ["CI", "Spell Check with Typos"] 40 | # enforce_admins: false 41 | # restrictions: null 42 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - '**/Cargo.toml' 10 | - '**/Cargo.lock' 11 | push: 12 | branches: 13 | - main 14 | 15 | env: 16 | RUST_BACKTRACE: 1 17 | CARGO_TERM_COLOR: always 18 | CLICOLOR: 1 19 | 20 | concurrency: 21 | group: "${{ github.workflow }}-${{ github.ref }}" 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | security_audit: 26 | permissions: 27 | issues: write # to create issues (actions-rs/audit-check) 28 | checks: write # to create check (actions-rs/audit-check) 29 | runs-on: ubuntu-latest 30 | # Prevent sudden announcement of a new advisory from failing ci: 31 | continue-on-error: true 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | - uses: actions-rs/audit-check@v1 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | cargo_deny: 40 | permissions: 41 | issues: write # to create issues (actions-rs/audit-check) 42 | checks: write # to create check (actions-rs/audit-check) 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | checks: 47 | - bans licenses sources 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: EmbarkStudios/cargo-deny-action@v2 51 | with: 52 | command: check ${{ matrix.checks }} 53 | rust-version: stable 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | 12 | env: 13 | RUST_BACKTRACE: 1 14 | CARGO_TERM_COLOR: always 15 | CLICOLOR: 1 16 | 17 | concurrency: 18 | group: "${{ github.workflow }}-${{ github.ref }}" 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | ci: 23 | permissions: 24 | contents: none 25 | name: CI 26 | needs: [test, msrv, lockfile, docs, rustfmt, clippy, minimal-versions] 27 | runs-on: ubuntu-latest 28 | if: "always()" 29 | steps: 30 | - name: Failed 31 | run: exit 1 32 | if: "contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')" 33 | test: 34 | name: Test 35 | strategy: 36 | matrix: 37 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 38 | rust: ["stable"] 39 | continue-on-error: ${{ matrix.rust != 'stable' }} 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | - name: Install Rust 45 | uses: dtolnay/rust-toolchain@stable 46 | with: 47 | toolchain: ${{ matrix.rust }} 48 | - uses: Swatinem/rust-cache@v2 49 | - uses: taiki-e/install-action@cargo-hack 50 | - name: Build 51 | run: cargo test --workspace --no-run 52 | - name: Test 53 | run: cargo hack test --each-feature --workspace 54 | - name: Run crate example 55 | run: cargo run --example default 56 | msrv: 57 | name: "Check MSRV" 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Checkout repository 61 | uses: actions/checkout@v4 62 | - name: Install Rust 63 | uses: dtolnay/rust-toolchain@stable 64 | with: 65 | toolchain: stable 66 | - uses: Swatinem/rust-cache@v2 67 | - uses: taiki-e/install-action@cargo-hack 68 | - name: Default features 69 | run: cargo hack check --each-feature --locked --rust-version --ignore-private --workspace --all-targets --keep-going 70 | minimal-versions: 71 | name: Minimal versions 72 | runs-on: ubuntu-latest 73 | steps: 74 | - name: Checkout repository 75 | uses: actions/checkout@v4 76 | - name: Install stable Rust 77 | uses: dtolnay/rust-toolchain@stable 78 | with: 79 | toolchain: stable 80 | - name: Install nightly Rust 81 | uses: dtolnay/rust-toolchain@stable 82 | with: 83 | toolchain: nightly 84 | - name: Downgrade dependencies to minimal versions 85 | run: cargo +nightly generate-lockfile -Z minimal-versions 86 | - name: Compile with minimal versions 87 | run: cargo +stable check --workspace --all-features --locked --keep-going 88 | lockfile: 89 | runs-on: ubuntu-latest 90 | steps: 91 | - name: Checkout repository 92 | uses: actions/checkout@v4 93 | - name: Install Rust 94 | uses: dtolnay/rust-toolchain@stable 95 | with: 96 | toolchain: stable 97 | - uses: Swatinem/rust-cache@v2 98 | - name: "Is lockfile updated?" 99 | run: cargo update --workspace --locked 100 | docs: 101 | name: Docs 102 | runs-on: ubuntu-latest 103 | steps: 104 | - name: Checkout repository 105 | uses: actions/checkout@v4 106 | - name: Install Rust 107 | uses: dtolnay/rust-toolchain@stable 108 | with: 109 | toolchain: "1.87" # STABLE 110 | - uses: Swatinem/rust-cache@v2 111 | - name: Check documentation 112 | env: 113 | RUSTDOCFLAGS: -D warnings 114 | run: cargo doc --workspace --all-features --no-deps --document-private-items --keep-going 115 | rustfmt: 116 | name: rustfmt 117 | runs-on: ubuntu-latest 118 | steps: 119 | - name: Checkout repository 120 | uses: actions/checkout@v4 121 | - name: Install Rust 122 | uses: dtolnay/rust-toolchain@stable 123 | with: 124 | toolchain: "1.87" # STABLE 125 | components: rustfmt 126 | - uses: Swatinem/rust-cache@v2 127 | - name: Check formatting 128 | run: cargo fmt --all -- --check 129 | clippy: 130 | name: clippy 131 | runs-on: ubuntu-latest 132 | permissions: 133 | security-events: write # to upload sarif results 134 | steps: 135 | - name: Checkout repository 136 | uses: actions/checkout@v4 137 | - name: Install Rust 138 | uses: dtolnay/rust-toolchain@stable 139 | with: 140 | toolchain: "1.87" # STABLE 141 | components: clippy 142 | - uses: Swatinem/rust-cache@v2 143 | - name: Install SARIF tools 144 | run: cargo install clippy-sarif --locked 145 | - name: Install SARIF tools 146 | run: cargo install sarif-fmt --locked 147 | - name: Check 148 | run: > 149 | cargo clippy --workspace --all-features --all-targets --message-format=json 150 | | clippy-sarif 151 | | tee clippy-results.sarif 152 | | sarif-fmt 153 | continue-on-error: true 154 | - name: Upload 155 | uses: github/codeql-action/upload-sarif@v3 156 | with: 157 | sarif_file: clippy-results.sarif 158 | wait-for-processing: true 159 | - name: Report status 160 | run: cargo clippy --workspace --all-features --all-targets --keep-going -- -D warnings --allow deprecated 161 | coverage: 162 | name: Coverage 163 | runs-on: ubuntu-latest 164 | steps: 165 | - name: Checkout repository 166 | uses: actions/checkout@v4 167 | - name: Install Rust 168 | uses: dtolnay/rust-toolchain@stable 169 | with: 170 | toolchain: stable 171 | - uses: Swatinem/rust-cache@v2 172 | - name: Install cargo-tarpaulin 173 | run: cargo install cargo-tarpaulin 174 | - name: Gather coverage 175 | run: cargo tarpaulin --output-dir coverage --out lcov 176 | - name: Publish to Coveralls 177 | uses: coverallsapp/github-action@master 178 | with: 179 | github-token: ${{ secrets.GITHUB_TOKEN }} 180 | -------------------------------------------------------------------------------- /.github/workflows/committed.yml: -------------------------------------------------------------------------------- 1 | # Not run as part of pre-commit checks because they don't handle sending the correct commit 2 | # range to `committed` 3 | name: Lint Commits 4 | on: [pull_request] 5 | 6 | permissions: 7 | contents: read 8 | 9 | env: 10 | RUST_BACKTRACE: 1 11 | CARGO_TERM_COLOR: always 12 | CLICOLOR: 1 13 | 14 | concurrency: 15 | group: "${{ github.workflow }}-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | committed: 20 | name: Lint Commits 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout Actions Repository 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Lint Commits 28 | uses: crate-ci/committed@master 29 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yml: -------------------------------------------------------------------------------- 1 | name: post-release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | create-release: 11 | permissions: 12 | contents: write # for actions/create-release to create a release 13 | name: create-release 14 | runs-on: ubuntu-latest 15 | outputs: 16 | upload_url: ${{ steps.release.outputs.upload_url }} 17 | release_version: ${{ env.RELEASE_VERSION }} 18 | steps: 19 | - name: Get the release version from the tag 20 | shell: bash 21 | if: env.RELEASE_VERSION == '' 22 | run: | 23 | # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027 24 | echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV 25 | echo "version is: ${{ env.RELEASE_VERSION }}" 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | fetch-depth: 1 30 | - name: Generate Release Notes 31 | run: | 32 | ./.github/workflows/release-notes.py --tag ${{ env.RELEASE_VERSION }} --output notes-${{ env.RELEASE_VERSION }}.md 33 | cat notes-${{ env.RELEASE_VERSION }}.md 34 | - name: Create GitHub release 35 | id: release 36 | uses: actions/create-release@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | tag_name: ${{ env.RELEASE_VERSION }} 41 | release_name: ${{ env.RELEASE_VERSION }} 42 | body_path: notes-${{ env.RELEASE_VERSION }}.md 43 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | permissions: {} # none 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: [main] 9 | 10 | env: 11 | RUST_BACKTRACE: 1 12 | CARGO_TERM_COLOR: always 13 | CLICOLOR: 1 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}-${{ github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pre-commit: 21 | permissions: 22 | contents: read 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-python@v5 27 | with: 28 | python-version: '3.x' 29 | - uses: pre-commit/action@v3.0.1 30 | -------------------------------------------------------------------------------- /.github/workflows/release-notes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import re 5 | import pathlib 6 | import sys 7 | 8 | 9 | _STDIO = pathlib.Path("-") 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument("-i", "--input", type=pathlib.Path, default="CHANGELOG.md") 15 | parser.add_argument("--tag", required=True) 16 | parser.add_argument("-o", "--output", type=pathlib.Path, required=True) 17 | args = parser.parse_args() 18 | 19 | if args.input == _STDIO: 20 | lines = sys.stdin.readlines() 21 | else: 22 | with args.input.open() as fh: 23 | lines = fh.readlines() 24 | version = args.tag.lstrip("v") 25 | 26 | note_lines = [] 27 | for line in lines: 28 | if line.startswith("## ") and version in line: 29 | note_lines.append(line) 30 | elif note_lines and line.startswith("## "): 31 | break 32 | elif note_lines: 33 | note_lines.append(line) 34 | 35 | notes = "".join(note_lines).strip() 36 | if args.output == _STDIO: 37 | print(notes) 38 | else: 39 | args.output.write_text(notes) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /.github/workflows/rust-next.yml: -------------------------------------------------------------------------------- 1 | name: rust-next 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | schedule: 8 | - cron: '5 5 5 * *' 9 | 10 | env: 11 | RUST_BACKTRACE: 1 12 | CARGO_TERM_COLOR: always 13 | CLICOLOR: 1 14 | 15 | concurrency: 16 | group: "${{ github.workflow }}-${{ github.ref }}" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | test: 21 | name: Test 22 | strategy: 23 | matrix: 24 | os: ["ubuntu-latest", "windows-latest", "macos-latest"] 25 | rust: ["stable", "beta"] 26 | include: 27 | - os: ubuntu-latest 28 | rust: "nightly" 29 | continue-on-error: ${{ matrix.rust != 'stable' }} 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | toolchain: ${{ matrix.rust }} 38 | - uses: Swatinem/rust-cache@v2 39 | - uses: taiki-e/install-action@cargo-hack 40 | - name: Build 41 | run: cargo test --workspace --no-run 42 | - name: Test 43 | run: cargo hack test --each-feature --workspace 44 | - name: Run crate example 45 | run: cargo run --example default 46 | latest: 47 | name: "Check latest dependencies" 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v4 52 | - name: Install Rust 53 | uses: dtolnay/rust-toolchain@stable 54 | with: 55 | toolchain: stable 56 | - uses: Swatinem/rust-cache@v2 57 | - uses: taiki-e/install-action@cargo-hack 58 | - name: Update dependencies 59 | run: cargo update 60 | - name: Build 61 | run: cargo test --workspace --no-run 62 | - name: Test 63 | run: cargo hack test --each-feature --workspace 64 | - name: Run crate example 65 | run: cargo run --example default 66 | -------------------------------------------------------------------------------- /.github/workflows/spelling.yml: -------------------------------------------------------------------------------- 1 | name: Spelling 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: [pull_request] 7 | 8 | env: 9 | RUST_BACKTRACE: 1 10 | CARGO_TERM_COLOR: always 11 | CLICOLOR: 1 12 | 13 | concurrency: 14 | group: "${{ github.workflow }}-${{ github.ref }}" 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | spelling: 19 | name: Spell Check with Typos 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout Actions Repository 23 | uses: actions/checkout@v4 24 | - name: Spell Check Repo 25 | uses: crate-ci/typos@master 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_install_hook_types: ["pre-commit", "commit-msg"] 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v5.0.0 5 | hooks: 6 | - id: check-yaml 7 | - id: check-json 8 | - id: check-toml 9 | - id: check-merge-conflict 10 | - id: check-case-conflict 11 | - id: detect-private-key 12 | - repo: https://github.com/crate-ci/typos 13 | rev: v1.32.0 14 | hooks: 15 | - id: typos 16 | - repo: https://github.com/crate-ci/committed 17 | rev: v1.1.7 18 | hooks: 19 | - id: committed 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | ## [Unreleased] - ReleaseDate 9 | 10 | ## [0.11.8] - 2025-04-01 11 | 12 | ### Compatibility 13 | 14 | - *(kv)* Deprecate the `unstable-kv` feature which may be removed in a future patch release 15 | 16 | ### Features 17 | 18 | - *(kv)* Stabilize key-value support behind the `kv` feature 19 | - Expose `ConfigurableFormat` to build custom [`Builder::format`]s that leverage this 20 | 21 | ## [0.11.7] - 2025-03-10 22 | 23 | ### Internal 24 | 25 | - Replaced `humantime` with `jiff` 26 | 27 | ## [0.11.6] - 2024-12-20 28 | 29 | ### Features 30 | 31 | - Opt-in file and line rendering 32 | 33 | ## [0.11.5] - 2024-07-25 34 | 35 | ## [0.11.4] - 2024-07-23 36 | 37 | ## [0.11.3] - 2024-03-05 38 | 39 | ### Features 40 | 41 | - Experimental support for key-value logging behind `unstable-kv` 42 | 43 | ## [0.11.2] - 2024-02-13 44 | 45 | ## [0.11.1] - 2024-01-27 46 | 47 | ### Fixes 48 | 49 | - Allow styling with `Target::Pipe` 50 | 51 | ## [0.11.0] - 2024-01-19 52 | 53 | ### Migration Guide 54 | 55 | **env_logger::fmt::Style:** 56 | The bespoke styling API, behind `color`, was removed, in favor of accepting any 57 | ANSI styled string and adapting it to the target stream's capabilities. 58 | 59 | Possible styling libraries include: 60 | - [anstyle](https://docs.rs/anstyle) is a minimal, runtime string styling API and is re-exported as `env_logger::fmt::style` 61 | - [owo-colors](https://docs.rs/owo-colors) is a feature rich runtime string styling API 62 | - [color-print](https://docs.rs/color-print) for feature-rich compile-time styling API 63 | 64 | [custom_format.rs](https://docs.rs/env_logger/latest/src/custom_format/custom_format.rs.html) 65 | uses `anstyle` via 66 | [`Formatter::default_level_style`](https://docs.rs/env_logger/latest/env_logger/fmt/struct.Formatter.html#method.default_level_style) 67 | 68 | ### Breaking Change 69 | 70 | - Removed bespoke styling API 71 | - `env_logger::fmt::Formatter::style` 72 | - `env_logger::fmt::Formatter::default_styled_level` 73 | - `env_logger::fmt::Style` 74 | - `env_logger::fmt::Color` 75 | - `env_logger::fmt::StyledValue` 76 | - Removed `env_logger::filter` in favor of `env_filter` 77 | 78 | ### Compatibility 79 | 80 | MSRV changed to 1.71 81 | 82 | ### Features 83 | 84 | - Automatically adapt ANSI escape codes in logged messages to the current terminal's capabilities 85 | - Add support for `NO_COLOR` and `CLICOLOR_FORCE`, see https://bixense.com/clicolors/ 86 | 87 | ### Fixes 88 | 89 | - Print colors when `is_test(true)` 90 | 91 | ## [0.10.2] - 2024-01-18 92 | 93 | ### Performance 94 | 95 | - Avoid extra UTF-8 validation performed in some cases 96 | 97 | ### Fixes 98 | 99 | - Ensure custom pipes/stdout get flushed 100 | - Don't panic on broken pipes when `color` is disabled 101 | 102 | ## [0.10.1] - 2023-11-10 103 | 104 | ### Performance 105 | 106 | - Avoid hashing directives and accessing RNG on startup 107 | 108 | ### Documentation 109 | 110 | - Tweak `RUST_LOG` documentation 111 | 112 | ## [0.10.0] - 2022-11-24 113 | 114 | MSRV changed to 1.60 to hide optional dependencies 115 | 116 | ### Fixes 117 | 118 | - Resolved soundness issue by switching from `atty` to `is-terminal` 119 | 120 | ### Breaking Changes 121 | 122 | To open room for changing dependencies: 123 | - Renamed `termcolor` feature to `color` 124 | - Renamed `atty` feature to `auto-color` 125 | 126 | ## [0.9.3] - 2022-11-07 127 | 128 | - Fix a regression from v0.9.2 where env_logger would fail to compile with the termcolor feature turned off. 129 | 130 | ## [0.9.2] - 2022-11-07 131 | 132 | - Fix and un-deprecate Target::Pipe, which was basically not working at all before and deprecated in 0.9.1. 133 | 134 | ## [0.9.0] -- 2022-07-14 135 | 136 | ### Breaking Changes 137 | 138 | - Default message format now prints the target instead of the module 139 | 140 | ### Improvements 141 | 142 | - Added a method to print the module instead of the target 143 | 144 | 145 | [Unreleased]: https://github.com/rust-cli/env_logger/compare/v0.11.8...HEAD 146 | [0.11.8]: https://github.com/rust-cli/env_logger/compare/v0.11.7...v0.11.8 147 | [0.11.7]: https://github.com/rust-cli/env_logger/compare/v0.11.6...v0.11.7 148 | [0.11.6]: https://github.com/rust-cli/env_logger/compare/v0.11.5...v0.11.6 149 | [0.11.5]: https://github.com/rust-cli/env_logger/compare/v0.11.4...v0.11.5 150 | [0.11.4]: https://github.com/rust-cli/env_logger/compare/v0.11.3...v0.11.4 151 | [0.11.3]: https://github.com/rust-cli/env_logger/compare/v0.11.2...v0.11.3 152 | [0.11.2]: https://github.com/rust-cli/env_logger/compare/v0.11.1...v0.11.2 153 | [0.11.1]: https://github.com/rust-cli/env_logger/compare/v0.11.0...v0.11.1 154 | [0.11.0]: https://github.com/rust-cli/env_logger/compare/v0.10.2...v0.11.0 155 | [0.10.2]: https://github.com/rust-cli/env_logger/compare/v0.10.1...v0.10.2 156 | [0.10.1]: https://github.com/rust-cli/env_logger/compare/v0.10.0...v0.10.1 157 | [0.10.0]: https://github.com/rust-cli/env_logger/compare/v0.9.3...v0.10.0 158 | [0.9.3]: https://github.com/rust-cli/env_logger/compare/v0.9.2...v0.9.3 159 | [0.9.2]: https://github.com/rust-cli/env_logger/compare/v0.9.0...v0.9.2 160 | [0.9.0]: https://github.com/rust-cli/env_logger/compare/v0.8.4...v0.9.0 161 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to env_logger 2 | 3 | Thanks for wanting to contribute! There are many ways to contribute and we 4 | appreciate any level you're willing to do. 5 | 6 | ## Feature Requests 7 | 8 | Need some new functionality to help? You can let us know by opening an 9 | [issue][new issue]. It's helpful to look through [all issues][all issues] in 10 | case it's already being talked about. 11 | 12 | ## Bug Reports 13 | 14 | Please let us know about what problems you run into, whether in behavior or 15 | ergonomics of API. You can do this by opening an [issue][new issue]. It's 16 | helpful to look through [all issues][all issues] in case it's already being 17 | talked about. 18 | 19 | ## Pull Requests 20 | 21 | Looking for an idea? Check our [issues][issues]. If the issue looks open ended, 22 | it is probably best to post on the issue how you are thinking of resolving the 23 | issue so you can get feedback early in the process. We want you to be 24 | successful and it can be discouraging to find out a lot of re-work is needed. 25 | 26 | Already have an idea? It might be good to first [create an issue][new issue] 27 | to propose it so we can make sure we are aligned and lower the risk of having 28 | to re-work some of it and the discouragement that goes along with that. 29 | 30 | ### Process 31 | 32 | As a heads up, we'll be running your PR through the following gauntlet: 33 | - warnings turned to compile errors 34 | - `cargo test` 35 | - `rustfmt` 36 | - `clippy` 37 | - `rustdoc` 38 | - [`committed`](https://github.com/crate-ci/committed) as we use [Conventional](https://www.conventionalcommits.org) commit style 39 | - [`typos`](https://github.com/crate-ci/typos) to check spelling 40 | 41 | Not everything can be checked automatically though. 42 | 43 | We request that the commit history gets cleaned up. 44 | 45 | We ask that commits are atomic, meaning they are complete and have a single responsibility. 46 | A complete commit should build, pass tests, update documentation and tests, and not have dead code. 47 | 48 | PRs should tell a cohesive story, with refactor and test commits that keep the 49 | fix or feature commits simple and clear. 50 | 51 | Specifically, we would encourage 52 | - File renames be isolated into their own commit 53 | - Add tests in a commit before their feature or fix, showing the current behavior (i.e. they should pass). 54 | The diff for the feature/fix commit will then show how the behavior changed, 55 | making the commit's intent clearer to reviewers and the community, and showing people that the 56 | test is verifying the expected state. 57 | - e.g. [clap#5520](https://github.com/clap-rs/clap/pull/5520) 58 | 59 | Note that we are talking about ideals. 60 | We understand having a clean history requires more advanced git skills; 61 | feel free to ask us for help! 62 | We might even suggest where it would work to be lax. 63 | We also understand that editing some early commits may cause a lot of churn 64 | with merge conflicts which can make it not worth editing all of the history. 65 | 66 | For code organization, we recommend 67 | - Grouping `impl` blocks next to their type (or trait) 68 | - Grouping private items after the `pub` item that uses them. 69 | - The intent is to help people quickly find the "relevant" details, allowing them to "dig deeper" as needed. Or put another way, the `pub` items serve as a table-of-contents. 70 | - The exact order is fuzzy; do what makes sense 71 | 72 | ## Releasing 73 | 74 | Pre-requisites 75 | - Running `cargo login` 76 | - A member of `rust-cli:Maintainers` 77 | - Push permission to the repo 78 | - [`cargo-release`](https://github.com/crate-ci/cargo-release/) 79 | 80 | When we're ready to release, a project owner should do the following 81 | 1. Update the changelog (see `cargo release changes` for ideas) 82 | 2. Determine what the next version is, according to semver 83 | 3. Run [`cargo release -x `](https://github.com/crate-ci/cargo-release) 84 | 85 | [issues]: https://github.com/rust-cli/env_logger/issues 86 | [new issue]: https://github.com/rust-cli/env_logger/issues/new 87 | [all issues]: https://github.com/rust-cli/env_logger/issues?utf8=%E2%9C%93&q=is%3Aissue 88 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.19" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anstream" 16 | version = "0.6.11" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "utf8parse", 26 | ] 27 | 28 | [[package]] 29 | name = "anstyle" 30 | version = "1.0.6" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 33 | 34 | [[package]] 35 | name = "anstyle-parse" 36 | version = "0.2.3" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 39 | dependencies = [ 40 | "utf8parse", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-query" 45 | version = "1.0.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 48 | dependencies = [ 49 | "windows-sys", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-wincon" 54 | version = "3.0.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 57 | dependencies = [ 58 | "anstyle", 59 | "windows-sys", 60 | ] 61 | 62 | [[package]] 63 | name = "colorchoice" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 67 | 68 | [[package]] 69 | name = "env_filter" 70 | version = "0.1.3" 71 | dependencies = [ 72 | "log", 73 | "regex", 74 | "snapbox", 75 | ] 76 | 77 | [[package]] 78 | name = "env_logger" 79 | version = "0.11.8" 80 | dependencies = [ 81 | "anstream", 82 | "anstyle", 83 | "env_filter", 84 | "jiff", 85 | "log", 86 | ] 87 | 88 | [[package]] 89 | name = "jiff" 90 | version = "0.2.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "5c163c633eb184a4ad2a5e7a5dacf12a58c830d717a7963563d4eceb4ced079f" 93 | dependencies = [ 94 | "jiff-static", 95 | "log", 96 | "portable-atomic", 97 | "portable-atomic-util", 98 | "serde", 99 | ] 100 | 101 | [[package]] 102 | name = "jiff-static" 103 | version = "0.2.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "dbc3e0019b0f5f43038cf46471b1312136f29e36f54436c6042c8f155fec8789" 106 | dependencies = [ 107 | "proc-macro2", 108 | "quote", 109 | "syn", 110 | ] 111 | 112 | [[package]] 113 | name = "log" 114 | version = "0.4.21" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 117 | 118 | [[package]] 119 | name = "memchr" 120 | version = "2.5.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 123 | 124 | [[package]] 125 | name = "normalize-line-endings" 126 | version = "0.3.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 129 | 130 | [[package]] 131 | name = "portable-atomic" 132 | version = "1.11.0" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 135 | 136 | [[package]] 137 | name = "portable-atomic-util" 138 | version = "0.2.4" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 141 | dependencies = [ 142 | "portable-atomic", 143 | ] 144 | 145 | [[package]] 146 | name = "proc-macro2" 147 | version = "1.0.94" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 150 | dependencies = [ 151 | "unicode-ident", 152 | ] 153 | 154 | [[package]] 155 | name = "quote" 156 | version = "1.0.39" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 159 | dependencies = [ 160 | "proc-macro2", 161 | ] 162 | 163 | [[package]] 164 | name = "regex" 165 | version = "1.7.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" 168 | dependencies = [ 169 | "aho-corasick", 170 | "memchr", 171 | "regex-syntax", 172 | ] 173 | 174 | [[package]] 175 | name = "regex-syntax" 176 | version = "0.6.28" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 179 | 180 | [[package]] 181 | name = "serde" 182 | version = "1.0.218" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 185 | dependencies = [ 186 | "serde_derive", 187 | ] 188 | 189 | [[package]] 190 | name = "serde_derive" 191 | version = "1.0.218" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 194 | dependencies = [ 195 | "proc-macro2", 196 | "quote", 197 | "syn", 198 | ] 199 | 200 | [[package]] 201 | name = "similar" 202 | version = "2.5.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" 205 | 206 | [[package]] 207 | name = "snapbox" 208 | version = "0.6.21" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b" 211 | dependencies = [ 212 | "anstream", 213 | "anstyle", 214 | "normalize-line-endings", 215 | "similar", 216 | "snapbox-macros", 217 | ] 218 | 219 | [[package]] 220 | name = "snapbox-macros" 221 | version = "0.3.10" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" 224 | dependencies = [ 225 | "anstream", 226 | ] 227 | 228 | [[package]] 229 | name = "syn" 230 | version = "2.0.99" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" 233 | dependencies = [ 234 | "proc-macro2", 235 | "quote", 236 | "unicode-ident", 237 | ] 238 | 239 | [[package]] 240 | name = "unicode-ident" 241 | version = "1.0.18" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 244 | 245 | [[package]] 246 | name = "utf8parse" 247 | version = "0.2.1" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 250 | 251 | [[package]] 252 | name = "windows-sys" 253 | version = "0.52.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 256 | dependencies = [ 257 | "windows-targets", 258 | ] 259 | 260 | [[package]] 261 | name = "windows-targets" 262 | version = "0.52.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 265 | dependencies = [ 266 | "windows_aarch64_gnullvm", 267 | "windows_aarch64_msvc", 268 | "windows_i686_gnu", 269 | "windows_i686_msvc", 270 | "windows_x86_64_gnu", 271 | "windows_x86_64_gnullvm", 272 | "windows_x86_64_msvc", 273 | ] 274 | 275 | [[package]] 276 | name = "windows_aarch64_gnullvm" 277 | version = "0.52.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 280 | 281 | [[package]] 282 | name = "windows_aarch64_msvc" 283 | version = "0.52.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 286 | 287 | [[package]] 288 | name = "windows_i686_gnu" 289 | version = "0.52.0" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 292 | 293 | [[package]] 294 | name = "windows_i686_msvc" 295 | version = "0.52.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 298 | 299 | [[package]] 300 | name = "windows_x86_64_gnu" 301 | version = "0.52.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 304 | 305 | [[package]] 306 | name = "windows_x86_64_gnullvm" 307 | version = "0.52.0" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 310 | 311 | [[package]] 312 | name = "windows_x86_64_msvc" 313 | version = "0.52.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 316 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/*"] 4 | 5 | [workspace.package] 6 | repository = "https://github.com/rust-cli/env_logger" 7 | license = "MIT OR Apache-2.0" 8 | edition = "2021" 9 | rust-version = "1.71" # MSRV 10 | include = [ 11 | "build.rs", 12 | "src/**/*", 13 | "Cargo.toml", 14 | "Cargo.lock", 15 | "LICENSE*", 16 | "README.md", 17 | "examples/**/*" 18 | ] 19 | 20 | [workspace.lints.rust] 21 | rust_2018_idioms = { level = "warn", priority = -1 } 22 | unnameable_types = "warn" 23 | unreachable_pub = "warn" 24 | unsafe_op_in_unsafe_fn = "warn" 25 | unused_lifetimes = "warn" 26 | unused_macro_rules = "warn" 27 | unused_qualifications = "warn" 28 | 29 | [workspace.lints.clippy] 30 | bool_assert_comparison = "allow" 31 | branches_sharing_code = "allow" 32 | checked_conversions = "warn" 33 | collapsible_else_if = "allow" 34 | create_dir = "warn" 35 | dbg_macro = "warn" 36 | debug_assert_with_mut_call = "warn" 37 | doc_markdown = "warn" 38 | empty_enum = "warn" 39 | enum_glob_use = "warn" 40 | expl_impl_clone_on_copy = "warn" 41 | explicit_deref_methods = "warn" 42 | explicit_into_iter_loop = "warn" 43 | fallible_impl_from = "warn" 44 | filter_map_next = "warn" 45 | flat_map_option = "warn" 46 | float_cmp_const = "warn" 47 | fn_params_excessive_bools = "warn" 48 | from_iter_instead_of_collect = "warn" 49 | if_same_then_else = "allow" 50 | implicit_clone = "warn" 51 | imprecise_flops = "warn" 52 | inconsistent_struct_constructor = "warn" 53 | inefficient_to_string = "warn" 54 | infinite_loop = "warn" 55 | invalid_upcast_comparisons = "warn" 56 | large_digit_groups = "warn" 57 | large_stack_arrays = "warn" 58 | large_types_passed_by_value = "warn" 59 | let_and_return = "allow" # sometimes good to name what you are returning 60 | linkedlist = "warn" 61 | lossy_float_literal = "warn" 62 | macro_use_imports = "warn" 63 | mem_forget = "warn" 64 | mutex_integer = "warn" 65 | needless_continue = "allow" 66 | needless_for_each = "warn" 67 | negative_feature_names = "warn" 68 | path_buf_push_overwrite = "warn" 69 | ptr_as_ptr = "warn" 70 | rc_mutex = "warn" 71 | redundant_feature_names = "warn" 72 | ref_option_ref = "warn" 73 | rest_pat_in_fully_bound_structs = "warn" 74 | result_large_err = "allow" 75 | same_functions_in_if_condition = "warn" 76 | self_named_module_files = "warn" 77 | semicolon_if_nothing_returned = "warn" 78 | str_to_string = "warn" 79 | string_add = "warn" 80 | string_add_assign = "warn" 81 | string_lit_as_bytes = "warn" 82 | string_to_string = "warn" 83 | todo = "warn" 84 | trait_duplication_in_bounds = "warn" 85 | uninlined_format_args = "warn" 86 | verbose_file_reads = "warn" 87 | wildcard_imports = "warn" 88 | zero_sized_map_values = "warn" 89 | 90 | [profile.dev] 91 | panic = "abort" 92 | 93 | [profile.release] 94 | panic = "abort" 95 | codegen-units = 1 96 | lto = true 97 | # debug = "line-tables-only" # requires Cargo 1.71 98 | 99 | [package] 100 | name = "env_logger" 101 | version = "0.11.8" 102 | description = """ 103 | A logging implementation for `log` which is configured via an environment 104 | variable. 105 | """ 106 | categories = ["development-tools::debugging"] 107 | keywords = ["logging", "log", "logger"] 108 | repository.workspace = true 109 | license.workspace = true 110 | edition.workspace = true 111 | rust-version.workspace = true 112 | include.workspace = true 113 | 114 | [package.metadata.docs.rs] 115 | all-features = true 116 | rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] 117 | 118 | [package.metadata.release] 119 | pre-release-replacements = [ 120 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 121 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 122 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 123 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, 124 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-cli/env_logger/compare/{{tag_name}}...HEAD", exactly=1}, 125 | ] 126 | 127 | [features] 128 | default = ["auto-color", "humantime", "regex"] 129 | color = ["dep:anstream", "dep:anstyle"] 130 | auto-color = ["color", "anstream/auto"] 131 | humantime = ["dep:jiff"] 132 | regex = ["env_filter/regex"] 133 | kv = ["log/kv"] 134 | # Deprecated 135 | unstable-kv = ["kv"] 136 | 137 | [dependencies] 138 | log = { version = "0.4.21", features = ["std"] } 139 | env_filter = { version = "0.1.0", path = "crates/env_filter", default-features = false } 140 | jiff = { version = "0.2.3", default-features = false, features = ["std"], optional = true } 141 | anstream = { version = "0.6.11", default-features = false, features = ["wincon"], optional = true } 142 | anstyle = { version = "1.0.6", optional = true } 143 | 144 | [[test]] 145 | name = "regexp_filter" 146 | harness = false 147 | 148 | [[test]] 149 | name = "log-in-log" 150 | harness = false 151 | 152 | [[test]] 153 | name = "log_tls_dtors" 154 | harness = false 155 | 156 | [[test]] 157 | name = "init-twice-retains-filter" 158 | harness = false 159 | 160 | [lints] 161 | workspace = true 162 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) Individual contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # env_logger 2 | 3 | [![crates.io](https://img.shields.io/crates/v/env_logger.svg)](https://crates.io/crates/env_logger) 4 | [![Documentation](https://docs.rs/env_logger/badge.svg)](https://docs.rs/env_logger) 5 | 6 | Implements a logger that can be configured via environment variables. 7 | 8 | ## Usage 9 | 10 | ### In libraries 11 | 12 | `env_logger` makes sense when used in executables (binary projects). Libraries should use the [`log`](https://docs.rs/log) crate instead. 13 | 14 | ### In executables 15 | 16 | It must be added along with `log` to the project dependencies: 17 | 18 | ```console 19 | $ cargo add log env_logger 20 | ``` 21 | 22 | `env_logger` must be initialized as early as possible in the project. After it's initialized, you can use the `log` macros to do actual logging. 23 | 24 | ```rust 25 | use log::info; 26 | 27 | fn main() { 28 | env_logger::init(); 29 | 30 | info!("starting up"); 31 | 32 | // ... 33 | } 34 | ``` 35 | 36 | Then when running the executable, specify a value for the **`RUST_LOG`** 37 | environment variable that corresponds with the log messages you want to show. 38 | 39 | ```bash 40 | $ RUST_LOG=info ./main 41 | [2018-11-03T06:09:06Z INFO default] starting up 42 | ``` 43 | 44 | The letter case is not significant for the logging level names; e.g., `debug`, 45 | `DEBUG`, and `dEbuG` all represent the same logging level. Therefore, the 46 | previous example could also have been written this way, specifying the log 47 | level as `INFO` rather than as `info`: 48 | 49 | ```bash 50 | $ RUST_LOG=INFO ./main 51 | [2018-11-03T06:09:06Z INFO default] starting up 52 | ``` 53 | 54 | So which form should you use? For consistency, our convention is to use lower 55 | case names. Where our docs do use other forms, they do so in the context of 56 | specific examples, so you won't be surprised if you see similar usage in the 57 | wild. 58 | 59 | The log levels that may be specified correspond to the [`log::Level`][level-enum] 60 | enum from the `log` crate. They are: 61 | 62 | * `error` 63 | * `warn` 64 | * `info` 65 | * `debug` 66 | * `trace` 67 | 68 | [level-enum]: https://docs.rs/log/latest/log/enum.Level.html "log::Level (docs.rs)" 69 | 70 | There is also a pseudo logging level, `off`, which may be specified to disable 71 | all logging for a given module or for the entire application. As with the 72 | logging levels, the letter case is not significant. 73 | 74 | `env_logger` can be configured in other ways besides an environment variable. See [the examples](https://github.com/rust-cli/env_logger/tree/main/examples) for more approaches. 75 | 76 | ### In tests 77 | 78 | Tests can use the `env_logger` crate to see log messages generated during that test: 79 | 80 | ```console 81 | $ cargo add log 82 | $ cargo add --dev env_logger 83 | ``` 84 | 85 | ```rust 86 | use log::info; 87 | 88 | fn add_one(num: i32) -> i32 { 89 | info!("add_one called with {}", num); 90 | num + 1 91 | } 92 | 93 | #[cfg(test)] 94 | mod tests { 95 | use super::*; 96 | 97 | fn init() { 98 | let _ = env_logger::builder().is_test(true).try_init(); 99 | } 100 | 101 | #[test] 102 | fn it_adds_one() { 103 | init(); 104 | 105 | info!("can log from the test too"); 106 | assert_eq!(3, add_one(2)); 107 | } 108 | 109 | #[test] 110 | fn it_handles_negative_numbers() { 111 | init(); 112 | 113 | info!("logging from another test"); 114 | assert_eq!(-7, add_one(-8)); 115 | } 116 | } 117 | ``` 118 | 119 | Assuming the module under test is called `my_lib`, running the tests with the 120 | `RUST_LOG` filtering to info messages from this module looks like: 121 | 122 | ```bash 123 | $ RUST_LOG=my_lib=info cargo test 124 | Running target/debug/my_lib-... 125 | 126 | running 2 tests 127 | [INFO my_lib::tests] logging from another test 128 | [INFO my_lib] add_one called with -8 129 | test tests::it_handles_negative_numbers ... ok 130 | [INFO my_lib::tests] can log from the test too 131 | [INFO my_lib] add_one called with 2 132 | test tests::it_adds_one ... ok 133 | 134 | test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured 135 | ``` 136 | 137 | Note that `env_logger::try_init()` needs to be called in each test in which you 138 | want to enable logging. Additionally, the default behavior of tests to 139 | run in parallel means that logging output may be interleaved with test output. 140 | Either run tests in a single thread by specifying `RUST_TEST_THREADS=1` or by 141 | running one test by specifying its name as an argument to the test binaries as 142 | directed by the `cargo test` help docs: 143 | 144 | ```bash 145 | $ RUST_LOG=my_lib=info cargo test it_adds_one 146 | Running target/debug/my_lib-... 147 | 148 | running 1 test 149 | [INFO my_lib::tests] can log from the test too 150 | [INFO my_lib] add_one called with 2 151 | test tests::it_adds_one ... ok 152 | 153 | test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured 154 | ``` 155 | 156 | ## Configuring log target 157 | 158 | By default, `env_logger` logs to stderr. If you want to log to stdout instead, 159 | you can use the `Builder` to change the log target: 160 | 161 | ```rust 162 | use std::env; 163 | use env_logger::{Builder, Target}; 164 | 165 | let mut builder = Builder::from_default_env(); 166 | builder.target(Target::Stdout); 167 | 168 | builder.init(); 169 | ``` 170 | 171 | ## Stability of the default format 172 | 173 | The default format won't optimise for long-term stability, and explicitly makes no guarantees about the stability of its output across major, minor or patch version bumps during `0.x`. 174 | 175 | If you want to capture or interpret the output of `env_logger` programmatically then you should use a custom format. 176 | -------------------------------------------------------------------------------- /committed.toml: -------------------------------------------------------------------------------- 1 | style="conventional" 2 | ignore_author_re="(dependabot|renovate)" 3 | merge_commit = false 4 | -------------------------------------------------------------------------------- /crates/env_filter/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | 8 | ## [Unreleased] - ReleaseDate 9 | 10 | ## [0.1.3] - 2024-12-20 11 | 12 | ## [0.1.2] - 2024-07-25 13 | 14 | ## [0.1.1] - 2024-07-23 15 | 16 | ### Features 17 | 18 | - Added `env_filter::Builder::try_parse(&self, &str)` method (failable version of `env_filter::Builder::parse()`) 19 | 20 | ## [0.1.0] - 2024-01-19 21 | 22 | 23 | [Unreleased]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.3...HEAD 24 | [0.1.3]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.2...env_filter-v0.1.3 25 | [0.1.2]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.1...env_filter-v0.1.2 26 | [0.1.1]: https://github.com/rust-cli/env_logger/compare/env_filter-v0.1.0...env_filter-v0.1.1 27 | [0.1.0]: https://github.com/rust-cli/env_logger/compare/b4a2c304c16d1db4a2998f24c00e00c0f776113b...env_filter-v0.1.0 28 | -------------------------------------------------------------------------------- /crates/env_filter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "env_filter" 3 | version = "0.1.3" 4 | description = """ 5 | Filter log events using environment variables 6 | """ 7 | categories = ["development-tools::debugging"] 8 | keywords = ["logging", "log", "logger"] 9 | repository.workspace = true 10 | license.workspace = true 11 | edition.workspace = true 12 | rust-version.workspace = true 13 | include.workspace = true 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | rustdoc-args = ["--cfg", "docsrs"] 18 | 19 | [package.metadata.release] 20 | pre-release-replacements = [ 21 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, 22 | {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, 23 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, 24 | {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, 25 | {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/rust-cli/env_logger/compare/{{tag_name}}...HEAD", exactly=1}, 26 | ] 27 | 28 | [features] 29 | default = ["regex"] 30 | regex = ["dep:regex"] 31 | 32 | [dependencies] 33 | log = { version = "0.4.8", features = ["std"] } 34 | regex = { version = "1.0.3", optional = true, default-features=false, features=["std", "perf"] } 35 | 36 | [dev-dependencies] 37 | snapbox = "0.6" 38 | 39 | [lints] 40 | workspace = true 41 | -------------------------------------------------------------------------------- /crates/env_filter/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../../LICENSE-APACHE -------------------------------------------------------------------------------- /crates/env_filter/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../../LICENSE-MIT -------------------------------------------------------------------------------- /crates/env_filter/README.md: -------------------------------------------------------------------------------- 1 | # env_filter 2 | 3 | [![crates.io](https://img.shields.io/crates/v/env_filter.svg)](https://crates.io/crates/env_filter) 4 | [![Documentation](https://docs.rs/env_filter/badge.svg)](https://docs.rs/env_filter) 5 | 6 | > Filter log events using environment variables 7 | -------------------------------------------------------------------------------- /crates/env_filter/src/directive.rs: -------------------------------------------------------------------------------- 1 | use log::Level; 2 | use log::LevelFilter; 3 | 4 | #[derive(Debug)] 5 | pub(crate) struct Directive { 6 | pub(crate) name: Option, 7 | pub(crate) level: LevelFilter, 8 | } 9 | 10 | // Check whether a level and target are enabled by the set of directives. 11 | pub(crate) fn enabled(directives: &[Directive], level: Level, target: &str) -> bool { 12 | // Search for the longest match, the vector is assumed to be pre-sorted. 13 | for directive in directives.iter().rev() { 14 | match directive.name { 15 | Some(ref name) if !target.starts_with(&**name) => {} 16 | Some(..) | None => return level <= directive.level, 17 | } 18 | } 19 | false 20 | } 21 | -------------------------------------------------------------------------------- /crates/env_filter/src/filter.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fmt; 3 | use std::mem; 4 | 5 | use log::{LevelFilter, Metadata, Record}; 6 | 7 | use crate::enabled; 8 | use crate::parse_spec; 9 | use crate::parser::ParseResult; 10 | use crate::Directive; 11 | use crate::FilterOp; 12 | use crate::ParseError; 13 | 14 | /// A builder for a log filter. 15 | /// 16 | /// It can be used to parse a set of directives from a string before building 17 | /// a [`Filter`] instance. 18 | /// 19 | /// ## Example 20 | /// 21 | /// ``` 22 | /// # use std::env; 23 | /// use env_filter::Builder; 24 | /// 25 | /// let mut builder = Builder::new(); 26 | /// 27 | /// // Parse a logging filter from an environment variable. 28 | /// if let Ok(rust_log) = env::var("RUST_LOG") { 29 | /// builder.parse(&rust_log); 30 | /// } 31 | /// 32 | /// let filter = builder.build(); 33 | /// ``` 34 | pub struct Builder { 35 | directives: Vec, 36 | filter: Option, 37 | built: bool, 38 | } 39 | 40 | impl Builder { 41 | /// Initializes the filter builder with defaults. 42 | pub fn new() -> Builder { 43 | Builder { 44 | directives: Vec::new(), 45 | filter: None, 46 | built: false, 47 | } 48 | } 49 | 50 | /// Initializes the filter builder from an environment. 51 | pub fn from_env(env: &str) -> Builder { 52 | let mut builder = Builder::new(); 53 | 54 | if let Ok(s) = env::var(env) { 55 | builder.parse(&s); 56 | } 57 | 58 | builder 59 | } 60 | 61 | /// Insert the directive replacing any directive with the same name. 62 | fn insert_directive(&mut self, mut directive: Directive) { 63 | if let Some(pos) = self 64 | .directives 65 | .iter() 66 | .position(|d| d.name == directive.name) 67 | { 68 | mem::swap(&mut self.directives[pos], &mut directive); 69 | } else { 70 | self.directives.push(directive); 71 | } 72 | } 73 | 74 | /// Adds a directive to the filter for a specific module. 75 | pub fn filter_module(&mut self, module: &str, level: LevelFilter) -> &mut Self { 76 | self.filter(Some(module), level) 77 | } 78 | 79 | /// Adds a directive to the filter for all modules. 80 | pub fn filter_level(&mut self, level: LevelFilter) -> &mut Self { 81 | self.filter(None, level) 82 | } 83 | 84 | /// Adds a directive to the filter. 85 | /// 86 | /// The given module (if any) will log at most the specified level provided. 87 | /// If no module is provided then the filter will apply to all log messages. 88 | pub fn filter(&mut self, module: Option<&str>, level: LevelFilter) -> &mut Self { 89 | self.insert_directive(Directive { 90 | name: module.map(|s| s.to_owned()), 91 | level, 92 | }); 93 | self 94 | } 95 | 96 | /// Parses the directives string. 97 | /// 98 | /// See the [Enabling Logging] section for more details. 99 | /// 100 | /// [Enabling Logging]: ../index.html#enabling-logging 101 | pub fn parse(&mut self, filters: &str) -> &mut Self { 102 | #![allow(clippy::print_stderr)] // compatibility 103 | 104 | let ParseResult { 105 | directives, 106 | filter, 107 | errors, 108 | } = parse_spec(filters); 109 | 110 | for error in errors { 111 | eprintln!("warning: {error}, ignoring it"); 112 | } 113 | 114 | self.filter = filter; 115 | 116 | for directive in directives { 117 | self.insert_directive(directive); 118 | } 119 | self 120 | } 121 | 122 | /// Parses the directive string, returning an error if the given directive string is invalid. 123 | /// 124 | /// See the [Enabling Logging] section for more details. 125 | /// 126 | /// [Enabling Logging]: ../index.html#enabling-logging 127 | pub fn try_parse(&mut self, filters: &str) -> Result<&mut Self, ParseError> { 128 | let (directives, filter) = parse_spec(filters).ok()?; 129 | 130 | self.filter = filter; 131 | 132 | for directive in directives { 133 | self.insert_directive(directive); 134 | } 135 | Ok(self) 136 | } 137 | 138 | /// Build a log filter. 139 | pub fn build(&mut self) -> Filter { 140 | assert!(!self.built, "attempt to re-use consumed builder"); 141 | self.built = true; 142 | 143 | let mut directives = Vec::new(); 144 | if self.directives.is_empty() { 145 | // Adds the default filter if none exist 146 | directives.push(Directive { 147 | name: None, 148 | level: LevelFilter::Error, 149 | }); 150 | } else { 151 | // Consume directives. 152 | directives = mem::take(&mut self.directives); 153 | // Sort the directives by length of their name, this allows a 154 | // little more efficient lookup at runtime. 155 | directives.sort_by(|a, b| { 156 | let alen = a.name.as_ref().map(|a| a.len()).unwrap_or(0); 157 | let blen = b.name.as_ref().map(|b| b.len()).unwrap_or(0); 158 | alen.cmp(&blen) 159 | }); 160 | } 161 | 162 | Filter { 163 | directives: mem::take(&mut directives), 164 | filter: mem::take(&mut self.filter), 165 | } 166 | } 167 | } 168 | 169 | impl Default for Builder { 170 | fn default() -> Self { 171 | Builder::new() 172 | } 173 | } 174 | 175 | impl fmt::Debug for Builder { 176 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 177 | if self.built { 178 | f.debug_struct("Filter").field("built", &true).finish() 179 | } else { 180 | f.debug_struct("Filter") 181 | .field("filter", &self.filter) 182 | .field("directives", &self.directives) 183 | .finish() 184 | } 185 | } 186 | } 187 | 188 | /// A log filter. 189 | /// 190 | /// This struct can be used to determine whether or not a log record 191 | /// should be written to the output. 192 | /// Use the [`Builder`] type to parse and construct a `Filter`. 193 | /// 194 | /// [`Builder`]: struct.Builder.html 195 | pub struct Filter { 196 | directives: Vec, 197 | filter: Option, 198 | } 199 | 200 | impl Filter { 201 | /// Returns the maximum `LevelFilter` that this filter instance is 202 | /// configured to output. 203 | /// 204 | /// # Example 205 | /// 206 | /// ```rust 207 | /// use log::LevelFilter; 208 | /// use env_filter::Builder; 209 | /// 210 | /// let mut builder = Builder::new(); 211 | /// builder.filter(Some("module1"), LevelFilter::Info); 212 | /// builder.filter(Some("module2"), LevelFilter::Error); 213 | /// 214 | /// let filter = builder.build(); 215 | /// assert_eq!(filter.filter(), LevelFilter::Info); 216 | /// ``` 217 | pub fn filter(&self) -> LevelFilter { 218 | self.directives 219 | .iter() 220 | .map(|d| d.level) 221 | .max() 222 | .unwrap_or(LevelFilter::Off) 223 | } 224 | 225 | /// Checks if this record matches the configured filter. 226 | pub fn matches(&self, record: &Record<'_>) -> bool { 227 | if !self.enabled(record.metadata()) { 228 | return false; 229 | } 230 | 231 | if let Some(filter) = self.filter.as_ref() { 232 | if !filter.is_match(&record.args().to_string()) { 233 | return false; 234 | } 235 | } 236 | 237 | true 238 | } 239 | 240 | /// Determines if a log message with the specified metadata would be logged. 241 | pub fn enabled(&self, metadata: &Metadata<'_>) -> bool { 242 | let level = metadata.level(); 243 | let target = metadata.target(); 244 | 245 | enabled(&self.directives, level, target) 246 | } 247 | } 248 | 249 | impl fmt::Debug for Filter { 250 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 251 | f.debug_struct("Filter") 252 | .field("filter", &self.filter) 253 | .field("directives", &self.directives) 254 | .finish() 255 | } 256 | } 257 | 258 | #[cfg(test)] 259 | mod tests { 260 | use log::{Level, LevelFilter}; 261 | use snapbox::{assert_data_eq, str}; 262 | 263 | use super::{enabled, Builder, Directive, Filter}; 264 | 265 | fn make_logger_filter(dirs: Vec) -> Filter { 266 | let mut logger = Builder::new().build(); 267 | logger.directives = dirs; 268 | logger 269 | } 270 | 271 | #[test] 272 | fn filter_info() { 273 | let logger = Builder::new().filter(None, LevelFilter::Info).build(); 274 | assert!(enabled(&logger.directives, Level::Info, "crate1")); 275 | assert!(!enabled(&logger.directives, Level::Debug, "crate1")); 276 | } 277 | 278 | #[test] 279 | fn filter_beginning_longest_match() { 280 | let logger = Builder::new() 281 | .filter(Some("crate2"), LevelFilter::Info) 282 | .filter(Some("crate2::mod"), LevelFilter::Debug) 283 | .filter(Some("crate1::mod1"), LevelFilter::Warn) 284 | .build(); 285 | assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); 286 | assert!(!enabled(&logger.directives, Level::Debug, "crate2")); 287 | } 288 | 289 | // Some of our tests are only correct or complete when they cover the full 290 | // universe of variants for log::Level. In the unlikely event that a new 291 | // variant is added in the future, this test will detect the scenario and 292 | // alert us to the need to review and update the tests. In such a 293 | // situation, this test will fail to compile, and the error message will 294 | // look something like this: 295 | // 296 | // error[E0004]: non-exhaustive patterns: `NewVariant` not covered 297 | // --> src/filter/mod.rs:413:15 298 | // | 299 | // 413 | match level_universe { 300 | // | ^^^^^^^^^^^^^^ pattern `NewVariant` not covered 301 | #[test] 302 | fn ensure_tests_cover_level_universe() { 303 | let level_universe: Level = Level::Trace; // use of trace variant is arbitrary 304 | match level_universe { 305 | Level::Error | Level::Warn | Level::Info | Level::Debug | Level::Trace => (), 306 | } 307 | } 308 | 309 | #[test] 310 | fn parse_default() { 311 | let logger = Builder::new().parse("info,crate1::mod1=warn").build(); 312 | assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); 313 | assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); 314 | } 315 | 316 | #[test] 317 | fn parse_default_bare_level_off_lc() { 318 | let logger = Builder::new().parse("off").build(); 319 | assert!(!enabled(&logger.directives, Level::Error, "")); 320 | assert!(!enabled(&logger.directives, Level::Warn, "")); 321 | assert!(!enabled(&logger.directives, Level::Info, "")); 322 | assert!(!enabled(&logger.directives, Level::Debug, "")); 323 | assert!(!enabled(&logger.directives, Level::Trace, "")); 324 | } 325 | 326 | #[test] 327 | fn parse_default_bare_level_off_uc() { 328 | let logger = Builder::new().parse("OFF").build(); 329 | assert!(!enabled(&logger.directives, Level::Error, "")); 330 | assert!(!enabled(&logger.directives, Level::Warn, "")); 331 | assert!(!enabled(&logger.directives, Level::Info, "")); 332 | assert!(!enabled(&logger.directives, Level::Debug, "")); 333 | assert!(!enabled(&logger.directives, Level::Trace, "")); 334 | } 335 | 336 | #[test] 337 | fn parse_default_bare_level_error_lc() { 338 | let logger = Builder::new().parse("error").build(); 339 | assert!(enabled(&logger.directives, Level::Error, "")); 340 | assert!(!enabled(&logger.directives, Level::Warn, "")); 341 | assert!(!enabled(&logger.directives, Level::Info, "")); 342 | assert!(!enabled(&logger.directives, Level::Debug, "")); 343 | assert!(!enabled(&logger.directives, Level::Trace, "")); 344 | } 345 | 346 | #[test] 347 | fn parse_default_bare_level_error_uc() { 348 | let logger = Builder::new().parse("ERROR").build(); 349 | assert!(enabled(&logger.directives, Level::Error, "")); 350 | assert!(!enabled(&logger.directives, Level::Warn, "")); 351 | assert!(!enabled(&logger.directives, Level::Info, "")); 352 | assert!(!enabled(&logger.directives, Level::Debug, "")); 353 | assert!(!enabled(&logger.directives, Level::Trace, "")); 354 | } 355 | 356 | #[test] 357 | fn parse_default_bare_level_warn_lc() { 358 | let logger = Builder::new().parse("warn").build(); 359 | assert!(enabled(&logger.directives, Level::Error, "")); 360 | assert!(enabled(&logger.directives, Level::Warn, "")); 361 | assert!(!enabled(&logger.directives, Level::Info, "")); 362 | assert!(!enabled(&logger.directives, Level::Debug, "")); 363 | assert!(!enabled(&logger.directives, Level::Trace, "")); 364 | } 365 | 366 | #[test] 367 | fn parse_default_bare_level_warn_uc() { 368 | let logger = Builder::new().parse("WARN").build(); 369 | assert!(enabled(&logger.directives, Level::Error, "")); 370 | assert!(enabled(&logger.directives, Level::Warn, "")); 371 | assert!(!enabled(&logger.directives, Level::Info, "")); 372 | assert!(!enabled(&logger.directives, Level::Debug, "")); 373 | assert!(!enabled(&logger.directives, Level::Trace, "")); 374 | } 375 | 376 | #[test] 377 | fn parse_default_bare_level_info_lc() { 378 | let logger = Builder::new().parse("info").build(); 379 | assert!(enabled(&logger.directives, Level::Error, "")); 380 | assert!(enabled(&logger.directives, Level::Warn, "")); 381 | assert!(enabled(&logger.directives, Level::Info, "")); 382 | assert!(!enabled(&logger.directives, Level::Debug, "")); 383 | assert!(!enabled(&logger.directives, Level::Trace, "")); 384 | } 385 | 386 | #[test] 387 | fn parse_default_bare_level_info_uc() { 388 | let logger = Builder::new().parse("INFO").build(); 389 | assert!(enabled(&logger.directives, Level::Error, "")); 390 | assert!(enabled(&logger.directives, Level::Warn, "")); 391 | assert!(enabled(&logger.directives, Level::Info, "")); 392 | assert!(!enabled(&logger.directives, Level::Debug, "")); 393 | assert!(!enabled(&logger.directives, Level::Trace, "")); 394 | } 395 | 396 | #[test] 397 | fn parse_default_bare_level_debug_lc() { 398 | let logger = Builder::new().parse("debug").build(); 399 | assert!(enabled(&logger.directives, Level::Error, "")); 400 | assert!(enabled(&logger.directives, Level::Warn, "")); 401 | assert!(enabled(&logger.directives, Level::Info, "")); 402 | assert!(enabled(&logger.directives, Level::Debug, "")); 403 | assert!(!enabled(&logger.directives, Level::Trace, "")); 404 | } 405 | 406 | #[test] 407 | fn parse_default_bare_level_debug_uc() { 408 | let logger = Builder::new().parse("DEBUG").build(); 409 | assert!(enabled(&logger.directives, Level::Error, "")); 410 | assert!(enabled(&logger.directives, Level::Warn, "")); 411 | assert!(enabled(&logger.directives, Level::Info, "")); 412 | assert!(enabled(&logger.directives, Level::Debug, "")); 413 | assert!(!enabled(&logger.directives, Level::Trace, "")); 414 | } 415 | 416 | #[test] 417 | fn parse_default_bare_level_trace_lc() { 418 | let logger = Builder::new().parse("trace").build(); 419 | assert!(enabled(&logger.directives, Level::Error, "")); 420 | assert!(enabled(&logger.directives, Level::Warn, "")); 421 | assert!(enabled(&logger.directives, Level::Info, "")); 422 | assert!(enabled(&logger.directives, Level::Debug, "")); 423 | assert!(enabled(&logger.directives, Level::Trace, "")); 424 | } 425 | 426 | #[test] 427 | fn parse_default_bare_level_trace_uc() { 428 | let logger = Builder::new().parse("TRACE").build(); 429 | assert!(enabled(&logger.directives, Level::Error, "")); 430 | assert!(enabled(&logger.directives, Level::Warn, "")); 431 | assert!(enabled(&logger.directives, Level::Info, "")); 432 | assert!(enabled(&logger.directives, Level::Debug, "")); 433 | assert!(enabled(&logger.directives, Level::Trace, "")); 434 | } 435 | 436 | // In practice, the desired log level is typically specified by a token 437 | // that is either all lowercase (e.g., 'trace') or all uppercase (.e.g, 438 | // 'TRACE'), but this tests serves as a reminder that 439 | // log::Level::from_str() ignores all case variants. 440 | #[test] 441 | fn parse_default_bare_level_debug_mixed() { 442 | { 443 | let logger = Builder::new().parse("Debug").build(); 444 | assert!(enabled(&logger.directives, Level::Error, "")); 445 | assert!(enabled(&logger.directives, Level::Warn, "")); 446 | assert!(enabled(&logger.directives, Level::Info, "")); 447 | assert!(enabled(&logger.directives, Level::Debug, "")); 448 | assert!(!enabled(&logger.directives, Level::Trace, "")); 449 | } 450 | { 451 | let logger = Builder::new().parse("debuG").build(); 452 | assert!(enabled(&logger.directives, Level::Error, "")); 453 | assert!(enabled(&logger.directives, Level::Warn, "")); 454 | assert!(enabled(&logger.directives, Level::Info, "")); 455 | assert!(enabled(&logger.directives, Level::Debug, "")); 456 | assert!(!enabled(&logger.directives, Level::Trace, "")); 457 | } 458 | { 459 | let logger = Builder::new().parse("deBug").build(); 460 | assert!(enabled(&logger.directives, Level::Error, "")); 461 | assert!(enabled(&logger.directives, Level::Warn, "")); 462 | assert!(enabled(&logger.directives, Level::Info, "")); 463 | assert!(enabled(&logger.directives, Level::Debug, "")); 464 | assert!(!enabled(&logger.directives, Level::Trace, "")); 465 | } 466 | { 467 | let logger = Builder::new().parse("DeBuG").build(); // LaTeX flavor! 468 | assert!(enabled(&logger.directives, Level::Error, "")); 469 | assert!(enabled(&logger.directives, Level::Warn, "")); 470 | assert!(enabled(&logger.directives, Level::Info, "")); 471 | assert!(enabled(&logger.directives, Level::Debug, "")); 472 | assert!(!enabled(&logger.directives, Level::Trace, "")); 473 | } 474 | } 475 | 476 | #[test] 477 | fn try_parse_valid_filter() { 478 | let logger = Builder::new() 479 | .try_parse("info,crate1::mod1=warn") 480 | .expect("valid filter returned error") 481 | .build(); 482 | assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); 483 | assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); 484 | } 485 | 486 | #[test] 487 | fn try_parse_invalid_filter() { 488 | let error = Builder::new().try_parse("info,crate1=invalid").unwrap_err(); 489 | assert_data_eq!( 490 | error, 491 | str!["error parsing logger filter: invalid logging spec 'invalid'"] 492 | ); 493 | } 494 | 495 | #[test] 496 | fn match_full_path() { 497 | let logger = make_logger_filter(vec![ 498 | Directive { 499 | name: Some("crate2".to_owned()), 500 | level: LevelFilter::Info, 501 | }, 502 | Directive { 503 | name: Some("crate1::mod1".to_owned()), 504 | level: LevelFilter::Warn, 505 | }, 506 | ]); 507 | assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); 508 | assert!(!enabled(&logger.directives, Level::Info, "crate1::mod1")); 509 | assert!(enabled(&logger.directives, Level::Info, "crate2")); 510 | assert!(!enabled(&logger.directives, Level::Debug, "crate2")); 511 | } 512 | 513 | #[test] 514 | fn no_match() { 515 | let logger = make_logger_filter(vec![ 516 | Directive { 517 | name: Some("crate2".to_owned()), 518 | level: LevelFilter::Info, 519 | }, 520 | Directive { 521 | name: Some("crate1::mod1".to_owned()), 522 | level: LevelFilter::Warn, 523 | }, 524 | ]); 525 | assert!(!enabled(&logger.directives, Level::Warn, "crate3")); 526 | } 527 | 528 | #[test] 529 | fn match_beginning() { 530 | let logger = make_logger_filter(vec![ 531 | Directive { 532 | name: Some("crate2".to_owned()), 533 | level: LevelFilter::Info, 534 | }, 535 | Directive { 536 | name: Some("crate1::mod1".to_owned()), 537 | level: LevelFilter::Warn, 538 | }, 539 | ]); 540 | assert!(enabled(&logger.directives, Level::Info, "crate2::mod1")); 541 | } 542 | 543 | #[test] 544 | fn match_beginning_longest_match() { 545 | let logger = make_logger_filter(vec![ 546 | Directive { 547 | name: Some("crate2".to_owned()), 548 | level: LevelFilter::Info, 549 | }, 550 | Directive { 551 | name: Some("crate2::mod".to_owned()), 552 | level: LevelFilter::Debug, 553 | }, 554 | Directive { 555 | name: Some("crate1::mod1".to_owned()), 556 | level: LevelFilter::Warn, 557 | }, 558 | ]); 559 | assert!(enabled(&logger.directives, Level::Debug, "crate2::mod1")); 560 | assert!(!enabled(&logger.directives, Level::Debug, "crate2")); 561 | } 562 | 563 | #[test] 564 | fn match_default() { 565 | let logger = make_logger_filter(vec![ 566 | Directive { 567 | name: None, 568 | level: LevelFilter::Info, 569 | }, 570 | Directive { 571 | name: Some("crate1::mod1".to_owned()), 572 | level: LevelFilter::Warn, 573 | }, 574 | ]); 575 | assert!(enabled(&logger.directives, Level::Warn, "crate1::mod1")); 576 | assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); 577 | } 578 | 579 | #[test] 580 | fn zero_level() { 581 | let logger = make_logger_filter(vec![ 582 | Directive { 583 | name: None, 584 | level: LevelFilter::Info, 585 | }, 586 | Directive { 587 | name: Some("crate1::mod1".to_owned()), 588 | level: LevelFilter::Off, 589 | }, 590 | ]); 591 | assert!(!enabled(&logger.directives, Level::Error, "crate1::mod1")); 592 | assert!(enabled(&logger.directives, Level::Info, "crate2::mod2")); 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /crates/env_filter/src/filtered_log.rs: -------------------------------------------------------------------------------- 1 | use log::Log; 2 | 3 | use crate::Filter; 4 | 5 | /// Decorate a [`log::Log`] with record [`Filter`]ing. 6 | /// 7 | /// Records that match the filter will be forwarded to the wrapped log. 8 | /// Other records will be ignored. 9 | #[derive(Debug)] 10 | pub struct FilteredLog { 11 | log: T, 12 | filter: Filter, 13 | } 14 | 15 | impl FilteredLog { 16 | /// Create a new filtered log. 17 | pub fn new(log: T, filter: Filter) -> Self { 18 | Self { log, filter } 19 | } 20 | } 21 | 22 | impl Log for FilteredLog { 23 | /// Determines if a log message with the specified metadata would be logged. 24 | /// 25 | /// For the wrapped log, this returns `true` only if both the filter and the wrapped log return `true`. 26 | fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { 27 | self.filter.enabled(metadata) && self.log.enabled(metadata) 28 | } 29 | 30 | /// Logs the record. 31 | /// 32 | /// Forwards the record to the wrapped log, but only if the record matches the filter. 33 | fn log(&self, record: &log::Record<'_>) { 34 | if self.filter.matches(record) { 35 | self.log.log(record); 36 | } 37 | } 38 | 39 | /// Flushes any buffered records. 40 | /// 41 | /// Forwards directly to the wrapped log. 42 | fn flush(&self) { 43 | self.log.flush(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/env_filter/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Filtering for log records. 2 | //! 3 | //! You can use the [`Filter`] type in your own logger implementation to use the same 4 | //! filter parsing and matching as `env_logger`. 5 | //! 6 | //! ## Using `env_filter` in your own logger 7 | //! 8 | //! You can use `env_filter`'s filtering functionality with your own logger. 9 | //! Call [`Builder::parse`] to parse directives from a string when constructing 10 | //! your logger. Call [`Filter::matches`] to check whether a record should be 11 | //! logged based on the parsed filters when log records are received. 12 | //! 13 | //! ``` 14 | //! use env_filter::Filter; 15 | //! use log::{Log, Metadata, Record}; 16 | //! 17 | //! struct PrintLogger; 18 | //! 19 | //! impl Log for PrintLogger { 20 | //! fn enabled(&self, metadata: &Metadata) -> bool { 21 | //! true 22 | //! } 23 | //! 24 | //! fn log(&self, record: &Record) { 25 | //! println!("{:?}", record); 26 | //! } 27 | //! 28 | //! fn flush(&self) {} 29 | //! } 30 | //! 31 | //! let mut builder = env_filter::Builder::new(); 32 | //! // Parse a directives string from an environment variable 33 | //! if let Ok(ref filter) = std::env::var("MY_LOG_LEVEL") { 34 | //! builder.parse(filter); 35 | //! } 36 | //! 37 | //! let logger = env_filter::FilteredLog::new(PrintLogger, builder.build()); 38 | //! ``` 39 | 40 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 41 | #![warn(missing_docs)] 42 | #![warn(clippy::print_stderr)] 43 | #![warn(clippy::print_stdout)] 44 | 45 | mod directive; 46 | mod filter; 47 | mod filtered_log; 48 | mod op; 49 | mod parser; 50 | 51 | use directive::enabled; 52 | use directive::Directive; 53 | use op::FilterOp; 54 | use parser::parse_spec; 55 | 56 | pub use filter::Builder; 57 | pub use filter::Filter; 58 | pub use filtered_log::FilteredLog; 59 | pub use parser::ParseError; 60 | 61 | #[doc = include_str!("../README.md")] 62 | #[cfg(doctest)] 63 | pub struct ReadmeDoctests; 64 | -------------------------------------------------------------------------------- /crates/env_filter/src/op.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug)] 4 | pub(crate) struct FilterOp { 5 | #[cfg(feature = "regex")] 6 | inner: regex::Regex, 7 | #[cfg(not(feature = "regex"))] 8 | inner: String, 9 | } 10 | 11 | #[cfg(feature = "regex")] 12 | impl FilterOp { 13 | pub(crate) fn new(spec: &str) -> Result { 14 | match regex::Regex::new(spec) { 15 | Ok(r) => Ok(Self { inner: r }), 16 | Err(e) => Err(e.to_string()), 17 | } 18 | } 19 | 20 | pub(crate) fn is_match(&self, s: &str) -> bool { 21 | self.inner.is_match(s) 22 | } 23 | } 24 | 25 | #[cfg(not(feature = "regex"))] 26 | impl FilterOp { 27 | pub fn new(spec: &str) -> Result { 28 | Ok(Self { 29 | inner: spec.to_string(), 30 | }) 31 | } 32 | 33 | pub fn is_match(&self, s: &str) -> bool { 34 | s.contains(&self.inner) 35 | } 36 | } 37 | 38 | impl fmt::Display for FilterOp { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | self.inner.fmt(f) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/env_filter/src/parser.rs: -------------------------------------------------------------------------------- 1 | use log::LevelFilter; 2 | use std::error::Error; 3 | use std::fmt::{Display, Formatter}; 4 | 5 | use crate::Directive; 6 | use crate::FilterOp; 7 | 8 | #[derive(Default, Debug)] 9 | pub(crate) struct ParseResult { 10 | pub(crate) directives: Vec, 11 | pub(crate) filter: Option, 12 | pub(crate) errors: Vec, 13 | } 14 | 15 | impl ParseResult { 16 | fn add_directive(&mut self, directive: Directive) { 17 | self.directives.push(directive); 18 | } 19 | 20 | fn set_filter(&mut self, filter: FilterOp) { 21 | self.filter = Some(filter); 22 | } 23 | 24 | fn add_error(&mut self, message: String) { 25 | self.errors.push(message); 26 | } 27 | 28 | pub(crate) fn ok(self) -> Result<(Vec, Option), ParseError> { 29 | let Self { 30 | directives, 31 | filter, 32 | errors, 33 | } = self; 34 | if let Some(error) = errors.into_iter().next() { 35 | Err(ParseError { details: error }) 36 | } else { 37 | Ok((directives, filter)) 38 | } 39 | } 40 | } 41 | 42 | /// Error during logger directive parsing process. 43 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 44 | pub struct ParseError { 45 | details: String, 46 | } 47 | 48 | impl Display for ParseError { 49 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 50 | write!(f, "error parsing logger filter: {}", self.details) 51 | } 52 | } 53 | 54 | impl Error for ParseError {} 55 | 56 | /// Parse a logging specification string (e.g: `crate1,crate2::mod3,crate3::x=error/foo`) 57 | /// and return a vector with log directives. 58 | pub(crate) fn parse_spec(spec: &str) -> ParseResult { 59 | let mut result = ParseResult::default(); 60 | 61 | let mut parts = spec.split('/'); 62 | let mods = parts.next(); 63 | let filter = parts.next(); 64 | if parts.next().is_some() { 65 | result.add_error(format!("invalid logging spec '{spec}' (too many '/'s)")); 66 | return result; 67 | } 68 | if let Some(m) = mods { 69 | for s in m.split(',').map(|ss| ss.trim()) { 70 | if s.is_empty() { 71 | continue; 72 | } 73 | let mut parts = s.split('='); 74 | let (log_level, name) = 75 | match (parts.next(), parts.next().map(|s| s.trim()), parts.next()) { 76 | (Some(part0), None, None) => { 77 | // if the single argument is a log-level string or number, 78 | // treat that as a global fallback 79 | match part0.parse() { 80 | Ok(num) => (num, None), 81 | Err(_) => (LevelFilter::max(), Some(part0)), 82 | } 83 | } 84 | (Some(part0), Some(""), None) => (LevelFilter::max(), Some(part0)), 85 | (Some(part0), Some(part1), None) => { 86 | if let Ok(num) = part1.parse() { 87 | (num, Some(part0)) 88 | } else { 89 | result.add_error(format!("invalid logging spec '{part1}'")); 90 | continue; 91 | } 92 | } 93 | _ => { 94 | result.add_error(format!("invalid logging spec '{s}'")); 95 | continue; 96 | } 97 | }; 98 | 99 | result.add_directive(Directive { 100 | name: name.map(|s| s.to_owned()), 101 | level: log_level, 102 | }); 103 | } 104 | } 105 | 106 | if let Some(filter) = filter { 107 | match FilterOp::new(filter) { 108 | Ok(filter_op) => result.set_filter(filter_op), 109 | Err(err) => result.add_error(format!("invalid regex filter - {err}")), 110 | } 111 | } 112 | 113 | result 114 | } 115 | 116 | #[cfg(test)] 117 | mod tests { 118 | use crate::ParseError; 119 | use log::LevelFilter; 120 | use snapbox::{assert_data_eq, str, Data, IntoData}; 121 | 122 | use super::{parse_spec, ParseResult}; 123 | 124 | impl IntoData for ParseError { 125 | fn into_data(self) -> Data { 126 | self.to_string().into_data() 127 | } 128 | } 129 | 130 | #[test] 131 | fn parse_spec_valid() { 132 | let ParseResult { 133 | directives: dirs, 134 | filter, 135 | errors, 136 | } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug"); 137 | 138 | assert_eq!(dirs.len(), 3); 139 | assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned())); 140 | assert_eq!(dirs[0].level, LevelFilter::Error); 141 | 142 | assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned())); 143 | assert_eq!(dirs[1].level, LevelFilter::max()); 144 | 145 | assert_eq!(dirs[2].name, Some("crate2".to_owned())); 146 | assert_eq!(dirs[2].level, LevelFilter::Debug); 147 | assert!(filter.is_none()); 148 | 149 | assert!(errors.is_empty()); 150 | } 151 | 152 | #[test] 153 | fn parse_spec_invalid_crate() { 154 | // test parse_spec with multiple = in specification 155 | let ParseResult { 156 | directives: dirs, 157 | filter, 158 | errors, 159 | } = parse_spec("crate1::mod1=warn=info,crate2=debug"); 160 | 161 | assert_eq!(dirs.len(), 1); 162 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 163 | assert_eq!(dirs[0].level, LevelFilter::Debug); 164 | assert!(filter.is_none()); 165 | 166 | assert_eq!(errors.len(), 1); 167 | assert_data_eq!( 168 | &errors[0], 169 | str!["invalid logging spec 'crate1::mod1=warn=info'"] 170 | ); 171 | } 172 | 173 | #[test] 174 | fn parse_spec_invalid_level() { 175 | // test parse_spec with 'noNumber' as log level 176 | let ParseResult { 177 | directives: dirs, 178 | filter, 179 | errors, 180 | } = parse_spec("crate1::mod1=noNumber,crate2=debug"); 181 | 182 | assert_eq!(dirs.len(), 1); 183 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 184 | assert_eq!(dirs[0].level, LevelFilter::Debug); 185 | assert!(filter.is_none()); 186 | 187 | assert_eq!(errors.len(), 1); 188 | assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]); 189 | } 190 | 191 | #[test] 192 | fn parse_spec_string_level() { 193 | // test parse_spec with 'warn' as log level 194 | let ParseResult { 195 | directives: dirs, 196 | filter, 197 | errors, 198 | } = parse_spec("crate1::mod1=wrong,crate2=warn"); 199 | 200 | assert_eq!(dirs.len(), 1); 201 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 202 | assert_eq!(dirs[0].level, LevelFilter::Warn); 203 | assert!(filter.is_none()); 204 | 205 | assert_eq!(errors.len(), 1); 206 | assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]); 207 | } 208 | 209 | #[test] 210 | fn parse_spec_empty_level() { 211 | // test parse_spec with '' as log level 212 | let ParseResult { 213 | directives: dirs, 214 | filter, 215 | errors, 216 | } = parse_spec("crate1::mod1=wrong,crate2="); 217 | 218 | assert_eq!(dirs.len(), 1); 219 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 220 | assert_eq!(dirs[0].level, LevelFilter::max()); 221 | assert!(filter.is_none()); 222 | 223 | assert_eq!(errors.len(), 1); 224 | assert_data_eq!(&errors[0], str!["invalid logging spec 'wrong'"]); 225 | } 226 | 227 | #[test] 228 | fn parse_spec_empty_level_isolated() { 229 | // test parse_spec with "" as log level (and the entire spec str) 230 | let ParseResult { 231 | directives: dirs, 232 | filter, 233 | errors, 234 | } = parse_spec(""); // should be ignored 235 | assert_eq!(dirs.len(), 0); 236 | assert!(filter.is_none()); 237 | assert!(errors.is_empty()); 238 | } 239 | 240 | #[test] 241 | fn parse_spec_blank_level_isolated() { 242 | // test parse_spec with a white-space-only string specified as the log 243 | // level (and the entire spec str) 244 | let ParseResult { 245 | directives: dirs, 246 | filter, 247 | errors, 248 | } = parse_spec(" "); // should be ignored 249 | assert_eq!(dirs.len(), 0); 250 | assert!(filter.is_none()); 251 | assert!(errors.is_empty()); 252 | } 253 | 254 | #[test] 255 | fn parse_spec_blank_level_isolated_comma_only() { 256 | // The spec should contain zero or more comma-separated string slices, 257 | // so a comma-only string should be interpreted as two empty strings 258 | // (which should both be treated as invalid, so ignored). 259 | let ParseResult { 260 | directives: dirs, 261 | filter, 262 | errors, 263 | } = parse_spec(","); // should be ignored 264 | assert_eq!(dirs.len(), 0); 265 | assert!(filter.is_none()); 266 | assert!(errors.is_empty()); 267 | } 268 | 269 | #[test] 270 | fn parse_spec_blank_level_isolated_comma_blank() { 271 | // The spec should contain zero or more comma-separated string slices, 272 | // so this bogus spec should be interpreted as containing one empty 273 | // string and one blank string. Both should both be treated as 274 | // invalid, so ignored. 275 | let ParseResult { 276 | directives: dirs, 277 | filter, 278 | errors, 279 | } = parse_spec(", "); // should be ignored 280 | assert_eq!(dirs.len(), 0); 281 | assert!(filter.is_none()); 282 | assert!(errors.is_empty()); 283 | } 284 | 285 | #[test] 286 | fn parse_spec_blank_level_isolated_blank_comma() { 287 | // The spec should contain zero or more comma-separated string slices, 288 | // so this bogus spec should be interpreted as containing one blank 289 | // string and one empty string. Both should both be treated as 290 | // invalid, so ignored. 291 | let ParseResult { 292 | directives: dirs, 293 | filter, 294 | errors, 295 | } = parse_spec(" ,"); // should be ignored 296 | assert_eq!(dirs.len(), 0); 297 | assert!(filter.is_none()); 298 | assert!(errors.is_empty()); 299 | } 300 | 301 | #[test] 302 | fn parse_spec_global() { 303 | // test parse_spec with no crate 304 | let ParseResult { 305 | directives: dirs, 306 | filter, 307 | errors, 308 | } = parse_spec("warn,crate2=debug"); 309 | assert_eq!(dirs.len(), 2); 310 | assert_eq!(dirs[0].name, None); 311 | assert_eq!(dirs[0].level, LevelFilter::Warn); 312 | assert_eq!(dirs[1].name, Some("crate2".to_owned())); 313 | assert_eq!(dirs[1].level, LevelFilter::Debug); 314 | assert!(filter.is_none()); 315 | assert!(errors.is_empty()); 316 | } 317 | 318 | #[test] 319 | fn parse_spec_global_bare_warn_lc() { 320 | // test parse_spec with no crate, in isolation, all lowercase 321 | let ParseResult { 322 | directives: dirs, 323 | filter, 324 | errors, 325 | } = parse_spec("warn"); 326 | assert_eq!(dirs.len(), 1); 327 | assert_eq!(dirs[0].name, None); 328 | assert_eq!(dirs[0].level, LevelFilter::Warn); 329 | assert!(filter.is_none()); 330 | assert!(errors.is_empty()); 331 | } 332 | 333 | #[test] 334 | fn parse_spec_global_bare_warn_uc() { 335 | // test parse_spec with no crate, in isolation, all uppercase 336 | let ParseResult { 337 | directives: dirs, 338 | filter, 339 | errors, 340 | } = parse_spec("WARN"); 341 | assert_eq!(dirs.len(), 1); 342 | assert_eq!(dirs[0].name, None); 343 | assert_eq!(dirs[0].level, LevelFilter::Warn); 344 | assert!(filter.is_none()); 345 | assert!(errors.is_empty()); 346 | } 347 | 348 | #[test] 349 | fn parse_spec_global_bare_warn_mixed() { 350 | // test parse_spec with no crate, in isolation, mixed case 351 | let ParseResult { 352 | directives: dirs, 353 | filter, 354 | errors, 355 | } = parse_spec("wArN"); 356 | assert_eq!(dirs.len(), 1); 357 | assert_eq!(dirs[0].name, None); 358 | assert_eq!(dirs[0].level, LevelFilter::Warn); 359 | assert!(filter.is_none()); 360 | assert!(errors.is_empty()); 361 | } 362 | 363 | #[test] 364 | fn parse_spec_valid_filter() { 365 | let ParseResult { 366 | directives: dirs, 367 | filter, 368 | errors, 369 | } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug/abc"); 370 | assert_eq!(dirs.len(), 3); 371 | assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned())); 372 | assert_eq!(dirs[0].level, LevelFilter::Error); 373 | 374 | assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned())); 375 | assert_eq!(dirs[1].level, LevelFilter::max()); 376 | 377 | assert_eq!(dirs[2].name, Some("crate2".to_owned())); 378 | assert_eq!(dirs[2].level, LevelFilter::Debug); 379 | assert!(filter.is_some() && filter.unwrap().to_string() == "abc"); 380 | assert!(errors.is_empty()); 381 | } 382 | 383 | #[test] 384 | fn parse_spec_invalid_crate_filter() { 385 | let ParseResult { 386 | directives: dirs, 387 | filter, 388 | errors, 389 | } = parse_spec("crate1::mod1=error=warn,crate2=debug/a.c"); 390 | 391 | assert_eq!(dirs.len(), 1); 392 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 393 | assert_eq!(dirs[0].level, LevelFilter::Debug); 394 | assert!(filter.is_some() && filter.unwrap().to_string() == "a.c"); 395 | 396 | assert_eq!(errors.len(), 1); 397 | assert_data_eq!( 398 | &errors[0], 399 | str!["invalid logging spec 'crate1::mod1=error=warn'"] 400 | ); 401 | } 402 | 403 | #[test] 404 | fn parse_spec_empty_with_filter() { 405 | let ParseResult { 406 | directives: dirs, 407 | filter, 408 | errors, 409 | } = parse_spec("crate1/a*c"); 410 | assert_eq!(dirs.len(), 1); 411 | assert_eq!(dirs[0].name, Some("crate1".to_owned())); 412 | assert_eq!(dirs[0].level, LevelFilter::max()); 413 | assert!(filter.is_some() && filter.unwrap().to_string() == "a*c"); 414 | assert!(errors.is_empty()); 415 | } 416 | 417 | #[test] 418 | fn parse_spec_with_multiple_filters() { 419 | let ParseResult { 420 | directives: dirs, 421 | filter, 422 | errors, 423 | } = parse_spec("debug/abc/a.c"); 424 | assert!(dirs.is_empty()); 425 | assert!(filter.is_none()); 426 | 427 | assert_eq!(errors.len(), 1); 428 | assert_data_eq!( 429 | &errors[0], 430 | str!["invalid logging spec 'debug/abc/a.c' (too many '/'s)"] 431 | ); 432 | } 433 | 434 | #[test] 435 | fn parse_spec_multiple_invalid_crates() { 436 | // test parse_spec with multiple = in specification 437 | let ParseResult { 438 | directives: dirs, 439 | filter, 440 | errors, 441 | } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error"); 442 | 443 | assert_eq!(dirs.len(), 1); 444 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 445 | assert_eq!(dirs[0].level, LevelFilter::Debug); 446 | assert!(filter.is_none()); 447 | 448 | assert_eq!(errors.len(), 2); 449 | assert_data_eq!( 450 | &errors[0], 451 | str!["invalid logging spec 'crate1::mod1=warn=info'"] 452 | ); 453 | assert_data_eq!( 454 | &errors[1], 455 | str!["invalid logging spec 'crate3=error=error'"] 456 | ); 457 | } 458 | 459 | #[test] 460 | fn parse_spec_multiple_invalid_levels() { 461 | // test parse_spec with 'noNumber' as log level 462 | let ParseResult { 463 | directives: dirs, 464 | filter, 465 | errors, 466 | } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid"); 467 | 468 | assert_eq!(dirs.len(), 1); 469 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 470 | assert_eq!(dirs[0].level, LevelFilter::Debug); 471 | assert!(filter.is_none()); 472 | 473 | assert_eq!(errors.len(), 2); 474 | assert_data_eq!(&errors[0], str!["invalid logging spec 'noNumber'"]); 475 | assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]); 476 | } 477 | 478 | #[test] 479 | fn parse_spec_invalid_crate_and_level() { 480 | // test parse_spec with 'noNumber' as log level 481 | let ParseResult { 482 | directives: dirs, 483 | filter, 484 | errors, 485 | } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid"); 486 | 487 | assert_eq!(dirs.len(), 1); 488 | assert_eq!(dirs[0].name, Some("crate2".to_owned())); 489 | assert_eq!(dirs[0].level, LevelFilter::Debug); 490 | assert!(filter.is_none()); 491 | 492 | assert_eq!(errors.len(), 2); 493 | assert_data_eq!( 494 | &errors[0], 495 | str!["invalid logging spec 'crate1::mod1=debug=info'"] 496 | ); 497 | assert_data_eq!(&errors[1], str!["invalid logging spec 'invalid'"]); 498 | } 499 | 500 | #[test] 501 | fn parse_error_message_single_error() { 502 | let error = parse_spec("crate1::mod1=debug=info,crate2=debug") 503 | .ok() 504 | .unwrap_err(); 505 | assert_data_eq!( 506 | error, 507 | str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"] 508 | ); 509 | } 510 | 511 | #[test] 512 | fn parse_error_message_multiple_errors() { 513 | let error = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid") 514 | .ok() 515 | .unwrap_err(); 516 | assert_data_eq!( 517 | error, 518 | str!["error parsing logger filter: invalid logging spec 'crate1::mod1=debug=info'"] 519 | ); 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Note that all fields that take a lint level have these possible values: 2 | # * deny - An error will be produced and the check will fail 3 | # * warn - A warning will be produced, but the check will not fail 4 | # * allow - No warning or error will be produced, though in some cases a note 5 | # will be 6 | 7 | # Root options 8 | 9 | # The graph table configures how the dependency graph is constructed and thus 10 | # which crates the checks are performed against 11 | [graph] 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #"x86_64-unknown-linux-musl", 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | # When creating the dependency graph used as the source of truth when checks are 30 | # executed, this field can be used to prune crates from the graph, removing them 31 | # from the view of cargo-deny. This is an extremely heavy hammer, as if a crate 32 | # is pruned from the graph, all of its dependencies will also be pruned unless 33 | # they are connected to another crate in the graph that hasn't been pruned, 34 | # so it should be used with care. The identifiers are [Package ID Specifications] 35 | # (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) 36 | #exclude = [] 37 | # If true, metadata will be collected with `--all-features`. Note that this can't 38 | # be toggled off if true, if you want to conditionally enable `--all-features` it 39 | # is recommended to pass `--all-features` on the cmd line instead 40 | all-features = false 41 | # If true, metadata will be collected with `--no-default-features`. The same 42 | # caveat with `all-features` applies 43 | no-default-features = false 44 | # If set, these feature will be enabled when collecting metadata. If `--features` 45 | # is specified on the cmd line they will take precedence over this option. 46 | #features = [] 47 | 48 | # The output table provides options for how/if diagnostics are outputted 49 | [output] 50 | # When outputting inclusion graphs in diagnostics that include features, this 51 | # option can be used to specify the depth at which feature edges will be added. 52 | # This option is included since the graphs can be quite large and the addition 53 | # of features from the crate(s) to all of the graph roots can be far too verbose. 54 | # This option can be overridden via `--feature-depth` on the cmd line 55 | feature-depth = 1 56 | 57 | # This section is considered when running `cargo deny check advisories` 58 | # More documentation for the advisories section can be found here: 59 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 60 | [advisories] 61 | # The path where the advisory databases are cloned/fetched into 62 | #db-path = "$CARGO_HOME/advisory-dbs" 63 | # The url(s) of the advisory databases to use 64 | #db-urls = ["https://github.com/rustsec/advisory-db"] 65 | # A list of advisory IDs to ignore. Note that ignored advisories will still 66 | # output a note when they are encountered. 67 | ignore = [ 68 | #"RUSTSEC-0000-0000", 69 | #{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, 70 | #"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish 71 | #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" }, 72 | ] 73 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 74 | # If this is false, then it uses a built-in git library. 75 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 76 | # See Git Authentication for more information about setting up git authentication. 77 | #git-fetch-with-cli = true 78 | 79 | # This section is considered when running `cargo deny check licenses` 80 | # More documentation for the licenses section can be found here: 81 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 82 | [licenses] 83 | # List of explicitly allowed licenses 84 | # See https://spdx.org/licenses/ for list of possible licenses 85 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 86 | allow = [ 87 | "MIT", 88 | "MIT-0", 89 | "Apache-2.0", 90 | "BSD-2-Clause", 91 | "BSD-3-Clause", 92 | "MPL-2.0", 93 | "Unicode-DFS-2016", 94 | "CC0-1.0", 95 | "ISC", 96 | "OpenSSL", 97 | "Zlib", 98 | ] 99 | # The confidence threshold for detecting a license from license text. 100 | # The higher the value, the more closely the license text must be to the 101 | # canonical license text of a valid SPDX license file. 102 | # [possible values: any between 0.0 and 1.0]. 103 | confidence-threshold = 0.8 104 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 105 | # aren't accepted for every possible crate as with the normal allow list 106 | exceptions = [ 107 | # Each entry is the crate and version constraint, and its specific allow 108 | # list 109 | #{ allow = ["Zlib"], crate = "adler32" }, 110 | ] 111 | 112 | # Some crates don't have (easily) machine readable licensing information, 113 | # adding a clarification entry for it allows you to manually specify the 114 | # licensing information 115 | [[licenses.clarify]] 116 | # The package spec the clarification applies to 117 | crate = "ring" 118 | # The SPDX expression for the license requirements of the crate 119 | expression = "MIT AND ISC AND OpenSSL" 120 | # One or more files in the crate's source used as the "source of truth" for 121 | # the license expression. If the contents match, the clarification will be used 122 | # when running the license check, otherwise the clarification will be ignored 123 | # and the crate will be checked normally, which may produce warnings or errors 124 | # depending on the rest of your configuration 125 | license-files = [ 126 | # Each entry is a crate relative path, and the (opaque) hash of its contents 127 | { path = "LICENSE", hash = 0xbd0eed23 } 128 | ] 129 | 130 | [licenses.private] 131 | # If true, ignores workspace crates that aren't published, or are only 132 | # published to private registries. 133 | # To see how to mark a crate as unpublished (to the official registry), 134 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 135 | ignore = true 136 | # One or more private registries that you might publish crates to, if a crate 137 | # is only published to private registries, and ignore is true, the crate will 138 | # not have its license(s) checked 139 | registries = [ 140 | #"https://sekretz.com/registry 141 | ] 142 | 143 | # This section is considered when running `cargo deny check bans`. 144 | # More documentation about the 'bans' section can be found here: 145 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 146 | [bans] 147 | # Lint level for when multiple versions of the same crate are detected 148 | multiple-versions = "warn" 149 | # Lint level for when a crate version requirement is `*` 150 | wildcards = "allow" 151 | # The graph highlighting used when creating dotgraphs for crates 152 | # with multiple versions 153 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 154 | # * simplest-path - The path to the version with the fewest edges is highlighted 155 | # * all - Both lowest-version and simplest-path are used 156 | highlight = "all" 157 | # The default lint level for `default` features for crates that are members of 158 | # the workspace that is being checked. This can be overridden by allowing/denying 159 | # `default` on a crate-by-crate basis if desired. 160 | workspace-default-features = "allow" 161 | # The default lint level for `default` features for external crates that are not 162 | # members of the workspace. This can be overridden by allowing/denying `default` 163 | # on a crate-by-crate basis if desired. 164 | external-default-features = "allow" 165 | # List of crates that are allowed. Use with care! 166 | allow = [ 167 | #"ansi_term@0.11.0", 168 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" }, 169 | ] 170 | # List of crates to deny 171 | deny = [ 172 | #"ansi_term@0.11.0", 173 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, 174 | # Wrapper crates can optionally be specified to allow the crate when it 175 | # is a direct dependency of the otherwise banned crate 176 | #{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, 177 | ] 178 | 179 | # List of features to allow/deny 180 | # Each entry the name of a crate and a version range. If version is 181 | # not specified, all versions will be matched. 182 | #[[bans.features]] 183 | #crate = "reqwest" 184 | # Features to not allow 185 | #deny = ["json"] 186 | # Features to allow 187 | #allow = [ 188 | # "rustls", 189 | # "__rustls", 190 | # "__tls", 191 | # "hyper-rustls", 192 | # "rustls", 193 | # "rustls-pemfile", 194 | # "rustls-tls-webpki-roots", 195 | # "tokio-rustls", 196 | # "webpki-roots", 197 | #] 198 | # If true, the allowed features must exactly match the enabled feature set. If 199 | # this is set there is no point setting `deny` 200 | #exact = true 201 | 202 | # Certain crates/versions that will be skipped when doing duplicate detection. 203 | skip = [ 204 | #"ansi_term@0.11.0", 205 | #{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" }, 206 | ] 207 | # Similarly to `skip` allows you to skip certain crates during duplicate 208 | # detection. Unlike skip, it also includes the entire tree of transitive 209 | # dependencies starting at the specified crate, up to a certain depth, which is 210 | # by default infinite. 211 | skip-tree = [ 212 | #"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies 213 | #{ crate = "ansi_term@0.11.0", depth = 20 }, 214 | ] 215 | 216 | # This section is considered when running `cargo deny check sources`. 217 | # More documentation about the 'sources' section can be found here: 218 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 219 | [sources] 220 | # Lint level for what to happen when a crate from a crate registry that is not 221 | # in the allow list is encountered 222 | unknown-registry = "deny" 223 | # Lint level for what to happen when a crate from a git repository that is not 224 | # in the allow list is encountered 225 | unknown-git = "deny" 226 | # List of URLs for allowed crate registries. Defaults to the crates.io index 227 | # if not specified. If it is specified but empty, no registries are allowed. 228 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 229 | # List of URLs for allowed Git repositories 230 | allow-git = [] 231 | 232 | [sources.allow-org] 233 | # 1 or more github.com organizations to allow git sources for 234 | github = [] 235 | # 1 or more gitlab.com organizations to allow git sources for 236 | gitlab = [] 237 | # 1 or more bitbucket.org organizations to allow git sources for 238 | bitbucket = [] 239 | -------------------------------------------------------------------------------- /examples/custom_default_format.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Disabling parts of the default format. 3 | 4 | Before running this example, try setting the `MY_LOG_LEVEL` environment variable to `info`: 5 | 6 | ```no_run,shell 7 | $ export MY_LOG_LEVEL='info' 8 | ``` 9 | 10 | Also try setting the `MY_LOG_STYLE` environment variable to `never` to disable colors 11 | or `auto` to enable them: 12 | 13 | ```no_run,shell 14 | $ export MY_LOG_STYLE=never 15 | ``` 16 | 17 | If you want to control the logging output completely, see the `custom_logger` example. 18 | */ 19 | 20 | use log::info; 21 | 22 | use env_logger::{Builder, Env}; 23 | 24 | fn init_logger() { 25 | let env = Env::default() 26 | .filter("MY_LOG_LEVEL") 27 | .write_style("MY_LOG_STYLE"); 28 | 29 | Builder::from_env(env) 30 | .format_level(false) 31 | .format_timestamp_nanos() 32 | .init(); 33 | } 34 | 35 | fn main() { 36 | init_logger(); 37 | 38 | info!("a log from `MyLogger`"); 39 | } 40 | -------------------------------------------------------------------------------- /examples/custom_format.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Changing the default logging format. 3 | 4 | Before running this example, try setting the `MY_LOG_LEVEL` environment variable to `info`: 5 | 6 | ```no_run,shell 7 | $ export MY_LOG_LEVEL='info' 8 | ``` 9 | 10 | Also try setting the `MY_LOG_STYLE` environment variable to `never` to disable colors 11 | or `auto` to enable them: 12 | 13 | ```no_run,shell 14 | $ export MY_LOG_STYLE=never 15 | ``` 16 | 17 | If you want to control the logging output completely, see the `custom_logger` example. 18 | */ 19 | 20 | #[cfg(all(feature = "color", feature = "humantime"))] 21 | fn main() { 22 | use env_logger::{Builder, Env}; 23 | 24 | use std::io::Write; 25 | 26 | fn init_logger() { 27 | let env = Env::default() 28 | .filter("MY_LOG_LEVEL") 29 | .write_style("MY_LOG_STYLE"); 30 | 31 | Builder::from_env(env) 32 | .format(|buf, record| { 33 | // We are reusing `anstyle` but there are `anstyle-*` crates to adapt it to your 34 | // preferred styling crate. 35 | let warn_style = buf.default_level_style(log::Level::Warn); 36 | let timestamp = buf.timestamp(); 37 | 38 | writeln!( 39 | buf, 40 | "My formatted log ({timestamp}): {warn_style}{}{warn_style:#}", 41 | record.args() 42 | ) 43 | }) 44 | .init(); 45 | } 46 | 47 | init_logger(); 48 | 49 | log::info!("a log from `MyLogger`"); 50 | } 51 | 52 | #[cfg(not(all(feature = "color", feature = "humantime")))] 53 | fn main() {} 54 | -------------------------------------------------------------------------------- /examples/default.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Using `env_logger`. 3 | 4 | Before running this example, try setting the `MY_LOG_LEVEL` environment variable to `info`: 5 | 6 | ```no_run,shell 7 | $ export MY_LOG_LEVEL='info' 8 | ``` 9 | 10 | Also try setting the `MY_LOG_STYLE` environment variable to `never` to disable colors 11 | or `auto` to enable them: 12 | 13 | ```no_run,shell 14 | $ export MY_LOG_STYLE=never 15 | ``` 16 | */ 17 | 18 | use log::{debug, error, info, trace, warn}; 19 | 20 | use env_logger::Env; 21 | 22 | fn main() { 23 | // The `Env` lets us tweak what the environment 24 | // variables to read are and what the default 25 | // value is if they're missing 26 | let env = Env::default() 27 | .filter_or("MY_LOG_LEVEL", "trace") 28 | .write_style_or("MY_LOG_STYLE", "always"); 29 | 30 | env_logger::init_from_env(env); 31 | 32 | trace!("some trace log"); 33 | debug!("some debug log"); 34 | info!("some information log"); 35 | warn!("some warning log"); 36 | error!("some error log"); 37 | } 38 | -------------------------------------------------------------------------------- /examples/direct_logger.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Using `env_logger::Logger` and the `log::Log` trait directly. 3 | 4 | This example doesn't rely on environment variables, or having a static logger installed. 5 | */ 6 | 7 | use env_logger::{Builder, WriteStyle}; 8 | 9 | use log::{Level, LevelFilter, Log, MetadataBuilder, Record}; 10 | 11 | #[cfg(feature = "kv")] 12 | static KVS: (&str, &str) = ("test", "something"); 13 | 14 | fn record() -> Record<'static> { 15 | let error_metadata = MetadataBuilder::new() 16 | .target("myApp") 17 | .level(Level::Error) 18 | .build(); 19 | 20 | let mut builder = Record::builder(); 21 | builder 22 | .metadata(error_metadata) 23 | .args(format_args!("Error!")) 24 | .line(Some(433)) 25 | .file(Some("app.rs")) 26 | .module_path(Some("server")); 27 | #[cfg(feature = "kv")] 28 | { 29 | builder.key_values(&KVS); 30 | } 31 | builder.build() 32 | } 33 | 34 | fn main() { 35 | let stylish_logger = Builder::new() 36 | .filter(None, LevelFilter::Error) 37 | .write_style(WriteStyle::Always) 38 | .build(); 39 | 40 | let unstylish_logger = Builder::new() 41 | .filter(None, LevelFilter::Error) 42 | .write_style(WriteStyle::Never) 43 | .build(); 44 | 45 | stylish_logger.log(&record()); 46 | unstylish_logger.log(&record()); 47 | } 48 | -------------------------------------------------------------------------------- /examples/filters_from_code.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Specify logging filters in code instead of using an environment variable. 3 | */ 4 | 5 | use env_logger::Builder; 6 | 7 | use log::{debug, error, info, trace, warn, LevelFilter}; 8 | 9 | fn main() { 10 | Builder::new().filter_level(LevelFilter::max()).init(); 11 | 12 | trace!("some trace log"); 13 | debug!("some debug log"); 14 | info!("some information log"); 15 | warn!("some warning log"); 16 | error!("some error log"); 17 | } 18 | -------------------------------------------------------------------------------- /examples/in_tests.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | Using `env_logger` in tests. 3 | 4 | Log events will be captured by `cargo` and only printed if the test fails. 5 | You can run this example by calling: 6 | 7 | ```text 8 | cargo test --example in_tests 9 | ``` 10 | 11 | You should see the `it_does_not_work` test fail and include its log output. 12 | */ 13 | 14 | fn main() {} 15 | 16 | #[cfg(test)] 17 | mod tests { 18 | use log::debug; 19 | 20 | fn init_logger() { 21 | let _ = env_logger::builder() 22 | // Include all events in tests 23 | .filter_level(log::LevelFilter::max()) 24 | // Ensure events are captured by `cargo test` 25 | .is_test(true) 26 | // Ignore errors initializing the logger if tests race to configure it 27 | .try_init(); 28 | } 29 | 30 | #[test] 31 | fn it_works() { 32 | init_logger(); 33 | 34 | let a = 1; 35 | let b = 2; 36 | 37 | debug!("checking whether {} + {} = 3", a, b); 38 | 39 | assert_eq!(3, a + b); 40 | } 41 | 42 | #[test] 43 | fn it_does_not_work() { 44 | init_logger(); 45 | 46 | let a = 1; 47 | let b = 2; 48 | 49 | debug!("checking whether {} + {} = 6", a, b); 50 | 51 | assert_eq!(6, a + b); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/syslog_friendly_format.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | fn main() { 4 | match std::env::var("RUST_LOG_STYLE") { 5 | Ok(s) if s == "SYSTEMD" => env_logger::builder() 6 | .format(|buf, record| { 7 | writeln!( 8 | buf, 9 | "<{}>{}: {}", 10 | match record.level() { 11 | log::Level::Error => 3, 12 | log::Level::Warn => 4, 13 | log::Level::Info => 6, 14 | log::Level::Debug => 7, 15 | log::Level::Trace => 7, 16 | }, 17 | record.target(), 18 | record.args() 19 | ) 20 | }) 21 | .init(), 22 | _ => env_logger::init(), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | owners = ["github:rust-cli:Maintainers"] 2 | dependent-version = "fix" 3 | allow-branch = ["main"] 4 | -------------------------------------------------------------------------------- /src/fmt/humantime.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::SystemTime; 3 | 4 | use crate::fmt::{Formatter, TimestampPrecision}; 5 | 6 | impl Formatter { 7 | /// Get a [`Timestamp`] for the current date and time in UTC. 8 | /// 9 | /// # Examples 10 | /// 11 | /// Include the current timestamp with the log record: 12 | /// 13 | /// ``` 14 | /// use std::io::Write; 15 | /// 16 | /// let mut builder = env_logger::Builder::new(); 17 | /// 18 | /// builder.format(|buf, record| { 19 | /// let ts = buf.timestamp(); 20 | /// 21 | /// writeln!(buf, "{}: {}: {}", ts, record.level(), record.args()) 22 | /// }); 23 | /// ``` 24 | pub fn timestamp(&self) -> Timestamp { 25 | Timestamp { 26 | time: SystemTime::now(), 27 | precision: TimestampPrecision::Seconds, 28 | } 29 | } 30 | 31 | /// Get a [`Timestamp`] for the current date and time in UTC with full 32 | /// second precision. 33 | pub fn timestamp_seconds(&self) -> Timestamp { 34 | Timestamp { 35 | time: SystemTime::now(), 36 | precision: TimestampPrecision::Seconds, 37 | } 38 | } 39 | 40 | /// Get a [`Timestamp`] for the current date and time in UTC with 41 | /// millisecond precision. 42 | pub fn timestamp_millis(&self) -> Timestamp { 43 | Timestamp { 44 | time: SystemTime::now(), 45 | precision: TimestampPrecision::Millis, 46 | } 47 | } 48 | 49 | /// Get a [`Timestamp`] for the current date and time in UTC with 50 | /// microsecond precision. 51 | pub fn timestamp_micros(&self) -> Timestamp { 52 | Timestamp { 53 | time: SystemTime::now(), 54 | precision: TimestampPrecision::Micros, 55 | } 56 | } 57 | 58 | /// Get a [`Timestamp`] for the current date and time in UTC with 59 | /// nanosecond precision. 60 | pub fn timestamp_nanos(&self) -> Timestamp { 61 | Timestamp { 62 | time: SystemTime::now(), 63 | precision: TimestampPrecision::Nanos, 64 | } 65 | } 66 | } 67 | 68 | /// An [RFC3339] formatted timestamp. 69 | /// 70 | /// The timestamp implements [`Display`] and can be written to a [`Formatter`]. 71 | /// 72 | /// [RFC3339]: https://www.ietf.org/rfc/rfc3339.txt 73 | /// [`Display`]: std::fmt::Display 74 | pub struct Timestamp { 75 | time: SystemTime, 76 | precision: TimestampPrecision, 77 | } 78 | 79 | impl fmt::Debug for Timestamp { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | /// A `Debug` wrapper for `Timestamp` that uses the `Display` implementation. 82 | struct TimestampValue<'a>(&'a Timestamp); 83 | 84 | impl fmt::Debug for TimestampValue<'_> { 85 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 86 | fmt::Display::fmt(&self.0, f) 87 | } 88 | } 89 | 90 | f.debug_tuple("Timestamp") 91 | .field(&TimestampValue(self)) 92 | .finish() 93 | } 94 | } 95 | 96 | impl fmt::Display for Timestamp { 97 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 98 | let Ok(ts) = jiff::Timestamp::try_from(self.time) else { 99 | return Err(fmt::Error); 100 | }; 101 | 102 | match self.precision { 103 | TimestampPrecision::Seconds => write!(f, "{ts:.0}"), 104 | TimestampPrecision::Millis => write!(f, "{ts:.3}"), 105 | TimestampPrecision::Micros => write!(f, "{ts:.6}"), 106 | TimestampPrecision::Nanos => write!(f, "{ts:.9}"), 107 | } 108 | } 109 | } 110 | 111 | #[cfg(test)] 112 | mod tests { 113 | use super::Timestamp; 114 | use crate::TimestampPrecision; 115 | 116 | #[test] 117 | fn test_display_timestamp() { 118 | let mut ts = Timestamp { 119 | time: std::time::SystemTime::UNIX_EPOCH, 120 | precision: TimestampPrecision::Nanos, 121 | }; 122 | 123 | assert_eq!("1970-01-01T00:00:00.000000000Z", format!("{ts}")); 124 | 125 | ts.precision = TimestampPrecision::Micros; 126 | assert_eq!("1970-01-01T00:00:00.000000Z", format!("{ts}")); 127 | 128 | ts.precision = TimestampPrecision::Millis; 129 | assert_eq!("1970-01-01T00:00:00.000Z", format!("{ts}")); 130 | 131 | ts.precision = TimestampPrecision::Seconds; 132 | assert_eq!("1970-01-01T00:00:00Z", format!("{ts}")); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/fmt/kv.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | #[cfg(feature = "color")] 4 | use super::WriteStyle; 5 | use super::{Formatter, StyledValue}; 6 | #[cfg(feature = "color")] 7 | use anstyle::Style; 8 | use log::kv::{Error, Key, Source, Value, VisitSource}; 9 | 10 | /// Format function for serializing key/value pairs 11 | /// 12 | /// This function determines how key/value pairs for structured logs are serialized within the default 13 | /// format. 14 | pub(crate) type KvFormatFn = dyn Fn(&mut Formatter, &dyn Source) -> io::Result<()> + Sync + Send; 15 | 16 | /// Null Key Value Format 17 | /// 18 | /// This function is intended to be passed to 19 | /// [`Builder::format_key_values`](crate::Builder::format_key_values). 20 | /// 21 | /// This key value format simply ignores any key/value fields and doesn't include them in the 22 | /// output. 23 | pub fn hidden_kv_format(_formatter: &mut Formatter, _fields: &dyn Source) -> io::Result<()> { 24 | Ok(()) 25 | } 26 | 27 | /// Default Key Value Format 28 | /// 29 | /// This function is intended to be passed to 30 | /// [`Builder::format_key_values`](crate::Builder::format_key_values). 31 | /// 32 | /// This is the default key/value format. Which uses an "=" as the separator between the key and 33 | /// value and a " " between each pair. 34 | /// 35 | /// For example: `ip=127.0.0.1 port=123456 path=/example` 36 | pub fn default_kv_format(formatter: &mut Formatter, fields: &dyn Source) -> io::Result<()> { 37 | fields 38 | .visit(&mut DefaultVisitSource(formatter)) 39 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) 40 | } 41 | 42 | struct DefaultVisitSource<'a>(&'a mut Formatter); 43 | 44 | impl<'kvs> VisitSource<'kvs> for DefaultVisitSource<'_> { 45 | fn visit_pair(&mut self, key: Key<'_>, value: Value<'kvs>) -> Result<(), Error> { 46 | write!(self.0, " {}={}", self.style_key(key), value)?; 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl DefaultVisitSource<'_> { 52 | fn style_key<'k>(&self, text: Key<'k>) -> StyledValue> { 53 | #[cfg(feature = "color")] 54 | { 55 | StyledValue { 56 | style: if self.0.write_style == WriteStyle::Never { 57 | Style::new() 58 | } else { 59 | Style::new().italic() 60 | }, 61 | value: text, 62 | } 63 | } 64 | #[cfg(not(feature = "color"))] 65 | { 66 | text 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/fmt/mod.rs: -------------------------------------------------------------------------------- 1 | //! Formatting for log records. 2 | //! 3 | //! This module contains a [`Formatter`] that can be used to format log records 4 | //! into without needing temporary allocations. Usually you won't need to worry 5 | //! about the contents of this module and can use the `Formatter` like an ordinary 6 | //! [`Write`]. 7 | //! 8 | //! # Formatting log records 9 | //! 10 | //! The format used to print log records can be customised using the [`Builder::format`] 11 | //! method. 12 | //! 13 | //! Terminal styling is done through ANSI escape codes and will be adapted to the capabilities of 14 | //! the target stream.s 15 | //! 16 | //! For example, you could use one of: 17 | //! - [anstyle](https://docs.rs/anstyle) is a minimal, runtime string styling API and is re-exported as [`style`] 18 | //! - [owo-colors](https://docs.rs/owo-colors) is a feature rich runtime string styling API 19 | //! - [color-print](https://docs.rs/color-print) for feature-rich compile-time styling API 20 | //! 21 | //! See also [`Formatter::default_level_style`] 22 | //! 23 | //! ``` 24 | //! use std::io::Write; 25 | //! 26 | //! let mut builder = env_logger::Builder::new(); 27 | //! 28 | //! builder.format(|buf, record| { 29 | //! writeln!(buf, "{}: {}", 30 | //! record.level(), 31 | //! record.args()) 32 | //! }); 33 | //! ``` 34 | //! 35 | //! # Key Value arguments 36 | //! 37 | //! If the `kv` feature is enabled, then the default format will include key values from 38 | //! the log by default, but this can be disabled by calling [`Builder::format_key_values`] 39 | //! with [`hidden_kv_format`] as the format function. 40 | //! 41 | //! The way these keys and values are formatted can also be customized with a separate format 42 | //! function that is called by the default format with [`Builder::format_key_values`]. 43 | //! 44 | //! ``` 45 | //! # #[cfg(feature= "kv")] 46 | //! # { 47 | //! use log::info; 48 | //! env_logger::init(); 49 | //! info!(x="45"; "Some message"); 50 | //! info!(x="12"; "Another message {x}", x="12"); 51 | //! # } 52 | //! ``` 53 | //! 54 | //! See . 55 | //! 56 | //! [`Builder::format`]: crate::Builder::format 57 | //! [`Write`]: std::io::Write 58 | //! [`Builder::format_key_values`]: crate::Builder::format_key_values 59 | 60 | use std::cell::RefCell; 61 | use std::fmt::Display; 62 | use std::io::prelude::Write; 63 | use std::rc::Rc; 64 | use std::{fmt, io, mem}; 65 | 66 | #[cfg(feature = "color")] 67 | use log::Level; 68 | use log::Record; 69 | 70 | #[cfg(feature = "humantime")] 71 | mod humantime; 72 | #[cfg(feature = "kv")] 73 | mod kv; 74 | 75 | #[cfg(feature = "color")] 76 | pub use anstyle as style; 77 | 78 | #[cfg(feature = "humantime")] 79 | pub use self::humantime::Timestamp; 80 | #[cfg(feature = "kv")] 81 | pub use self::kv::*; 82 | pub use crate::writer::Target; 83 | pub use crate::writer::WriteStyle; 84 | 85 | use crate::writer::{Buffer, Writer}; 86 | 87 | /// Formatting precision of timestamps. 88 | /// 89 | /// Seconds give precision of full seconds, milliseconds give thousands of a 90 | /// second (3 decimal digits), microseconds are millionth of a second (6 decimal 91 | /// digits) and nanoseconds are billionth of a second (9 decimal digits). 92 | #[allow(clippy::exhaustive_enums)] // compatibility 93 | #[derive(Copy, Clone, Debug)] 94 | pub enum TimestampPrecision { 95 | /// Full second precision (0 decimal digits) 96 | Seconds, 97 | /// Millisecond precision (3 decimal digits) 98 | Millis, 99 | /// Microsecond precision (6 decimal digits) 100 | Micros, 101 | /// Nanosecond precision (9 decimal digits) 102 | Nanos, 103 | } 104 | 105 | /// The default timestamp precision is seconds. 106 | impl Default for TimestampPrecision { 107 | fn default() -> Self { 108 | TimestampPrecision::Seconds 109 | } 110 | } 111 | 112 | /// A formatter to write logs into. 113 | /// 114 | /// `Formatter` implements the standard [`Write`] trait for writing log records. 115 | /// It also supports terminal styling using ANSI escape codes. 116 | /// 117 | /// # Examples 118 | /// 119 | /// Use the [`writeln`] macro to format a log record. 120 | /// An instance of a `Formatter` is passed to an `env_logger` format as `buf`: 121 | /// 122 | /// ``` 123 | /// use std::io::Write; 124 | /// 125 | /// let mut builder = env_logger::Builder::new(); 126 | /// 127 | /// builder.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args())); 128 | /// ``` 129 | /// 130 | /// [`Write`]: std::io::Write 131 | /// [`writeln`]: std::writeln 132 | pub struct Formatter { 133 | buf: Rc>, 134 | write_style: WriteStyle, 135 | } 136 | 137 | impl Formatter { 138 | pub(crate) fn new(writer: &Writer) -> Self { 139 | Formatter { 140 | buf: Rc::new(RefCell::new(writer.buffer())), 141 | write_style: writer.write_style(), 142 | } 143 | } 144 | 145 | pub(crate) fn write_style(&self) -> WriteStyle { 146 | self.write_style 147 | } 148 | 149 | pub(crate) fn print(&self, writer: &Writer) -> io::Result<()> { 150 | writer.print(&self.buf.borrow()) 151 | } 152 | 153 | pub(crate) fn clear(&mut self) { 154 | self.buf.borrow_mut().clear(); 155 | } 156 | } 157 | 158 | #[cfg(feature = "color")] 159 | impl Formatter { 160 | /// Get the default [`style::Style`] for the given level. 161 | /// 162 | /// The style can be used to print other values besides the level. 163 | /// 164 | /// See [`style`] for how to adapt it to the styling crate of your choice 165 | pub fn default_level_style(&self, level: Level) -> style::Style { 166 | if self.write_style == WriteStyle::Never { 167 | style::Style::new() 168 | } else { 169 | match level { 170 | Level::Trace => style::AnsiColor::Cyan.on_default(), 171 | Level::Debug => style::AnsiColor::Blue.on_default(), 172 | Level::Info => style::AnsiColor::Green.on_default(), 173 | Level::Warn => style::AnsiColor::Yellow.on_default(), 174 | Level::Error => style::AnsiColor::Red 175 | .on_default() 176 | .effects(style::Effects::BOLD), 177 | } 178 | } 179 | } 180 | } 181 | 182 | impl Write for Formatter { 183 | fn write(&mut self, buf: &[u8]) -> io::Result { 184 | self.buf.borrow_mut().write(buf) 185 | } 186 | 187 | fn flush(&mut self) -> io::Result<()> { 188 | self.buf.borrow_mut().flush() 189 | } 190 | } 191 | 192 | impl fmt::Debug for Formatter { 193 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 194 | let buf = self.buf.borrow(); 195 | f.debug_struct("Formatter") 196 | .field("buf", &buf) 197 | .field("write_style", &self.write_style) 198 | .finish() 199 | } 200 | } 201 | 202 | pub(crate) trait RecordFormat { 203 | fn format(&self, formatter: &mut Formatter, record: &Record<'_>) -> io::Result<()>; 204 | } 205 | 206 | impl RecordFormat for F 207 | where 208 | F: Fn(&mut Formatter, &Record<'_>) -> io::Result<()>, 209 | { 210 | fn format(&self, formatter: &mut Formatter, record: &Record<'_>) -> io::Result<()> { 211 | (self)(formatter, record) 212 | } 213 | } 214 | 215 | pub(crate) type FormatFn = Box; 216 | 217 | #[derive(Default)] 218 | pub(crate) struct Builder { 219 | pub(crate) default_format: ConfigurableFormat, 220 | pub(crate) custom_format: Option, 221 | built: bool, 222 | } 223 | 224 | impl Builder { 225 | /// Convert the format into a callable function. 226 | /// 227 | /// If the `custom_format` is `Some`, then any `default_format` switches are ignored. 228 | /// If the `custom_format` is `None`, then a default format is returned. 229 | /// Any `default_format` switches set to `false` won't be written by the format. 230 | pub(crate) fn build(&mut self) -> FormatFn { 231 | assert!(!self.built, "attempt to re-use consumed builder"); 232 | 233 | let built = mem::replace( 234 | self, 235 | Builder { 236 | built: true, 237 | ..Default::default() 238 | }, 239 | ); 240 | 241 | if let Some(fmt) = built.custom_format { 242 | fmt 243 | } else { 244 | Box::new(built.default_format) 245 | } 246 | } 247 | } 248 | 249 | #[cfg(feature = "color")] 250 | type SubtleStyle = StyledValue<&'static str>; 251 | #[cfg(not(feature = "color"))] 252 | type SubtleStyle = &'static str; 253 | 254 | /// A value that can be printed using the given styles. 255 | #[cfg(feature = "color")] 256 | struct StyledValue { 257 | style: style::Style, 258 | value: T, 259 | } 260 | 261 | #[cfg(feature = "color")] 262 | impl Display for StyledValue { 263 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 264 | let style = self.style; 265 | 266 | // We need to make sure `f`s settings don't get passed onto the styling but do get passed 267 | // to the value 268 | write!(f, "{style}")?; 269 | self.value.fmt(f)?; 270 | write!(f, "{style:#}")?; 271 | Ok(()) 272 | } 273 | } 274 | 275 | #[cfg(not(feature = "color"))] 276 | type StyledValue = T; 277 | 278 | /// A [custom format][crate::Builder::format] with settings for which fields to show 279 | pub struct ConfigurableFormat { 280 | // This format needs to work with any combination of crate features. 281 | pub(crate) timestamp: Option, 282 | pub(crate) module_path: bool, 283 | pub(crate) target: bool, 284 | pub(crate) level: bool, 285 | pub(crate) source_file: bool, 286 | pub(crate) source_line_number: bool, 287 | pub(crate) indent: Option, 288 | pub(crate) suffix: &'static str, 289 | #[cfg(feature = "kv")] 290 | pub(crate) kv_format: Option>, 291 | } 292 | 293 | impl ConfigurableFormat { 294 | /// Format the [`Record`] as configured for outputting 295 | pub fn format(&self, formatter: &mut Formatter, record: &Record<'_>) -> io::Result<()> { 296 | let fmt = ConfigurableFormatWriter { 297 | format: self, 298 | buf: formatter, 299 | written_header_value: false, 300 | }; 301 | 302 | fmt.write(record) 303 | } 304 | } 305 | 306 | impl ConfigurableFormat { 307 | /// Whether or not to write the level in the default format. 308 | pub fn level(&mut self, write: bool) -> &mut Self { 309 | self.level = write; 310 | self 311 | } 312 | 313 | /// Whether or not to write the source file path in the default format. 314 | pub fn file(&mut self, write: bool) -> &mut Self { 315 | self.source_file = write; 316 | self 317 | } 318 | 319 | /// Whether or not to write the source line number path in the default format. 320 | /// 321 | /// Only has effect if `format_file` is also enabled 322 | pub fn line_number(&mut self, write: bool) -> &mut Self { 323 | self.source_line_number = write; 324 | self 325 | } 326 | 327 | /// Whether or not to write the module path in the default format. 328 | pub fn module_path(&mut self, write: bool) -> &mut Self { 329 | self.module_path = write; 330 | self 331 | } 332 | 333 | /// Whether or not to write the target in the default format. 334 | pub fn target(&mut self, write: bool) -> &mut Self { 335 | self.target = write; 336 | self 337 | } 338 | 339 | /// Configures the amount of spaces to use to indent multiline log records. 340 | /// A value of `None` disables any kind of indentation. 341 | pub fn indent(&mut self, indent: Option) -> &mut Self { 342 | self.indent = indent; 343 | self 344 | } 345 | 346 | /// Configures if timestamp should be included and in what precision. 347 | pub fn timestamp(&mut self, timestamp: Option) -> &mut Self { 348 | self.timestamp = timestamp; 349 | self 350 | } 351 | 352 | /// Configures the end of line suffix. 353 | pub fn suffix(&mut self, suffix: &'static str) -> &mut Self { 354 | self.suffix = suffix; 355 | self 356 | } 357 | 358 | /// Set the format for structured key/value pairs in the log record 359 | /// 360 | /// With the default format, this function is called for each record and should format 361 | /// the structured key-value pairs as returned by [`log::Record::key_values`]. 362 | /// 363 | /// The format function is expected to output the string directly to the `Formatter` so that 364 | /// implementations can use the [`std::fmt`] macros, similar to the main format function. 365 | /// 366 | /// The default format uses a space to separate each key-value pair, with an "=" between 367 | /// the key and value. 368 | #[cfg(feature = "kv")] 369 | pub fn key_values(&mut self, format: F) -> &mut Self 370 | where 371 | F: Fn(&mut Formatter, &dyn log::kv::Source) -> io::Result<()> + Sync + Send + 'static, 372 | { 373 | self.kv_format = Some(Box::new(format)); 374 | self 375 | } 376 | } 377 | 378 | impl Default for ConfigurableFormat { 379 | fn default() -> Self { 380 | Self { 381 | timestamp: Some(Default::default()), 382 | module_path: false, 383 | target: true, 384 | level: true, 385 | source_file: false, 386 | source_line_number: false, 387 | indent: Some(4), 388 | suffix: "\n", 389 | #[cfg(feature = "kv")] 390 | kv_format: None, 391 | } 392 | } 393 | } 394 | 395 | impl RecordFormat for ConfigurableFormat { 396 | fn format(&self, formatter: &mut Formatter, record: &Record<'_>) -> io::Result<()> { 397 | self.format(formatter, record) 398 | } 399 | } 400 | 401 | /// The default format. 402 | /// 403 | /// This format needs to work with any combination of crate features. 404 | struct ConfigurableFormatWriter<'a> { 405 | format: &'a ConfigurableFormat, 406 | buf: &'a mut Formatter, 407 | written_header_value: bool, 408 | } 409 | 410 | impl ConfigurableFormatWriter<'_> { 411 | fn write(mut self, record: &Record<'_>) -> io::Result<()> { 412 | self.write_timestamp()?; 413 | self.write_level(record)?; 414 | self.write_module_path(record)?; 415 | self.write_source_location(record)?; 416 | self.write_target(record)?; 417 | self.finish_header()?; 418 | 419 | self.write_args(record)?; 420 | #[cfg(feature = "kv")] 421 | self.write_kv(record)?; 422 | write!(self.buf, "{}", self.format.suffix) 423 | } 424 | 425 | fn subtle_style(&self, text: &'static str) -> SubtleStyle { 426 | #[cfg(feature = "color")] 427 | { 428 | StyledValue { 429 | style: if self.buf.write_style == WriteStyle::Never { 430 | style::Style::new() 431 | } else { 432 | style::AnsiColor::BrightBlack.on_default() 433 | }, 434 | value: text, 435 | } 436 | } 437 | #[cfg(not(feature = "color"))] 438 | { 439 | text 440 | } 441 | } 442 | 443 | fn write_header_value(&mut self, value: T) -> io::Result<()> 444 | where 445 | T: Display, 446 | { 447 | if !self.written_header_value { 448 | self.written_header_value = true; 449 | 450 | let open_brace = self.subtle_style("["); 451 | write!(self.buf, "{open_brace}{value}") 452 | } else { 453 | write!(self.buf, " {value}") 454 | } 455 | } 456 | 457 | fn write_level(&mut self, record: &Record<'_>) -> io::Result<()> { 458 | if !self.format.level { 459 | return Ok(()); 460 | } 461 | 462 | let level = { 463 | let level = record.level(); 464 | #[cfg(feature = "color")] 465 | { 466 | StyledValue { 467 | style: self.buf.default_level_style(level), 468 | value: level, 469 | } 470 | } 471 | #[cfg(not(feature = "color"))] 472 | { 473 | level 474 | } 475 | }; 476 | 477 | self.write_header_value(format_args!("{level:<5}")) 478 | } 479 | 480 | fn write_timestamp(&mut self) -> io::Result<()> { 481 | #[cfg(feature = "humantime")] 482 | { 483 | use self::TimestampPrecision::{Micros, Millis, Nanos, Seconds}; 484 | let ts = match self.format.timestamp { 485 | None => return Ok(()), 486 | Some(Seconds) => self.buf.timestamp_seconds(), 487 | Some(Millis) => self.buf.timestamp_millis(), 488 | Some(Micros) => self.buf.timestamp_micros(), 489 | Some(Nanos) => self.buf.timestamp_nanos(), 490 | }; 491 | 492 | self.write_header_value(ts) 493 | } 494 | #[cfg(not(feature = "humantime"))] 495 | { 496 | // Trick the compiler to think we have used self.timestamp 497 | // Workaround for "field is never used: `timestamp`" compiler nag. 498 | let _ = self.format.timestamp; 499 | Ok(()) 500 | } 501 | } 502 | 503 | fn write_module_path(&mut self, record: &Record<'_>) -> io::Result<()> { 504 | if !self.format.module_path { 505 | return Ok(()); 506 | } 507 | 508 | if let Some(module_path) = record.module_path() { 509 | self.write_header_value(module_path) 510 | } else { 511 | Ok(()) 512 | } 513 | } 514 | 515 | fn write_source_location(&mut self, record: &Record<'_>) -> io::Result<()> { 516 | if !self.format.source_file { 517 | return Ok(()); 518 | } 519 | 520 | if let Some(file_path) = record.file() { 521 | let line = self 522 | .format 523 | .source_line_number 524 | .then(|| record.line()) 525 | .flatten(); 526 | match line { 527 | Some(line) => self.write_header_value(format_args!("{file_path}:{line}")), 528 | None => self.write_header_value(file_path), 529 | } 530 | } else { 531 | Ok(()) 532 | } 533 | } 534 | 535 | fn write_target(&mut self, record: &Record<'_>) -> io::Result<()> { 536 | if !self.format.target { 537 | return Ok(()); 538 | } 539 | 540 | match record.target() { 541 | "" => Ok(()), 542 | target => self.write_header_value(target), 543 | } 544 | } 545 | 546 | fn finish_header(&mut self) -> io::Result<()> { 547 | if self.written_header_value { 548 | let close_brace = self.subtle_style("]"); 549 | write!(self.buf, "{close_brace} ") 550 | } else { 551 | Ok(()) 552 | } 553 | } 554 | 555 | fn write_args(&mut self, record: &Record<'_>) -> io::Result<()> { 556 | match self.format.indent { 557 | // Fast path for no indentation 558 | None => write!(self.buf, "{}", record.args()), 559 | 560 | Some(indent_count) => { 561 | // Create a wrapper around the buffer only if we have to actually indent the message 562 | 563 | struct IndentWrapper<'a, 'b> { 564 | fmt: &'a mut ConfigurableFormatWriter<'b>, 565 | indent_count: usize, 566 | } 567 | 568 | impl Write for IndentWrapper<'_, '_> { 569 | fn write(&mut self, buf: &[u8]) -> io::Result { 570 | let mut first = true; 571 | for chunk in buf.split(|&x| x == b'\n') { 572 | if !first { 573 | write!( 574 | self.fmt.buf, 575 | "{}{:width$}", 576 | self.fmt.format.suffix, 577 | "", 578 | width = self.indent_count 579 | )?; 580 | } 581 | self.fmt.buf.write_all(chunk)?; 582 | first = false; 583 | } 584 | 585 | Ok(buf.len()) 586 | } 587 | 588 | fn flush(&mut self) -> io::Result<()> { 589 | self.fmt.buf.flush() 590 | } 591 | } 592 | 593 | // The explicit scope here is just to make older versions of Rust happy 594 | { 595 | let mut wrapper = IndentWrapper { 596 | fmt: self, 597 | indent_count, 598 | }; 599 | write!(wrapper, "{}", record.args())?; 600 | } 601 | 602 | Ok(()) 603 | } 604 | } 605 | } 606 | 607 | #[cfg(feature = "kv")] 608 | fn write_kv(&mut self, record: &Record<'_>) -> io::Result<()> { 609 | let format = self 610 | .format 611 | .kv_format 612 | .as_deref() 613 | .unwrap_or(&default_kv_format); 614 | format(self.buf, record.key_values()) 615 | } 616 | } 617 | 618 | #[cfg(test)] 619 | mod tests { 620 | use super::*; 621 | 622 | use log::{Level, Record}; 623 | 624 | fn write_record(record: Record<'_>, fmt: ConfigurableFormatWriter<'_>) -> String { 625 | let buf = fmt.buf.buf.clone(); 626 | 627 | fmt.write(&record).expect("failed to write record"); 628 | 629 | let buf = buf.borrow(); 630 | String::from_utf8(buf.as_bytes().to_vec()).expect("failed to read record") 631 | } 632 | 633 | fn write_target(target: &str, fmt: ConfigurableFormatWriter<'_>) -> String { 634 | write_record( 635 | Record::builder() 636 | .args(format_args!("log\nmessage")) 637 | .level(Level::Info) 638 | .file(Some("test.rs")) 639 | .line(Some(144)) 640 | .module_path(Some("test::path")) 641 | .target(target) 642 | .build(), 643 | fmt, 644 | ) 645 | } 646 | 647 | fn write(fmt: ConfigurableFormatWriter<'_>) -> String { 648 | write_target("", fmt) 649 | } 650 | 651 | fn formatter() -> Formatter { 652 | let writer = crate::writer::Builder::new() 653 | .write_style(WriteStyle::Never) 654 | .build(); 655 | 656 | Formatter::new(&writer) 657 | } 658 | 659 | #[test] 660 | fn format_with_header() { 661 | let mut f = formatter(); 662 | 663 | let written = write(ConfigurableFormatWriter { 664 | format: &ConfigurableFormat { 665 | timestamp: None, 666 | module_path: true, 667 | target: false, 668 | level: true, 669 | source_file: false, 670 | source_line_number: false, 671 | #[cfg(feature = "kv")] 672 | kv_format: Some(Box::new(hidden_kv_format)), 673 | indent: None, 674 | suffix: "\n", 675 | }, 676 | written_header_value: false, 677 | buf: &mut f, 678 | }); 679 | 680 | assert_eq!("[INFO test::path] log\nmessage\n", written); 681 | } 682 | 683 | #[test] 684 | fn format_no_header() { 685 | let mut f = formatter(); 686 | 687 | let written = write(ConfigurableFormatWriter { 688 | format: &ConfigurableFormat { 689 | timestamp: None, 690 | module_path: false, 691 | target: false, 692 | level: false, 693 | source_file: false, 694 | source_line_number: false, 695 | #[cfg(feature = "kv")] 696 | kv_format: Some(Box::new(hidden_kv_format)), 697 | indent: None, 698 | suffix: "\n", 699 | }, 700 | written_header_value: false, 701 | buf: &mut f, 702 | }); 703 | 704 | assert_eq!("log\nmessage\n", written); 705 | } 706 | 707 | #[test] 708 | fn format_indent_spaces() { 709 | let mut f = formatter(); 710 | 711 | let written = write(ConfigurableFormatWriter { 712 | format: &ConfigurableFormat { 713 | timestamp: None, 714 | module_path: true, 715 | target: false, 716 | level: true, 717 | source_file: false, 718 | source_line_number: false, 719 | #[cfg(feature = "kv")] 720 | kv_format: Some(Box::new(hidden_kv_format)), 721 | indent: Some(4), 722 | suffix: "\n", 723 | }, 724 | written_header_value: false, 725 | buf: &mut f, 726 | }); 727 | 728 | assert_eq!("[INFO test::path] log\n message\n", written); 729 | } 730 | 731 | #[test] 732 | fn format_indent_zero_spaces() { 733 | let mut f = formatter(); 734 | 735 | let written = write(ConfigurableFormatWriter { 736 | format: &ConfigurableFormat { 737 | timestamp: None, 738 | module_path: true, 739 | target: false, 740 | level: true, 741 | source_file: false, 742 | source_line_number: false, 743 | #[cfg(feature = "kv")] 744 | kv_format: Some(Box::new(hidden_kv_format)), 745 | indent: Some(0), 746 | suffix: "\n", 747 | }, 748 | written_header_value: false, 749 | buf: &mut f, 750 | }); 751 | 752 | assert_eq!("[INFO test::path] log\nmessage\n", written); 753 | } 754 | 755 | #[test] 756 | fn format_indent_spaces_no_header() { 757 | let mut f = formatter(); 758 | 759 | let written = write(ConfigurableFormatWriter { 760 | format: &ConfigurableFormat { 761 | timestamp: None, 762 | module_path: false, 763 | target: false, 764 | level: false, 765 | source_file: false, 766 | source_line_number: false, 767 | #[cfg(feature = "kv")] 768 | kv_format: Some(Box::new(hidden_kv_format)), 769 | indent: Some(4), 770 | suffix: "\n", 771 | }, 772 | written_header_value: false, 773 | buf: &mut f, 774 | }); 775 | 776 | assert_eq!("log\n message\n", written); 777 | } 778 | 779 | #[test] 780 | fn format_suffix() { 781 | let mut f = formatter(); 782 | 783 | let written = write(ConfigurableFormatWriter { 784 | format: &ConfigurableFormat { 785 | timestamp: None, 786 | module_path: false, 787 | target: false, 788 | level: false, 789 | source_file: false, 790 | source_line_number: false, 791 | #[cfg(feature = "kv")] 792 | kv_format: Some(Box::new(hidden_kv_format)), 793 | indent: None, 794 | suffix: "\n\n", 795 | }, 796 | written_header_value: false, 797 | buf: &mut f, 798 | }); 799 | 800 | assert_eq!("log\nmessage\n\n", written); 801 | } 802 | 803 | #[test] 804 | fn format_suffix_with_indent() { 805 | let mut f = formatter(); 806 | 807 | let written = write(ConfigurableFormatWriter { 808 | format: &ConfigurableFormat { 809 | timestamp: None, 810 | module_path: false, 811 | target: false, 812 | level: false, 813 | source_file: false, 814 | source_line_number: false, 815 | #[cfg(feature = "kv")] 816 | kv_format: Some(Box::new(hidden_kv_format)), 817 | indent: Some(4), 818 | suffix: "\n\n", 819 | }, 820 | written_header_value: false, 821 | buf: &mut f, 822 | }); 823 | 824 | assert_eq!("log\n\n message\n\n", written); 825 | } 826 | 827 | #[test] 828 | fn format_target() { 829 | let mut f = formatter(); 830 | 831 | let written = write_target( 832 | "target", 833 | ConfigurableFormatWriter { 834 | format: &ConfigurableFormat { 835 | timestamp: None, 836 | module_path: true, 837 | target: true, 838 | level: true, 839 | source_file: false, 840 | source_line_number: false, 841 | #[cfg(feature = "kv")] 842 | kv_format: Some(Box::new(hidden_kv_format)), 843 | indent: None, 844 | suffix: "\n", 845 | }, 846 | written_header_value: false, 847 | buf: &mut f, 848 | }, 849 | ); 850 | 851 | assert_eq!("[INFO test::path target] log\nmessage\n", written); 852 | } 853 | 854 | #[test] 855 | fn format_empty_target() { 856 | let mut f = formatter(); 857 | 858 | let written = write(ConfigurableFormatWriter { 859 | format: &ConfigurableFormat { 860 | timestamp: None, 861 | module_path: true, 862 | target: true, 863 | level: true, 864 | source_file: false, 865 | source_line_number: false, 866 | #[cfg(feature = "kv")] 867 | kv_format: Some(Box::new(hidden_kv_format)), 868 | indent: None, 869 | suffix: "\n", 870 | }, 871 | written_header_value: false, 872 | buf: &mut f, 873 | }); 874 | 875 | assert_eq!("[INFO test::path] log\nmessage\n", written); 876 | } 877 | 878 | #[test] 879 | fn format_no_target() { 880 | let mut f = formatter(); 881 | 882 | let written = write_target( 883 | "target", 884 | ConfigurableFormatWriter { 885 | format: &ConfigurableFormat { 886 | timestamp: None, 887 | module_path: true, 888 | target: false, 889 | level: true, 890 | source_file: false, 891 | source_line_number: false, 892 | #[cfg(feature = "kv")] 893 | kv_format: Some(Box::new(hidden_kv_format)), 894 | indent: None, 895 | suffix: "\n", 896 | }, 897 | written_header_value: false, 898 | buf: &mut f, 899 | }, 900 | ); 901 | 902 | assert_eq!("[INFO test::path] log\nmessage\n", written); 903 | } 904 | 905 | #[test] 906 | fn format_with_source_file_and_line_number() { 907 | let mut f = formatter(); 908 | 909 | let written = write(ConfigurableFormatWriter { 910 | format: &ConfigurableFormat { 911 | timestamp: None, 912 | module_path: false, 913 | target: false, 914 | level: true, 915 | source_file: true, 916 | source_line_number: true, 917 | #[cfg(feature = "kv")] 918 | kv_format: Some(Box::new(hidden_kv_format)), 919 | indent: None, 920 | suffix: "\n", 921 | }, 922 | written_header_value: false, 923 | buf: &mut f, 924 | }); 925 | 926 | assert_eq!("[INFO test.rs:144] log\nmessage\n", written); 927 | } 928 | 929 | #[cfg(feature = "kv")] 930 | #[test] 931 | fn format_kv_default() { 932 | let kvs = &[("a", 1u32), ("b", 2u32)][..]; 933 | let mut f = formatter(); 934 | let record = Record::builder() 935 | .args(format_args!("log message")) 936 | .level(Level::Info) 937 | .module_path(Some("test::path")) 938 | .key_values(&kvs) 939 | .build(); 940 | 941 | let written = write_record( 942 | record, 943 | ConfigurableFormatWriter { 944 | format: &ConfigurableFormat { 945 | timestamp: None, 946 | module_path: false, 947 | target: false, 948 | level: true, 949 | source_file: false, 950 | source_line_number: false, 951 | kv_format: Some(Box::new(default_kv_format)), 952 | indent: None, 953 | suffix: "\n", 954 | }, 955 | written_header_value: false, 956 | buf: &mut f, 957 | }, 958 | ); 959 | 960 | assert_eq!("[INFO ] log message a=1 b=2\n", written); 961 | } 962 | 963 | #[cfg(feature = "kv")] 964 | #[test] 965 | fn format_kv_default_full() { 966 | let kvs = &[("a", 1u32), ("b", 2u32)][..]; 967 | let mut f = formatter(); 968 | let record = Record::builder() 969 | .args(format_args!("log\nmessage")) 970 | .level(Level::Info) 971 | .module_path(Some("test::path")) 972 | .target("target") 973 | .file(Some("test.rs")) 974 | .line(Some(42)) 975 | .key_values(&kvs) 976 | .build(); 977 | 978 | let written = write_record( 979 | record, 980 | ConfigurableFormatWriter { 981 | format: &ConfigurableFormat { 982 | timestamp: None, 983 | module_path: true, 984 | target: true, 985 | level: true, 986 | source_file: true, 987 | source_line_number: true, 988 | kv_format: Some(Box::new(default_kv_format)), 989 | indent: None, 990 | suffix: "\n", 991 | }, 992 | written_header_value: false, 993 | buf: &mut f, 994 | }, 995 | ); 996 | 997 | assert_eq!( 998 | "[INFO test::path test.rs:42 target] log\nmessage a=1 b=2\n", 999 | written 1000 | ); 1001 | } 1002 | } 1003 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 or the MIT license 3 | // , at your 4 | // option. This file may not be copied, modified, or distributed 5 | // except according to those terms. 6 | 7 | //! A simple logger that can be configured via environment variables, for use 8 | //! with the logging facade exposed by the [`log` crate][log-crate-url]. 9 | //! 10 | //! Despite having "env" in its name, **`env_logger`** can also be configured by 11 | //! other means besides environment variables. See [the examples][gh-repo-examples] 12 | //! in the source repository for more approaches. 13 | //! 14 | //! By default, `env_logger` writes logs to `stderr`, but can be configured to 15 | //! instead write them to `stdout`. 16 | //! 17 | //! ## Example 18 | //! 19 | //! ``` 20 | //! use log::{debug, error, log_enabled, info, Level}; 21 | //! 22 | //! env_logger::init(); 23 | //! 24 | //! debug!("this is a debug {}", "message"); 25 | //! error!("this is printed by default"); 26 | //! 27 | //! if log_enabled!(Level::Info) { 28 | //! let x = 3 * 4; // expensive computation 29 | //! info!("the answer was: {}", x); 30 | //! } 31 | //! ``` 32 | //! 33 | //! Assumes the binary is `main`: 34 | //! 35 | //! ```console 36 | //! $ RUST_LOG=error ./main 37 | //! [2017-11-09T02:12:24Z ERROR main] this is printed by default 38 | //! ``` 39 | //! 40 | //! ```console 41 | //! $ RUST_LOG=info ./main 42 | //! [2017-11-09T02:12:24Z ERROR main] this is printed by default 43 | //! [2017-11-09T02:12:24Z INFO main] the answer was: 12 44 | //! ``` 45 | //! 46 | //! ```console 47 | //! $ RUST_LOG=debug ./main 48 | //! [2017-11-09T02:12:24Z DEBUG main] this is a debug message 49 | //! [2017-11-09T02:12:24Z ERROR main] this is printed by default 50 | //! [2017-11-09T02:12:24Z INFO main] the answer was: 12 51 | //! ``` 52 | //! 53 | //! You can also set the log level on a per module basis: 54 | //! 55 | //! ```console 56 | //! $ RUST_LOG=main=info ./main 57 | //! [2017-11-09T02:12:24Z ERROR main] this is printed by default 58 | //! [2017-11-09T02:12:24Z INFO main] the answer was: 12 59 | //! ``` 60 | //! 61 | //! And enable all logging: 62 | //! 63 | //! ```console 64 | //! $ RUST_LOG=main ./main 65 | //! [2017-11-09T02:12:24Z DEBUG main] this is a debug message 66 | //! [2017-11-09T02:12:24Z ERROR main] this is printed by default 67 | //! [2017-11-09T02:12:24Z INFO main] the answer was: 12 68 | //! ``` 69 | //! 70 | //! If the binary name contains hyphens, you will need to replace 71 | //! them with underscores: 72 | //! 73 | //! ```console 74 | //! $ RUST_LOG=my_app ./my-app 75 | //! [2017-11-09T02:12:24Z DEBUG my_app] this is a debug message 76 | //! [2017-11-09T02:12:24Z ERROR my_app] this is printed by default 77 | //! [2017-11-09T02:12:24Z INFO my_app] the answer was: 12 78 | //! ``` 79 | //! 80 | //! This is because Rust modules and crates cannot contain hyphens 81 | //! in their name, although `cargo` continues to accept them. 82 | //! 83 | //! See the documentation for the [`log` crate][log-crate-url] for more 84 | //! information about its API. 85 | //! 86 | //! ## Enabling logging 87 | //! 88 | //! **By default all logging is disabled except for the `error` level** 89 | //! 90 | //! The **`RUST_LOG`** environment variable controls logging with the syntax: 91 | //! ```console 92 | //! RUST_LOG=[target][=][level][,...] 93 | //! ``` 94 | //! Or in other words, its a comma-separated list of directives. 95 | //! Directives can filter by **target**, by **level**, or both (using `=`). 96 | //! 97 | //! For example, 98 | //! ```console 99 | //! RUST_LOG=data=debug,hardware=debug 100 | //! ``` 101 | //! 102 | //! **target** is typically the path of the module the message 103 | //! in question originated from, though it can be overridden. 104 | //! The path is rooted in the name of the crate it was compiled for, so if 105 | //! your program is in a file called, for example, `hello.rs`, the path would 106 | //! simply be `hello`. 107 | //! 108 | //! Furthermore, the log can be filtered using prefix-search based on the 109 | //! specified log target. 110 | //! 111 | //! For example, `RUST_LOG=example` would match the following targets: 112 | //! - `example` 113 | //! - `example::test` 114 | //! - `example::test::module::submodule` 115 | //! - `examples::and_more_examples` 116 | //! 117 | //! When providing the crate name or a module path, explicitly specifying the 118 | //! log level is optional. If omitted, all logging for the item will be 119 | //! enabled. 120 | //! 121 | //! **level** is the maximum [`log::Level`][level-enum] to be shown and includes: 122 | //! - `error` 123 | //! - `warn` 124 | //! - `info` 125 | //! - `debug` 126 | //! - `trace` 127 | //! - `off` (pseudo level to disable all logging for the target) 128 | //! 129 | //! Logging level names are case-insensitive; e.g., 130 | //! `debug`, `DEBUG`, and `dEbuG` all represent the same logging level. For 131 | //! consistency, our convention is to use the lower case names. Where our docs 132 | //! do use other forms, they do so in the context of specific examples, so you 133 | //! won't be surprised if you see similar usage in the wild. 134 | //! 135 | //! Some examples of valid values of `RUST_LOG` are: 136 | //! 137 | //! - `RUST_LOG=hello` turns on all logging for the `hello` module 138 | //! - `RUST_LOG=trace` turns on all logging for the application, regardless of its name 139 | //! - `RUST_LOG=TRACE` turns on all logging for the application, regardless of its name (same as previous) 140 | //! - `RUST_LOG=info` turns on all info logging 141 | //! - `RUST_LOG=INFO` turns on all info logging (same as previous) 142 | //! - `RUST_LOG=hello=debug` turns on debug logging for `hello` 143 | //! - `RUST_LOG=hello=DEBUG` turns on debug logging for `hello` (same as previous) 144 | //! - `RUST_LOG=hello,std::option` turns on `hello`, and std's option logging 145 | //! - `RUST_LOG=error,hello=warn` turn on global error logging and also warn for `hello` 146 | //! - `RUST_LOG=error,hello=off` turn on global error logging, but turn off logging for `hello` 147 | //! - `RUST_LOG=off` turns off all logging for the application 148 | //! - `RUST_LOG=OFF` turns off all logging for the application (same as previous) 149 | //! 150 | //! ## Filtering results 151 | //! 152 | //! A `RUST_LOG` directive may include a regex filter. The syntax is to append `/` 153 | //! followed by a regex. Each message is checked against the regex, and is only 154 | //! logged if it matches. Note that the matching is done after formatting the 155 | //! log string but before adding any logging meta-data. There is a single filter 156 | //! for all modules. 157 | //! 158 | //! Some examples: 159 | //! 160 | //! * `hello/foo` turns on all logging for the 'hello' module where the log 161 | //! message includes 'foo'. 162 | //! * `info/f.o` turns on all info logging where the log message includes 'foo', 163 | //! 'f1o', 'fao', etc. 164 | //! * `hello=debug/foo*foo` turns on debug logging for 'hello' where the log 165 | //! message includes 'foofoo' or 'fofoo' or 'fooooooofoo', etc. 166 | //! * `error,hello=warn/[0-9]scopes` turn on global error logging and also 167 | //! warn for hello. In both cases the log message must include a single digit 168 | //! number followed by 'scopes'. 169 | //! 170 | //! ## Capturing logs in tests 171 | //! 172 | //! Records logged during `cargo test` will not be captured by the test harness by default. 173 | //! The [`Builder::is_test`] method can be used in unit tests to ensure logs will be captured: 174 | //! 175 | //! ``` 176 | //! #[cfg(test)] 177 | //! mod tests { 178 | //! use log::info; 179 | //! 180 | //! fn init() { 181 | //! let _ = env_logger::builder().is_test(true).try_init(); 182 | //! } 183 | //! 184 | //! #[test] 185 | //! fn it_works() { 186 | //! init(); 187 | //! 188 | //! info!("This record will be captured by `cargo test`"); 189 | //! 190 | //! assert_eq!(2, 1 + 1); 191 | //! } 192 | //! } 193 | //! ``` 194 | //! 195 | //! Enabling test capturing comes at the expense of color and other style support 196 | //! and may have performance implications. 197 | //! 198 | //! ## Disabling colors 199 | //! 200 | //! Colors and other styles can be configured with the `RUST_LOG_STYLE` 201 | //! environment variable. It accepts the following values: 202 | //! 203 | //! * `auto` (default) will attempt to print style characters, but don't force the issue. 204 | //! If the console isn't available on Windows, or if TERM=dumb, for example, then don't print colors. 205 | //! * `always` will always print style characters even if they aren't supported by the terminal. 206 | //! This includes emitting ANSI colors on Windows if the console API is unavailable. 207 | //! * `never` will never print style characters. 208 | //! 209 | //! ## Tweaking the default format 210 | //! 211 | //! Parts of the default format can be excluded from the log output using the [`Builder`]. 212 | //! The following example excludes the timestamp from the log output: 213 | //! 214 | //! ``` 215 | //! env_logger::builder() 216 | //! .format_timestamp(None) 217 | //! .init(); 218 | //! ``` 219 | //! 220 | //! ### Stability of the default format 221 | //! 222 | //! The default format won't optimise for long-term stability, and explicitly makes no 223 | //! guarantees about the stability of its output across major, minor or patch version 224 | //! bumps during `0.x`. 225 | //! 226 | //! If you want to capture or interpret the output of `env_logger` programmatically 227 | //! then you should use a custom format. 228 | //! 229 | //! ### Using a custom format 230 | //! 231 | //! Custom formats can be provided as closures to the [`Builder`]. 232 | //! These closures take a [`Formatter`][crate::fmt::Formatter] and `log::Record` as arguments: 233 | //! 234 | //! ``` 235 | //! use std::io::Write; 236 | //! 237 | //! env_logger::builder() 238 | //! .format(|buf, record| { 239 | //! writeln!(buf, "{}: {}", record.level(), record.args()) 240 | //! }) 241 | //! .init(); 242 | //! ``` 243 | //! 244 | //! See the [`fmt`] module for more details about custom formats. 245 | //! 246 | //! ## Specifying defaults for environment variables 247 | //! 248 | //! `env_logger` can read configuration from environment variables. 249 | //! If these variables aren't present, the default value to use can be tweaked with the [`Env`] type. 250 | //! The following example defaults to log `warn` and above if the `RUST_LOG` environment variable 251 | //! isn't set: 252 | //! 253 | //! ``` 254 | //! use env_logger::Env; 255 | //! 256 | //! env_logger::Builder::from_env(Env::default().default_filter_or("warn")).init(); 257 | //! ``` 258 | //! 259 | //! [gh-repo-examples]: https://github.com/rust-cli/env_logger/tree/main/examples 260 | //! [level-enum]: https://docs.rs/log/latest/log/enum.Level.html 261 | //! [log-crate-url]: https://docs.rs/log 262 | 263 | #![cfg_attr(docsrs, feature(doc_auto_cfg))] 264 | #![warn(clippy::print_stderr)] 265 | #![warn(clippy::print_stdout)] 266 | 267 | mod logger; 268 | mod writer; 269 | 270 | pub mod fmt; 271 | 272 | pub use self::fmt::{Target, TimestampPrecision, WriteStyle}; 273 | pub use self::logger::*; 274 | 275 | #[doc = include_str!("../README.md")] 276 | #[cfg(doctest)] 277 | pub struct ReadmeDoctests; 278 | -------------------------------------------------------------------------------- /src/writer/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync::Mutex}; 2 | 3 | use crate::writer::WriteStyle; 4 | 5 | #[derive(Debug)] 6 | pub(crate) struct BufferWriter { 7 | target: WritableTarget, 8 | write_style: WriteStyle, 9 | } 10 | 11 | impl BufferWriter { 12 | pub(crate) fn stderr(is_test: bool, write_style: WriteStyle) -> Self { 13 | BufferWriter { 14 | target: if is_test { 15 | WritableTarget::PrintStderr 16 | } else { 17 | WritableTarget::WriteStderr 18 | }, 19 | write_style, 20 | } 21 | } 22 | 23 | pub(crate) fn stdout(is_test: bool, write_style: WriteStyle) -> Self { 24 | BufferWriter { 25 | target: if is_test { 26 | WritableTarget::PrintStdout 27 | } else { 28 | WritableTarget::WriteStdout 29 | }, 30 | write_style, 31 | } 32 | } 33 | 34 | pub(crate) fn pipe( 35 | pipe: Box>, 36 | write_style: WriteStyle, 37 | ) -> Self { 38 | BufferWriter { 39 | target: WritableTarget::Pipe(pipe), 40 | write_style, 41 | } 42 | } 43 | 44 | pub(crate) fn write_style(&self) -> WriteStyle { 45 | self.write_style 46 | } 47 | 48 | pub(crate) fn buffer(&self) -> Buffer { 49 | Buffer(Vec::new()) 50 | } 51 | 52 | pub(crate) fn print(&self, buf: &Buffer) -> io::Result<()> { 53 | #![allow(clippy::print_stdout)] // enabled for tests only 54 | #![allow(clippy::print_stderr)] // enabled for tests only 55 | 56 | use std::io::Write as _; 57 | 58 | let buf = buf.as_bytes(); 59 | match &self.target { 60 | WritableTarget::WriteStdout => { 61 | let stream = io::stdout(); 62 | #[cfg(feature = "color")] 63 | let stream = anstream::AutoStream::new(stream, self.write_style.into()); 64 | let mut stream = stream.lock(); 65 | stream.write_all(buf)?; 66 | stream.flush()?; 67 | } 68 | WritableTarget::PrintStdout => { 69 | #[cfg(feature = "color")] 70 | let buf = adapt(buf, self.write_style)?; 71 | #[cfg(feature = "color")] 72 | let buf = &buf; 73 | let buf = String::from_utf8_lossy(buf); 74 | print!("{buf}"); 75 | } 76 | WritableTarget::WriteStderr => { 77 | let stream = io::stderr(); 78 | #[cfg(feature = "color")] 79 | let stream = anstream::AutoStream::new(stream, self.write_style.into()); 80 | let mut stream = stream.lock(); 81 | stream.write_all(buf)?; 82 | stream.flush()?; 83 | } 84 | WritableTarget::PrintStderr => { 85 | #[cfg(feature = "color")] 86 | let buf = adapt(buf, self.write_style)?; 87 | #[cfg(feature = "color")] 88 | let buf = &buf; 89 | let buf = String::from_utf8_lossy(buf); 90 | eprint!("{buf}"); 91 | } 92 | WritableTarget::Pipe(pipe) => { 93 | #[cfg(feature = "color")] 94 | let buf = adapt(buf, self.write_style)?; 95 | #[cfg(feature = "color")] 96 | let buf = &buf; 97 | let mut stream = pipe.lock().expect("no panics while held"); 98 | stream.write_all(buf)?; 99 | stream.flush()?; 100 | } 101 | } 102 | 103 | Ok(()) 104 | } 105 | } 106 | 107 | #[cfg(feature = "color")] 108 | fn adapt(buf: &[u8], write_style: WriteStyle) -> io::Result> { 109 | use std::io::Write as _; 110 | 111 | let adapted = Vec::with_capacity(buf.len()); 112 | let mut stream = anstream::AutoStream::new(adapted, write_style.into()); 113 | stream.write_all(buf)?; 114 | let adapted = stream.into_inner(); 115 | Ok(adapted) 116 | } 117 | 118 | pub(crate) struct Buffer(Vec); 119 | 120 | impl Buffer { 121 | pub(crate) fn clear(&mut self) { 122 | self.0.clear(); 123 | } 124 | 125 | pub(crate) fn write(&mut self, buf: &[u8]) -> io::Result { 126 | self.0.extend(buf); 127 | Ok(buf.len()) 128 | } 129 | 130 | pub(crate) fn flush(&mut self) -> io::Result<()> { 131 | Ok(()) 132 | } 133 | 134 | pub(crate) fn as_bytes(&self) -> &[u8] { 135 | &self.0 136 | } 137 | } 138 | 139 | impl std::fmt::Debug for Buffer { 140 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 141 | String::from_utf8_lossy(self.as_bytes()).fmt(f) 142 | } 143 | } 144 | 145 | /// Log target, either `stdout`, `stderr` or a custom pipe. 146 | /// 147 | /// Same as `Target`, except the pipe is wrapped in a mutex for interior mutability. 148 | pub(crate) enum WritableTarget { 149 | /// Logs will be written to standard output. 150 | WriteStdout, 151 | /// Logs will be printed to standard output. 152 | PrintStdout, 153 | /// Logs will be written to standard error. 154 | WriteStderr, 155 | /// Logs will be printed to standard error. 156 | PrintStderr, 157 | /// Logs will be sent to a custom pipe. 158 | Pipe(Box>), 159 | } 160 | 161 | impl std::fmt::Debug for WritableTarget { 162 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 163 | write!( 164 | f, 165 | "{}", 166 | match self { 167 | Self::WriteStdout => "stdout", 168 | Self::PrintStdout => "stdout", 169 | Self::WriteStderr => "stderr", 170 | Self::PrintStderr => "stderr", 171 | Self::Pipe(_) => "pipe", 172 | } 173 | ) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/writer/mod.rs: -------------------------------------------------------------------------------- 1 | mod buffer; 2 | mod target; 3 | 4 | use std::{io, mem, sync::Mutex}; 5 | 6 | use buffer::BufferWriter; 7 | 8 | pub(crate) use buffer::Buffer; 9 | 10 | pub use target::Target; 11 | 12 | /// Whether or not to print styles to the target. 13 | #[allow(clippy::exhaustive_enums)] // By definition don't need more 14 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Default)] 15 | pub enum WriteStyle { 16 | /// Try to print styles, but don't force the issue. 17 | #[default] 18 | Auto, 19 | /// Try very hard to print styles. 20 | Always, 21 | /// Never print styles. 22 | Never, 23 | } 24 | 25 | #[cfg(feature = "color")] 26 | impl From for WriteStyle { 27 | fn from(choice: anstream::ColorChoice) -> Self { 28 | match choice { 29 | anstream::ColorChoice::Auto => Self::Auto, 30 | anstream::ColorChoice::Always => Self::Always, 31 | anstream::ColorChoice::AlwaysAnsi => Self::Always, 32 | anstream::ColorChoice::Never => Self::Never, 33 | } 34 | } 35 | } 36 | 37 | #[cfg(feature = "color")] 38 | impl From for anstream::ColorChoice { 39 | fn from(choice: WriteStyle) -> Self { 40 | match choice { 41 | WriteStyle::Auto => anstream::ColorChoice::Auto, 42 | WriteStyle::Always => anstream::ColorChoice::Always, 43 | WriteStyle::Never => anstream::ColorChoice::Never, 44 | } 45 | } 46 | } 47 | 48 | /// A terminal target with color awareness. 49 | #[derive(Debug)] 50 | pub(crate) struct Writer { 51 | inner: BufferWriter, 52 | } 53 | 54 | impl Writer { 55 | pub(crate) fn write_style(&self) -> WriteStyle { 56 | self.inner.write_style() 57 | } 58 | 59 | pub(crate) fn buffer(&self) -> Buffer { 60 | self.inner.buffer() 61 | } 62 | 63 | pub(crate) fn print(&self, buf: &Buffer) -> io::Result<()> { 64 | self.inner.print(buf) 65 | } 66 | } 67 | 68 | /// A builder for a terminal writer. 69 | /// 70 | /// The target and style choice can be configured before building. 71 | #[derive(Debug)] 72 | pub(crate) struct Builder { 73 | target: Target, 74 | write_style: WriteStyle, 75 | is_test: bool, 76 | built: bool, 77 | } 78 | 79 | impl Builder { 80 | /// Initialize the writer builder with defaults. 81 | pub(crate) fn new() -> Self { 82 | Builder { 83 | target: Default::default(), 84 | write_style: Default::default(), 85 | is_test: false, 86 | built: false, 87 | } 88 | } 89 | 90 | /// Set the target to write to. 91 | pub(crate) fn target(&mut self, target: Target) -> &mut Self { 92 | self.target = target; 93 | self 94 | } 95 | 96 | /// Parses a style choice string. 97 | /// 98 | /// See the [Disabling colors] section for more details. 99 | /// 100 | /// [Disabling colors]: ../index.html#disabling-colors 101 | pub(crate) fn parse_write_style(&mut self, write_style: &str) -> &mut Self { 102 | self.write_style(parse_write_style(write_style)) 103 | } 104 | 105 | /// Whether or not to print style characters when writing. 106 | pub(crate) fn write_style(&mut self, write_style: WriteStyle) -> &mut Self { 107 | self.write_style = write_style; 108 | self 109 | } 110 | 111 | /// Whether or not to capture logs for `cargo test`. 112 | #[allow(clippy::wrong_self_convention)] 113 | pub(crate) fn is_test(&mut self, is_test: bool) -> &mut Self { 114 | self.is_test = is_test; 115 | self 116 | } 117 | 118 | /// Build a terminal writer. 119 | pub(crate) fn build(&mut self) -> Writer { 120 | assert!(!self.built, "attempt to re-use consumed builder"); 121 | self.built = true; 122 | 123 | let color_choice = self.write_style; 124 | #[cfg(feature = "auto-color")] 125 | let color_choice = if color_choice == WriteStyle::Auto { 126 | match &self.target { 127 | Target::Stdout => anstream::AutoStream::choice(&io::stdout()).into(), 128 | Target::Stderr => anstream::AutoStream::choice(&io::stderr()).into(), 129 | Target::Pipe(_) => color_choice, 130 | } 131 | } else { 132 | color_choice 133 | }; 134 | let color_choice = if color_choice == WriteStyle::Auto { 135 | WriteStyle::Never 136 | } else { 137 | color_choice 138 | }; 139 | 140 | let writer = match mem::take(&mut self.target) { 141 | Target::Stdout => BufferWriter::stdout(self.is_test, color_choice), 142 | Target::Stderr => BufferWriter::stderr(self.is_test, color_choice), 143 | Target::Pipe(pipe) => BufferWriter::pipe(Box::new(Mutex::new(pipe)), color_choice), 144 | }; 145 | 146 | Writer { inner: writer } 147 | } 148 | } 149 | 150 | impl Default for Builder { 151 | fn default() -> Self { 152 | Builder::new() 153 | } 154 | } 155 | 156 | fn parse_write_style(spec: &str) -> WriteStyle { 157 | match spec { 158 | "auto" => WriteStyle::Auto, 159 | "always" => WriteStyle::Always, 160 | "never" => WriteStyle::Never, 161 | _ => Default::default(), 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::*; 168 | 169 | #[test] 170 | fn parse_write_style_valid() { 171 | let inputs = vec![ 172 | ("auto", WriteStyle::Auto), 173 | ("always", WriteStyle::Always), 174 | ("never", WriteStyle::Never), 175 | ]; 176 | 177 | for (input, expected) in inputs { 178 | assert_eq!(expected, parse_write_style(input)); 179 | } 180 | } 181 | 182 | #[test] 183 | fn parse_write_style_invalid() { 184 | let inputs = vec!["", "true", "false", "NEVER!!"]; 185 | 186 | for input in inputs { 187 | assert_eq!(WriteStyle::Auto, parse_write_style(input)); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/writer/target.rs: -------------------------------------------------------------------------------- 1 | /// Log target, either `stdout`, `stderr` or a custom pipe. 2 | #[non_exhaustive] 3 | #[derive(Default)] 4 | pub enum Target { 5 | /// Logs will be sent to standard output. 6 | Stdout, 7 | /// Logs will be sent to standard error. 8 | #[default] 9 | Stderr, 10 | /// Logs will be sent to a custom pipe. 11 | Pipe(Box), 12 | } 13 | 14 | impl std::fmt::Debug for Target { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | write!( 17 | f, 18 | "{}", 19 | match self { 20 | Self::Stdout => "stdout", 21 | Self::Stderr => "stderr", 22 | Self::Pipe(_) => "pipe", 23 | } 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/init-twice-retains-filter.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | 3 | use std::env; 4 | use std::process; 5 | use std::str; 6 | 7 | fn main() { 8 | if env::var("YOU_ARE_TESTING_NOW").is_ok() { 9 | // Init from the env (which should set the max level to `Debug`) 10 | env_logger::init(); 11 | 12 | assert_eq!(log::LevelFilter::Debug, log::max_level()); 13 | 14 | // Init again using a different max level 15 | // This shouldn't clobber the level that was previously set 16 | env_logger::Builder::new() 17 | .parse_filters("info") 18 | .try_init() 19 | .unwrap_err(); 20 | 21 | assert_eq!(log::LevelFilter::Debug, log::max_level()); 22 | return; 23 | } 24 | 25 | let exe = env::current_exe().unwrap(); 26 | let out = process::Command::new(exe) 27 | .env("YOU_ARE_TESTING_NOW", "1") 28 | .env("RUST_LOG", "debug") 29 | .output() 30 | .unwrap_or_else(|e| panic!("Unable to start child process: {e}")); 31 | if out.status.success() { 32 | return; 33 | } 34 | 35 | println!("test failed: {}", out.status); 36 | println!("--- stdout\n{}", str::from_utf8(&out.stdout).unwrap()); 37 | println!("--- stderr\n{}", str::from_utf8(&out.stderr).unwrap()); 38 | process::exit(1); 39 | } 40 | -------------------------------------------------------------------------------- /tests/log-in-log.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | 3 | #[macro_use] 4 | extern crate log; 5 | 6 | use std::env; 7 | use std::fmt; 8 | use std::process; 9 | use std::str; 10 | 11 | struct Foo; 12 | 13 | impl fmt::Display for Foo { 14 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 15 | info!("test"); 16 | f.write_str("bar") 17 | } 18 | } 19 | 20 | fn main() { 21 | env_logger::init(); 22 | if env::var("YOU_ARE_TESTING_NOW").is_ok() { 23 | return info!("{}", Foo); 24 | } 25 | 26 | let exe = env::current_exe().unwrap(); 27 | let out = process::Command::new(exe) 28 | .env("YOU_ARE_TESTING_NOW", "1") 29 | .env("RUST_LOG", "debug") 30 | .output() 31 | .unwrap_or_else(|e| panic!("Unable to start child process: {e}")); 32 | if out.status.success() { 33 | return; 34 | } 35 | 36 | println!("test failed: {}", out.status); 37 | println!("--- stdout\n{}", str::from_utf8(&out.stdout).unwrap()); 38 | println!("--- stderr\n{}", str::from_utf8(&out.stderr).unwrap()); 39 | process::exit(1); 40 | } 41 | -------------------------------------------------------------------------------- /tests/log_tls_dtors.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | 3 | #[macro_use] 4 | extern crate log; 5 | 6 | use std::env; 7 | use std::process; 8 | use std::str; 9 | use std::thread; 10 | 11 | struct DropMe; 12 | 13 | impl Drop for DropMe { 14 | fn drop(&mut self) { 15 | debug!("Dropping now"); 16 | } 17 | } 18 | 19 | fn run() { 20 | // Use multiple thread local values to increase the chance that our TLS 21 | // value will get destroyed after the FORMATTER key in the library 22 | thread_local! { 23 | static DROP_ME_0: DropMe = const { DropMe }; 24 | static DROP_ME_1: DropMe = const { DropMe }; 25 | static DROP_ME_2: DropMe = const { DropMe }; 26 | static DROP_ME_3: DropMe = const { DropMe }; 27 | static DROP_ME_4: DropMe = const { DropMe }; 28 | static DROP_ME_5: DropMe = const { DropMe }; 29 | static DROP_ME_6: DropMe = const { DropMe }; 30 | static DROP_ME_7: DropMe = const { DropMe }; 31 | static DROP_ME_8: DropMe = const { DropMe }; 32 | static DROP_ME_9: DropMe = const { DropMe }; 33 | } 34 | DROP_ME_0.with(|_| {}); 35 | DROP_ME_1.with(|_| {}); 36 | DROP_ME_2.with(|_| {}); 37 | DROP_ME_3.with(|_| {}); 38 | DROP_ME_4.with(|_| {}); 39 | DROP_ME_5.with(|_| {}); 40 | DROP_ME_6.with(|_| {}); 41 | DROP_ME_7.with(|_| {}); 42 | DROP_ME_8.with(|_| {}); 43 | DROP_ME_9.with(|_| {}); 44 | } 45 | 46 | fn main() { 47 | env_logger::init(); 48 | if env::var("YOU_ARE_TESTING_NOW").is_ok() { 49 | // Run on a separate thread because TLS values on the main thread 50 | // won't have their destructors run if pthread is used. 51 | // https://doc.rust-lang.org/std/thread/struct.LocalKey.html#platform-specific-behavior 52 | thread::spawn(run).join().unwrap(); 53 | } else { 54 | let exe = env::current_exe().unwrap(); 55 | let out = process::Command::new(exe) 56 | .env("YOU_ARE_TESTING_NOW", "1") 57 | .env("RUST_LOG", "debug") 58 | .output() 59 | .unwrap_or_else(|e| panic!("Unable to start child process: {e}")); 60 | if !out.status.success() { 61 | println!("test failed: {}", out.status); 62 | println!("--- stdout\n{}", str::from_utf8(&out.stdout).unwrap()); 63 | println!("--- stderr\n{}", str::from_utf8(&out.stderr).unwrap()); 64 | process::exit(1); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/regexp_filter.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unwrap_used)] 2 | 3 | #[macro_use] 4 | extern crate log; 5 | 6 | use std::env; 7 | use std::process; 8 | use std::str; 9 | 10 | fn main() { 11 | if env::var("LOG_REGEXP_TEST").ok() == Some(String::from("1")) { 12 | child_main(); 13 | } else { 14 | parent_main(); 15 | } 16 | } 17 | 18 | fn child_main() { 19 | env_logger::init(); 20 | info!("XYZ Message"); 21 | } 22 | 23 | fn run_child(rust_log: String) -> bool { 24 | let exe = env::current_exe().unwrap(); 25 | let out = process::Command::new(exe) 26 | .env("LOG_REGEXP_TEST", "1") 27 | .env("RUST_LOG", rust_log) 28 | .output() 29 | .unwrap_or_else(|e| panic!("Unable to start child process: {e}")); 30 | str::from_utf8(out.stderr.as_ref()) 31 | .unwrap() 32 | .contains("XYZ Message") 33 | } 34 | 35 | fn assert_message_printed(rust_log: &str) { 36 | if !run_child(rust_log.to_owned()) { 37 | panic!("RUST_LOG={rust_log} should allow the test log message") 38 | } 39 | } 40 | 41 | fn assert_message_not_printed(rust_log: &str) { 42 | if run_child(rust_log.to_owned()) { 43 | panic!("RUST_LOG={rust_log} should not allow the test log message") 44 | } 45 | } 46 | 47 | fn parent_main() { 48 | // test normal log severity levels 49 | assert_message_printed("info"); 50 | assert_message_not_printed("warn"); 51 | 52 | // test of regular expression filters 53 | assert_message_printed("info/XYZ"); 54 | assert_message_not_printed("info/XXX"); 55 | } 56 | --------------------------------------------------------------------------------