├── .git_archival.txt ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md ├── dependabot.yml └── workflows │ ├── build-docset.yml │ ├── ci.yml │ ├── codeql-analysis.yml │ ├── pypi-package.yml │ └── zizmor.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .python-version-default ├── .readthedocs.yaml ├── CHANGELOG.md ├── COPYRIGHT ├── LICENSE-APACHE ├── LICENSE-MIT ├── NOTICE ├── README.md ├── docs ├── Makefile ├── _static │ ├── BoundLogger.svg │ ├── Justfile │ ├── console_renderer.png │ ├── custom.css │ ├── docset-icon.png │ ├── docset-icon@2x.png │ ├── social card.afdesign │ ├── sponsors │ │ ├── FilePreviews.svg │ │ ├── Klaviyo.svg │ │ ├── Polar.svg │ │ ├── Privacy-Solutions.svg │ │ ├── Sentry.svg │ │ ├── Tidelift.svg │ │ ├── Variomedia.svg │ │ └── emsys-renewables.svg │ ├── structlog_logo.afdesign │ ├── structlog_logo.png │ ├── structlog_logo.svg │ ├── structlog_logo_horizontal.afdesign │ ├── structlog_logo_horizontal.svg │ └── structlog_logo_small.png ├── api.rst ├── bound-loggers.md ├── conf.py ├── configuration.md ├── console-output.md ├── contextvars.md ├── exceptions.md ├── frameworks.md ├── getting-started.md ├── glossary.md ├── index.md ├── license.md ├── logging-best-practices.md ├── make.bat ├── performance.md ├── processors.md ├── recipes.md ├── standard-library.md ├── testing.md ├── thread-local.md ├── twisted.md ├── typing.md └── why.md ├── pyproject.toml ├── show_off.py ├── src └── structlog │ ├── __init__.py │ ├── _base.py │ ├── _config.py │ ├── _frames.py │ ├── _generic.py │ ├── _greenlets.py │ ├── _log_levels.py │ ├── _native.py │ ├── _output.py │ ├── _utils.py │ ├── contextvars.py │ ├── dev.py │ ├── exceptions.py │ ├── processors.py │ ├── py.typed │ ├── stdlib.py │ ├── testing.py │ ├── threadlocal.py │ ├── tracebacks.py │ ├── twisted.py │ ├── types.py │ └── typing.py ├── tests ├── __init__.py ├── additional_frame.py ├── conftest.py ├── processors │ ├── __init__.py │ ├── test_processors.py │ └── test_renderers.py ├── test_base.py ├── test_config.py ├── test_contextvars.py ├── test_dev.py ├── test_frames.py ├── test_generic.py ├── test_native.py ├── test_output.py ├── test_packaging.py ├── test_stdlib.py ├── test_testing.py ├── test_threadlocal.py ├── test_tracebacks.py ├── test_twisted.py ├── test_utils.py ├── typing │ └── api.py └── utils.py ├── tox.ini └── zizmor.yml /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: 480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31 2 | node-date: 2025-06-02T10:16:03+02:00 3 | describe-name: 25.4.0-1-g480ae5f2 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force LF line endings for text files 2 | * text=auto eol=lf 3 | 4 | # Needed for hatch-vcs / setuptools-scm-git-archive 5 | .git_archival.txt export-subst 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socioeconomic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | . 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: hynek 3 | tidelift: pypi/structlog 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | 4 | 5 | 6 | # Pull Request Check List 7 | 8 | 15 | 16 | - [ ] Do **not** open pull requests from your `main` branch – **use a separate branch**! 17 | - There's a ton of footguns waiting if you don't heed this warning. You can still go back to your project, create a branch from your main branch, push it, and open the pull request from the new branch. 18 | - This is not a pre-requisite for your pull request to be accepted, but **you have been warned**. 19 | - [ ] Added **tests** for changed code. 20 | - The CI fails with less than 100% coverage. 21 | - [ ] **New APIs** are added to our typing tests in [`api.py`](https://github.com/hynek/structlog/blob/main/tests/typing/api.py). 22 | - [ ] Updated **documentation** for changed code. 23 | - [ ] New functions/classes have to be added to `docs/api.rst` by hand. 24 | - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded). 25 | - The next version is the second number in the current release + 1. The first number represents the current year. So if the current version on PyPI is 23.1.0, the next version is gonna be 23.2.0. If the next version is the first in the new year, it'll be 24.1.0. 26 | - [ ] Documentation in `.rst` and `.md` files is written using [**semantic newlines**](https://rhodesmill.org/brandon/2012/one-sentence-per-line/). 27 | - [ ] Changes (and possible deprecations) are documented in the [**changelog**](https://github.com/hynek/structlog/blob/main/CHANGELOG.md). 28 | - [ ] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you. 29 | 30 | 34 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported versions 4 | 5 | We are following [Calendar Versioning](https://calver.org) with generous backwards-compatibility guarantees. 6 | Therefore we only support the latest version. 7 | 8 | Put simply, you shouldn't ever be afraid to upgrade as long as you're only using our public APIs. 9 | Whenever there is a need to break compatibility, it is announced in the changelog, and raises a `DeprecationWarning` for a year (if possible) before it's finally really broken. 10 | 11 | You **can't** rely on the default settings and the `structlog.dev` module, though. 12 | They may be adjusted in the future to provide a better experience when starting to use *structlog*. 13 | So please make sure to **always** properly configure your applications. 14 | 15 | 16 | ## Reporting a vulnerability 17 | 18 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 19 | Tidelift will coordinate the fix and disclosure. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/workflows/build-docset.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build docset 3 | 4 | on: 5 | push: 6 | tags: ["*"] 7 | workflow_dispatch: 8 | 9 | env: 10 | PIP_DISABLE_PIP_VERSION_CHECK: 1 11 | PIP_NO_PYTHON_VERSION_WARNING: 1 12 | 13 | permissions: {} 14 | 15 | jobs: 16 | docset: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 # get correct version 22 | persist-credentials: false 23 | - uses: actions/setup-python@v5 24 | with: 25 | python-version: "3.12" 26 | 27 | - run: pip install tox 28 | 29 | - run: tox run -e docset 30 | - run: tar --exclude='.DS_Store' -cvzf structlog.tgz structlog.docset 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | name: docset 35 | path: structlog.tgz 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | FORCE_COLOR: "1" # Make tools pretty. 12 | PIP_DISABLE_PIP_VERSION_CHECK: "1" 13 | PIP_NO_PYTHON_VERSION_WARNING: "1" 14 | 15 | permissions: {} 16 | 17 | 18 | jobs: 19 | build-package: 20 | name: Build & verify package 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | persist-credentials: false 28 | 29 | - uses: hynek/build-and-inspect-python-package@v2 30 | id: baipp 31 | 32 | outputs: 33 | # Used to define the matrix for tests below. The value is based on 34 | # packaging metadata (trove classifiers). 35 | python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }} 36 | 37 | 38 | tests: 39 | name: Tests & Mypy API on ${{ matrix.python-version }} 40 | runs-on: ubuntu-latest 41 | needs: build-package 42 | 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | # Created by the build-and-inspect-python-package action above. 47 | python-version: ${{ fromJson(needs.build-package.outputs.python-versions) }} 48 | 49 | env: 50 | PYTHON: ${{ matrix.python-version }} 51 | 52 | steps: 53 | - name: Download pre-built packages 54 | uses: actions/download-artifact@v4 55 | with: 56 | name: Packages 57 | path: dist 58 | - run: | 59 | tar xf dist/*.tar.gz --strip-components=1 60 | rm -rf src 61 | - uses: actions/setup-python@v5 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | allow-prereleases: true 65 | - uses: hynek/setup-cached-uv@v2 66 | 67 | - name: Run tests 68 | run: > 69 | uvx --with tox-uv tox run 70 | --installpkg dist/*.whl 71 | -f py${PYTHON//./}-tests 72 | 73 | - name: Upload coverage data 74 | uses: actions/upload-artifact@v4 75 | with: 76 | name: coverage-data-${{ matrix.python-version }} 77 | path: .coverage.* 78 | include-hidden-files: true 79 | if-no-files-found: ignore 80 | 81 | - name: Check public API with Mypy 82 | run: > 83 | uvx --with tox-uv tox run 84 | --installpkg dist/*.whl 85 | -e py${PYTHON//./}-mypy 86 | 87 | 88 | coverage: 89 | name: Ensure 100% test coverage 90 | runs-on: ubuntu-latest 91 | needs: tests 92 | if: always() 93 | 94 | steps: 95 | - uses: actions/checkout@v4 96 | with: 97 | persist-credentials: false 98 | - uses: actions/setup-python@v5 99 | with: 100 | python-version-file: .python-version-default 101 | - uses: hynek/setup-cached-uv@v2 102 | 103 | - name: Download coverage data 104 | uses: actions/download-artifact@v4 105 | with: 106 | pattern: coverage-data-* 107 | merge-multiple: true 108 | 109 | - name: Combine coverage and fail if it's <100%. 110 | run: | 111 | uv tool install coverage 112 | 113 | coverage combine 114 | coverage html --skip-covered --skip-empty 115 | 116 | # Report and write to summary. 117 | coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 118 | 119 | # Report again and fail if under 100%. 120 | coverage report --fail-under=100 121 | 122 | - name: Upload HTML report if check failed. 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: html-report 126 | path: htmlcov 127 | if: ${{ failure() }} 128 | 129 | 130 | mypy-pkg: 131 | name: Mypy Codebase 132 | runs-on: ubuntu-latest 133 | needs: build-package 134 | 135 | steps: 136 | - name: Download pre-built packages 137 | uses: actions/download-artifact@v4 138 | with: 139 | name: Packages 140 | path: dist 141 | - run: tar xf dist/*.tar.gz --strip-components=1 142 | - uses: actions/setup-python@v5 143 | with: 144 | python-version-file: .python-version-default 145 | - uses: hynek/setup-cached-uv@v2 146 | 147 | - run: > 148 | uvx --with tox-uv 149 | tox run -e mypy-pkg 150 | 151 | 152 | pyright: 153 | name: Pyright Codebase 154 | runs-on: ubuntu-latest 155 | needs: build-package 156 | steps: 157 | - name: Download pre-built packages 158 | uses: actions/download-artifact@v4 159 | with: 160 | name: Packages 161 | path: dist 162 | - run: tar xf dist/*.tar.gz --strip-components=1 163 | - uses: actions/setup-python@v5 164 | with: 165 | python-version-file: .python-version-default 166 | - uses: hynek/setup-cached-uv@v2 167 | 168 | - run: > 169 | uvx --with tox-uv 170 | tox run -e pyright 171 | 172 | 173 | docs: 174 | name: Run doctests 175 | needs: build-package 176 | runs-on: ubuntu-latest 177 | steps: 178 | - name: Download pre-built packages 179 | uses: actions/download-artifact@v4 180 | with: 181 | name: Packages 182 | path: dist 183 | - run: tar xf dist/*.tar.gz --strip-components=1 184 | - uses: actions/setup-python@v5 185 | with: 186 | # Keep in sync with tox.ini/docs & .readthedocs.yaml 187 | python-version: "3.13" 188 | - uses: hynek/setup-cached-uv@v2 189 | 190 | - run: > 191 | uvx --with tox-uv 192 | tox run -e docs-doctests 193 | 194 | 195 | install-dev: 196 | name: Verify dev env 197 | runs-on: ${{ matrix.os }} 198 | strategy: 199 | matrix: 200 | os: [ubuntu-latest, windows-latest] 201 | 202 | steps: 203 | - uses: actions/checkout@v4 204 | with: 205 | persist-credentials: false 206 | - uses: actions/setup-python@v5 207 | with: 208 | python-version-file: .python-version-default 209 | - uses: hynek/setup-cached-uv@v2 210 | 211 | - run: uv venv 212 | - run: uv pip install -e . --group dev 213 | 214 | - run: .venv/bin/python -Ic 'import structlog; print(structlog.__version__)' 215 | if: runner.os != 'Windows' 216 | - run: .\.venv\Scripts\python.exe -Ic 'import structlog; print(structlog.__version__)' 217 | if: runner.os == 'Windows' 218 | 219 | 220 | required-checks-pass: 221 | name: Ensure everything required is passing for branch protection 222 | if: always() 223 | 224 | needs: 225 | - coverage 226 | - install-dev 227 | - mypy-pkg 228 | - pyright 229 | - docs 230 | 231 | runs-on: ubuntu-latest 232 | 233 | steps: 234 | - name: Decide whether the needed jobs succeeded or failed 235 | uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 236 | with: 237 | jobs: ${{ toJSON(needs) }} 238 | 239 | 240 | colors: 241 | name: Visual check for color settings using env variables 242 | needs: build-package 243 | runs-on: ubuntu-latest 244 | 245 | steps: 246 | - uses: actions/checkout@v4 247 | with: 248 | persist-credentials: false 249 | - uses: actions/setup-python@v5 250 | with: 251 | python-version-file: .python-version-default 252 | - uses: hynek/setup-cached-uv@v2 253 | 254 | - run: > 255 | uvx --with=tox-uv 256 | tox run 257 | -f color 258 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CodeQL 3 | 4 | on: 5 | schedule: 6 | - cron: "41 3 * * 6" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [python] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v3 41 | -------------------------------------------------------------------------------- /.github/workflows/pypi-package.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build & upload PyPI package 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | tags: ["*"] 8 | release: 9 | types: 10 | - published 11 | workflow_dispatch: 12 | 13 | 14 | jobs: 15 | # Always build & lint package. 16 | build-package: 17 | name: Build & verify package 18 | runs-on: ubuntu-latest 19 | permissions: 20 | attestations: write 21 | id-token: write 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | persist-credentials: false 28 | 29 | - uses: hynek/build-and-inspect-python-package@v2 30 | with: 31 | attest-build-provenance-github: 'true' 32 | 33 | # Upload to Test PyPI on every commit on main. 34 | release-test-pypi: 35 | name: Publish in-dev package to test.pypi.org 36 | environment: release-test-pypi 37 | if: github.repository_owner == 'hynek' && github.event_name == 'push' && github.ref == 'refs/heads/main' 38 | runs-on: ubuntu-latest 39 | needs: build-package 40 | 41 | permissions: 42 | id-token: write 43 | 44 | steps: 45 | - name: Download packages built by build-and-inspect-python-package 46 | uses: actions/download-artifact@v4 47 | with: 48 | name: Packages 49 | path: dist 50 | 51 | - name: Upload package to Test PyPI 52 | uses: pypa/gh-action-pypi-publish@release/v1 53 | with: 54 | repository-url: https://test.pypi.org/legacy/ 55 | 56 | # Upload to real PyPI on GitHub Releases. 57 | release-pypi: 58 | name: Publish released package to pypi.org 59 | environment: release-pypi 60 | if: github.repository_owner == 'hynek' && github.event.action == 'published' 61 | runs-on: ubuntu-latest 62 | needs: build-package 63 | 64 | permissions: 65 | id-token: write 66 | 67 | steps: 68 | - name: Download packages built by build-and-inspect-python-package 69 | uses: actions/download-artifact@v4 70 | with: 71 | name: Packages 72 | path: dist 73 | 74 | - name: Upload package to PyPI 75 | uses: pypa/gh-action-pypi-publish@release/v1 76 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/woodruffw/zizmor 2 | name: Zizmor 3 | 4 | on: 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | branches: ["*"] 9 | 10 | permissions: 11 | contents: read 12 | 13 | 14 | jobs: 15 | zizmor: 16 | name: Zizmor latest via PyPI 17 | runs-on: ubuntu-latest 18 | permissions: 19 | security-events: write 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - uses: hynek/setup-cached-uv@v2 26 | 27 | - name: Run zizmor 🌈 28 | run: uvx zizmor --format sarif . > results.sarif 29 | env: 30 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Upload SARIF file 33 | uses: github/codeql-action/upload-sarif@v3 34 | with: 35 | # Path to SARIF file relative to the root of the repository 36 | sarif_file: results.sarif 37 | # Optional category for the results 38 | # Used to differentiate multiple results for one commit 39 | category: zizmor 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | .DS_Store 4 | .cache 5 | .coverage* 6 | .direnv 7 | .envrc 8 | .mypy_cache 9 | .pytest_cache 10 | .tox 11 | .vscode 12 | .idea 13 | benchmarks 14 | build 15 | dist 16 | docs/_build 17 | htmlcov 18 | tmp 19 | structlog.docset 20 | structlog.tgz 21 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ci: 3 | autoupdate_schedule: monthly 4 | 5 | repos: 6 | - repo: https://github.com/astral-sh/ruff-pre-commit 7 | rev: v0.11.12 8 | hooks: 9 | - id: ruff-check 10 | args: [--fix, --exit-non-zero-on-fix] 11 | - id: ruff-format 12 | 13 | - repo: https://github.com/econchick/interrogate 14 | rev: 1.7.0 15 | hooks: 16 | - id: interrogate 17 | args: [tests] 18 | 19 | - repo: https://github.com/codespell-project/codespell 20 | rev: v2.4.1 21 | hooks: 22 | - id: codespell 23 | args: [-L, alog, -L, abl] 24 | 25 | - repo: https://github.com/pre-commit/pre-commit-hooks 26 | rev: v5.0.0 27 | hooks: 28 | - id: trailing-whitespace 29 | - id: end-of-file-fixer 30 | exclude: docs/_static 31 | - id: check-toml 32 | - id: check-yaml 33 | -------------------------------------------------------------------------------- /.python-version-default: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | build: 5 | os: ubuntu-lts-latest 6 | tools: 7 | # Keep version in sync with tox.ini/docs and ci.yml/docs. 8 | python: "3.13" 9 | jobs: 10 | create_environment: 11 | # Need the tags to calculate the version (sometimes). 12 | - git fetch --tags 13 | 14 | - asdf plugin add uv 15 | - asdf install uv latest 16 | - asdf global uv latest 17 | 18 | build: 19 | html: 20 | - uvx --with tox-uv tox run -e docs-sponsors 21 | - uvx --with tox-uv tox run -e docs-build -- $READTHEDOCS_OUTPUT 22 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Licensed under either of 2 | 3 | - Apache License, Version 2.0 (LICENSE-APACHE or ) 4 | - or MIT license (LICENSE-MIT or ) 5 | 6 | at your option. 7 | 8 | Any contribution intentionally submitted for inclusion in the work by you, as 9 | defined in the Apache-2.0 license, shall be dual-licensed as above, without any 10 | additional terms or conditions. 11 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Hynek Schlawack and the structlog contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | structlog 2 | Copyright 2013 Hynek Schlawack and the structlog contributors 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # *structlog*: Structured Logging for Python 2 | 3 |

4 | 5 | structlog: Structured Logging for Python 6 | 7 |

8 | 9 |

10 | Documentation 11 | License: MIT / Apache 2.0 12 | 13 | DOI 14 | Supported Python versions of the current PyPI release. 15 | Downloads per month 16 |

17 | 18 |

Simple. Powerful. Fast. Pick three.

19 | 20 | 21 | 22 | *structlog* is *the* production-ready logging solution for Python: 23 | 24 | - **Simple**: Everything is about **functions** that take and return **dictionaries** – all hidden behind **familiar APIs**. 25 | - **Powerful**: Functions and dictionaries aren’t just simple but also powerful. 26 | *structlog* leaves *you* in control. 27 | - **Fast**: *structlog* is not hamstrung by designs of yore. 28 | Its flexibility comes not at the price of performance. 29 | 30 | Thanks to its flexible design, *you* choose whether you want *structlog* to take care of the **output** of your log entries or whether you prefer to **forward** them to an existing logging system like the standard library's `logging` module. 31 | 32 | The output format is just as flexible and *structlog* comes with support for JSON, [*logfmt*](https://brandur.org/logfmt), as well as pretty console output out-of-the-box: 33 | 34 | [![image](https://github.com/hynek/structlog/blob/main/docs/_static/console_renderer.png?raw=true)](https://github.com/hynek/structlog/blob/main/docs/_static/console_renderer.png?raw=true) 35 | 36 | 37 | ## Sponsors 38 | 39 | *structlog* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek). 40 | Especially those generously supporting us at the *The Organization* tier and higher: 41 | 42 | 43 | 44 |

45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |

62 | 63 | 64 | 65 |

66 | Please consider joining them to help make structlog’s maintenance more sustainable! 67 |

68 | 69 | ## Introduction 70 | 71 | *structlog* has been successfully used in production at every scale since **2013**, while embracing cutting-edge technologies like *asyncio*, context variables, or type hints as they emerged. 72 | Its paradigms proved influential enough to [help design](https://twitter.com/sirupsen/status/638330548361019392) structured logging [packages across ecosystems](https://github.com/sirupsen/logrus). 73 | 74 | 75 | 76 | A short explanation on *why* structured logging is good for you, and why *structlog* is the right tool for the job can be found in the [Why chapter](https://www.structlog.org/en/stable/why.html) of our documentation. 77 | 78 | Once you feel inspired to try it out, check out our friendly [Getting Started tutorial](https://www.structlog.org/en/stable/getting-started.html). 79 | 80 | 81 | For a fully-fledged zero-to-hero tutorial, check out [*A Comprehensive Guide to Python Logging with structlog*](https://betterstack.com/community/guides/logging/structlog/). 82 | 83 | If you prefer videos over reading, check out [Markus Holtermann](https://chaos.social/@markush)'s talk *Logging Rethought 2: The Actions of Frank Taylor Jr.*: 84 | 85 |

86 | 87 | 88 | 89 |

90 | 91 | 92 | ## Credits 93 | 94 | *structlog* is written and maintained by [Hynek Schlawack](https://hynek.me/). 95 | The idea of bound loggers is inspired by previous work by [Jean-Paul Calderone](https://github.com/exarkun) and [David Reid](https://github.com/dreid). 96 | 97 | The development is kindly supported by my employer [Variomedia AG](https://www.variomedia.de/), *structlog*’s [Tidelift subscribers](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek), and all my amazing [GitHub Sponsors](https://github.com/sponsors/hynek). 98 | 99 | The logs-loving beaver logo has been contributed by [Lynn Root](https://www.roguelynn.com). 100 | 101 | 102 | 103 | 104 | ## Project Links 105 | 106 | - [**Get Help**](https://stackoverflow.com/questions/tagged/structlog) (use the *structlog* tag on Stack Overflow) 107 | - [**PyPI**](https://pypi.org/project/structlog/) 108 | - [**GitHub**](https://github.com/hynek/structlog) 109 | - [**Documentation**](https://www.structlog.org/) 110 | - [**Changelog**](https://github.com/hynek/structlog/tree/main/CHANGELOG.md) 111 | - [**Third-party Extensions**](https://github.com/hynek/structlog/wiki/Third-party-Extensions) 112 | 113 | 114 | ## *structlog* for Enterprise 115 | 116 | Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). 117 | 118 | The maintainers of *structlog* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. 119 | Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. 120 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -n -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/structlog.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/structlog.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/structlog" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/structlog" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/_static/Justfile: -------------------------------------------------------------------------------- 1 | rebuild-logos: && compress-logos 2 | magick structlog_logo.svg structlog_logo.png 3 | magick structlog_logo.svg -resize 220 structlog_logo_small.png 4 | magick structlog_logo.svg -resize 16x16 docset-icon.png 5 | magick structlog_logo.svg -resize 32x32 docset-icon@2x.png 6 | 7 | 8 | compress-logos: 9 | svgo *.svg 10 | oxipng --opt max --strip safe --zopfli *.png 11 | 12 | -------------------------------------------------------------------------------- /docs/_static/console_renderer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/console_renderer.png -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.bunny.net/css?family=b612:400,400i,700,700i); 2 | @import url('https://assets.hynek.me/css/bm.css'); 3 | 4 | 5 | :root { 6 | font-feature-settings: 'liga' 1, 'calt' 1; 7 | /* fix for Chrome */ 8 | } 9 | 10 | @supports (font-variation-settings: normal) { 11 | :root { 12 | font-family: 'B612', sans-serif; 13 | } 14 | } 15 | 16 | 17 | /* Hide ToC caption text within the main body (but leave them in the side-bar). */ 18 | #furo-main-content span.caption-text { 19 | display: none; 20 | } 21 | -------------------------------------------------------------------------------- /docs/_static/docset-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/docset-icon.png -------------------------------------------------------------------------------- /docs/_static/docset-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/docset-icon@2x.png -------------------------------------------------------------------------------- /docs/_static/social card.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/social card.afdesign -------------------------------------------------------------------------------- /docs/_static/sponsors/FilePreviews.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Klaviyo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Polar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Privacy-Solutions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Sentry.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Tidelift.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_static/sponsors/Variomedia.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/_static/structlog_logo.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/structlog_logo.afdesign -------------------------------------------------------------------------------- /docs/_static/structlog_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/structlog_logo.png -------------------------------------------------------------------------------- /docs/_static/structlog_logo_horizontal.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/structlog_logo_horizontal.afdesign -------------------------------------------------------------------------------- /docs/_static/structlog_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/docs/_static/structlog_logo_small.png -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import os 7 | 8 | from importlib import metadata 9 | 10 | 11 | # Set canonical URL from the Read the Docs Domain 12 | html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") 13 | 14 | # Tell Jinja2 templates the build is running on Read the Docs 15 | if os.environ.get("READTHEDOCS", "") == "True": 16 | html_context = {"READTHEDOCS": True} 17 | 18 | # We want an image in the README and include the README in the docs. 19 | suppress_warnings = ["image.nonlocal_uri"] 20 | 21 | 22 | # -- General configuration ---------------------------------------------------- 23 | 24 | extensions = [ 25 | "myst_parser", 26 | "notfound.extension", 27 | "sphinx.ext.autodoc", 28 | "sphinx.ext.autodoc.typehints", 29 | "sphinx.ext.napoleon", 30 | "sphinx.ext.doctest", 31 | "sphinx.ext.intersphinx", 32 | "sphinx.ext.viewcode", 33 | "sphinxcontrib.mermaid", 34 | "sphinxext.opengraph", 35 | ] 36 | 37 | myst_enable_extensions = [ 38 | "colon_fence", 39 | "smartquotes", 40 | "deflist", 41 | ] 42 | mermaid_init_js = "mermaid.initialize({startOnLoad:true,theme:'neutral'});" 43 | 44 | ogp_image = "_static/structlog_logo.png" 45 | 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ["_templates"] 49 | 50 | # The suffix of source filenames. 51 | source_suffix = [".rst", ".md"] 52 | 53 | # The master toctree document. 54 | master_doc = "index" 55 | 56 | # General information about the project. 57 | project = "structlog" 58 | author = "Hynek Schlawack" 59 | copyright = f"2013, {author}" 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | 65 | # The full version, including alpha/beta/rc tags. 66 | release = metadata.version("structlog") 67 | # The short X.Y version. 68 | version = release.rsplit(".", 1)[0] 69 | 70 | if "dev" in release: 71 | release = version = "UNRELEASED" 72 | 73 | exclude_patterns = ["_build"] 74 | 75 | # The reST default role (used for this markup: `text`) to use for all 76 | # documents. 77 | default_role = "any" 78 | 79 | nitpick_ignore = [ 80 | ("py:class", "Context"), 81 | ("py:class", "EventDict"), 82 | ("py:class", "ILogObserver"), 83 | ("py:class", "PlainFileObserver"), 84 | ("py:class", "Processor"), 85 | ("py:class", "Styles"), 86 | ("py:class", "WrappedLogger"), 87 | ("py:class", "structlog.threadlocal.TLLogger"), 88 | ("py:class", "structlog.typing.EventDict"), 89 | ("py:class", "ModuleType"), 90 | ] 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | add_function_parentheses = True 94 | 95 | # Move type hints into the description block, instead of the func definition. 96 | autodoc_typehints = "description" 97 | autodoc_typehints_description_target = "documented" 98 | 99 | # -- Options for HTML output -------------------------------------------------- 100 | 101 | html_theme = "furo" 102 | html_theme_options = { 103 | "top_of_page_buttons": [], 104 | "light_css_variables": { 105 | "font-stack": "B612, sans-serif", 106 | "font-stack--monospace": "BerkeleyMono, MonoLisa, ui-monospace, " 107 | "SFMono-Regular, Menlo, Consolas, Liberation Mono, monospace", 108 | }, 109 | } 110 | html_logo = "_static/structlog_logo.svg" 111 | html_static_path = ["_static"] 112 | html_css_files = ["custom.css"] 113 | 114 | htmlhelp_basename = "structlogdoc" 115 | 116 | latex_documents = [ 117 | ("index", "structlog.tex", "structlog Documentation", "Author", "manual") 118 | ] 119 | 120 | # -- Options for manual page output ------------------------------------------- 121 | 122 | # One entry per manual page. List of tuples 123 | # (source start file, name, description, authors, manual section). 124 | man_pages = [("index", "structlog", "structlog Documentation", ["Author"], 1)] 125 | 126 | 127 | # -- Options for Texinfo output ----------------------------------------------- 128 | 129 | # Grouping the document tree into Texinfo files. List of tuples 130 | # (source start file, target name, title, author, 131 | # dir menu entry, description, category) 132 | texinfo_documents = [ 133 | ( 134 | "index", 135 | "structlog", 136 | "structlog Documentation", 137 | "Author", 138 | "structlog", 139 | "One line description of project.", 140 | "Miscellaneous", 141 | ) 142 | ] 143 | 144 | 145 | # -- Options for Epub output -------------------------------------------------- 146 | 147 | # Bibliographic Dublin Core info. 148 | epub_title = project 149 | epub_author = author 150 | epub_publisher = author 151 | epub_copyright = copyright 152 | 153 | # GitHub has rate limits 154 | linkcheck_ignore = [ 155 | r"https://github.com/.*/(issues|pull|compare)/\d+", 156 | r"https://twitter.com/.*", 157 | ] 158 | 159 | # Twisted's trac tends to be slow 160 | linkcheck_timeout = 300 161 | 162 | intersphinx_mapping = { 163 | "python": ("https://docs.python.org/3", None), 164 | "rich": ("https://rich.readthedocs.io/en/stable/", None), 165 | } 166 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The focus of *structlog* has always been to be flexible to a fault. 4 | The goal is that a user can use it with *any* logger of their own that is wrapped by *structlog*. 5 | 6 | That's the reason why there's an overwhelming amount of knobs to tweak, but 7 | – ideally – once you find your configuration, you don't touch it ever again and, more importantly: 8 | don't see any of it in your application code. 9 | 10 | --- 11 | 12 | Let's start at the end and introduce the ultimate convenience function that relies purely on configuration: {func}`structlog.get_logger`. 13 | 14 | The goal is to reduce your per-file application logging boilerplate to: 15 | 16 | ``` 17 | import structlog 18 | 19 | logger = structlog.get_logger() 20 | ``` 21 | 22 | To that end, you'll have to call {func}`structlog.configure` on app initialization. 23 | You can call {func}`structlog.configure` repeatedly and only set one or more settings -- the rest will not be affected. 24 | 25 | If necessary, you can always reset your global configuration back to default values using {func}`structlog.reset_defaults`. 26 | That can be handy in tests. 27 | 28 | At any time, you can check whether and how *structlog* is configured using {func}`structlog.is_configured` and {func}`structlog.get_config`}: 29 | 30 | ```pycon 31 | >>> structlog.is_configured() 32 | False 33 | >>> structlog.configure(logger_factory=structlog.stdlib.LoggerFactory()) 34 | >>> structlog.is_configured() 35 | True 36 | >>> cfg = structlog.get_config() 37 | >>> cfg["logger_factory"] 38 | 39 | ``` 40 | 41 | :::{important} 42 | Since you'll call {func}`structlog.get_logger` in module scope, it runs at import time *before* you had a chance to configure *structlog*. 43 | Therefore it returns a **lazy proxy** that returns a correctly configured *bound logger* on its first call to one of the context-managing methods like `bind()`. 44 | 45 | Thus, you must never call `new()` or `bind()` in module or class scope because , you will receive a logger configured with *structlog*'s default values. 46 | Use {func}`~structlog.get_logger`'s `initial_values` to achieve pre-populated contexts. 47 | 48 | To enable you to log with the module-global logger, it will create a temporary *bound logger* **on each call**. 49 | Therefore if you have nothing to bind but intend to do lots of log calls in a function, it makes sense performance-wise to create a local logger by calling `bind()` or `new()` without any parameters. 50 | See also {doc}`performance`. 51 | ::: 52 | 53 | 54 | ## What to configure 55 | 56 | You can find the details in the API documentation of {func}`structlog.configure`, but let's introduce the most important ones at a high level first. 57 | 58 | 59 | ### Wrapper classes 60 | 61 | You've met {doc}`bound-loggers` in the last chapter. 62 | They're the objects returned by {func}`~structlog.get_logger` and allow to bind key-value pairs into their private context. 63 | You can configure their type using the `wrapper_class` keyword. 64 | 65 | Whenever you bind or unbind data to a *bound logger*, this class is instantiated with the new context and returned. 66 | 67 | 68 | ### Logger factories 69 | 70 | We've already talked about wrapped loggers responsible for the output, but we haven't explained where they come from until now. 71 | Unlike with *bound loggers*, you often need more flexibility when instantiating them. 72 | Therefore you don't configure a class; you configure a *factory* using the `logger_factory` keyword. 73 | 74 | It's a callable that returns the logger that gets wrapped and returned. 75 | In the simplest case, it's a function that returns a logger -- or just a class. 76 | But you can also pass in an instance of a class with a `__call__` method for more complicated setups. 77 | 78 | The arguments you pass to `structlog.get_logger()` will be passed to the logger factory. 79 | For example, if you use `structlog.get_logger("a name")` and configure *structlog* to use the standard library {class}`~structlog.stdlib.LoggerFactory`, which has support for positional parameters, the returned logger will have the name `"a name"`. 80 | 81 | For the common cases of standard library logging and Twisted logging, *structlog* comes with two factories built right in: 82 | 83 | - {class}`structlog.stdlib.LoggerFactory` 84 | - {class}`structlog.twisted.LoggerFactory` 85 | 86 | So all it takes to use standard library {mod}`logging` for output is: 87 | 88 | ``` 89 | >>> from structlog import get_logger, configure 90 | >>> from structlog.stdlib import LoggerFactory 91 | >>> configure(logger_factory=LoggerFactory()) 92 | >>> log = get_logger() 93 | >>> log.critical("this is too easy!") 94 | event='this is too easy!' 95 | ``` 96 | 97 | By using *structlog*'s {class}`structlog.stdlib.LoggerFactory`, it is also ensured that variables like function names and line numbers are expanded correctly in your log format. 98 | See {doc}`standard-library` for more details. 99 | 100 | Calling {func}`structlog.get_logger` without configuration gives you a perfectly useful {class}`structlog.PrintLogger`. 101 | We don't believe silent loggers are a sensible default. 102 | 103 | 104 | ### Processors 105 | 106 | You will meet {doc}`processors` in the next chapter. 107 | They are configured using the `processors` keyword that takes an {class}`~collections.abc.Iterable` of callables that act as processors. 108 | -------------------------------------------------------------------------------- /docs/console-output.md: -------------------------------------------------------------------------------- 1 | # Console Output 2 | 3 | To make development a more pleasurable experience, *structlog* comes with the {mod}`structlog.dev` module. 4 | 5 | The highlight is {class}`structlog.dev.ConsoleRenderer` that offers nicely aligned and colorful[^win] console output. 6 | 7 | [^win]: Requires the [Colorama package](https://pypi.org/project/colorama/) on Windows. 8 | 9 | If either of the [Rich](https://rich.readthedocs.io/) or [*better-exceptions*](https://github.com/Qix-/better-exceptions) packages is installed, it will also pretty-print exceptions with helpful contextual data. 10 | Rich takes precedence over *better-exceptions*, but you can configure it by passing {func}`structlog.dev.plain_traceback` or {func}`structlog.dev.better_traceback` for the `exception_formatter` parameter of {class}`~structlog.dev.ConsoleRenderer`. 11 | 12 | The following output is rendered using Rich: 13 | 14 | ```{figure} _static/console_renderer.png 15 | Colorful console output by ConsoleRenderer. 16 | ``` 17 | 18 | You can find the code for the output above [in the repo](https://github.com/hynek/structlog/blob/main/show_off.py). 19 | 20 | To use it, just add it as a renderer to your processor chain. 21 | It will recognize logger names, log levels, time stamps, stack infos, and `exc_info` as produced by *structlog*'s processors and render them in special ways. 22 | 23 | :::{warning} 24 | For pretty exceptions to work, {func}`~structlog.processors.format_exc_info` must be **absent** from the processors chain. 25 | ::: 26 | 27 | *structlog*'s default configuration already uses {class}`~structlog.dev.ConsoleRenderer`, therefore if you want nice colorful output on the console, you don't have to do anything except installing Rich or *better-exceptions* (and Colorama on Windows). 28 | If you want to use it along with standard library logging, there's the {func}`structlog.stdlib.recreate_defaults` helper. 29 | 30 | :::{seealso} 31 | {doc}`exceptions` for more information on how to configure exception rendering. 32 | For the console and beyond. 33 | ::: 34 | 35 | (columns-config)= 36 | 37 | ## Console output configuration 38 | 39 | :::{versionadded} 23.3.0 40 | ::: 41 | 42 | You can freely configure how the key-value pairs are formatted: colors, order, and how values are stringified. 43 | 44 | For that {class}`~structlog.dev.ConsoleRenderer` accepts the *columns* parameter that takes a list of {class}`~structlog.dev.Column`s. 45 | It allows you to assign a formatter to each key and a default formatter for the rest (by passing an empty key name). 46 | The order of the column definitions is the order in which the columns are rendered; 47 | the rest is -- depending on the *sort_keys* argument to {class}`~structlog.dev.ConsoleRenderer` -- either sorted alphabetically or in the order of the keys in the event dictionary. 48 | 49 | You can use a column definition to drop a key-value pair from the output by returning an empty string from the formatter. 50 | 51 | When the API talks about "styles", it means ANSI control strings. 52 | You can find them, for example, in [Colorama](https://github.com/tartley/colorama). 53 | 54 | 55 | It's best demonstrated by an example: 56 | 57 | ```python 58 | import structlog 59 | import colorama 60 | 61 | cr = structlog.dev.ConsoleRenderer( 62 | columns=[ 63 | # Render the timestamp without the key name in yellow. 64 | structlog.dev.Column( 65 | "timestamp", 66 | structlog.dev.KeyValueColumnFormatter( 67 | key_style=None, 68 | value_style=colorama.Fore.YELLOW, 69 | reset_style=colorama.Style.RESET_ALL, 70 | value_repr=str, 71 | ), 72 | ), 73 | # Render the event without the key name in bright magenta. 74 | structlog.dev.Column( 75 | "event", 76 | structlog.dev.KeyValueColumnFormatter( 77 | key_style=None, 78 | value_style=colorama.Style.BRIGHT + colorama.Fore.MAGENTA, 79 | reset_style=colorama.Style.RESET_ALL, 80 | value_repr=str, 81 | ), 82 | ), 83 | # Default formatter for all keys not explicitly mentioned. The key is 84 | # cyan, the value is green. 85 | structlog.dev.Column( 86 | "", 87 | structlog.dev.KeyValueColumnFormatter( 88 | key_style=colorama.Fore.CYAN, 89 | value_style=colorama.Fore.GREEN, 90 | reset_style=colorama.Style.RESET_ALL, 91 | value_repr=str, 92 | ), 93 | ), 94 | ] 95 | ) 96 | 97 | structlog.configure(processors=structlog.get_config()["processors"][:-1]+[cr]) 98 | ``` 99 | 100 | :::{hint} 101 | You can replace only the last processor using: 102 | 103 | ```python 104 | structlog.configure(processors=structlog.get_config()["processors"][:-1]+[cr]) 105 | ``` 106 | ::: 107 | 108 | 109 | ## Standard environment variables 110 | 111 | *structlog*'s default configuration uses colors if standard out is a TTY (that is, an interactive session). 112 | 113 | It's possible to override this behavior by setting two standard environment variables to any value except an empty string: 114 | 115 | - `FORCE_COLOR` *activates* colors, regardless of where output is going. 116 | - [`NO_COLOR`](https://no-color.org) *disables* colors, regardless of where the output is going and regardless the value of `FORCE_COLOR`. 117 | Please note that `NO_COLOR` disables _all_ styling, including bold and italics. 118 | 119 | 120 | ## Disabling exception pretty-printing 121 | 122 | If you prefer the default terse Exception rendering, but still want Rich installed, you can disable the pretty-printing by instantiating {class}`structlog.dev.ConsoleRenderer()` yourself and passing `exception_formatter=structlog.dev.plain_traceback`. 123 | -------------------------------------------------------------------------------- /docs/contextvars.md: -------------------------------------------------------------------------------- 1 | (contextvars)= 2 | 3 | # Context Variables 4 | 5 | ```{testsetup} 6 | import structlog 7 | ``` 8 | 9 | ```{testcleanup} 10 | import structlog 11 | structlog.reset_defaults() 12 | ``` 13 | 14 | The {mod}`contextvars` module in the Python standard library allows having a global *structlog* context that is local to the current execution context. 15 | The execution context can be thread-local if using threads, stored in the {mod}`asyncio` event loop, or [*greenlet*](https://greenlet.readthedocs.io/) respectively. 16 | 17 | For example, you may want to bind certain values like a request ID or the peer's IP address at the beginning of a web request and have them logged out along with the local contexts you build within our views. 18 | 19 | For that *structlog* provides the {mod}`structlog.contextvars` module with a set of functions to bind variables to a context-local context. 20 | This context is safe to be used both in threaded as well as asynchronous code. 21 | 22 | :::{warning} 23 | Since the storage mechanics of your context variables is different for each concurrency method, they are _isolated_ from each other. 24 | 25 | This can be a problem in hybrid applications like those based on [*starlette*](https://www.starlette.io) (this [includes FastAPI](https://github.com/tiangolo/fastapi/discussions/5999)) where context variables set in a synchronous context don't appear in logs from an async context and vice versa. 26 | ::: 27 | 28 | The general flow is: 29 | 30 | - Use {func}`structlog.configure` with {func}`structlog.contextvars.merge_contextvars` as your first processor (part of default configuration). 31 | - Call {func}`structlog.contextvars.clear_contextvars` at the beginning of your request handler (or whenever you want to reset the context-local context). 32 | - Call {func}`structlog.contextvars.bind_contextvars` and {func}`structlog.contextvars.unbind_contextvars` instead of your bound logger's `bind()` and `unbind()` when you want to bind and unbind key-value pairs to the context-local context. 33 | You can also use the {func}`structlog.contextvars.bound_contextvars` context manager / decorator. 34 | - Use *structlog* as normal. 35 | Loggers act as they always do, but the {func}`structlog.contextvars.merge_contextvars` processor ensures that any context-local binds get included in all of your log messages. 36 | - If you want to access the context-local storage, you use {func}`structlog.contextvars.get_contextvars` and {func}`structlog.contextvars.get_merged_contextvars`. 37 | 38 | We're sorry the word *context* means three different things in this itemization depending on ... context. 39 | 40 | ```{doctest} 41 | >>> from structlog.contextvars import ( 42 | ... bind_contextvars, 43 | ... bound_contextvars, 44 | ... clear_contextvars, 45 | ... merge_contextvars, 46 | ... unbind_contextvars, 47 | ... ) 48 | >>> from structlog import configure 49 | >>> configure( 50 | ... processors=[ 51 | ... merge_contextvars, 52 | ... structlog.processors.KeyValueRenderer(key_order=["event", "a"]), 53 | ... ] 54 | ... ) 55 | >>> log = structlog.get_logger() 56 | >>> # At the top of your request handler (or, ideally, some general 57 | >>> # middleware), clear the contextvars-local context and bind some common 58 | >>> # values: 59 | >>> clear_contextvars() 60 | >>> bind_contextvars(a=1, b=2) 61 | {'a': at ...>, 'b': at ...>} 62 | >>> # Then use loggers as per normal 63 | >>> # (perhaps by using structlog.get_logger() to create them). 64 | >>> log.info("hello") 65 | event='hello' a=1 b=2 66 | >>> # Use unbind_contextvars to remove a variable from the context. 67 | >>> unbind_contextvars("b") 68 | >>> log.info("world") 69 | event='world' a=1 70 | >>> # You can also bind key-value pairs temporarily. 71 | >>> with bound_contextvars(b=2): 72 | ... log.info("hi") 73 | event='hi' a=1 b=2 74 | >>> # Now it's gone again. 75 | >>> log.info("hi") 76 | event='hi' a=1 77 | >>> # And when we clear the contextvars state again, it goes away. 78 | >>> # a=None is printed due to the key_order argument passed to 79 | >>> # KeyValueRenderer, but it is NOT present anymore. 80 | >>> clear_contextvars() 81 | >>> log.info("hi there") 82 | event='hi there' a=None 83 | ``` 84 | 85 | 86 | ## Support for `contextvars.Token` 87 | 88 | If, for example, your request handler calls a helper function that needs to temporarily override some contextvars before restoring them back to their original values, you can use the {class}`~contextvars.Token`s returned by {func}`~structlog.contextvars.bind_contextvars` along with {func}`~structlog.contextvars.reset_contextvars` to accomplish this (much like how {meth}`contextvars.ContextVar.reset` works): 89 | 90 | ```python 91 | def foo(): 92 | bind_contextvars(a=1) 93 | _helper() 94 | log.info("a is restored!") # a=1 95 | 96 | def _helper(): 97 | tokens = bind_contextvars(a=2) 98 | log.info("a is overridden") # a=2 99 | reset_contextvars(**tokens) 100 | ``` 101 | 102 | (flask-example)= 103 | 104 | ## Example: Flask and thread-local data 105 | 106 | Let's assume you want to bind a unique request ID, the URL path, and the peer's IP to every log entry by storing it in thread-local storage that is managed by context variables: 107 | 108 | ```python 109 | import logging 110 | import sys 111 | import uuid 112 | 113 | import flask 114 | 115 | from .some_module import some_function 116 | 117 | import structlog 118 | 119 | logger = structlog.get_logger() 120 | app = flask.Flask(__name__) 121 | 122 | @app.route("/login", methods=["POST", "GET"]) 123 | def some_route(): 124 | # You would put this into some kind of middleware or processor so it's set 125 | # automatically for all requests in all views. 126 | structlog.contextvars.clear_contextvars() 127 | structlog.contextvars.bind_contextvars( 128 | view=flask.request.path, 129 | request_id=str(uuid.uuid4()), 130 | peer=flask.request.access_route[0], 131 | ) 132 | # End of belongs-to-middleware. 133 | 134 | log = logger.bind() 135 | # do something 136 | # ... 137 | log.info("user logged in", user="test-user") 138 | # ... 139 | some_function() 140 | # ... 141 | return "logged in!" 142 | 143 | 144 | if __name__ == "__main__": 145 | logging.basicConfig( 146 | format="%(message)s", stream=sys.stdout, level=logging.INFO 147 | ) 148 | structlog.configure( 149 | processors=[ 150 | structlog.contextvars.merge_contextvars, # <--!!! 151 | structlog.processors.KeyValueRenderer( 152 | key_order=["event", "view", "peer"] 153 | ), 154 | ], 155 | logger_factory=structlog.stdlib.LoggerFactory(), 156 | ) 157 | app.run() 158 | 159 | ``` 160 | 161 | `some_module.py`: 162 | 163 | ```python 164 | from structlog import get_logger 165 | 166 | logger = get_logger() 167 | 168 | def some_function(): 169 | # ... 170 | logger.error("user did something", something="shot_in_foot") 171 | # ... 172 | ``` 173 | 174 | This would result among other the following lines to be printed: 175 | 176 | ```text 177 | event='user logged in' view='/login' peer='127.0.0.1' user='test-user' request_id='e08ddf0d-23a5-47ce-b20e-73ab8877d736' 178 | event='user did something' view='/login' peer='127.0.0.1' something='shot_in_foot' request_id='e08ddf0d-23a5-47ce-b20e-73ab8877d736' 179 | ``` 180 | 181 | As you can see, `view`, `peer`, and `request_id` are present in **both** log entries. 182 | -------------------------------------------------------------------------------- /docs/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | While you should use a proper crash reporter like [Sentry](https://sentry.io) in production, *structlog* has helpers for formatting exceptions for humans and machines. 4 | 5 | All *structlog*'s exception features center around passing an `exc_info` key-value pair in the event dict. 6 | There are three possible behaviors depending on its value: 7 | 8 | 1. If the value is a tuple, render it as if it was returned by {func}`sys.exc_info`. 9 | 2. If the value is an Exception, render it. 10 | 3. If the value is true but no tuple, call {func}`sys.exc_info` and render that. 11 | 12 | If there is no `exc_info` key or false, the event dict is not touched. 13 | This behavior is analog to the one of the stdlib's logging. 14 | 15 | 16 | ## Transformations 17 | 18 | *structlog* comes with {class}`structlog.processors.ExceptionRenderer` that deduces and removes the `exc_info` key as outlined above, calls a user-supplied function with the synthesized `exc_info`, and stores its return value in the `exception` key. 19 | The most common use-cases are already covered by the following processors: 20 | 21 | {func}`structlog.processors.format_exc_info` 22 | 23 | : Formats it to a flat string like the standard library would on the console. 24 | 25 | {obj}`structlog.processors.dict_tracebacks` 26 | 27 | : Uses {class}`structlog.tracebacks.ExceptionDictTransformer` to give you a structured and JSON-serializable `exception` key. 28 | 29 | 30 | ## Console rendering 31 | 32 | Our {doc}`console-output`'s {class}`structlog.dev.ConsoleRenderer` takes an *exception_formatter* argument that allows for customizing the output of exceptions. 33 | 34 | {func}`structlog.dev.plain_traceback` 35 | 36 | : Is the default if neither [Rich] nor [*better-exceptions*] are installed. 37 | As the name suggests, it renders a plain traceback. 38 | 39 | {func}`structlog.dev.better_traceback` 40 | 41 | : Uses [*better-exceptions*] to render a colorful traceback. 42 | : It's the default if *better-exceptions* is installed and Rich is not. 43 | 44 | {class}`structlog.dev.RichTracebackFormatter` 45 | 46 | : Uses [Rich] to render a colorful traceback. 47 | It's a class because it allows for customizing the output by passing arguments to Rich. 48 | : It's the default if Rich is installed. 49 | 50 | :::{seealso} 51 | {doc}`console-output` for more information on *structlog*'s console features. 52 | ::: 53 | 54 | [*better-exceptions*]: https://github.com/qix-/better-exceptions 55 | [Rich]: https://github.com/Textualize/rich 56 | -------------------------------------------------------------------------------- /docs/frameworks.md: -------------------------------------------------------------------------------- 1 | # Frameworks 2 | 3 | To have consistent log output, it makes sense to configure *structlog* *before* any logging is done. 4 | The best place to perform your configuration varies with applications and frameworks. 5 | If you use standard library's {mod}`logging`, it makes sense to configure them next to each other. 6 | 7 | 8 | ## Celery 9 | 10 | [Celery](https://docs.celeryq.dev/)'s multi-process architecture leads unavoidably to race conditions that show up as interleaved logs. 11 | It ships standard library-based helpers in the form of [`celery.utils.log.get_task_logger()`](https://docs.celeryq.dev/en/stable/userguide/tasks.html#logging) that you should use inside of tasks to prevent that problem. 12 | 13 | The most straight-forward way to integrate that with *structlog* is using {doc}`standard-library` and wrapping that logger using {func}`structlog.wrap_logger`: 14 | 15 | ```python 16 | from celery.utils.log import get_task_logger 17 | 18 | logger = structlog.wrap_logger(get_task_logger(__name__)) 19 | ``` 20 | 21 | If you want to automatically bind task metadata to your {doc}`contextvars`, you can use [Celery's signals](https://docs.celeryq.dev/en/stable/userguide/signals.html): 22 | 23 | ```python 24 | from celery import signals 25 | 26 | @signals.task_prerun.connect 27 | def on_task_prerun(sender, task_id, task, args, kwargs, **_): 28 | structlog.contextvars.bind_contextvars(task_id=task_id, task_name=task.name) 29 | ``` 30 | 31 | See [this issue](https://github.com/hynek/structlog/issues/287) for more details. 32 | 33 | 34 | ## Django 35 | 36 | [*django-structlog*](https://pypi.org/project/django-structlog/) is a popular and well-maintained package that does all the heavy lifting. 37 | 38 | 39 | ## Flask 40 | 41 | See Flask's [Logging docs](https://flask.palletsprojects.com/en/latest/logging/). 42 | 43 | Generally speaking: configure *structlog* *before* instantiating `flask.Flask`. 44 | 45 | Here's a [signal handler](https://flask.palletsprojects.com/en/latest/signals/) that binds various request details into [*context variables*](contextvars.md): 46 | 47 | ```python 48 | def bind_request_details(sender: Flask, **extras: dict[str, Any]) -> None: 49 | structlog.contextvars.clear_contextvars() 50 | structlog.contextvars.bind_contextvars( 51 | request_id=request.headers.get("X-Unique-ID", "NONE"), 52 | peer=peer, 53 | ) 54 | 55 | if current_user.is_authenticated: 56 | structlog.contextvars.bind_contextvars( 57 | user_id=current_user.get_id(), 58 | ) 59 | ``` 60 | 61 | You add it to an existing `app` like this: 62 | 63 | ```python 64 | from flask import request_started 65 | 66 | request_started.connect(bind_request_details, app) 67 | ``` 68 | 69 | 70 | ## Litestar 71 | 72 | [Litestar](https://docs.litestar.dev/) comes with *structlog* support [out of the box](https://docs.litestar.dev/latest/usage/logging.html). 73 | 74 | 75 | ## OpenTelemetry 76 | 77 | The [Python OpenTelemetry SDK](https://opentelemetry.io/docs/languages/python/) offers an easy API to get the current span, so you can enrich your logs with a straight-forward processor: 78 | 79 | ```python 80 | from opentelemetry import trace 81 | 82 | def add_open_telemetry_spans(_, __, event_dict): 83 | span = trace.get_current_span() 84 | if not span.is_recording(): 85 | event_dict["span"] = None 86 | return event_dict 87 | 88 | ctx = span.get_span_context() 89 | parent = getattr(span, "parent", None) 90 | 91 | event_dict["span"] = { 92 | "span_id": format(ctx.span_id, "016x"), 93 | "trace_id": format(ctx.trace_id, "032x"), 94 | "parent_span_id": None if not parent else format(parent.span_id, "016x"), 95 | } 96 | 97 | return event_dict 98 | ``` 99 | 100 | 101 | ## Pyramid 102 | 103 | Configure it in the [application constructor](https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/startup.html#the-startup-process). 104 | 105 | Here's an example for a Pyramid [*tween*](https://docs.pylonsproject.org/projects/pyramid/en/latest/glossary.html#term-tween) that stores various request-specific data into [*context variables*](contextvars.md): 106 | 107 | ```python 108 | @dataclass 109 | class StructLogTween: 110 | handler: Callable[[Request], Response] 111 | registry: Registry 112 | 113 | def __call__(self, request: Request) -> Response: 114 | structlog.contextvars.clear_contextvars() 115 | structlog.contextvars.bind_contextvars( 116 | peer=request.client_addr, 117 | request_id=request.headers.get("X-Unique-ID", "NONE"), 118 | user_agent=request.environ.get("HTTP_USER_AGENT", "UNKNOWN"), 119 | user=request.authenticated_userid, 120 | ) 121 | 122 | return self.handler(request) 123 | ``` 124 | 125 | 126 | ## Twisted 127 | 128 | The [plugin definition](https://docs.twisted.org/en/stable/core/howto/plugin.html) is the best place. 129 | If your app is not a plugin, put it into your [tac file](https://docs.twisted.org/en/stable/core/howto/application.html). 130 | -------------------------------------------------------------------------------- /docs/glossary.md: -------------------------------------------------------------------------------- 1 | # Glossary 2 | 3 | Please feel free to [file an issue](https://github.com/hynek/structlog/issues) if you think some important concept is missing here. 4 | 5 | :::{glossary} 6 | 7 | Event Dictionary 8 | Often abbreviated as *event dict*. 9 | It's a dictionary that contains all the information that is logged, with the `event` key having the special role of being the name of the event. 10 | 11 | It's the result of the values bound to the {term}`bound logger`'s context and the key-value pairs passed to the logging method. 12 | It is then passed through the {term}`processor` chain that can add, modify, and even remove key-value pairs. 13 | 14 | Bound Logger 15 | An instance of a {class}`structlog.typing.BindableLogger` that is returned by either {func}`structlog.get_logger` or the bind/unbind/new methods on it. 16 | 17 | As the name suggests, it's possible to bind key-value pairs to it -- this data is called the {term}`context` of the logger. 18 | 19 | Its methods are the user's logging API and depend on the type of the bound logger. 20 | The two most common implementations are {class}`structlog.BoundLogger` and {class}`structlog.stdlib.BoundLogger`. 21 | 22 | Bound loggers are **immutable**. 23 | The context can only be modified by creating a new bound logger using its `bind()`and `unbind()` methods. 24 | 25 | :::{seealso} 26 | {doc}`bound-loggers` 27 | ::: 28 | 29 | Context 30 | A dictionary of key-value pairs belonging to a {term}`bound logger`. 31 | When a log entry is logged out, the context is the base for the event dictionary with the keyword arguments of the logging method call merged in. 32 | 33 | Bound loggers are **immutable**, so it's not possible to modify a context directly. 34 | But you can create a new bound logger with a different context using its `bind()` and `unbind()` methods. 35 | 36 | Native Loggers 37 | Loggers created using {func}`structlog.make_filtering_bound_logger` which includes the default configuration. 38 | 39 | These loggers are very fast and do **not** use the standard library. 40 | 41 | Wrapped Logger 42 | The logger that is wrapped by *structlog* and that is responsible for the actual output. 43 | 44 | By default it's a {class}`structlog.PrintLogger` for native logging. 45 | Another popular choice is {class}`logging.Logger` for standard library logging. 46 | 47 | :::{seealso} 48 | {doc}`standard-library` 49 | ::: 50 | 51 | Processor 52 | A callable that is called on every log entry. 53 | 54 | It receives the return value of its predecessor as an argument and returns a new event dictionary. 55 | This allows for composable transformations of the event dictionary. 56 | 57 | The result of the final processor is passed to the {term}`wrapped logger`. 58 | 59 | :::{seealso} 60 | {doc}`processors` 61 | ::: 62 | 63 | ::: 64 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # structlog 2 | 3 | *Simple. Powerful. Fast. Pick three.* 4 | 5 | Release **{sub-ref}`release`** ([What's new?](https://github.com/hynek/structlog/blob/main/CHANGELOG.md)) 6 | 7 | --- 8 | 9 | ```{include} ../README.md 10 | :start-after: 11 | :end-before: 12 | ``` 13 | 14 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ```{include} ../README.md 32 | :start-after: 33 | :end-before: 34 | ``` 35 | 36 | If you’d like more information on why structured logging in general – and *structlog* in particular – are good ideas, we’ve prepared a [summary](why.md) just for you. 37 | 38 | Otherwise, let’s dive right in! 39 | 40 | ```{toctree} 41 | :hidden: true 42 | 43 | why 44 | ``` 45 | 46 | 47 | ## Basics 48 | 49 | The first chapters teach you all you need to use *structlog* productively. 50 | They build gently on each other, so ideally, read them in order. 51 | If anything seems confusing, don't hesitate to have a look at our {doc}`glossary`! 52 | 53 | 54 | ```{toctree} 55 | :maxdepth: 2 56 | :caption: Basics 57 | 58 | getting-started 59 | bound-loggers 60 | configuration 61 | processors 62 | contextvars 63 | exceptions 64 | ``` 65 | 66 | 67 | ## Development affordances 68 | 69 | *structlog*'s focus is on production systems, but it comes with **pretty console logging** and handy in-development helpers both for your **comfort** and your code's **quality**. 70 | 71 | ```{toctree} 72 | :maxdepth: 2 73 | :caption: Development Affordances 74 | 75 | console-output 76 | testing 77 | typing 78 | ``` 79 | 80 | (integration)= 81 | 82 | ## Integration with existing systems 83 | 84 | *structlog* is both zero-config as well as highly configurable. 85 | You can use it on its own or integrate with existing systems. 86 | Dedicated support for the standard library and Twisted is shipped out-of-the-box. 87 | 88 | ```{toctree} 89 | :maxdepth: 2 90 | :caption: Integrations 91 | 92 | frameworks 93 | standard-library 94 | twisted 95 | ``` 96 | 97 | 98 | ## *structlog* in practice 99 | 100 | The following chapters deal with considerations of using *structlog* in the real world. 101 | 102 | 103 | ```{toctree} 104 | :maxdepth: 2 105 | :caption: In Practice 106 | 107 | recipes 108 | logging-best-practices 109 | performance 110 | ``` 111 | 112 | 113 | ## Reference 114 | 115 | ```{toctree} 116 | :maxdepth: 2 117 | :caption: Reference 118 | 119 | api 120 | glossary 121 | genindex 122 | modindex 123 | ``` 124 | 125 | 126 | ## Deprecated features 127 | 128 | ```{toctree} 129 | :maxdepth: 1 130 | :caption: Deprecated Features 131 | 132 | thread-local 133 | ``` 134 | 135 | 136 | ```{toctree} 137 | :hidden: 138 | :caption: Meta 139 | 140 | license 141 | PyPI 142 | GitHub 143 | Changelog 144 | Contributing 145 | Security Policy 146 | Funding 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License and Hall of Fame 2 | 3 | *structlog* is licensed both under the [Apache License, Version 2](https://choosealicense.com/licenses/apache/) and the [MIT license](https://choosealicense.com/licenses/mit/). 4 | 5 | Any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 6 | 7 | --- 8 | 9 | The reason for that is to be both protected against patent claims by own contributors and still allow the usage within GPLv2 software. For more legal details, see [this issue](https://github.com/pyca/cryptography/issues/1209) on the bug tracker of PyCA's *cryptography* project. 10 | 11 | The full license texts can be also found in the source code repository: 12 | 13 | - [Apache License 2.0](https://github.com/hynek/structlog/blob/main/LICENSE-APACHE) 14 | - [MIT](https://github.com/hynek/structlog/blob/main/LICENSE-MIT) 15 | 16 | 17 | ## Credits 18 | 19 | ```{include} ../README.md 20 | :parser: myst_parser.sphinx_ 21 | :start-after: "## Credits" 22 | :end-before: 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/logging-best-practices.md: -------------------------------------------------------------------------------- 1 | # Logging Best Practices 2 | 3 | Logging is not a new concept and is in no way unique to Python. 4 | Logfiles have existed for decades, and there's little reason to reinvent the wheel in our little world. 5 | Therefore, let's rely on proven tools as much as possible and do only the bare minimum inside Python applications[^unix]. 6 | 7 | A simple but powerful approach is to log to unbuffered [standard out](https://en.wikipedia.org/wiki/Standard_out#Standard_output_.28stdout.29 8 | ) and let other tools take care of the rest. 9 | 10 | That can be your terminal window while developing; it can be [*systemd*](https://en.wikipedia.org/wiki/Systemd) redirecting your log entries to [*syslogd*](https://en.wikipedia.org/wiki/Syslogd) and rotating them using [*logrotate*](https://github.com/logrotate/logrotate); or it can be your [cluster manager](https://kubernetes.io/docs/concepts/cluster-administration/logging/) forwarding them to an obscenely expensive log aggregator service. 11 | 12 | It doesn't matter where or how your application runs -- it just works, and the reason why the popular [*Twelve-Factor App* methodology](https://12factor.net/logs) suggests just that. 13 | 14 | [^unix]: This is obviously a privileged UNIX-centric view but even Windows has tools and means for log management although we won't be able to discuss them here. 15 | 16 | 17 | ## Canonical log lines 18 | 19 | Generally speaking, having as few log entries per request as possible is a good thing. 20 | The less noise, the more insights. 21 | 22 | *structlog*'s ability to {ref}`bind data to loggers incrementally ` -- plus {doc}`loggers that are local to the current execution context ` -- can help you to minimize the output to a *single log entry*. 23 | 24 | At Stripe, this concept is called [Canonical Log Lines](https://brandur.org/canonical-log-lines). 25 | 26 | 27 | ## Pretty printing vs. structured output 28 | 29 | Colorful and pretty printed log messages are nice during development when you locally run your code. 30 | 31 | However, in production you should emit structured output (like JSON) which is a lot easier to parse by log aggregators. 32 | Since you already log in a structured way, writing JSON output with *structlog* comes naturally. 33 | You can even generate structured exception tracebacks. 34 | This makes analyzing errors easier, since log aggregators can render JSON much better than multiline strings with a lot escaped quotation marks. 35 | 36 | Here is a simple example of how you can have pretty logs during development and JSON output when your app is running in a production context: 37 | 38 | ```{doctest} 39 | >>> import sys 40 | >>> import structlog 41 | >>> 42 | >>> shared_processors = [ 43 | ... # Processors that have nothing to do with output, 44 | ... # e.g., add timestamps or log level names. 45 | ... ] 46 | >>> if sys.stderr.isatty(): 47 | ... # Pretty printing when we run in a terminal session. 48 | ... # Automatically prints pretty tracebacks when "rich" is installed 49 | ... processors = shared_processors + [ 50 | ... structlog.dev.ConsoleRenderer(), 51 | ... ] 52 | ... else: 53 | ... # Print JSON when we run, e.g., in a Docker container. 54 | ... # Also print structured tracebacks. 55 | ... processors = shared_processors + [ 56 | ... structlog.processors.dict_tracebacks, 57 | ... structlog.processors.JSONRenderer(), 58 | ... ] 59 | >>> structlog.configure(processors) 60 | 61 | ``` 62 | 63 | 64 | ## Centralized logging 65 | 66 | Nowadays you usually don't want your log files in compressed archives distributed over dozens -- if not thousands -- of servers or cluster nodes. 67 | You want them in a single location. 68 | Parsed, indexed, and easy to search. 69 | 70 | 71 | ### ELK 72 | 73 | The ELK stack ([**E**lasticsearch][elasticsearch], [**L**ogstash][logstash], [**K**ibana][kibana]) from Elastic is a great way to store, parse, and search your logs. 74 | 75 | The way it works is that you have local log shippers like [Filebeat] that parse your log files and forward the log entries to your [Logstash] server. 76 | Logstash parses the log entries and stores them in [Elasticsearch]. 77 | Finally, you can view and search them in [Kibana]. 78 | 79 | If your log entries consist of a JSON dictionary, this is fairly easy and efficient. 80 | All you have to do is to tell [Logstash] either that your log entries are prepended with a timestamp from {class}`~structlog.processors.TimeStamper` or the name of your timestamp field. 81 | 82 | 83 | ### Graylog 84 | 85 | [Graylog](https://graylog.org/) goes one step further. 86 | It not only supports everything those above do (and then some); you can also directly log JSON entries towards it -- optionally even through an AMQP server (like [RabbitMQ](https://www.rabbitmq.com/)) for better reliability. 87 | Additionally, [Graylog's Extended Log Format](https://go2docs.graylog.org/current/getting_in_log_data/gelf.html) (GELF) allows for structured data which makes it an obvious choice to use together with *structlog*. 88 | 89 | 90 | [elasticsearch]: https://www.elastic.co/elasticsearch 91 | [filebeat]: https://github.com/elastic/beats/tree/main/filebeat 92 | [kibana]: https://www.elastic.co/kibana 93 | [logstash]: https://www.elastic.co/logstash 94 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\structlog.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\structlog.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/performance.md: -------------------------------------------------------------------------------- 1 | # Performance 2 | 3 | Here are a few hints how to get the best performance out of *structlog* in production: 4 | 5 | - Use *structlog*'s native *BoundLogger* (created using {func}`structlog.make_filtering_bound_logger`) if you want to use level-based filtering. 6 | `return None` is hard to beat. 7 | 8 | - Avoid (frequently) calling log methods on loggers you get back from {func}`structlog.get_logger` or {func}`structlog.wrap_logger`. 9 | Since those functions are usually called in module scope and thus before you are able to configure them, they return a proxy object that assembles the correct logger on demand. 10 | 11 | Create a local logger if you expect to log frequently without binding: 12 | 13 | ```python 14 | logger = structlog.get_logger() 15 | def f(): 16 | log = logger.bind() 17 | for i in range(1000000000): 18 | log.info("iterated", i=i) 19 | ``` 20 | 21 | Since global scope lookups are expensive in Python, it's generally a good idea to copy frequently-used symbols into local scope. 22 | 23 | - Set the *cache_logger_on_first_use* option to `True` so the aforementioned on-demand loggers will be assembled only once and cached for future uses: 24 | 25 | ```python 26 | configure(cache_logger_on_first_use=True) 27 | ``` 28 | 29 | This has two drawbacks: 30 | 31 | 1. Later calls of {func}`~structlog.configure` don't have any effect on already cached loggers -- that shouldn't matter outside of {doc}`testing ` though. 32 | 2. The resulting bound logger is not pickleable. 33 | Therefore, you can't set this option if you, for example, plan on passing loggers around using {mod}`multiprocessing`. 34 | 35 | - Avoid sending your log entries through the standard library if you can: its dynamic nature and flexibility make it a major bottleneck. 36 | Instead use {class}`structlog.WriteLoggerFactory` or -- if your serializer returns bytes (for example, [*orjson*] or [*msgspec*]) -- {class}`structlog.BytesLoggerFactory`. 37 | 38 | You can still configure `logging` for packages that you don't control, but avoid it for your *own* log entries. 39 | 40 | - Configure {class}`~structlog.processors.JSONRenderer` to use a faster JSON serializer than the standard library. 41 | Possible alternatives are among others are [*orjson*], [*msgspec*], or [RapidJSON](https://pypi.org/project/python-rapidjson/). 42 | 43 | - Be conscious about whether and how you use *structlog*'s *asyncio* support. 44 | While it's true that moving log processing into separate threads prevents your application from hanging, it also comes with a performance cost. 45 | 46 | Decide judiciously whether or not you're willing to pay that price. 47 | If your processor chain has a good and predictable performance without external dependencies (as it should), it might not be worth it. 48 | 49 | 50 | ## Example 51 | 52 | Here's an example for a production-ready *structlog* configuration that's as fast as it gets: 53 | 54 | ```python 55 | import logging 56 | import orjson 57 | import structlog 58 | 59 | structlog.configure( 60 | cache_logger_on_first_use=True, 61 | wrapper_class=structlog.make_filtering_bound_logger(logging.INFO), 62 | processors=[ 63 | structlog.contextvars.merge_contextvars, 64 | structlog.processors.add_log_level, 65 | structlog.processors.format_exc_info, 66 | structlog.processors.TimeStamper(fmt="iso", utc=True), 67 | structlog.processors.JSONRenderer(serializer=orjson.dumps), 68 | ], 69 | logger_factory=structlog.BytesLoggerFactory(), 70 | ) 71 | ``` 72 | 73 | It has the following properties: 74 | 75 | - Caches all loggers on first use. 76 | - Filters all log entries below the `info` log level **very** efficiently. 77 | The `debug` method literally consists of `return None`. 78 | - Supports {doc}`contextvars` (thread-local contexts outside of *asyncio*). 79 | - Adds the log level name. 80 | - Renders exceptions into the `exception` key. 81 | - Adds an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp under the `timestamp` key in the UTC timezone. 82 | - Renders the log entries as JSON using [*orjson*] which is faster than *plain* logging in {mod}`logging`. 83 | - Uses {class}`structlog.BytesLoggerFactory` because *orjson* returns bytes. 84 | That saves encoding ping-pong. 85 | 86 | Therefore a log entry might look like this: 87 | 88 | ```json 89 | {"event":"hello","level":"info","timestamp":"2023-11-02T08:03:38.298565Z"} 90 | ``` 91 | 92 | --- 93 | 94 | If you need standard library support for external projects, you can either just use a JSON formatter like [*python-json-logger*](https://pypi.org/project/python-json-logger/), or pipe them through *structlog* as documented in {doc}`standard-library`. 95 | 96 | [*orjson*]: https://github.com/ijl/orjson 97 | [*msgspec*]: https://jcristharif.com/msgspec/ 98 | -------------------------------------------------------------------------------- /docs/processors.md: -------------------------------------------------------------------------------- 1 | # Processors 2 | 3 | The true power of *structlog* lies in its *combinable log processors*. 4 | A log processor is a regular callable or in other words: 5 | A function or an instance of a class with a `__call__()` method. 6 | 7 | (chains)= 8 | 9 | ## Chains 10 | 11 | The *processor chain* is a list of processors. 12 | Each processors receives three positional arguments: 13 | 14 | **logger** 15 | 16 | : Your wrapped logger object. 17 | For example {class}`logging.Logger` or {class}`structlog.typing.FilteringBoundLogger` (default). 18 | 19 | **method_name** 20 | 21 | : The name of the wrapped method. 22 | If you called `log.warning("foo")`, it will be `"warning"`. 23 | 24 | **event_dict** 25 | 26 | : Current context together with the current event. 27 | If the context was `{"a": 42}` and the event is `"foo"`, the initial `event_dict` will be `{"a":42, "event": "foo"}`. 28 | 29 | The return value of each processor is passed on to the next one as `event_dict` until finally the return value of the last processor gets passed into the wrapped logging method. 30 | 31 | :::{note} 32 | *structlog* only looks at the return value of the **last** processor. 33 | That means that as long as you control the next processor in the chain (the processor that will get your return value passed as an argument), you can return whatever you want. 34 | 35 | Returning a modified event dictionary from your processors is just a convention to make processors composable. 36 | ::: 37 | 38 | 39 | ### Examples 40 | 41 | If you set up your logger like: 42 | 43 | ```python 44 | structlog.configure(processors=[f1, f2, f3]) 45 | log = structlog.get_logger().bind(x=42) 46 | ``` 47 | 48 | and call `log.info("some_event", y=23)`, it results in the following call chain: 49 | 50 | ```python 51 | wrapped_logger.info( 52 | f3(wrapped_logger, "info", 53 | f2(wrapped_logger, "info", 54 | f1(wrapped_logger, "info", {"event": "some_event", "x": 42, "y": 23}) 55 | ) 56 | ) 57 | ) 58 | ``` 59 | 60 | In this case, `f3` has to make sure it returns something `wrapped_logger.info` can handle (see {ref}`adapting`). 61 | For the example with `PrintLogger` above, this means `f3` must return a string. 62 | 63 | The simplest modification a processor can make is adding new values to the `event_dict`. 64 | Parsing human-readable timestamps is tedious, not so [UNIX timestamps](https://en.wikipedia.org/wiki/UNIX_time) -- let's add one to each log entry: 65 | 66 | ```python 67 | import calendar 68 | import time 69 | 70 | def timestamper(logger, log_method, event_dict): 71 | event_dict["timestamp"] = calendar.timegm(time.gmtime()) 72 | return event_dict 73 | ``` 74 | 75 | :::{important} 76 | You're explicitly allowed to modify the `event_dict` parameter, because a copy has been created before calling the first processor. 77 | ::: 78 | 79 | Please note that *structlog* comes with such a processor built in: {class}`~structlog.processors.TimeStamper`. 80 | 81 | 82 | ## Filtering 83 | 84 | If a processor raises {class}`structlog.DropEvent`, the event is silently dropped. 85 | 86 | Therefore, the following processor drops every entry: 87 | 88 | ```python 89 | from structlog import DropEvent 90 | 91 | def dropper(logger, method_name, event_dict): 92 | raise DropEvent 93 | ``` 94 | 95 | But we can do better than that! 96 | 97 | (cond-drop)= 98 | 99 | How about dropping only log entries that are marked as coming from a certain peer (for example, monitoring)? 100 | 101 | ```python 102 | class ConditionalDropper: 103 | def __init__(self, peer_to_ignore): 104 | self._peer_to_ignore = peer_to_ignore 105 | 106 | def __call__(self, logger, method_name, event_dict): 107 | """ 108 | >>> cd = ConditionalDropper("127.0.0.1") 109 | >>> cd(None, "", {"event": "foo", "peer": "10.0.0.1"}) 110 | {'peer': '10.0.0.1', 'event': 'foo'} 111 | >>> cd(None, "", {"event": "foo", "peer": "127.0.0.1"}) 112 | Traceback (most recent call last): 113 | ... 114 | DropEvent 115 | """ 116 | if event_dict.get("peer") == self._peer_to_ignore: 117 | raise DropEvent 118 | 119 | return event_dict 120 | ``` 121 | 122 | Since it's so common to filter by the log level, *structlog* comes with {func}`structlog.make_filtering_bound_logger` that filters log entries before they even enter the processor chain. 123 | It does **not** use the standard library, but it does use its names and order of log levels. 124 | 125 | (adapting)= 126 | 127 | ## Adapting and rendering 128 | 129 | An important role is played by the *last* processor because its duty is to adapt the `event_dict` into something the logging methods of the *wrapped logger* understand. 130 | With that, it's also the *only* processor that needs to know anything about the underlying system. 131 | 132 | It can return one of three types: 133 | 134 | - An Unicode string ({any}`str`), a bytes string ({any}`bytes`), or a {any}`bytearray` that is passed as the first (and only) positional argument to the underlying logger. 135 | - A tuple of `(args, kwargs)` that are passed as `log_method(*args, **kwargs)`. 136 | - A dictionary which is passed as `log_method(**kwargs)`. 137 | 138 | Therefore `return "hello world"` is a shortcut for `return (("hello world",), {})` (the example in {ref}`chains` assumes this shortcut has been taken). 139 | 140 | This should give you enough power to use *structlog* with any logging system while writing agnostic processors that operate on dictionaries. 141 | 142 | :::{versionchanged} 14.0.0 Allow final processor to return a {any}`dict`. 143 | ::: 144 | 145 | :::{versionchanged} 20.2.0 Allow final processor to return a {any}`bytes`. 146 | ::: 147 | 148 | :::{versionchanged} 21.2.0 Allow final processor to return a {any}`bytearray`. 149 | ::: 150 | 151 | ### Examples 152 | 153 | The probably most useful formatter for string based loggers is {class}`structlog.processors.JSONRenderer`. 154 | Advanced log aggregation and analysis tools like [*Logstash*](https://www.elastic.co/logstash) offer features like telling them "this is JSON, deal with it" instead of fiddling with regular expressions. 155 | 156 | For a list of shipped processors, check out the {ref}`API documentation `. 157 | 158 | 159 | ## Third-Party packages 160 | 161 | *structlog* was specifically designed to be as composable and reusable as possible, so whatever you're missing: 162 | chances are, you can solve it with a processor! 163 | Since processors are self-contained callables, it's easy to write your own and to share them in separate packages. 164 | 165 | We collect those packages in our [GitHub Wiki](https://github.com/hynek/structlog/wiki/Third-Party-Extensions) and encourage you to add your package too! 166 | -------------------------------------------------------------------------------- /docs/recipes.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | Because *structlog* is entirely based on dictionaries and callables, the sky is the limit with what you can achieve. 4 | That can be daunting in the beginning, so here are a few examples of tasks that have come up repeatedly. 5 | 6 | Please note that recipes related to integration with frameworks have an [own chapter](frameworks.md). 7 | 8 | (rename-event)= 9 | 10 | ## Renaming the `event` key 11 | 12 | The name of the event is hard-coded in *structlog* to `event`. 13 | But that doesn't mean it has to be called that in your logs. 14 | 15 | With the {class}`structlog.processors.EventRenamer` processor, you can, for instance, rename the log message to `msg` and use `event` for something custom, that you bind to `_event` in your code: 16 | 17 | ```pycon 18 | >>> from structlog.processors import EventRenamer 19 | >>> event_dict = {"event": "something happened", "_event": "our event!"} 20 | >>> EventRenamer("msg", "_event")(None, "", event_dict) 21 | {'msg': 'something happened', 'event': 'our event!'} 22 | ``` 23 | 24 | (finer-filtering)= 25 | 26 | ## Fine-grained log-level filtering 27 | 28 | *structlog*'s native log levels as provided by {func}`structlog.make_filtering_bound_logger` only know **one** log level – the one that is passed to `make_filtering_bound_logger()`. 29 | Sometimes, that can be a bit too coarse, though. 30 | 31 | You can achieve finer control by adding the {class}`~structlog.processors.CallsiteParameterAdder` processor and writing a simple processor that acts on the call site data added. 32 | 33 | Let's assume you have the following code: 34 | 35 | ```python 36 | logger = structlog.get_logger() 37 | 38 | def f(): 39 | logger.info("f called") 40 | 41 | def g(): 42 | logger.info("g called") 43 | 44 | f() 45 | g() 46 | ``` 47 | 48 | And you don't want to see log entries from function `f`. 49 | You add {class}`~structlog.processors.CallsiteParameterAdder` to the processor chain and then look at the `func_name` field in the *event dict*: 50 | 51 | ```python 52 | def filter_f(_, __, event_dict): 53 | if event_dict.get("func_name") == "f": 54 | raise structlog.DropEvent 55 | 56 | return event_dict 57 | 58 | structlog.configure( 59 | processors=[ 60 | structlog.processors.CallsiteParameterAdder( 61 | [structlog.processors.CallsiteParameter.FUNC_NAME] 62 | ), 63 | filter_f, # <-- your processor! 64 | structlog.processors.KeyValueRenderer(), 65 | ] 66 | ) 67 | ``` 68 | 69 | Running this gives you: 70 | 71 | ``` 72 | event='g called' func_name='g' 73 | ``` 74 | 75 | {class}`~structlog.processors.CallsiteParameterAdder` is *very* powerful in what info it can add, so your possibilities are limitless. 76 | Pick the data you're interested in from the {class}`structlog.processors.CallsiteParameter` {class}`~enum.Enum`. 77 | 78 | 79 | (custom-wrappers)= 80 | 81 | ## Custom wrappers 82 | 83 | ```{testsetup} 84 | import structlog 85 | structlog.configure( 86 | processors=[structlog.processors.KeyValueRenderer()], 87 | ) 88 | ``` 89 | 90 | ```{testcleanup} 91 | import structlog 92 | structlog.reset_defaults() 93 | ``` 94 | 95 | The type of the *bound loggers* that are returned by {func}`structlog.get_logger()` is called the *wrapper class*, because it wraps the original logger that takes care of the output. 96 | This wrapper class is [configurable](configuration.md). 97 | 98 | Originally, *structlog* used a generic wrapper class {class}`structlog.BoundLogger` by default. 99 | That class still ships with *structlog* and can wrap *any* logger class by intercepting unknown method names and proxying them to the wrapped logger. 100 | 101 | Nowadays, the default is a {class}`structlog.typing.FilteringBoundLogger` that imitates standard library’s log levels with the possibility of efficiently filtering at a certain level (inactive log methods are a plain `return None` each). 102 | 103 | If you’re integrating with {mod}`logging` or Twisted, you may want to use one of their specific *bound loggers* ({class}`structlog.stdlib.BoundLogger` and {class}`structlog.twisted.BoundLogger`, respectively). 104 | 105 | --- 106 | 107 | On top of that all, you can also write your own wrapper classes. 108 | To make it easy for you, *structlog* comes with the class {class}`structlog.BoundLoggerBase` which takes care of all data binding duties so you just add your log methods if you choose to sub-class it. 109 | 110 | (wrapper-class-example)= 111 | 112 | ### Example 113 | 114 | It’s easiest to demonstrate with an example: 115 | 116 | ```{doctest} 117 | >>> from structlog import BoundLoggerBase, PrintLogger, wrap_logger 118 | >>> class SemanticLogger(BoundLoggerBase): 119 | ... def info(self, event, **kw): 120 | ... if not "status" in kw: 121 | ... return self._proxy_to_logger("info", event, status="ok", **kw) 122 | ... else: 123 | ... return self._proxy_to_logger("info", event, **kw) 124 | ... 125 | ... def user_error(self, event, **kw): 126 | ... self.info(event, status="user_error", **kw) 127 | >>> log = wrap_logger(PrintLogger(), wrapper_class=SemanticLogger) 128 | >>> log = log.bind(user="fprefect") 129 | >>> log.user_error("user.forgot_towel") 130 | user='fprefect' status='user_error' event='user.forgot_towel' 131 | ``` 132 | 133 | You can observe the following: 134 | 135 | - The wrapped logger can be found in the instance variable {attr}`structlog.BoundLoggerBase._logger`. 136 | - The helper method {meth}`structlog.BoundLoggerBase._proxy_to_logger` that is a [DRY] convenience function that runs the processor chain, handles possible {class}`structlog.DropEvent`s and calls a named function on `_logger`. 137 | - You can run the chain by hand through using {meth}`structlog.BoundLoggerBase._process_event` . 138 | 139 | These two methods and one attribute are all you need to write own *bound loggers*. 140 | 141 | [dry]: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself 142 | 143 | 144 | ## Passing context to worker threads 145 | 146 | Thread-local context data based on [context variables](contextvars.md) is -- as the name says -- local to the thread that binds it. 147 | When using threads to process work in parallel, you have to pass the thread-local context **into** the worker threads. 148 | One way is to retrieve the context vars and pass them along to the worker threads. 149 | Then, Inside of the worker, re-bind them using `bind_contextvars`. 150 | 151 | The following example uses [*pathos*](https://pypi.org/project/pathos/) to create a `ThreadPool`. 152 | The context variables are retrieved and passed as the first argument to the partial function. 153 | The pool invokes the partial function, once for each element of `workers`. 154 | Inside of `do_some_work`, the context vars are bound and a message about the great work being performed is logged -- including the `request_id` key / value pair. 155 | 156 | ``` 157 | from functools import partial 158 | 159 | import structlog 160 | 161 | from structlog.contextvars import bind_contextvars 162 | from pathos.threading import ThreadPool 163 | 164 | logger = structlog.get_logger(__name__) 165 | 166 | 167 | def do_some_work(ctx, this_worker): 168 | bind_contextvars(**ctx) 169 | logger.info("WorkerDidSomeWork", worker=this_worker) 170 | 171 | 172 | def structlog_with_threadpool(f): 173 | ctx = structlog.contextvars.get_contextvars() 174 | func = partial(f, ctx) 175 | workers = ["1", "2", "3"] 176 | 177 | with ThreadPool() as pool: 178 | return list(pool.map(func, workers)) 179 | 180 | 181 | def manager(request_id: str): 182 | bind_contextvars(request_id=request_id) 183 | logger.info("StartingWorkers") 184 | structlog_with_threadpool(do_some_work) 185 | 186 | ``` 187 | 188 | See the [issue 425](https://github.com/hynek/structlog/issues/425) for a more complete example. 189 | 190 | 191 | ## Switching console output to standard error 192 | 193 | When using structlog without standard library integration and want the log output to go to standard error (*stderr*) instead of standard out (*stdout*), you can switch with a single line of configuration: 194 | 195 | ```python 196 | structlog.configure(logger_factory=structlog.PrintLoggerFactory(sys.stderr)) 197 | ``` 198 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | *structlog* comes with tools for testing the logging behavior of your application. 4 | 5 | If you need functionality similar to {meth}`unittest.TestCase.assertLogs`, or you want to capture all logs for some other reason, you can use the {func}`structlog.testing.capture_logs` context manager: 6 | 7 | ```{doctest} 8 | >>> from structlog import get_logger 9 | >>> from structlog.testing import capture_logs 10 | >>> with capture_logs() as cap_logs: 11 | ... get_logger().bind(x="y").info("hello") 12 | >>> cap_logs 13 | [{'x': 'y', 'event': 'hello', 'log_level': 'info'}] 14 | ``` 15 | 16 | Note that inside the context manager all configured processors are disabled. 17 | 18 | :::{note} 19 | `capture_logs()` relies on changing the configuration. 20 | If you have *cache_logger_on_first_use* enabled for {doc}`performance `, any cached loggers will not be affected, so it’s recommended you do not enable it during tests. 21 | ::: 22 | 23 | You can build your own helpers using {class}`structlog.testing.LogCapture`. 24 | For example a [*pytest*](https://docs.pytest.org/) fixture to capture log output could look like this: 25 | 26 | ``` 27 | @pytest.fixture(name="log_output") 28 | def fixture_log_output(): 29 | return LogCapture() 30 | 31 | @pytest.fixture(autouse=True) 32 | def fixture_configure_structlog(log_output): 33 | structlog.configure( 34 | processors=[log_output] 35 | ) 36 | 37 | def test_my_stuff(log_output): 38 | do_something() 39 | assert log_output.entries == [...] 40 | ``` 41 | 42 | --- 43 | 44 | You can also use {class}`structlog.testing.CapturingLogger` (directly, or via {class}`~structlog.testing.CapturingLoggerFactory` that always returns the same logger) that is more low-level and great for unit tests: 45 | 46 | ```{doctest} 47 | >>> import structlog 48 | >>> cf = structlog.testing.CapturingLoggerFactory() 49 | >>> structlog.configure(logger_factory=cf, processors=[structlog.processors.JSONRenderer()]) 50 | >>> log = get_logger() 51 | >>> log.info("test!") 52 | >>> cf.logger.calls 53 | [CapturedCall(method_name='info', args=('{"event": "test!"}',), kwargs={})] 54 | ``` 55 | 56 | ```{testcleanup} 57 | import structlog 58 | structlog.reset_defaults() 59 | ``` 60 | 61 | --- 62 | 63 | Additionally *structlog* also ships with a logger that just returns whatever it gets passed into it: {class}`structlog.testing.ReturnLogger`. 64 | 65 | ```{doctest} 66 | >>> from structlog import ReturnLogger 67 | >>> ReturnLogger().info(42) == 42 68 | True 69 | >>> obj = ["hi"] 70 | >>> ReturnLogger().info(obj) is obj 71 | True 72 | >>> ReturnLogger().info("hello", when="again") 73 | (('hello',), {'when': 'again'}) 74 | ``` 75 | -------------------------------------------------------------------------------- /docs/thread-local.md: -------------------------------------------------------------------------------- 1 | # Legacy Thread-local Context 2 | 3 | :::{attention} 4 | The `structlog.threadlocal` module is deprecated as of *structlog* 22.1.0 in favor of {doc}`contextvars`. 5 | 6 | The standard library {mod}`contextvars` module provides a more feature-rich superset of the thread-local APIs and works with thread-local data, async code, and greenlets. 7 | 8 | The plan was to remove this module after 2023, however people are reporting [odd crashes with {doc}`contextvars`, that we can't reproduce](https://github.com/hynek/structlog/issues/591). 9 | Until we find a solution, this module will **not** be removed, since it's a viable workaround. 10 | ::: 11 | 12 | ```{testsetup} * 13 | import structlog 14 | structlog.configure( 15 | processors=[structlog.processors.KeyValueRenderer()], 16 | ) 17 | ``` 18 | 19 | ```{testcleanup} * 20 | import structlog 21 | structlog.reset_defaults() 22 | ``` 23 | 24 | 25 | ## The `merge_threadlocal` processor 26 | 27 | *structlog* provides a simple set of functions that allow explicitly binding certain fields to a global (thread-local) context and merge them later using a processor into the event dict. 28 | 29 | The general flow of using these functions is: 30 | 31 | - Use {func}`structlog.configure` with {func}`structlog.threadlocal.merge_threadlocal` as your first processor. 32 | - Call {func}`structlog.threadlocal.clear_threadlocal` at the beginning of your request handler (or whenever you want to reset the thread-local context). 33 | - Call {func}`structlog.threadlocal.bind_threadlocal` as an alternative to your bound logger's `bind()` when you want to bind a particular variable to the thread-local context. 34 | - Use *structlog* as normal. 35 | Loggers act as they always do, but the {func}`structlog.threadlocal.merge_threadlocal` processor ensures that any thread-local binds get included in all of your log messages. 36 | - If you want to access the thread-local storage, you use {func}`structlog.threadlocal.get_threadlocal` and {func}`structlog.threadlocal.get_merged_threadlocal`. 37 | 38 | These functions map 1:1 to the {doc}`contextvars` APIs, so please use those instead: 39 | 40 | - {func}`structlog.contextvars.merge_contextvars` 41 | - {func}`structlog.contextvars.clear_contextvars` 42 | - {func}`structlog.contextvars.bind_contextvars` 43 | - {func}`structlog.contextvars.get_contextvars` 44 | - {func}`structlog.contextvars.get_merged_contextvars` 45 | 46 | 47 | ## Thread-local contexts 48 | 49 | *structlog* also provides thread-local context storage in a form that you may already know from [*Flask*](https://flask.palletsprojects.com/en/latest/design/#thread-locals) and that makes the *entire context* global to your thread or greenlet. 50 | 51 | This makes its behavior more difficult to reason about which is why we generally recommend to use the {func}`~structlog.contextvars.merge_contextvars` route. 52 | Therefore, there are currently no plans to re-implement this behavior on top of context variables. 53 | 54 | 55 | ### Wrapped dicts 56 | 57 | In order to make your context thread-local, *structlog* ships with a function that can wrap any dict-like class to make it usable for thread-local storage: {func}`structlog.threadlocal.wrap_dict`. 58 | 59 | Within one thread, every instance of the returned class will have a *common* instance of the wrapped dict-like class: 60 | 61 | ```{doctest} 62 | >>> from structlog.threadlocal import wrap_dict 63 | >>> WrappedDictClass = wrap_dict(dict) 64 | >>> d1 = WrappedDictClass({"a": 1}) 65 | >>> d2 = WrappedDictClass({"b": 2}) 66 | >>> d3 = WrappedDictClass() 67 | >>> d3["c"] = 3 68 | >>> d1 is d3 69 | False 70 | >>> d1 == d2 == d3 == WrappedDictClass() 71 | True 72 | >>> d3 # doctest: +ELLIPSIS 73 | 74 | ``` 75 | 76 | To enable thread-local context use the generated class as the context class: 77 | 78 | ```python 79 | configure(context_class=WrappedDictClass) 80 | ``` 81 | 82 | :::{note} 83 | Creation of a new `BoundLogger` initializes the logger's context as `context_class(initial_values)`, and then adds any values passed via `.bind()`. 84 | As all instances of a wrapped dict-like class share the same data, in the case above, the new logger's context will contain all previously bound values in addition to the new ones. 85 | ::: 86 | 87 | `structlog.threadlocal.wrap_dict` returns always a completely *new* wrapped class: 88 | 89 | ```{doctest} 90 | >>> from structlog.threadlocal import wrap_dict 91 | >>> WrappedDictClass = wrap_dict(dict) 92 | >>> AnotherWrappedDictClass = wrap_dict(dict) 93 | >>> WrappedDictClass() != AnotherWrappedDictClass() 94 | True 95 | >>> WrappedDictClass.__name__ # doctest: +SKIP 96 | WrappedDict-41e8382d-bee5-430e-ad7d-133c844695cc 97 | >>> AnotherWrappedDictClass.__name__ # doctest: +SKIP 98 | WrappedDict-e0fc330e-e5eb-42ee-bcec-ffd7bd09ad09 99 | ``` 100 | 101 | In order to be able to bind values temporarily to a logger, `structlog.threadlocal` comes with a [context manager](https://docs.python.org/2/library/stdtypes.html#context-manager-types): {func}`structlog.threadlocal.tmp_bind`: 102 | 103 | ```{testsetup} ctx 104 | from structlog import PrintLogger, wrap_logger 105 | from structlog.threadlocal import tmp_bind, wrap_dict 106 | WrappedDictClass = wrap_dict(dict) 107 | log = wrap_logger(PrintLogger(), context_class=WrappedDictClass) 108 | ``` 109 | 110 | ```{doctest} ctx 111 | >>> log.bind(x=42) # doctest: +ELLIPSIS 112 | , ...)> 113 | >>> log.msg("event!") 114 | x=42 event='event!' 115 | >>> with tmp_bind(log, x=23, y="foo") as tmp_log: 116 | ... tmp_log.msg("another event!") 117 | x=23 y='foo' event='another event!' 118 | >>> log.msg("one last event!") 119 | x=42 event='one last event!' 120 | ``` 121 | 122 | The state before the `with` statement is saved and restored once it's left. 123 | 124 | If you want to detach a logger from thread-local data, there's {func}`structlog.threadlocal.as_immutable`. 125 | 126 | 127 | #### Downsides & caveats 128 | 129 | The convenience of having a thread-local context comes at a price though: 130 | 131 | :::{warning} 132 | - If you can't rule out that your application reuses threads, you *must* remember to **initialize your thread-local context** at the start of each request using {func}`~structlog.BoundLogger.new` (instead of {func}`~structlog.BoundLogger.bind`). 133 | Otherwise you may start a new request with the context still filled with data from the request before. 134 | 135 | - **Don't** stop assigning the results of your `bind()`s and `new()`s! 136 | 137 | **Do**: 138 | 139 | ``` 140 | log = log.new(y=23) 141 | log = log.bind(x=42) 142 | ``` 143 | 144 | **Don't**: 145 | 146 | ``` 147 | log.new(y=23) 148 | log.bind(x=42) 149 | ``` 150 | 151 | Although the state is saved in a global data structure, you still need the global wrapped logger produce a real bound logger. 152 | Otherwise each log call will result in an instantiation of a temporary BoundLogger. 153 | 154 | See `configuration` for more details. 155 | 156 | - It [doesn't play well](https://github.com/hynek/structlog/issues/296) with `os.fork` and thus `multiprocessing` (unless configured to use the `spawn` start method). 157 | ::: 158 | 159 | 160 | ## API 161 | 162 | ```{eval-rst} 163 | .. module:: structlog.threadlocal 164 | 165 | .. autofunction:: bind_threadlocal 166 | 167 | .. autofunction:: unbind_threadlocal 168 | 169 | .. autofunction:: bound_threadlocal 170 | 171 | .. autofunction:: get_threadlocal 172 | 173 | .. autofunction:: get_merged_threadlocal 174 | 175 | .. autofunction:: merge_threadlocal 176 | 177 | .. autofunction:: clear_threadlocal 178 | 179 | .. autofunction:: wrap_dict 180 | 181 | .. autofunction:: tmp_bind(logger, **tmp_values) 182 | 183 | .. autofunction:: as_immutable 184 | ``` 185 | -------------------------------------------------------------------------------- /docs/twisted.md: -------------------------------------------------------------------------------- 1 | # Twisted 2 | 3 | :::{warning} 4 | Since `sys.exc_clear` has been dropped in Python 3, there is currently no way to avoid multiple tracebacks in your log files if using *structlog* together with Twisted on Python 3. 5 | ::: 6 | 7 | :::{note} 8 | *structlog* currently only supports the legacy -- but still perfectly working -- Twisted logging system found in `twisted.python.log`. 9 | ::: 10 | 11 | 12 | ## Concrete bound logger 13 | 14 | To make *structlog*'s behavior less magical, it ships with a Twisted-specific wrapper class that has an explicit API instead of improvising: `structlog.twisted.BoundLogger`. 15 | It behaves exactly like the generic `structlog.BoundLogger` except: 16 | 17 | - it's slightly faster due to less overhead, 18 | - has an explicit API ({func}`~structlog.twisted.BoundLogger.msg` and {func}`~structlog.twisted.BoundLogger.err`), 19 | - hence causing less cryptic error messages if you get method names wrong. 20 | 21 | In order to avoid that *structlog* disturbs your CamelCase harmony, it comes with an alias for `structlog.get_logger` called `structlog.getLogger`. 22 | 23 | 24 | ## Processors 25 | 26 | *structlog* comes with two Twisted-specific processors: 27 | 28 | {func}`structlog.twisted.EventAdapter` 29 | 30 | : This is useful if you have an existing Twisted application and just want to wrap your loggers for now. 31 | It takes care of transforming your event dictionary into something [twisted.python.log.err](https://docs.twisted.org/en/stable/api/twisted.python.log.html#err) can digest. 32 | 33 | For example: 34 | 35 | ```python 36 | def onError(fail): 37 | failure = fail.trap(MoonExploded) 38 | log.err(failure, _why="event-that-happened") 39 | ``` 40 | 41 | will still work as expected. 42 | 43 | Needs to be put at the end of the processing chain. 44 | It formats the event using a renderer that needs to be passed into the constructor: 45 | 46 | ```python 47 | configure(processors=[EventAdapter(KeyValueRenderer()]) 48 | ``` 49 | 50 | The drawback of this approach is that Twisted will format your exceptions as multi-line log entries which is painful to parse. 51 | Therefore *structlog* comes with: 52 | 53 | {func}`structlog.twisted.JSONRenderer` 54 | 55 | : Goes a step further and circumvents Twisted logger's Exception / Failure handling and renders it itself as JSON strings. 56 | That gives you regular and simple-to-parse single-line JSON log entries no matter what happens. 57 | 58 | 59 | ## Bending foreign logging to your will 60 | 61 | *structlog* comes with a wrapper for Twisted's log observers to ensure the rest of your logs are in JSON too: `structlog.twisted.JSONLogObserverWrapper`. 62 | 63 | What it does is determining whether a log entry has been formatted by `structlog.twisted.JSONRenderer` and if not, converts the log entry to JSON with `event` being the log message and putting Twisted's `system` into a second key. 64 | 65 | So for example: 66 | 67 | ``` 68 | 2013-09-15 22:02:18+0200 [-] Log opened. 69 | ``` 70 | 71 | becomes: 72 | 73 | ``` 74 | 2013-09-15 22:02:18+0200 [-] {"event": "Log opened.", "system": "-"} 75 | ``` 76 | 77 | There is obviously some redundancy here. 78 | Also, I'm presuming that if you write out JSON logs, you're going to let something else parse them which makes the human-readable date entries more trouble than they're worth. 79 | 80 | To get a clean log without timestamps and additional system fields (`[-]`), *structlog* comes with `structlog.twisted.PlainFileLogObserver` that writes only the plain message to a file and `structlog.twisted.plainJSONStdOutLogger` that composes it with the aforementioned `structlog.twisted.JSONLogObserverWrapper` and gives you a pure JSON log without any timestamps or other noise straight to [standard out]: 81 | 82 | ```console 83 | $ twistd -n --logger structlog.twisted.plainJSONStdOutLogger web 84 | {"event": "Log opened.", "system": "-"} 85 | {"event": "twistd 13.1.0 (python 2.7.3) starting up.", "system": "-"} 86 | {"event": "reactor class: twisted...EPollReactor.", "system": "-"} 87 | {"event": "Site starting on 8080", "system": "-"} 88 | {"event": "Starting factory ", ...} 89 | ... 90 | ``` 91 | 92 | ## Suggested configuration 93 | 94 | ```python 95 | import structlog 96 | 97 | structlog.configure( 98 | processors=[ 99 | structlog.processors.StackInfoRenderer(), 100 | structlog.twisted.JSONRenderer() 101 | ], 102 | context_class=dict, 103 | logger_factory=structlog.twisted.LoggerFactory(), 104 | wrapper_class=structlog.twisted.BoundLogger, 105 | cache_logger_on_first_use=True, 106 | ) 107 | ``` 108 | 109 | See also {doc}`logging-best-practices`. 110 | 111 | [standard out]: https://en.wikipedia.org/wiki/Standard_out#Standard_output_.28stdout.29 112 | -------------------------------------------------------------------------------- /docs/typing.md: -------------------------------------------------------------------------------- 1 | # Type Hints 2 | 3 | Static type hints -- together with a type checker like [Mypy](https://mypy.readthedocs.io/en/stable/) -- are an excellent way to make your code more robust, self-documenting, and maintainable in the long run. 4 | And as of 20.2.0, *structlog* comes with type hints for all of its APIs. 5 | 6 | Since *structlog* is highly configurable and tries to give a clean façade to its users, adding types without breaking compatibility -- while remaining useful! -- was a formidable task. 7 | 8 | --- 9 | 10 | The main problem is that `structlog.get_logger()` returns whatever you've configured the *bound logger* to be. 11 | The only commonality are the binding methods like `bind()` and we've extracted them into the {class}`structlog.typing.BindableLogger` {class}`~typing.Protocol`. 12 | But using that as a return type is worse than useless, because you'd have to use {func}`typing.cast` on every logger returned by `structlog.get_logger()`, if you wanted to actually call any logging methods. 13 | 14 | The second problem is that said `bind()` and its cousins are inherited from a common base class (a [big](https://www.youtube.com/watch?v=3MNVP9-hglc) [mistake](https://python-patterns.guide/gang-of-four/composition-over-inheritance/) in hindsight) and can't know what concrete class subclasses them and therefore what type they are returning. 15 | 16 | The chosen solution is adding {func}`structlog.stdlib.get_logger()` that just calls `structlog.get_logger()` but has the correct type hints and adding `structlog.stdlib.BoundLogger.bind` et al that also only delegate to the base class. 17 | 18 | `structlog.get_logger()` is typed as returning {any}`typing.Any` so you can use your own type annotation and stick to the old APIs, if that's what you prefer: 19 | 20 | ``` 21 | import structlog 22 | 23 | logger: structlog.stdlib.BoundLogger = structlog.get_logger() 24 | logger.info("hi") # <- ok 25 | logger.msg("hi") # <- Mypy: 'error: "BoundLogger" has no attribute "msg"' 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | # Why … 2 | 3 | ## … structured logging? 4 | 5 | > I believe the widespread use of format strings in logging is based on two presumptions: 6 | > 7 | > - The first level consumer of a log message is a human. 8 | > - The programmer knows what information is needed to debug an issue. 9 | > 10 | > I believe these presumptions are **no longer correct** in server side software. 11 | > 12 | > —[Paul Querna](https://paul.querna.org/articles/2011/12/26/log-for-machines-in-json/) 13 | 14 | Structured logging means that you don't write hard-to-parse and hard-to-keep-consistent prose in your log entries. 15 | Instead, you log *events* that happen in a *context* of key-value pairs. 16 | 17 | :::{tip} 18 | More general advice about production-grade logging can be found in the later chapter on {doc}`logging-best-practices`. 19 | ::: 20 | 21 | 22 | ## … structlog? 23 | 24 | ### Easier logging 25 | 26 | You can stop writing prose and start thinking in terms of an event that happens in the context of key-value pairs: 27 | 28 | ```pycon 29 | >>> from structlog import get_logger 30 | >>> log = get_logger() 31 | >>> log.info("key_value_logging", out_of_the_box=True, effort=0) 32 | 2020-11-18 09:17:09 [info ] key_value_logging effort=0 out_of_the_box=True 33 | ``` 34 | 35 | Each log entry is a meaningful dictionary instead of an opaque string now! 36 | 37 | That said, *structlog* is not taking anything away from you. 38 | You can still use string interpolation using positional arguments: 39 | 40 | ```pycon 41 | >>> log.info("Hello, %s!", "world") 42 | 2022-10-10 07:19:25 [info ] Hello, world! 43 | ``` 44 | 45 | ### Data binding 46 | 47 | Since log entries are dictionaries, you can start binding and re-binding key-value pairs to your loggers to ensure they are present in every following logging call: 48 | 49 | ```pycon 50 | >>> log = log.bind(user="anonymous", some_key=23) 51 | >>> log = log.bind(user="hynek", another_key=42) 52 | >>> log.info("user.logged_in", happy=True) 53 | 2020-11-18 09:18:28 [info ] user.logged_in another_key=42 happy=True some_key=23 user=hynek 54 | ``` 55 | 56 | You can also bind key-value pairs to {doc}`context variables ` that look global, but are local to your thread or *asyncio* context -- which usually means your web request. 57 | 58 | 59 | ### Powerful pipelines 60 | 61 | Each log entry goes through a [processor pipeline](processors.md) that is just a chain of functions that receive a dictionary and return a new dictionary that gets fed into the next function. 62 | That allows for simple but powerful data manipulation: 63 | 64 | ```python 65 | def timestamper(logger, log_method, event_dict): 66 | """Add a timestamp to each log entry.""" 67 | event_dict["timestamp"] = time.time() 68 | return event_dict 69 | ``` 70 | 71 | There are [plenty of processors](structlog.processors) for most common tasks coming with *structlog*: 72 | 73 | - Collectors of [call stack information](structlog.processors.StackInfoRenderer) ("How did this log entry happen?"), 74 | - …and [exceptions](structlog.processors.format_exc_info) ("What happened‽"). 75 | - Flexible [timestamping](structlog.processors.TimeStamper). 76 | 77 | 78 | ### Formatting 79 | 80 | *structlog* is completely flexible about *how* the resulting log entry is emitted. 81 | Since each log entry is a dictionary, it can be formatted to **any** format: 82 | 83 | - A colorful key-value format for [local development](console-output.md), 84 | - [JSON](structlog.processors.JSONRenderer) or [*logfmt*](structlog.processors.LogfmtRenderer) for easy parsing, 85 | - or some standard format you have parsers for like *nginx* or Apache *httpd*. 86 | 87 | Internally, formatters are processors whose return value (usually a string) is passed into loggers that are responsible for the output of your message. 88 | *structlog* comes with multiple useful formatters out-of-the-box. 89 | 90 | 91 | ### Output 92 | 93 | *structlog* is also flexible with the final output of your log entries: 94 | 95 | - A **built-in** lightweight printer like in the examples above. 96 | Easy to use and fast. 97 | - Use the [**standard library**](standard-library.md)'s or [**Twisted**](twisted.md)'s logging modules for compatibility. 98 | In this case *structlog* works like a wrapper that formats a string and passes them off into existing systems that won't know that *structlog* even exists. 99 | 100 | Or the other way round: *structlog* comes with a `logging` formatter that allows for processing third party log records. 101 | - Don't format it to a string at all! 102 | *structlog* passes you a dictionary and you can do with it whatever you want. 103 | Reported use cases are sending them out via network or saving them to a database. 104 | 105 | 106 | ### Highly testable 107 | 108 | *structlog* is thoroughly tested and we see it as our duty to help you to achieve the same in *your* applications. 109 | That's why it ships with a [test helpers](testing.md) to introspect your application's logging behavior with little-to-no boilerplate. 110 | -------------------------------------------------------------------------------- /show_off.py: -------------------------------------------------------------------------------- 1 | """ 2 | Show how console logging looks like. 3 | 4 | This is used for the screenshot in the readme and 5 | . 6 | """ 7 | 8 | from dataclasses import dataclass 9 | 10 | import structlog 11 | 12 | 13 | @dataclass 14 | class SomeClass: 15 | x: int 16 | y: str 17 | 18 | 19 | structlog.stdlib.recreate_defaults() # so we have logger names 20 | 21 | log = structlog.get_logger("some_logger") 22 | 23 | log.debug("debugging is hard", a_list=[1, 2, 3]) 24 | log.info("informative!", some_key="some_value") 25 | log.warning("uh-uh!") 26 | log.error("omg", a_dict={"a": 42, "b": "foo"}) 27 | log.critical("wtf", what=SomeClass(x=1, y="z")) 28 | 29 | 30 | log2 = structlog.get_logger("another_logger") 31 | 32 | 33 | def make_call_stack_more_impressive(): 34 | try: 35 | d = {"x": 42} 36 | print(SomeClass(d["y"], "foo")) 37 | except Exception: 38 | log2.exception("poor me") 39 | log.info("all better now!", stack_info=True) 40 | 41 | 42 | make_call_stack_more_impressive() 43 | -------------------------------------------------------------------------------- /src/structlog/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | 7 | from __future__ import annotations 8 | 9 | from structlog import ( 10 | contextvars, 11 | dev, 12 | processors, 13 | stdlib, 14 | testing, 15 | threadlocal, 16 | tracebacks, 17 | types, 18 | typing, 19 | ) 20 | from structlog._base import BoundLoggerBase, get_context 21 | from structlog._config import ( 22 | configure, 23 | configure_once, 24 | get_config, 25 | get_logger, 26 | getLogger, 27 | is_configured, 28 | reset_defaults, 29 | wrap_logger, 30 | ) 31 | from structlog._generic import BoundLogger 32 | from structlog._native import make_filtering_bound_logger 33 | from structlog._output import ( 34 | BytesLogger, 35 | BytesLoggerFactory, 36 | PrintLogger, 37 | PrintLoggerFactory, 38 | WriteLogger, 39 | WriteLoggerFactory, 40 | ) 41 | from structlog.exceptions import DropEvent 42 | from structlog.testing import ReturnLogger, ReturnLoggerFactory 43 | 44 | 45 | try: 46 | from structlog import twisted 47 | except ImportError: 48 | twisted = None # type: ignore[assignment] 49 | 50 | 51 | __title__ = "structlog" 52 | 53 | __author__ = "Hynek Schlawack" 54 | 55 | __license__ = "MIT or Apache License, Version 2.0" 56 | __copyright__ = "Copyright (c) 2013 " + __author__ 57 | 58 | 59 | __all__ = [ 60 | "BoundLogger", 61 | "BoundLoggerBase", 62 | "BytesLogger", 63 | "BytesLoggerFactory", 64 | "DropEvent", 65 | "PrintLogger", 66 | "PrintLoggerFactory", 67 | "ReturnLogger", 68 | "ReturnLoggerFactory", 69 | "WriteLogger", 70 | "WriteLoggerFactory", 71 | "configure", 72 | "configure_once", 73 | "contextvars", 74 | "dev", 75 | "getLogger", 76 | "get_config", 77 | "get_context", 78 | "get_logger", 79 | "is_configured", 80 | "make_filtering_bound_logger", 81 | "processors", 82 | "reset_defaults", 83 | "stdlib", 84 | "testing", 85 | "threadlocal", 86 | "tracebacks", 87 | "twisted", 88 | "types", 89 | "typing", 90 | "wrap_logger", 91 | ] 92 | 93 | 94 | def __getattr__(name: str) -> str: 95 | import warnings 96 | 97 | from importlib.metadata import metadata, version 98 | 99 | dunder_to_metadata = { 100 | "__description__": "summary", 101 | "__uri__": "", 102 | "__email__": "", 103 | "__version__": "", 104 | } 105 | if name not in dunder_to_metadata: 106 | msg = f"module {__name__} has no attribute {name}" 107 | raise AttributeError(msg) 108 | 109 | if name != "__version__": 110 | warnings.warn( 111 | f"Accessing structlog.{name} is deprecated and will be " 112 | "removed in a future release. Use importlib.metadata directly " 113 | "to query for structlog's packaging metadata.", 114 | DeprecationWarning, 115 | stacklevel=2, 116 | ) 117 | else: 118 | return version("structlog") 119 | 120 | meta = metadata("structlog") 121 | 122 | if name == "__uri__": 123 | return meta["Project-URL"].split(" ", 1)[-1] 124 | 125 | if name == "__email__": 126 | return meta["Author-email"].split("<", 1)[1].rstrip(">") 127 | 128 | return meta[dunder_to_metadata[name]] 129 | -------------------------------------------------------------------------------- /src/structlog/_frames.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | from __future__ import annotations 7 | 8 | import sys 9 | import traceback 10 | 11 | from io import StringIO 12 | from types import FrameType 13 | from typing import Callable 14 | 15 | from .contextvars import _ASYNC_CALLING_STACK 16 | from .typing import ExcInfo 17 | 18 | 19 | def _format_exception(exc_info: ExcInfo) -> str: 20 | """ 21 | Prettyprint an `exc_info` tuple. 22 | 23 | Shamelessly stolen from stdlib's logging module. 24 | """ 25 | sio = StringIO() 26 | 27 | traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], None, sio) 28 | s = sio.getvalue() 29 | sio.close() 30 | if s[-1:] == "\n": 31 | s = s[:-1] 32 | 33 | return s 34 | 35 | 36 | def _find_first_app_frame_and_name( 37 | additional_ignores: list[str] | None = None, 38 | *, 39 | _getframe: Callable[[], FrameType] = sys._getframe, 40 | ) -> tuple[FrameType, str]: 41 | """ 42 | Remove all intra-structlog calls and return the relevant app frame. 43 | 44 | Args: 45 | additional_ignores: 46 | Additional names with which the first frame must not start. 47 | 48 | _getframe: 49 | Callable to find current frame. Only for testing to avoid 50 | monkeypatching of sys._getframe. 51 | 52 | Returns: 53 | tuple of (frame, name) 54 | """ 55 | ignores = tuple(["structlog"] + (additional_ignores or [])) 56 | f = _ASYNC_CALLING_STACK.get(_getframe()) 57 | name = f.f_globals.get("__name__") or "?" 58 | while name.startswith(ignores): 59 | if f.f_back is None: 60 | name = "?" 61 | break 62 | f = f.f_back 63 | name = f.f_globals.get("__name__") or "?" 64 | return f, name 65 | 66 | 67 | def _format_stack(frame: FrameType) -> str: 68 | """ 69 | Pretty-print the stack of *frame* like logging would. 70 | """ 71 | sio = StringIO() 72 | 73 | sio.write("Stack (most recent call last):\n") 74 | traceback.print_stack(frame, file=sio) 75 | sinfo = sio.getvalue() 76 | if sinfo[-1] == "\n": 77 | sinfo = sinfo[:-1] 78 | sio.close() 79 | 80 | return sinfo 81 | -------------------------------------------------------------------------------- /src/structlog/_generic.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Generic bound logger that can wrap anything. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from functools import partial 13 | from typing import Any 14 | 15 | from structlog._base import BoundLoggerBase 16 | 17 | 18 | class BoundLogger(BoundLoggerBase): 19 | """ 20 | A generic BoundLogger that can wrap anything. 21 | 22 | Every unknown method will be passed to the wrapped *logger*. If that's too 23 | much magic for you, try `structlog.stdlib.BoundLogger` or 24 | `structlog.twisted.BoundLogger` which also take advantage of knowing the 25 | wrapped class which generally results in better performance. 26 | 27 | Not intended to be instantiated by yourself. See 28 | :func:`~structlog.wrap_logger` and :func:`~structlog.get_logger`. 29 | """ 30 | 31 | def __getattr__(self, method_name: str) -> Any: 32 | """ 33 | If not done so yet, wrap the desired logger method & cache the result. 34 | """ 35 | if method_name == "__deepcopy__": 36 | return None 37 | 38 | wrapped = partial(self._proxy_to_logger, method_name) 39 | setattr(self, method_name, wrapped) 40 | 41 | return wrapped 42 | 43 | def __getstate__(self) -> dict[str, Any]: 44 | """ 45 | Our __getattr__ magic makes this necessary. 46 | """ 47 | return self.__dict__ 48 | 49 | def __setstate__(self, state: dict[str, Any]) -> None: 50 | """ 51 | Our __getattr__ magic makes this necessary. 52 | """ 53 | for k, v in state.items(): 54 | setattr(self, k, v) 55 | -------------------------------------------------------------------------------- /src/structlog/_greenlets.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | greenlet-specific code that pretends to be a `threading.local`. 8 | 9 | Fails to import if not running under greenlet. 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from typing import Any 15 | from weakref import WeakKeyDictionary 16 | 17 | from greenlet import getcurrent 18 | 19 | 20 | class GreenThreadLocal: 21 | """ 22 | threading.local() replacement for greenlets. 23 | """ 24 | 25 | def __init__(self) -> None: 26 | self.__dict__["_weakdict"] = WeakKeyDictionary() 27 | 28 | def __getattr__(self, name: str) -> Any: 29 | key = getcurrent() 30 | try: 31 | return self._weakdict[key][name] 32 | except KeyError: 33 | raise AttributeError(name) from None 34 | 35 | def __setattr__(self, name: str, val: Any) -> None: 36 | key = getcurrent() 37 | self._weakdict.setdefault(key, {})[name] = val 38 | 39 | def __delattr__(self, name: str) -> None: 40 | key = getcurrent() 41 | try: 42 | del self._weakdict[key][name] 43 | except KeyError: 44 | raise AttributeError(name) from None 45 | -------------------------------------------------------------------------------- /src/structlog/_log_levels.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Extracted log level data used by both stdlib and native log level filters. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | from typing import Any 13 | 14 | from .typing import EventDict 15 | 16 | 17 | # Adapted from the stdlib 18 | CRITICAL = 50 19 | FATAL = CRITICAL 20 | ERROR = 40 21 | WARNING = 30 22 | WARN = WARNING 23 | INFO = 20 24 | DEBUG = 10 25 | NOTSET = 0 26 | 27 | NAME_TO_LEVEL = { 28 | "critical": CRITICAL, 29 | "exception": ERROR, 30 | "error": ERROR, 31 | "warn": WARNING, 32 | "warning": WARNING, 33 | "info": INFO, 34 | "debug": DEBUG, 35 | "notset": NOTSET, 36 | } 37 | 38 | LEVEL_TO_NAME = { 39 | v: k 40 | for k, v in NAME_TO_LEVEL.items() 41 | if k not in ("warn", "exception", "notset") 42 | } 43 | 44 | # Keep around for backwards-compatability in case someone imported them. 45 | _LEVEL_TO_NAME = LEVEL_TO_NAME 46 | _NAME_TO_LEVEL = NAME_TO_LEVEL 47 | 48 | 49 | def map_method_name(method_name: str) -> str: 50 | # warn is just a deprecated alias in the stdlib. 51 | if method_name == "warn": 52 | return "warning" 53 | 54 | # Calling exception("") is the same as error("", exc_info=True) 55 | if method_name == "exception": 56 | return "error" 57 | 58 | return method_name 59 | 60 | 61 | def add_log_level( 62 | logger: Any, method_name: str, event_dict: EventDict 63 | ) -> EventDict: 64 | """ 65 | Add the log level to the event dict under the ``level`` key. 66 | 67 | Since that's just the log method name, this processor works with non-stdlib 68 | logging as well. Therefore it's importable both from `structlog.processors` 69 | as well as from `structlog.stdlib`. 70 | 71 | .. versionadded:: 15.0.0 72 | .. versionchanged:: 20.2.0 73 | Importable from `structlog.processors` (additionally to 74 | `structlog.stdlib`). 75 | .. versionchanged:: 24.1.0 76 | Added mapping from "exception" to "error" 77 | """ 78 | 79 | event_dict["level"] = map_method_name(method_name) 80 | 81 | return event_dict 82 | -------------------------------------------------------------------------------- /src/structlog/_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Generic utilities. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import sys 13 | 14 | from contextlib import suppress 15 | from typing import Any 16 | 17 | 18 | def get_processname() -> str: 19 | # based on code from 20 | # https://github.com/python/cpython/blob/313f92a57bc3887026ec16adb536bb2b7580ce47/Lib/logging/__init__.py#L342-L352 21 | processname = "n/a" 22 | mp: Any = sys.modules.get("multiprocessing") 23 | if mp is not None: 24 | # Errors may occur if multiprocessing has not finished loading 25 | # yet - e.g. if a custom import hook causes third-party code 26 | # to run when multiprocessing calls import. 27 | with suppress(Exception): 28 | processname = mp.current_process().name 29 | 30 | return processname 31 | -------------------------------------------------------------------------------- /src/structlog/contextvars.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Primitives to deal with a concurrency supporting context, as introduced in 8 | Python 3.7 as :mod:`contextvars`. 9 | 10 | .. versionadded:: 20.1.0 11 | .. versionchanged:: 21.1.0 12 | Reimplemented without using a single dict as context carrier for improved 13 | isolation. Every key-value pair is a separate `contextvars.ContextVar` now. 14 | .. versionchanged:: 23.3.0 15 | Callsite parameters are now also collected under asyncio. 16 | 17 | See :doc:`contextvars`. 18 | """ 19 | 20 | from __future__ import annotations 21 | 22 | import contextlib 23 | import contextvars 24 | 25 | from types import FrameType 26 | from typing import Any, Generator, Mapping 27 | 28 | import structlog 29 | 30 | from .typing import BindableLogger, EventDict, WrappedLogger 31 | 32 | 33 | STRUCTLOG_KEY_PREFIX = "structlog_" 34 | STRUCTLOG_KEY_PREFIX_LEN = len(STRUCTLOG_KEY_PREFIX) 35 | 36 | _ASYNC_CALLING_STACK: contextvars.ContextVar[FrameType] = ( 37 | contextvars.ContextVar("_ASYNC_CALLING_STACK") 38 | ) 39 | 40 | # For proper isolation, we have to use a dict of ContextVars instead of a 41 | # single ContextVar with a dict. 42 | # See https://github.com/hynek/structlog/pull/302 for details. 43 | _CONTEXT_VARS: dict[str, contextvars.ContextVar[Any]] = {} 44 | 45 | 46 | def get_contextvars() -> dict[str, Any]: 47 | """ 48 | Return a copy of the *structlog*-specific context-local context. 49 | 50 | .. versionadded:: 21.2.0 51 | """ 52 | rv = {} 53 | ctx = contextvars.copy_context() 54 | 55 | for k in ctx: 56 | if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis: 57 | rv[k.name[STRUCTLOG_KEY_PREFIX_LEN:]] = ctx[k] 58 | 59 | return rv 60 | 61 | 62 | def get_merged_contextvars(bound_logger: BindableLogger) -> dict[str, Any]: 63 | """ 64 | Return a copy of the current context-local context merged with the context 65 | from *bound_logger*. 66 | 67 | .. versionadded:: 21.2.0 68 | """ 69 | ctx = get_contextvars() 70 | ctx.update(structlog.get_context(bound_logger)) 71 | 72 | return ctx 73 | 74 | 75 | def merge_contextvars( 76 | logger: WrappedLogger, method_name: str, event_dict: EventDict 77 | ) -> EventDict: 78 | """ 79 | A processor that merges in a global (context-local) context. 80 | 81 | Use this as your first processor in :func:`structlog.configure` to ensure 82 | context-local context is included in all log calls. 83 | 84 | .. versionadded:: 20.1.0 85 | .. versionchanged:: 21.1.0 See toplevel note. 86 | """ 87 | ctx = contextvars.copy_context() 88 | 89 | for k in ctx: 90 | if k.name.startswith(STRUCTLOG_KEY_PREFIX) and ctx[k] is not Ellipsis: 91 | event_dict.setdefault(k.name[STRUCTLOG_KEY_PREFIX_LEN:], ctx[k]) 92 | 93 | return event_dict 94 | 95 | 96 | def clear_contextvars() -> None: 97 | """ 98 | Clear the context-local context. 99 | 100 | The typical use-case for this function is to invoke it early in request- 101 | handling code. 102 | 103 | .. versionadded:: 20.1.0 104 | .. versionchanged:: 21.1.0 See toplevel note. 105 | """ 106 | ctx = contextvars.copy_context() 107 | for k in ctx: 108 | if k.name.startswith(STRUCTLOG_KEY_PREFIX): 109 | k.set(Ellipsis) 110 | 111 | 112 | def bind_contextvars(**kw: Any) -> Mapping[str, contextvars.Token[Any]]: 113 | r""" 114 | Put keys and values into the context-local context. 115 | 116 | Use this instead of :func:`~structlog.BoundLogger.bind` when you want some 117 | context to be global (context-local). 118 | 119 | Return the mapping of `contextvars.Token`\s resulting 120 | from setting the backing :class:`~contextvars.ContextVar`\s. 121 | Suitable for passing to :func:`reset_contextvars`. 122 | 123 | .. versionadded:: 20.1.0 124 | .. versionchanged:: 21.1.0 Return the `contextvars.Token` mapping 125 | rather than None. See also the toplevel note. 126 | """ 127 | rv = {} 128 | for k, v in kw.items(): 129 | structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 130 | try: 131 | var = _CONTEXT_VARS[structlog_k] 132 | except KeyError: 133 | var = contextvars.ContextVar(structlog_k, default=Ellipsis) 134 | _CONTEXT_VARS[structlog_k] = var 135 | 136 | rv[k] = var.set(v) 137 | 138 | return rv 139 | 140 | 141 | def reset_contextvars(**kw: contextvars.Token[Any]) -> None: 142 | r""" 143 | Reset contextvars corresponding to the given Tokens. 144 | 145 | .. versionadded:: 21.1.0 146 | """ 147 | for k, v in kw.items(): 148 | structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 149 | var = _CONTEXT_VARS[structlog_k] 150 | var.reset(v) 151 | 152 | 153 | def unbind_contextvars(*keys: str) -> None: 154 | """ 155 | Remove *keys* from the context-local context if they are present. 156 | 157 | Use this instead of :func:`~structlog.BoundLogger.unbind` when you want to 158 | remove keys from a global (context-local) context. 159 | 160 | .. versionadded:: 20.1.0 161 | .. versionchanged:: 21.1.0 See toplevel note. 162 | """ 163 | for k in keys: 164 | structlog_k = f"{STRUCTLOG_KEY_PREFIX}{k}" 165 | if structlog_k in _CONTEXT_VARS: 166 | _CONTEXT_VARS[structlog_k].set(Ellipsis) 167 | 168 | 169 | @contextlib.contextmanager 170 | def bound_contextvars(**kw: Any) -> Generator[None, None, None]: 171 | """ 172 | Bind *kw* to the current context-local context. Unbind or restore *kw* 173 | afterwards. Do **not** affect other keys. 174 | 175 | Can be used as a context manager or decorator. 176 | 177 | .. versionadded:: 21.4.0 178 | """ 179 | context = get_contextvars() 180 | saved = {k: context[k] for k in context.keys() & kw.keys()} 181 | 182 | bind_contextvars(**kw) 183 | try: 184 | yield 185 | finally: 186 | unbind_contextvars(*kw.keys()) 187 | bind_contextvars(**saved) 188 | -------------------------------------------------------------------------------- /src/structlog/exceptions.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Exceptions factored out to avoid import loops. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | 13 | class DropEvent(BaseException): 14 | """ 15 | If raised by an processor, the event gets silently dropped. 16 | 17 | Derives from BaseException because it's technically not an error. 18 | """ 19 | -------------------------------------------------------------------------------- /src/structlog/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/src/structlog/py.typed -------------------------------------------------------------------------------- /src/structlog/testing.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Helpers to test your application's logging behavior. 8 | 9 | .. versionadded:: 20.1.0 10 | 11 | See :doc:`testing`. 12 | """ 13 | 14 | from __future__ import annotations 15 | 16 | from contextlib import contextmanager 17 | from typing import Any, Generator, NamedTuple, NoReturn 18 | 19 | from ._config import configure, get_config 20 | from ._log_levels import map_method_name 21 | from .exceptions import DropEvent 22 | from .typing import EventDict, WrappedLogger 23 | 24 | 25 | __all__ = [ 26 | "CapturedCall", 27 | "CapturingLogger", 28 | "CapturingLoggerFactory", 29 | "LogCapture", 30 | "ReturnLogger", 31 | "ReturnLoggerFactory", 32 | "capture_logs", 33 | ] 34 | 35 | 36 | class LogCapture: 37 | """ 38 | Class for capturing log messages in its entries list. 39 | Generally you should use `structlog.testing.capture_logs`, 40 | but you can use this class if you want to capture logs with other patterns. 41 | 42 | :ivar List[structlog.typing.EventDict] entries: The captured log entries. 43 | 44 | .. versionadded:: 20.1.0 45 | 46 | .. versionchanged:: 24.3.0 47 | Added mapping from "exception" to "error" 48 | Added mapping from "warn" to "warning" 49 | """ 50 | 51 | entries: list[EventDict] 52 | 53 | def __init__(self) -> None: 54 | self.entries = [] 55 | 56 | def __call__( 57 | self, _: WrappedLogger, method_name: str, event_dict: EventDict 58 | ) -> NoReturn: 59 | event_dict["log_level"] = map_method_name(method_name) 60 | self.entries.append(event_dict) 61 | 62 | raise DropEvent 63 | 64 | 65 | @contextmanager 66 | def capture_logs() -> Generator[list[EventDict], None, None]: 67 | """ 68 | Context manager that appends all logging statements to its yielded list 69 | while it is active. Disables all configured processors for the duration 70 | of the context manager. 71 | 72 | Attention: this is **not** thread-safe! 73 | 74 | .. versionadded:: 20.1.0 75 | """ 76 | cap = LogCapture() 77 | # Modify `_Configuration.default_processors` set via `configure` but always 78 | # keep the list instance intact to not break references held by bound 79 | # loggers. 80 | processors = get_config()["processors"] 81 | old_processors = processors.copy() 82 | try: 83 | # clear processors list and use LogCapture for testing 84 | processors.clear() 85 | processors.append(cap) 86 | configure(processors=processors) 87 | yield cap.entries 88 | finally: 89 | # remove LogCapture and restore original processors 90 | processors.clear() 91 | processors.extend(old_processors) 92 | configure(processors=processors) 93 | 94 | 95 | class ReturnLogger: 96 | """ 97 | Return the arguments that it's called with. 98 | 99 | >>> from structlog import ReturnLogger 100 | >>> ReturnLogger().info("hello") 101 | 'hello' 102 | >>> ReturnLogger().info("hello", when="again") 103 | (('hello',), {'when': 'again'}) 104 | 105 | .. versionchanged:: 0.3.0 106 | Allow for arbitrary arguments and keyword arguments to be passed in. 107 | """ 108 | 109 | def msg(self, *args: Any, **kw: Any) -> Any: 110 | """ 111 | Return tuple of ``args, kw`` or just ``args[0]`` if only one arg passed 112 | """ 113 | # Slightly convoluted for backwards compatibility. 114 | if len(args) == 1 and not kw: 115 | return args[0] 116 | 117 | return args, kw 118 | 119 | log = debug = info = warn = warning = msg 120 | fatal = failure = err = error = critical = exception = msg 121 | 122 | 123 | class ReturnLoggerFactory: 124 | r""" 125 | Produce and cache `ReturnLogger`\ s. 126 | 127 | To be used with `structlog.configure`\ 's *logger_factory*. 128 | 129 | Positional arguments are silently ignored. 130 | 131 | .. versionadded:: 0.4.0 132 | """ 133 | 134 | def __init__(self) -> None: 135 | self._logger = ReturnLogger() 136 | 137 | def __call__(self, *args: Any) -> ReturnLogger: 138 | return self._logger 139 | 140 | 141 | class CapturedCall(NamedTuple): 142 | """ 143 | A call as captured by `CapturingLogger`. 144 | 145 | Can also be unpacked like a tuple. 146 | 147 | Args: 148 | method_name: The method name that got called. 149 | 150 | args: A tuple of the positional arguments. 151 | 152 | kwargs: A dict of the keyword arguments. 153 | 154 | .. versionadded:: 20.2.0 155 | """ 156 | 157 | method_name: str 158 | args: tuple[Any, ...] 159 | kwargs: dict[str, Any] 160 | 161 | 162 | class CapturingLogger: 163 | """ 164 | Store the method calls that it's been called with. 165 | 166 | This is nicer than `ReturnLogger` for unit tests because the bound logger 167 | doesn't have to cooperate. 168 | 169 | **Any** method name is supported. 170 | 171 | .. versionadded:: 20.2.0 172 | """ 173 | 174 | calls: list[CapturedCall] 175 | 176 | def __init__(self) -> None: 177 | self.calls = [] 178 | 179 | def __repr__(self) -> str: 180 | return f"" 181 | 182 | def __getattr__(self, name: str) -> Any: 183 | """ 184 | Capture call to `calls` 185 | """ 186 | 187 | def log(*args: Any, **kw: Any) -> None: 188 | self.calls.append(CapturedCall(name, args, kw)) 189 | 190 | return log 191 | 192 | 193 | class CapturingLoggerFactory: 194 | r""" 195 | Produce and cache `CapturingLogger`\ s. 196 | 197 | Each factory produces and reuses only **one** logger. 198 | 199 | You can access it via the ``logger`` attribute. 200 | 201 | To be used with `structlog.configure`\ 's *logger_factory*. 202 | 203 | Positional arguments are silently ignored. 204 | 205 | .. versionadded:: 20.2.0 206 | """ 207 | 208 | logger: CapturingLogger 209 | 210 | def __init__(self) -> None: 211 | self.logger = CapturingLogger() 212 | 213 | def __call__(self, *args: Any) -> CapturingLogger: 214 | return self.logger 215 | -------------------------------------------------------------------------------- /src/structlog/types.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Deprecated name for :mod:`structlog.typing`. 8 | 9 | .. versionadded:: 20.2.0 10 | .. deprecated:: 22.2.0 11 | """ 12 | 13 | from __future__ import annotations 14 | 15 | from .typing import ( 16 | BindableLogger, 17 | Context, 18 | EventDict, 19 | ExceptionRenderer, 20 | ExceptionTransformer, 21 | ExcInfo, 22 | FilteringBoundLogger, 23 | Processor, 24 | WrappedLogger, 25 | ) 26 | 27 | 28 | __all__ = ( 29 | "BindableLogger", 30 | "Context", 31 | "EventDict", 32 | "ExcInfo", 33 | "ExceptionRenderer", 34 | "ExceptionTransformer", 35 | "FilteringBoundLogger", 36 | "Processor", 37 | "WrappedLogger", 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | -------------------------------------------------------------------------------- /tests/additional_frame.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Helper function for testing the deduction of stdlib logger names. 8 | 9 | Since the logger factories are called from within structlog._config, they have 10 | to skip a frame. Calling them here emulates that. 11 | """ 12 | 13 | 14 | def additional_frame(callable): 15 | return callable() 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import logging 7 | 8 | from io import StringIO 9 | 10 | import pytest 11 | 12 | import structlog 13 | 14 | from structlog._log_levels import NAME_TO_LEVEL 15 | from structlog.testing import CapturingLogger 16 | 17 | 18 | try: 19 | import twisted 20 | except ImportError: 21 | twisted = None 22 | 23 | LOGGER = logging.getLogger() 24 | 25 | 26 | @pytest.fixture(autouse=True) 27 | def _ensure_logging_framework_not_altered(): 28 | """ 29 | Prevents 'ValueError: I/O operation on closed file.' errors. 30 | """ 31 | before_handlers = list(LOGGER.handlers) 32 | 33 | yield 34 | 35 | LOGGER.handlers = before_handlers 36 | 37 | 38 | @pytest.fixture(name="sio") 39 | def _sio(): 40 | """ 41 | A new StringIO instance. 42 | """ 43 | return StringIO() 44 | 45 | 46 | @pytest.fixture(name="event_dict") 47 | def _event_dict(): 48 | """ 49 | An example event dictionary with multiple value types w/o the event itself. 50 | """ 51 | 52 | class A: 53 | def __repr__(self): 54 | return r"" 55 | 56 | return {"a": A(), "b": [3, 4], "x": 7, "y": "test", "z": (1, 2)} 57 | 58 | 59 | @pytest.fixture( 60 | name="stdlib_log_method", 61 | params=[m for m in NAME_TO_LEVEL if m != "notset"], 62 | ) 63 | def _stdlib_log_methods(request): 64 | return request.param 65 | 66 | 67 | @pytest.fixture(name="cl") 68 | def _cl(): 69 | return CapturingLogger() 70 | 71 | 72 | @pytest.fixture(autouse=True) 73 | def _reset_config(): 74 | structlog.reset_defaults() 75 | -------------------------------------------------------------------------------- /tests/processors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hynek/structlog/480ae5f2bcd7e77669bd40b43c329bfbc5f5bd31/tests/processors/__init__.py -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import pytest 7 | 8 | from pretend import raiser, stub 9 | 10 | from structlog import get_context 11 | from structlog._base import BoundLoggerBase 12 | from structlog._config import _CONFIG 13 | from structlog.exceptions import DropEvent 14 | from structlog.processors import KeyValueRenderer 15 | from structlog.testing import ReturnLogger 16 | from tests.utils import CustomError 17 | 18 | 19 | def build_bl(logger=None, processors=None, context=None): 20 | """ 21 | Convenience function to build BoundLoggerBases with sane defaults. 22 | """ 23 | return BoundLoggerBase( 24 | logger if logger is not None else ReturnLogger(), 25 | processors if processors is not None else _CONFIG.default_processors, 26 | context if context is not None else _CONFIG.default_context_class(), 27 | ) 28 | 29 | 30 | class TestBinding: 31 | def test_repr(self): 32 | """ 33 | repr() of a BoundLoggerBase shows its context and processors. 34 | """ 35 | bl = build_bl(processors=[1, 2, 3], context={"A": "B"}) 36 | 37 | assert ( 38 | "" 39 | ) == repr(bl) 40 | 41 | def test_binds_independently(self): 42 | """ 43 | Ensure BoundLogger is immutable by default. 44 | """ 45 | b = build_bl(processors=[KeyValueRenderer(sort_keys=True)]) 46 | b = b.bind(x=42, y=23) 47 | b1 = b.bind(foo="bar") 48 | b2 = b.bind(foo="qux") 49 | 50 | assert b._context != b1._context != b2._context 51 | 52 | def test_new_clears_state(self): 53 | """ 54 | Calling new() on a logger clears the context. 55 | """ 56 | b = build_bl() 57 | b = b.bind(x=42) 58 | 59 | assert 42 == get_context(b)["x"] 60 | 61 | b = b.bind() 62 | 63 | assert 42 == get_context(b)["x"] 64 | 65 | b = b.new() 66 | 67 | assert {} == dict(get_context(b)) 68 | 69 | def test_comparison(self): 70 | """ 71 | Two bound loggers are equal if their context is equal. 72 | """ 73 | b = build_bl() 74 | 75 | assert b == b.bind() 76 | assert b is not b.bind() 77 | assert b != b.bind(x=5) 78 | assert b != "test" 79 | 80 | def test_bind_keeps_class(self): 81 | """ 82 | Binding values does not change the type of the bound logger. 83 | """ 84 | 85 | class Wrapper(BoundLoggerBase): 86 | pass 87 | 88 | b = Wrapper(None, [], {}) 89 | 90 | assert isinstance(b.bind(), Wrapper) 91 | 92 | def test_new_keeps_class(self): 93 | """ 94 | Clearing context does not change the type of the bound logger. 95 | """ 96 | 97 | class Wrapper(BoundLoggerBase): 98 | pass 99 | 100 | b = Wrapper(None, [], {}) 101 | 102 | assert isinstance(b.new(), Wrapper) 103 | 104 | def test_unbind(self): 105 | """ 106 | unbind() removes keys from context. 107 | """ 108 | b = build_bl().bind(x=42, y=23).unbind("x", "y") 109 | 110 | assert {} == b._context 111 | 112 | def test_unbind_fail(self): 113 | """ 114 | unbind() raises KeyError if the key is missing. 115 | """ 116 | with pytest.raises(KeyError): 117 | build_bl().bind(x=42, y=23).unbind("x", "z") 118 | 119 | def test_try_unbind(self): 120 | """ 121 | try_unbind() removes keys from context. 122 | """ 123 | b = build_bl().bind(x=42, y=23).try_unbind("x", "y") 124 | 125 | assert {} == b._context 126 | 127 | def test_try_unbind_fail(self): 128 | """ 129 | try_unbind() does nothing if the key is missing. 130 | """ 131 | b = build_bl().bind(x=42, y=23).try_unbind("x", "z") 132 | 133 | assert {"y": 23} == b._context 134 | 135 | 136 | class TestProcessing: 137 | def test_event_empty_string(self): 138 | """ 139 | Empty strings are a valid event. 140 | """ 141 | b = build_bl(processors=[], context={}) 142 | 143 | args, kw = b._process_event("meth", "", {"foo": "bar"}) 144 | 145 | assert () == args 146 | assert {"event": "", "foo": "bar"} == kw 147 | 148 | def test_copies_context_before_processing(self): 149 | """ 150 | BoundLoggerBase._process_event() gets called before relaying events 151 | to wrapped loggers. 152 | """ 153 | 154 | def chk(_, __, event_dict): 155 | assert b._context is not event_dict 156 | return "" 157 | 158 | b = build_bl(processors=[chk]) 159 | 160 | assert (("",), {}) == b._process_event("", "event", {}) 161 | assert "event" not in b._context 162 | 163 | def test_chain_does_not_swallow_all_exceptions(self): 164 | """ 165 | If the chain raises anything else than DropEvent, the error is not 166 | swallowed. 167 | """ 168 | b = build_bl(processors=[raiser(CustomError)]) 169 | 170 | with pytest.raises(CustomError): 171 | b._process_event("", "boom", {}) 172 | 173 | def test_last_processor_returns_string(self): 174 | """ 175 | If the final processor returns a string, ``(the_string,), {}`` is 176 | returned. 177 | """ 178 | logger = stub(msg=lambda *args, **kw: (args, kw)) 179 | b = build_bl(logger, processors=[lambda *_: "foo"]) 180 | 181 | assert (("foo",), {}) == b._process_event("", "foo", {}) 182 | 183 | def test_last_processor_returns_bytes(self): 184 | """ 185 | If the final processor returns bytes, ``(the_bytes,), {}`` is 186 | returned. 187 | """ 188 | logger = stub(msg=lambda *args, **kw: (args, kw)) 189 | b = build_bl(logger, processors=[lambda *_: b"foo"]) 190 | 191 | assert ((b"foo",), {}) == b._process_event(None, "name", {}) 192 | 193 | def test_last_processor_returns_bytearray(self): 194 | """ 195 | If the final processor returns a bytearray, ``(the_array,), {}`` is 196 | returned. 197 | """ 198 | logger = stub(msg=lambda *args, **kw: (args, kw)) 199 | b = build_bl(logger, processors=[lambda *_: bytearray(b"foo")]) 200 | 201 | assert ((bytearray(b"foo"),), {}) == b._process_event(None, "name", {}) 202 | 203 | def test_last_processor_returns_tuple(self): 204 | """ 205 | If the final processor returns a tuple, it is just passed through. 206 | """ 207 | logger = stub(msg=lambda *args, **kw: (args, kw)) 208 | b = build_bl( 209 | logger, processors=[lambda *_: (("foo",), {"key": "value"})] 210 | ) 211 | 212 | assert (("foo",), {"key": "value"}) == b._process_event("", "foo", {}) 213 | 214 | def test_last_processor_returns_dict(self): 215 | """ 216 | If the final processor returns a dict, ``(), the_dict`` is returned. 217 | """ 218 | logger = stub(msg=lambda *args, **kw: (args, kw)) 219 | b = build_bl(logger, processors=[lambda *_: {"event": "foo"}]) 220 | 221 | assert ((), {"event": "foo"}) == b._process_event("", "foo", {}) 222 | 223 | def test_last_processor_returns_unknown_value(self): 224 | """ 225 | If the final processor returns something unexpected, raise ValueError 226 | with a helpful error message. 227 | """ 228 | logger = stub(msg=lambda *args, **kw: (args, kw)) 229 | b = build_bl(logger, processors=[lambda *_: object()]) 230 | 231 | with pytest.raises(ValueError, match="Last processor didn't return"): 232 | b._process_event("", "foo", {}) 233 | 234 | 235 | class TestProxying: 236 | def test_processor_raising_DropEvent_silently_aborts_chain(self, capsys): 237 | """ 238 | If a processor raises DropEvent, the chain is aborted and nothing is 239 | proxied to the logger. 240 | """ 241 | b = build_bl(processors=[raiser(DropEvent), raiser(ValueError)]) 242 | b._proxy_to_logger("", None, x=5) 243 | 244 | assert ("", "") == capsys.readouterr() 245 | -------------------------------------------------------------------------------- /tests/test_frames.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import sys 7 | 8 | import pytest 9 | 10 | from pretend import stub 11 | 12 | from structlog._frames import ( 13 | _find_first_app_frame_and_name, 14 | _format_exception, 15 | _format_stack, 16 | ) 17 | 18 | 19 | class TestFindFirstAppFrameAndName: 20 | def test_ignores_structlog_by_default(self): 21 | """ 22 | No matter what you pass in, structlog frames get always ignored. 23 | """ 24 | f1 = stub(f_globals={"__name__": "test"}, f_back=None) 25 | f2 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f1) 26 | 27 | f, n = _find_first_app_frame_and_name(_getframe=lambda: f2) 28 | 29 | assert (f1, "test") == (f, n) 30 | 31 | def test_ignoring_of_additional_frame_names_works(self): 32 | """ 33 | Additional names are properly ignored too. 34 | """ 35 | f1 = stub(f_globals={"__name__": "test"}, f_back=None) 36 | f2 = stub(f_globals={"__name__": "ignored.bar"}, f_back=f1) 37 | f3 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f2) 38 | 39 | f, n = _find_first_app_frame_and_name( 40 | additional_ignores=["ignored"], _getframe=lambda: f3 41 | ) 42 | 43 | assert (f1, "test") == (f, n) 44 | 45 | def test_tolerates_missing_name(self): 46 | """ 47 | Use ``?`` if `f_globals` lacks a `__name__` key 48 | """ 49 | f1 = stub(f_globals={}, f_back=None) 50 | 51 | f, n = _find_first_app_frame_and_name(_getframe=lambda: f1) 52 | 53 | assert (f1, "?") == (f, n) 54 | 55 | def test_tolerates_name_explicitly_None_oneframe(self): 56 | """ 57 | Use ``?`` if `f_globals` has a `None` valued `__name__` key 58 | """ 59 | f1 = stub(f_globals={"__name__": None}, f_back=None) 60 | 61 | f, n = _find_first_app_frame_and_name(_getframe=lambda: f1) 62 | 63 | assert (f1, "?") == (f, n) 64 | 65 | def test_tolerates_name_explicitly_None_manyframe(self): 66 | """ 67 | Use ``?`` if `f_globals` has a `None` valued `__name__` key, 68 | multiple frames up. 69 | """ 70 | f1 = stub(f_globals={"__name__": None}, f_back=None) 71 | f2 = stub(f_globals={"__name__": "structlog.blubb"}, f_back=f1) 72 | f, n = _find_first_app_frame_and_name(_getframe=lambda: f2) 73 | 74 | assert (f1, "?") == (f, n) 75 | 76 | def test_tolerates_f_back_is_None(self): 77 | """ 78 | Use ``?`` if all frames are in ignored frames. 79 | """ 80 | f1 = stub(f_globals={"__name__": "structlog"}, f_back=None) 81 | 82 | f, n = _find_first_app_frame_and_name(_getframe=lambda: f1) 83 | 84 | assert (f1, "?") == (f, n) 85 | 86 | 87 | @pytest.fixture 88 | def exc_info(): 89 | """ 90 | Fake a valid exc_info. 91 | """ 92 | try: 93 | raise ValueError 94 | except ValueError: 95 | return sys.exc_info() 96 | 97 | 98 | class TestFormatException: 99 | def test_returns_str(self, exc_info): 100 | """ 101 | Always returns a native string. 102 | """ 103 | assert isinstance(_format_exception(exc_info), str) 104 | 105 | def test_formats(self, exc_info): 106 | """ 107 | The passed exc_info is formatted. 108 | """ 109 | assert _format_exception(exc_info).startswith( 110 | "Traceback (most recent call last):\n" 111 | ) 112 | 113 | def test_no_trailing_nl(self, exc_info, monkeypatch): 114 | """ 115 | Trailing newlines are snipped off but if the string does not contain 116 | one nothing is removed. 117 | """ 118 | from structlog._frames import traceback 119 | 120 | monkeypatch.setattr( 121 | traceback, "print_exception", lambda *a: a[-1].write("foo") 122 | ) 123 | 124 | assert "foo" == _format_exception(exc_info) 125 | 126 | 127 | class TestFormatStack: 128 | def test_returns_str(self): 129 | """ 130 | Always returns a native string. 131 | """ 132 | assert isinstance(_format_stack(sys._getframe()), str) 133 | 134 | def test_formats(self): 135 | """ 136 | The passed stack is formatted. 137 | """ 138 | assert _format_stack(sys._getframe()).startswith( 139 | "Stack (most recent call last):\n" 140 | ) 141 | 142 | def test_no_trailing_nl(self, monkeypatch): 143 | """ 144 | Trailing newlines are snipped off but if the string does not contain 145 | one nothing is removed. 146 | """ 147 | from structlog._frames import traceback 148 | 149 | monkeypatch.setattr( 150 | traceback, "print_stack", lambda frame, file: file.write("foo") 151 | ) 152 | 153 | assert _format_stack(sys._getframe()).endswith("foo") 154 | -------------------------------------------------------------------------------- /tests/test_generic.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import pickle 7 | 8 | import pytest 9 | 10 | from freezegun import freeze_time 11 | 12 | from structlog._config import _CONFIG 13 | from structlog._generic import BoundLogger 14 | from structlog.testing import ReturnLogger 15 | 16 | 17 | class TestLogger: 18 | def log(self, msg): 19 | return "log", msg 20 | 21 | def gol(self, msg): 22 | return "gol", msg 23 | 24 | 25 | class TestGenericBoundLogger: 26 | def test_caches(self): 27 | """ 28 | __getattr__() gets called only once per logger method. 29 | """ 30 | b = BoundLogger( 31 | ReturnLogger(), 32 | _CONFIG.default_processors, 33 | _CONFIG.default_context_class(), 34 | ) 35 | 36 | assert "msg" not in b.__dict__ 37 | 38 | b.msg("foo") 39 | 40 | assert "msg" in b.__dict__ 41 | 42 | @pytest.mark.parametrize("proto", range(3, pickle.HIGHEST_PROTOCOL + 1)) 43 | @freeze_time("2023-05-22 17:00") 44 | def test_pickle(self, proto): 45 | """ 46 | Can be pickled and unpickled. 47 | """ 48 | b = BoundLogger( 49 | ReturnLogger(), 50 | _CONFIG.default_processors, 51 | _CONFIG.default_context_class(), 52 | ).bind(x=1) 53 | 54 | assert b.info("hi") == pickle.loads(pickle.dumps(b, proto)).info("hi") 55 | 56 | def test_deepcopy(self): 57 | """ 58 | __getattr__ returns None for '__deepcopy__' 59 | """ 60 | b = BoundLogger( 61 | ReturnLogger(), 62 | _CONFIG.default_processors, 63 | _CONFIG.default_context_class(), 64 | ) 65 | 66 | assert b.__deepcopy__ is None 67 | -------------------------------------------------------------------------------- /tests/test_packaging.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | from importlib import metadata 7 | 8 | import pytest 9 | 10 | import structlog 11 | 12 | 13 | class TestLegacyMetadataHack: 14 | def test_version(self, recwarn): 15 | """ 16 | structlog.__version__ returns the correct version and doesn't warn. 17 | """ 18 | assert metadata.version("structlog") == structlog.__version__ 19 | assert [] == recwarn.list 20 | 21 | def test_description(self): 22 | """ 23 | structlog.__description__ returns the correct description. 24 | """ 25 | with pytest.deprecated_call(): 26 | assert "Structured Logging for Python" == structlog.__description__ 27 | 28 | def test_uri(self): 29 | """ 30 | structlog.__uri__ returns the correct project URL. 31 | """ 32 | with pytest.deprecated_call(): 33 | assert "https://www.structlog.org/" == structlog.__uri__ 34 | 35 | def test_email(self): 36 | """ 37 | structlog.__email__ returns Hynek's email address. 38 | """ 39 | with pytest.deprecated_call(): 40 | assert "hs@ox.cx" == structlog.__email__ 41 | 42 | def test_does_not_exist(self): 43 | """ 44 | Asking for unsupported dunders raises an AttributeError. 45 | """ 46 | with pytest.raises( 47 | AttributeError, match="module structlog has no attribute __yolo__" 48 | ): 49 | structlog.__yolo__ 50 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import pytest 7 | 8 | import structlog 9 | 10 | from structlog import get_config, get_logger, testing 11 | from structlog.testing import ( 12 | CapturedCall, 13 | CapturingLogger, 14 | CapturingLoggerFactory, 15 | LogCapture, 16 | ReturnLogger, 17 | ReturnLoggerFactory, 18 | ) 19 | 20 | 21 | class TestCaptureLogs: 22 | def test_captures_logs(self): 23 | """ 24 | Log entries are captured and retain their structure. 25 | """ 26 | with testing.capture_logs() as logs: 27 | get_logger().bind(x="y").info("hello", answer=42) 28 | get_logger().bind(a="b").info("goodbye", foo={"bar": "baz"}) 29 | assert [ 30 | {"event": "hello", "log_level": "info", "x": "y", "answer": 42}, 31 | { 32 | "a": "b", 33 | "event": "goodbye", 34 | "log_level": "info", 35 | "foo": {"bar": "baz"}, 36 | }, 37 | ] == logs 38 | 39 | def get_active_procs(self): 40 | return get_config()["processors"] 41 | 42 | def test_restores_processors_on_success(self): 43 | """ 44 | Processors are patched within the contextmanager and restored on 45 | exit. 46 | """ 47 | orig_procs = self.get_active_procs() 48 | assert len(orig_procs) > 1 49 | 50 | with testing.capture_logs(): 51 | modified_procs = self.get_active_procs() 52 | assert len(modified_procs) == 1 53 | assert isinstance(modified_procs[0], LogCapture) 54 | 55 | restored_procs = self.get_active_procs() 56 | assert orig_procs is restored_procs 57 | assert len(restored_procs) > 1 58 | 59 | def test_restores_processors_on_error(self): 60 | """ 61 | Processors are restored even on errors. 62 | """ 63 | orig_procs = self.get_active_procs() 64 | 65 | with pytest.raises(NotImplementedError), testing.capture_logs(): 66 | raise NotImplementedError("from test") 67 | 68 | assert orig_procs is self.get_active_procs() 69 | 70 | def test_captures_bound_logers(self): 71 | """ 72 | Even logs from already bound loggers are captured and their processors 73 | restored on exit. 74 | """ 75 | logger = get_logger("bound").bind(foo="bar") 76 | logger.info("ensure logger is bound") 77 | 78 | with testing.capture_logs() as logs: 79 | logger.info("hello", answer=42) 80 | 81 | assert logs == [ 82 | { 83 | "event": "hello", 84 | "answer": 42, 85 | "foo": "bar", 86 | "log_level": "info", 87 | } 88 | ] 89 | 90 | def test_captures_log_level_mapping(self): 91 | """ 92 | exceptions and warn log levels are mapped like in regular loggers. 93 | """ 94 | structlog.configure( 95 | processors=[ 96 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 97 | ], 98 | logger_factory=structlog.stdlib.LoggerFactory(), 99 | wrapper_class=structlog.stdlib.BoundLogger, 100 | ) 101 | with testing.capture_logs() as logs: 102 | get_logger().exception("hello", answer=42) 103 | get_logger().warn("again", answer=23) 104 | 105 | assert [ 106 | { 107 | "event": "hello", 108 | "answer": 42, 109 | "exc_info": True, 110 | "log_level": "error", 111 | }, 112 | { 113 | "answer": 23, 114 | "event": "again", 115 | "log_level": "warning", 116 | }, 117 | ] == logs 118 | 119 | 120 | class TestReturnLogger: 121 | # @pytest.mark.parametrize("method", stdlib_log_methods) 122 | def test_stdlib_methods_support(self, stdlib_log_method): 123 | """ 124 | ReturnLogger implements methods of stdlib loggers. 125 | """ 126 | v = getattr(ReturnLogger(), stdlib_log_method)("hello") 127 | 128 | assert "hello" == v 129 | 130 | def test_return_logger(self): 131 | """ 132 | Return logger returns exactly what's sent in. 133 | """ 134 | obj = ["hello"] 135 | 136 | assert obj is ReturnLogger().msg(obj) 137 | 138 | 139 | class TestReturnLoggerFactory: 140 | def test_builds_returnloggers(self): 141 | """ 142 | Factory returns ReturnLoggers. 143 | """ 144 | f = ReturnLoggerFactory() 145 | 146 | assert isinstance(f(), ReturnLogger) 147 | 148 | def test_caches(self): 149 | """ 150 | There's no need to have several loggers so we return the same one on 151 | each call. 152 | """ 153 | f = ReturnLoggerFactory() 154 | 155 | assert f() is f() 156 | 157 | def test_ignores_args(self): 158 | """ 159 | ReturnLogger doesn't take positional arguments. If any are passed to 160 | the factory, they are not passed to the logger. 161 | """ 162 | ReturnLoggerFactory()(1, 2, 3) 163 | 164 | 165 | class TestCapturingLogger: 166 | def test_factory_caches(self): 167 | """ 168 | CapturingLoggerFactory returns one CapturingLogger over and over again. 169 | """ 170 | clf = CapturingLoggerFactory() 171 | cl1 = clf() 172 | cl2 = clf() 173 | 174 | assert cl1 is cl2 175 | 176 | def test_repr(self): 177 | """ 178 | repr says how many calls there were. 179 | """ 180 | cl = CapturingLogger() 181 | 182 | cl.info("hi") 183 | cl.error("yolo") 184 | 185 | assert "" == repr(cl) 186 | 187 | def test_captures(self): 188 | """ 189 | All calls to all names are captured. 190 | """ 191 | cl = CapturingLogger() 192 | 193 | cl.info("hi", val=42) 194 | cl.trololo("yolo", foo={"bar": "baz"}) 195 | 196 | assert [ 197 | CapturedCall(method_name="info", args=("hi",), kwargs={"val": 42}), 198 | CapturedCall( 199 | method_name="trololo", 200 | args=("yolo",), 201 | kwargs={"foo": {"bar": "baz"}}, 202 | ), 203 | ] == cl.calls 204 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | import multiprocessing 7 | import sys 8 | 9 | import pytest 10 | 11 | from structlog._utils import get_processname 12 | 13 | 14 | class TestGetProcessname: 15 | def test_default(self): 16 | """ 17 | The returned process name matches the name of the current process from 18 | the `multiprocessing` module. 19 | """ 20 | assert get_processname() == multiprocessing.current_process().name 21 | 22 | def test_changed(self, monkeypatch: pytest.MonkeyPatch): 23 | """ 24 | The returned process name matches the name of the current process from 25 | the `multiprocessing` module if it is not the default. 26 | """ 27 | tmp_name = "fakename" 28 | monkeypatch.setattr( 29 | target=multiprocessing.current_process(), 30 | name="name", 31 | value=tmp_name, 32 | ) 33 | 34 | assert get_processname() == tmp_name 35 | 36 | def test_no_multiprocessing(self, monkeypatch: pytest.MonkeyPatch) -> None: 37 | """ 38 | The returned process name is the default process name if the 39 | `multiprocessing` module is not available. 40 | """ 41 | tmp_name = "fakename" 42 | monkeypatch.setattr( 43 | target=multiprocessing.current_process(), 44 | name="name", 45 | value=tmp_name, 46 | ) 47 | monkeypatch.setattr( 48 | target=sys, 49 | name="modules", 50 | value={}, 51 | ) 52 | 53 | assert get_processname() == "n/a" 54 | 55 | def test_exception(self, monkeypatch: pytest.MonkeyPatch) -> None: 56 | """ 57 | The returned process name is the default process name when an exception 58 | is thrown when an attempt is made to retrieve the current process name 59 | from the `multiprocessing` module. 60 | """ 61 | 62 | def _current_process() -> None: 63 | raise RuntimeError("test") 64 | 65 | monkeypatch.setattr( 66 | target=multiprocessing, 67 | name="current_process", 68 | value=_current_process, 69 | ) 70 | 71 | assert get_processname() == "n/a" 72 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: MIT OR Apache-2.0 2 | # This file is dual licensed under the terms of the Apache License, Version 3 | # 2.0, and the MIT License. See the LICENSE file in the root of this 4 | # repository for complete details. 5 | 6 | """ 7 | Shared test utilities. 8 | """ 9 | 10 | from structlog._log_levels import NAME_TO_LEVEL 11 | 12 | 13 | stdlib_log_methods = [m for m in NAME_TO_LEVEL if m != "notset"] 14 | 15 | 16 | class CustomError(Exception): 17 | """ 18 | Custom exception for testing purposes. 19 | """ 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | min_version = 4 3 | env_list = 4 | pre-commit, 5 | mypy-pkg, 6 | py3{8,9,10,11,12,13,14}-{tests,mypy} 7 | py3{8,13}-tests-{colorama,be,rich}, 8 | docs-{sponsors,doctests}, 9 | coverage-report 10 | 11 | 12 | [testenv] 13 | package = wheel 14 | wheel_build_env = .pkg 15 | dependency_groups = 16 | tests: tests 17 | mypy: typing 18 | commands = 19 | tests: pytest {posargs} 20 | mypy: mypy tests/typing 21 | 22 | 23 | # Run oldest and latest under Coverage. 24 | # Keep in-sync with coverage `depends below. 25 | [testenv:py3{8,13}-tests{,-colorama,-be,-rich}] 26 | deps = 27 | coverage[toml] 28 | py313: twisted 29 | colorama: colorama 30 | rich: rich 31 | be: better-exceptions 32 | commands = coverage run -m pytest {posargs} 33 | 34 | 35 | [testenv:coverage-report] 36 | deps = coverage[toml] 37 | skip_install = true 38 | parallel_show_output = true 39 | # Keep in-sync with test env definition above. 40 | depends = py3{8,13}-{tests,colorama,be,rich} 41 | commands = 42 | coverage combine 43 | coverage report 44 | 45 | 46 | [testenv:docs-{build,doctests,linkcheck}] 47 | # Keep base_python in sync with ci.yml/docs and .readthedocs.yaml. 48 | base_python = py313 49 | dependency_groups = docs 50 | commands = 51 | build: sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html 52 | doctests: sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs {posargs:docs/_build/}html 53 | linkcheck: sphinx-build -W -b linkcheck -d {envtmpdir}/doctrees docs docs/_build/html 54 | 55 | [testenv:docs-watch] 56 | package = editable 57 | base_python = {[testenv:docs-build]base_python} 58 | dependency_groups = {[testenv:docs-build]dependency_groups} 59 | deps = watchfiles 60 | commands = 61 | watchfiles \ 62 | --ignore-paths docs/_build/ \ 63 | 'sphinx-build -W -n --jobs auto -b html -d {envtmpdir}/doctrees docs docs/_build/html' \ 64 | src \ 65 | docs 66 | 67 | [testenv:docs-sponsors] 68 | description = Ensure sponsor logos are up to date. 69 | deps = cogapp 70 | commands = cog -rP README.md docs/index.md 71 | 72 | 73 | [testenv:pre-commit] 74 | skip_install = true 75 | deps = pre-commit 76 | commands = pre-commit run --all-files 77 | 78 | 79 | [testenv:mypy-pkg] 80 | dependency_groups = typing 81 | commands = mypy src 82 | 83 | 84 | [testenv:pyright] 85 | deps = pyright 86 | dependency_groups = typing 87 | commands = pyright tests/typing 88 | 89 | 90 | [testenv:color-force] 91 | help = A visual check that FORCE_COLOR is working. 92 | set_env = FORCE_COLOR=1 93 | commands = python -c "import structlog; structlog.get_logger().warning('should be colorful')" 94 | 95 | 96 | [testenv:color-no] 97 | help = A visual check that NO_COLOR is working. 98 | set_env = NO_COLOR=1 99 | commands = python -c "import structlog; structlog.get_logger().warning('should be plain')" 100 | 101 | 102 | [testenv:docset] 103 | deps = doc2dash 104 | dependency_groups = docs 105 | allowlist_externals = 106 | rm 107 | cp 108 | tar 109 | commands = 110 | rm -rf structlog.docset structlog.tgz docs/_build 111 | sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html 112 | doc2dash --index-page index.html --icon docs/_static/docset-icon.png --icon-2x docs/_static/docset-icon@2x.png --online-redirect-url https://www.structlog.org/en/latest/ docs/_build/html 113 | tar --exclude='.DS_Store' -cvzf structlog.tgz structlog.docset 114 | -------------------------------------------------------------------------------- /zizmor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | unpinned-uses: 4 | config: 5 | policies: 6 | # We trust GitHub, the PyPA, and ourselves. 7 | "actions/*": ref-pin 8 | "github/*": ref-pin 9 | "pypa/*": ref-pin 10 | "hynek/*": ref-pin 11 | --------------------------------------------------------------------------------