pax_global_header00006660000000000000000000000064146242153600014515gustar00rootroot0000000000000052 comment=4b757b81ddec7a6dddd93f8482087df24748d5f5 django-structlog-8.1.0/000077500000000000000000000000001462421536000150115ustar00rootroot00000000000000django-structlog-8.1.0/.dockerignore000066400000000000000000000000331462421536000174610ustar00rootroot00000000000000.* !.env !.pylintrc /logs/ django-structlog-8.1.0/.editorconfig000066400000000000000000000001041462421536000174610ustar00rootroot00000000000000root = true [*.{html,css,js}] indent_style = space indent_size = 2 django-structlog-8.1.0/.envs/000077500000000000000000000000001462421536000160425ustar00rootroot00000000000000django-structlog-8.1.0/.envs/.local/000077500000000000000000000000001462421536000172125ustar00rootroot00000000000000django-structlog-8.1.0/.envs/.local/.amqp000066400000000000000000000002311462421536000201450ustar00rootroot00000000000000# RabbitMQ # ------------------------------------------------------------------------------ CELERY_BROKER_URL=amqp://admin:unsecure-password@rabbit:5672 django-structlog-8.1.0/.envs/.local/.django000066400000000000000000000005141462421536000204550ustar00rootroot00000000000000# General # ------------------------------------------------------------------------------ USE_DOCKER=yes IPYTHONDIR=/app/.ipython # Celery # ------------------------------------------------------------------------------ CELERY_RESULT_BACKEND="redis://redis:6379/0" # Flower CELERY_FLOWER_USER=debug CELERY_FLOWER_PASSWORD=debug django-structlog-8.1.0/.envs/.local/.postgres000066400000000000000000000003361462421536000210630ustar00rootroot00000000000000# PostgreSQL # ------------------------------------------------------------------------------ POSTGRES_HOST=postgres POSTGRES_PORT=5432 POSTGRES_DB=django_structlog_demo_project POSTGRES_USER=debug POSTGRES_PASSWORD=debug django-structlog-8.1.0/.envs/.local/.redis000066400000000000000000000002001462421536000203110ustar00rootroot00000000000000# Redis # ------------------------------------------------------------------------------ CELERY_BROKER_URL=redis://redis:6379/0 django-structlog-8.1.0/.gitattributes000066400000000000000000000000141462421536000176770ustar00rootroot00000000000000* text=auto django-structlog-8.1.0/.github/000077500000000000000000000000001462421536000163515ustar00rootroot00000000000000django-structlog-8.1.0/.github/dependabot.yml000066400000000000000000000004531462421536000212030ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/requirements" schedule: interval: weekly - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly - package-ecosystem: pip directory: "/docs" schedule: interval: weekly django-structlog-8.1.0/.github/workflows/000077500000000000000000000000001462421536000204065ustar00rootroot00000000000000django-structlog-8.1.0/.github/workflows/gh.yml000066400000000000000000000013041462421536000215250ustar00rootroot00000000000000name: gh-pages on: workflow_run: workflows: ["ci"] branches: [main] types: - completed jobs: deploy-gh-pages: concurrency: ci-${{ github.ref }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r docs/requirements.txt pip install ghp-import - name: make html working-directory: docs run: make html - name: Deploy Documentation run: | pip install ghp-import ghp-import -n -p -m "Documentation git hash $(git rev-parse --short HEAD)" -f docs/_build/html django-structlog-8.1.0/.github/workflows/main.yml000066400000000000000000000154571462421536000220710ustar00rootroot00000000000000name: ci on: push: branches: - main - release/** tags: - '[0-9]+.[0-9]+.[0-9]+' - '[0-9]+.[0-9]+.[0-9]+.dev[0-9]+' pull_request: {} jobs: test-demo-app: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | pip install --upgrade pip pip install -r requirements.txt - name: Start Redis uses: supercharge/redis-github-action@1.8.0 - name: Test demo app env: CELERY_BROKER_URL: redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE: config.settings.test_demo_app run: pytest --cov=./django_structlog_demo_project --cov-append django_structlog_demo_project - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} test: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -U setuptools python -m pip install tox tox-gh-actions - name: Start Redis uses: supercharge/redis-github-action@1.8.0 - name: Test with tox run: tox - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} test-docs: needs: - test-demo-app - test runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r docs/requirements.txt - name: make html working-directory: docs run: make html - name: make doctest working-directory: docs run: make doctest black: needs: - test-demo-app - test runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements/black.txt - name: run black run: black --check --verbose . ruff: needs: - test-demo-app - test runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements/ruff.txt - name: run ruff run: ruff check . test-install-base: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -U setuptools python -m pip install . - name: Import django_structlog modules run: | python -c "import django_structlog" python -c "import django_structlog.apps" python -c "import django_structlog.signals" python -c "import django_structlog.app_settings" python -c "import django_structlog.middlewares.request" test-install-celery: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -U setuptools python -m pip install .[celery] - name: Import django_structlog modules run: | python -c "import django_structlog" python -c "import django_structlog.apps" python -c "import django_structlog.signals" python -c "import django_structlog.app_settings" python -c "import django_structlog.middlewares.request" - name: Import django_structlog celery modules run: | python -c "import django_structlog.celery.receivers" python -c "import django_structlog.celery.steps" python -c "import django_structlog.celery.signals" test-install-commands: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.12' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -U setuptools python -m pip install .[commands] - name: Import django_structlog modules run: | python -c "import django_structlog" python -c "import django_structlog.apps" python -c "import django_structlog.signals" python -c "import django_structlog.app_settings" python -c "import django_structlog.middlewares.request" - name: Import django_structlog commands modules run: | python -c "import django_structlog.commands" pypi-deployment: needs: - test-docs - test-install-base - test-install-celery - test-install-commands - black - ruff runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install -U -q build - name: build sdist run: python -m build --sdist - name: build wheel run: python -m build --wheel - name: Publish package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_DEPLOYMENT_KEY }} django-structlog-8.1.0/.github/workflows/pre-commit.yml000066400000000000000000000013471462421536000232120ustar00rootroot00000000000000name: pre-commit auto-update on: # every monday at 11am utc schedule: - cron: "0 11 * * 1" # on demand workflow_dispatch: jobs: auto-update: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 - uses: browniebroke/pre-commit-autoupdate-action@v1.0.0 - uses: peter-evans/create-pull-request@v6.0.5 with: token: ${{ secrets.GITHUB_TOKEN }} branch: update/pre-commit-hooks title: Update pre-commit hooks commit-message: "chore: update pre-commit hooks" body: Update versions of pre-commit hooks to latest version. #base: ${{ github.head_ref }} django-structlog-8.1.0/.gitignore000066400000000000000000000122741462421536000170070ustar00rootroot00000000000000### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: staticfiles/ # Sphinx documentation docs/_build/ # PyBuilder target/ # pyenv .python-version # celery beat schedule file celerybeat-schedule # Environments .venv venv/ ENV/ # Rope project settings .ropeproject # mkdocs documentation /site ### Node template # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity ### Linux template *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### VisualStudioCode template .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json # Provided default Pycharm Run/Debug Configurations should be tracked by git # In case of local modifications made by Pycharm, use update-index command # for each changed file, like this: # git update-index --assume-unchanged .idea/django_structlog_demo_project.iml ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/misc.xml .idea/google-java-format.xml .idea/**/workspace.xml .idea/**/tasks.xml .idea/dictionaries # Sensitive or high-churn files: .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.xml .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml # Gradle: .idea/**/gradle.xml .idea/**/libraries # CMake cmake-build-debug/ # Mongo Explorer plugin: .idea/**/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### Windows template # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk ### macOS template # General *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### SublimeText template # Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # Workspace files are user-specific *.sublime-workspace # Project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using Sublime Text # *.sublime-project # SFTP configuration file sftp-config.json # Package control specific files Package Control.last-run Package Control.ca-list Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ Package Control.merged-ca-bundle Package Control.user-ca-bundle oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file # https://packagecontrol.io/packages/sublime-github GitHub.sublime-settings ### Vim template # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist # Auto-generated tag files tags ### Project template django_structlog_demo_project/media/ .pytest_cache/ .ipython/ .env .envs/* !.envs/.local/ .vscode/ django-structlog-8.1.0/.idea/000077500000000000000000000000001462421536000157715ustar00rootroot00000000000000django-structlog-8.1.0/.idea/django-structlog.iml000066400000000000000000000017061462421536000217660ustar00rootroot00000000000000 django-structlog-8.1.0/.idea/encodings.xml000066400000000000000000000002071462421536000204630ustar00rootroot00000000000000 django-structlog-8.1.0/.idea/inspectionProfiles/000077500000000000000000000000001462421536000216505ustar00rootroot00000000000000django-structlog-8.1.0/.idea/inspectionProfiles/Project_Default.xml000066400000000000000000000030201462421536000254370ustar00rootroot00000000000000 django-structlog-8.1.0/.idea/modules.xml000066400000000000000000000004341462421536000201640ustar00rootroot00000000000000 django-structlog-8.1.0/.idea/vcs.xml000066400000000000000000000002471462421536000173110ustar00rootroot00000000000000 django-structlog-8.1.0/.pre-commit-config.yaml000066400000000000000000000004171462421536000212740ustar00rootroot00000000000000repos: - repo: https://github.com/ambv/black rev: 24.4.2 hooks: - id: black language_version: python3 - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] django-structlog-8.1.0/.readthedocs.yaml000066400000000000000000000010711462421536000202370ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.12" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # We recommend specifying your dependencies to enable reproducible builds: # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt django-structlog-8.1.0/LICENSE.rst000066400000000000000000000020671462421536000166320ustar00rootroot00000000000000MIT License Copyright (c) 2020 Jules Robichaud-Gagnon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. django-structlog-8.1.0/MANIFEST.in000066400000000000000000000000241462421536000165430ustar00rootroot00000000000000include LICENSE.rst django-structlog-8.1.0/README.rst000066400000000000000000000626151462421536000165120ustar00rootroot00000000000000.. inclusion-marker-introduction-begin django-structlog ================ | |pypi| |wheels| |build-status| |docs| |coverage| |open_issues| |pull_requests| | |django| |python| |license| |black| |ruff| | |watchers| |stars| |forks| .. |build-status| image:: https://github.com/jrobichaud/django-structlog/actions/workflows/main.yml/badge.svg?branch=main :target: https://github.com/jrobichaud/django-structlog/actions :alt: Build Status .. |pypi| image:: https://img.shields.io/pypi/v/django-structlog.svg :target: https://pypi.org/project/django-structlog/ :alt: PyPI version .. |docs| image:: https://readthedocs.org/projects/django-structlog/badge/?version=latest :target: https://django-structlog.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |coverage| image:: https://img.shields.io/codecov/c/github/jrobichaud/django-structlog.svg :target: https://codecov.io/gh/jrobichaud/django-structlog :alt: codecov .. |python| image:: https://img.shields.io/pypi/pyversions/django-structlog.svg :target: https://pypi.org/project/django-structlog/ :alt: Supported Python versions .. |license| image:: https://img.shields.io/pypi/l/django-structlog.svg :target: https://github.com/jrobichaud/django-structlog/blob/main/LICENSE.rst :alt: License .. |open_issues| image:: https://img.shields.io/github/issues/jrobichaud/django-structlog.svg :target: https://github.com/jrobichaud/django-structlog/issues :alt: GitHub issues .. |django| image:: https://img.shields.io/pypi/djversions/django-structlog.svg :target: https://pypi.org/project/django-structlog/ :alt: PyPI - Django Version .. |pull_requests| image:: https://img.shields.io/github/issues-pr/jrobichaud/django-structlog.svg :target: https://github.com/jrobichaud/django-structlog/pulls :alt: GitHub pull requests .. |forks| image:: https://img.shields.io/github/forks/jrobichaud/django-structlog.svg?style=social :target: https://github.com/jrobichaud/django-structlog/ :alt: GitHub forks .. |stars| image:: https://img.shields.io/github/stars/jrobichaud/django-structlog.svg?style=social :target: https://github.com/jrobichaud/django-structlog/ :alt: GitHub stars .. |watchers| image:: https://img.shields.io/github/watchers/jrobichaud/django-structlog.svg?style=social :target: https://github.com/jrobichaud/django-structlog/ :alt: GitHub watchers .. |wheels| image:: https://img.shields.io/pypi/wheel/django-structlog.svg :target: https://pypi.org/project/django-structlog/ :alt: PyPI - Wheel .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black :alt: Black .. |ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff django-structlog is a structured logging integration for `Django `_ project using `structlog `_ Logging will then produce additional cohesive metadata on each logs that makes it easier to track events or incidents. Additional Popular Integrations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Django REST framework `_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``Django REST framework`` is supported by default. But when using it with ``rest_framework.authentication.TokenAuthentication`` (or other DRF authentications) ``user_id`` will be only be in ``request_finished`` and ``request_failed`` instead of each logs. See `#37 `_ for details. `django-ninja `_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``django-ninja`` is supported by default 🥷. `Celery `_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Celery's task logging requires additional configurations, see `documentation `_ for details. Logging comparison ^^^^^^^^^^^^^^^^^^ Standard logging: ~~~~~~~~~~~~~~~~~ .. code-block:: python >>> import logging >>> logger = logging.get_logger(__name__) >>> logger.info("An error occurred") .. code-block:: bash An error occurred Well... ok With django-structlog and flat_line: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python >>> import structlog >>> logger = structlog.get_logger(__name__) >>> logger.info("an_error_occurred", bar="Buz") .. code-block:: bash timestamp='2019-04-13T19:39:31.089925Z' level='info' event='an_error_occurred' logger='my_awesome_project.my_awesome_module' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' bar='Buz' Then you can search with commands like: .. code-block:: bash $ cat logs/flat_line.log | grep request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' With django-structlog and json ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python >>> import structlog >>> logger = structlog.get_logger(__name__) >>> logger.info("an_error_occurred", bar="Buz") .. code-block:: json {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "an_error_occurred", "timestamp": "2019-04-13T19:39:31.089925Z", "logger": "my_awesome_project.my_awesome_module", "level": "info", "bar": "Buz"} Then you can search with commands like: .. code-block:: bash $ cat logs/json.log | jq '.[] | select(.request_id="3a8f801c-072b-4805-8f38-e1337f363ed4")' -s .. inclusion-marker-introduction-end .. inclusion-marker-getting-started-begin Getting Started =============== These steps will show how to integrate the middleware to your awesome application. Installation ^^^^^^^^^^^^ Install the library .. code-block:: bash pip install django-structlog Add app .. code-block:: python INSTALLED_APP = [ # ... "django_structlog", # ... ] Add middleware .. code-block:: python MIDDLEWARE = [ # ... "django_structlog.middlewares.RequestMiddleware", ] Add appropriate structlog configuration to your ``settings.py`` .. code-block:: python import structlog LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "json_formatter": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(), }, "plain_console": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(), }, "key_value": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']), }, }, "handlers": { # Important notes regarding handlers. # # 1. Make sure you use handlers adapted for your project. # These handlers configurations are only examples for this library. # See python's logging.handlers: https://docs.python.org/3/library/logging.handlers.html # # 2. You might also want to use different logging configurations depending of the environment. # Different files (local.py, tests.py, production.py, ci.py, etc.) or only conditions. # See https://docs.djangoproject.com/en/dev/topics/settings/#designating-the-settings "console": { "class": "logging.StreamHandler", "formatter": "plain_console", }, "json_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/json.log", "formatter": "json_formatter", }, "flat_line_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/flat_line.log", "formatter": "key_value", }, }, "loggers": { "django_structlog": { "handlers": ["console", "flat_line_file", "json_file"], "level": "INFO", }, # Make sure to replace the following logger's name for yours "django_structlog_demo_project": { "handlers": ["console", "flat_line_file", "json_file"], "level": "INFO", }, } } structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) Start logging with ``structlog`` instead of ``logging``. .. code-block:: python import structlog logger = structlog.get_logger(__name__) .. _django_signals: Extending Request Log Metadata ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default only a ``request_id`` and the ``user_id`` are bound from the request but pertinent log metadata may vary from a project to another. If you need to add more metadata from the request you can implement a convenient signal receiver to bind them. You can also override existing bound metadata the same way. .. code-block:: python from django.contrib.sites.shortcuts import get_current_site from django.dispatch import receiver from django_structlog import signals import structlog @receiver(signals.bind_extra_request_metadata) def bind_domain(request, logger, **kwargs): current_site = get_current_site(request) structlog.contextvars.bind_contextvars(domain=current_site.domain) Standard Loggers ^^^^^^^^^^^^^^^^ It is also possible to log using standard python logger. In your formatters, add the ``foreign_pre_chain`` section, and then add ``structlog.contextvars.merge_contextvars``: .. code-block:: python LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "json_formatter": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(), # Add this section: "foreign_pre_chain": [ structlog.contextvars.merge_contextvars, # <---- add this # customize the rest as you need structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), ], }, }, ... } .. inclusion-marker-getting-started-end .. inclusion-marker-example-outputs-begin Example outputs =============== Flat lines file (\ ``logs/flat_lines.log``\ ) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash timestamp='2019-04-13T19:39:29.321453Z' level='info' event='request_started' logger='django_structlog.middlewares.request' request_id='c53dff1d-3fc5-4257-a78a-9a567c937561' user_id=1 ip='0.0.0.0' request=GET / user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' timestamp='2019-04-13T19:39:29.345207Z' level='info' event='request_finished' logger='django_structlog.middlewares.request' request_id='c53dff1d-3fc5-4257-a78a-9a567c937561' user_id=1 ip='0.0.0.0' code=200 timestamp='2019-04-13T19:39:31.086155Z' level='info' event='request_started' logger='django_structlog.middlewares.request' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' request=POST /success_task user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' timestamp='2019-04-13T19:39:31.089925Z' level='info' event='Enqueuing successful task' logger='django_structlog_demo_project.home.views' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' timestamp='2019-04-13T19:39:31.147590Z' level='info' event='task_enqueued' logger='django_structlog.middlewares.celery' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' child_task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' timestamp='2019-04-13T19:39:31.153081Z' level='info' event='This is a successful task' logger='django_structlog_demo_project.taskapp.celery' task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' timestamp='2019-04-13T19:39:31.160043Z' level='info' event='request_finished' logger='django_structlog.middlewares.request' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' code=201 timestamp='2019-04-13T19:39:31.162372Z' level='info' event='task_succeed' logger='django_structlog.middlewares.celery' task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' result='None' Json file (\ ``logs/json.log``\ ) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: json {"request_id": "c53dff1d-3fc5-4257-a78a-9a567c937561", "user_id": 1, "ip": "0.0.0.0", "request": "GET /", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", "event": "request_started", "timestamp": "2019-04-13T19:39:29.321453Z", "logger": "django_structlog.middlewares.request", "level": "info"} {"request_id": "c53dff1d-3fc5-4257-a78a-9a567c937561", "user_id": 1, "ip": "0.0.0.0", "code": 200, "event": "request_finished", "timestamp": "2019-04-13T19:39:29.345207Z", "logger": "django_structlog.middlewares.request", "level": "info"} {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "request": "POST /success_task", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", "event": "request_started", "timestamp": "2019-04-13T19:39:31.086155Z", "logger": "django_structlog.middlewares.request", "level": "info"} {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "Enqueuing successful task", "timestamp": "2019-04-13T19:39:31.089925Z", "logger": "django_structlog_demo_project.home.views", "level": "info"} {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "child_task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "event": "task_enqueued", "timestamp": "2019-04-13T19:39:31.147590Z", "logger": "django_structlog.middlewares.celery", "level": "info"} {"task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "This is a successful task", "timestamp": "2019-04-13T19:39:31.153081Z", "logger": "django_structlog_demo_project.taskapp.celery", "level": "info"} {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "code": 201, "event": "request_finished", "timestamp": "2019-04-13T19:39:31.160043Z", "logger": "django_structlog.middlewares.request", "level": "info"} {"task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "result": "None", "event": "task_succeed", "timestamp": "2019-04-13T19:39:31.162372Z", "logger": "django_structlog.middlewares.celery", "level": "info"} .. inclusion-marker-example-outputs-end .. inclusion-marker-upgrade-guide-begin Upgrade Guide ============= .. _upgrade_8.0: Upgrading to 8.0+ ^^^^^^^^^^^^^^^^^ A new keyword argument ``log_kwargs`` was added to the the optional signals: - ``django_structlog.signals.bind_extra_request_metadata``; - ``django_structlog.signals.bind_extra_request_finished_metadata``; - ``django_structlog.signals.bind_extra_request_failed_metadata``. It should not affect you if you have a ``**kwargs`` in the signature of your receivers. ``log_kwargs`` is a dictionary containing the log metadata that will be added to their respective logs (``"request_started"``, ``"request_finished"``, ``"request_failed"``). If you use any of these signals, you may need to update your receiver to accept this new argument: .. code-block:: python from django.contrib.sites.shortcuts import get_current_site from django.dispatch import receiver from django_structlog import signals import structlog @receiver(signals.bind_extra_request_metadata) def my_receiver(request, logger, log_kwargs, **kwargs): # <- add `log_kwargs` if necessary ... @receiver(signals.bind_extra_request_finished_metadata) def my_receiver_finished(request, logger, response, log_kwargs, **kwargs): # <- add `log_kwargs` if necessary ... @receiver(signals.bind_extra_request_failed_metadata) def my_receiver_failed(request, logger, exception, log_kwargs, **kwargs): # <- add `log_kwargs` if necessary ... .. _upgrade_7.0: Upgrading to 7.0+ ^^^^^^^^^^^^^^^^^ The dependency `django-ipware `_ was upgraded to version 6. This library is used to retrieve the request's ip address. Version 6 may have some `breaking changes `_ if you did customizations. It should not affect most of the users but if you did some customizations, you might need to update your configurations. .. _upgrade_6.0: Upgrading to 6.0+ ^^^^^^^^^^^^^^^^^ Minimum requirements ~~~~~~~~~~~~~~~~~~~~ - requires python 3.8+ Changes to do ~~~~~~~~~~~~~ Add ``django_structlog`` to installed app ----------------------------------------- .. code-block:: python INSTALLED_APP = [ # ... "django_structlog", # ... ] Make sure you use ``django_structlog.middlewares.RequestMiddleware`` -------------------------------------------------------------------- If you used any of the experimental async or sync middlewares, you do not need to anymore. Make sure you use ``django_structlog.middlewares.RequestMiddleware`` instead of any of the other request middlewares commented below: .. code-block:: python MIDDLEWARE += [ # "django_structlog.middlewares.request_middleware_router", # <- remove # "django_structlog.middlewares.requests.SyncRequestMiddleware", # <- remove # "django_structlog.middlewares.requests.AsyncRequestMiddleware", # <- remove "django_structlog.middlewares.RequestMiddleware", # <- make sure you use this one ] (If you use celery) Make sure you use ``DJANGO_STRUCTLOG_CELERY_ENABLED = True`` -------------------------------------------------------------------------------- It is only applicable if you use celery integration. ``django_structlog.middlewares.CeleryMiddleware`` has been remove in favor of a django settings. .. code-block:: python MIDDLEWARE += [ "django_structlog.middlewares.RequestMiddleware", # "django_structlog.middlewares.CeleryMiddleware", # <- remove this ] DJANGO_STRUCTLOG_CELERY_ENABLED = True # <-- add this .. _upgrade_5.0: Upgrading to 5.0+ ^^^^^^^^^^^^^^^^^ Minimum requirements ~~~~~~~~~~~~~~~~~~~~ - requires asgiref 3.6+ .. _upgrade_4.0: Upgrading to 4.0+ ^^^^^^^^^^^^^^^^^ ``django-structlog`` drops support of django below 3.2. Minimum requirements ~~~~~~~~~~~~~~~~~~~~ - requires django 3.2+ - requires python 3.7+ - requires structlog 21.4.0+ - (optionally) requires celery 5.1+ Changes if you use ``celery`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can now install ``django-structlog`` explicitly with ``celery`` extra in order to validate the compatibility with your version of ``celery``. .. code-block:: bash django-structlog[celery]==4.0.0 See `Installing “Extras†`_ for more information about this ``pip`` feature. .. _upgrade_3.0: Upgrading to 3.0+ ^^^^^^^^^^^^^^^^^ ``django-structlog`` now use `structlog.contextvars.bind_contextvars `_ instead of ``threadlocal``. Minimum requirements ~~~~~~~~~~~~~~~~~~~~ - requires python 3.7+ - requires structlog 21.4.0+ Changes you need to do ~~~~~~~~~~~~~~~~~~~~~~ 1. Update structlog settings ---------------------------- - add ``structlog.contextvars.merge_contextvars`` as first ``processors`` - remove ``context_class=structlog.threadlocal.wrap_dict(dict),`` - (if you use standard loggers) add ``structlog.contextvars.merge_contextvars`` in `foreign_pre_chain` - (if you use standard loggers) remove ``django_structlog.processors.inject_context_dict,`` .. code-block:: python structlog.configure( processors=[ structlog.contextvars.merge_contextvars, # <---- add this structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], # context_class=structlog.threadlocal.wrap_dict(dict), # <---- remove this logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) # If you use standard logging LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "json_formatter": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(), "foreign_pre_chain": [ structlog.contextvars.merge_contextvars, # <---- add this # django_structlog.processors.inject_context_dict, # <---- remove this structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), ], }, }, ... } 2. Replace all ``logger.bind`` with ``structlog.contextvars.bind_contextvars`` ------------------------------------------------------------------------------ .. code-block:: python @receiver(bind_extra_request_metadata) def bind_domain(request, logger, **kwargs): current_site = get_current_site(request) # logger.bind(domain=current_site.domain) structlog.contextvars.bind_contextvars(domain=current_site.domain) .. _upgrade_2.0: Upgrading to 2.0+ ^^^^^^^^^^^^^^^^^ ``django-structlog`` was originally developed using the debug configuration `ExceptionPrettyPrinter `_ which led to incorrect handling of exception. - remove ``structlog.processors.ExceptionPrettyPrinter(),`` of your processors. - make sure you have ``structlog.processors.format_exc_info,`` in your processors if you want appropriate exception logging. .. inclusion-marker-upgrade-guide-end .. inclusion-marker-running-tests-begin Running the tests ================= Note: For the moment redis is needed to run the tests. The easiest way is to start docker demo's redis. .. code-block:: bash docker compose up -d redis pip install -r requirements.txt env CELERY_BROKER_URL=redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE=config.settings.test pytest test_app env CELERY_BROKER_URL=redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE=config.settings.test_demo_app pytest django_structlog_demo_project docker compose stop redis .. inclusion-marker-running-tests-end .. inclusion-marker-demo-begin Demo app ======== .. code-block:: bash docker compose up --build Open ``http://127.0.0.1:8000/`` in your browser. Navigate while looking into the log files and shell's output. .. inclusion-marker-demo-end .. inclusion-marker-authors-begin Authors ======= * **Jules Robichaud-Gagnon** - *Initial work* - `jrobichaud `_ See also the list of `contributors `_ who participated in this project. .. inclusion-marker-authors-end .. inclusion-marker-acknowledgements-begin Acknowledgments =============== * Very huge thanks to my awesome 🦄 and generous employer `TLM 🩵💜â¤ï¸ðŸ§¡ðŸ’šðŸˆâ€â¬› `_ for letting me maintain this project on my work hours because it believes in open source. * Big thanks to `@ferd `_ for his `bad opinions `_ that inspired the author enough to spend time on this library. * `This issue `_ helped the author to figure out how to integrate ``structlog`` in Django. * `This stack overflow question `_ was also helpful. .. inclusion-marker-acknowledgements-end License ======= This project is licensed under the MIT License - see the `LICENSE `_ file for details django-structlog-8.1.0/compose/000077500000000000000000000000001462421536000164565ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/000077500000000000000000000000001462421536000175505ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/django/000077500000000000000000000000001462421536000210125ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/django/Dockerfile000066400000000000000000000037211462421536000230070ustar00rootroot00000000000000#syntax=docker/dockerfile:1.4 ARG PYTHON_VERSION FROM python:${PYTHON_VERSION}-alpine as python FROM python AS python-build-stage ARG REQUIREMENTS_FILE=local.txt RUN <&2 echo 'Waiting for PostgreSQL to become available...' sleep 1 done >&2 echo 'PostgreSQL is available' exec "$@" django-structlog-8.1.0/compose/local/django/start000066400000000000000000000002011462421536000220630ustar00rootroot00000000000000#!/bin/sh set -o errexit set -o pipefail set -o nounset python manage.py migrate python manage.py runserver_plus 0.0.0.0:8000 django-structlog-8.1.0/compose/local/django/start_asgi000066400000000000000000000002061462421536000230730ustar00rootroot00000000000000#!/bin/sh set -o errexit set -o pipefail set -o nounset ASGI=true python -m uvicorn config.asgi:application --host=0.0.0.0 --reload django-structlog-8.1.0/compose/local/django/start_wsgi000066400000000000000000000001621462421536000231220ustar00rootroot00000000000000#!/bin/sh set -o errexit set -o pipefail set -o nounset WSGI=true gunicorn config.wsgi -b 0.0.0.0:8000 --reload django-structlog-8.1.0/compose/local/docs/000077500000000000000000000000001462421536000205005ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/docs/Dockerfile000066400000000000000000000014211462421536000224700ustar00rootroot00000000000000ARG PYTHON_VERSION FROM python:${PYTHON_VERSION}-alpine as python FROM python AS python-build-stage RUN apk update \ && apk add --virtual build-dependencies \ build-base COPY ./docs/requirements.txt /requirements.txt RUN pip wheel --wheel-dir /usr/src/app/wheels \ -r /requirements.txt FROM python AS python-run-stage ENV PYTHONUNBUFFERED 1 WORKDIR /app COPY --from=python-build-stage /usr/src/app/wheels /wheels/ RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ && rm -rf /wheels/ COPY ./compose/local/docs/start /start RUN sed -i 's/\r//' /start RUN chmod +x /start RUN addgroup --system django \ && adduser --system --ingroup django django RUN mkdir /docs RUN chown django:django /app RUN chown django:django /docs USER django django-structlog-8.1.0/compose/local/docs/start000066400000000000000000000012611462421536000215600ustar00rootroot00000000000000#!/bin/sh set -o errexit set -o pipefail set -o nounset # Basically we watch only README.rst, LICENCE.rst and everything under django_structlog sphinx-autobuild docs /docs/_build/html \ -b ${SPHINX_COMMAND} \ --port 5000 \ --host 0.0.0.0 \ --watch . \ --ignore "*___jb_*" \ --ignore ".*/*" \ --ignore ".*" \ --ignore "build/*" \ --ignore "compose/*" \ --ignore "config/*" \ --ignore "dist/*" \ --ignore "django_structlog.egg-info/*" \ --ignore "django_structlog_demo_project/*" \ --ignore "logs/*" \ --ignore "requirements/*" \ --ignore "requirements.txt" \ --ignore "docker*.yml" \ --ignore "manage.py" \ --ignore "MANIFEST.in" \ --ignore "*.toml" \ --ignore "*.log" \ --ignore "*.ini" django-structlog-8.1.0/compose/local/postgres/000077500000000000000000000000001462421536000214165ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/postgres/Dockerfile000066400000000000000000000003361462421536000234120ustar00rootroot00000000000000FROM postgres:14 COPY ./compose/local/postgres/maintenance /usr/local/bin/maintenance RUN chmod +x /usr/local/bin/maintenance/* RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ && rmdir /usr/local/bin/maintenance django-structlog-8.1.0/compose/local/postgres/maintenance/000077500000000000000000000000001462421536000237005ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/postgres/maintenance/_sourced/000077500000000000000000000000001462421536000255035ustar00rootroot00000000000000django-structlog-8.1.0/compose/local/postgres/maintenance/_sourced/constants.sh000066400000000000000000000001151462421536000300500ustar00rootroot00000000000000#!/usr/bin/env bash BACKUP_DIR_PATH='/backups' BACKUP_FILE_PREFIX='backup' django-structlog-8.1.0/compose/local/postgres/maintenance/_sourced/countdown.sh000066400000000000000000000005021462421536000300540ustar00rootroot00000000000000#!/usr/bin/env bash countdown() { declare desc="A simple countdown. Source: https://superuser.com/a/611582" local seconds="${1}" local d=$(($(date +%s) + "${seconds}")) while [ "$d" -ge `date +%s` ]; do echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; sleep 0.1 done } django-structlog-8.1.0/compose/local/postgres/maintenance/_sourced/messages.sh000066400000000000000000000007121462421536000276460ustar00rootroot00000000000000#!/usr/bin/env bash message_newline() { echo } message_debug() { echo -e "DEBUG: ${@}" } message_welcome() { echo -e "\e[1m${@}\e[0m" } message_warning() { echo -e "\e[33mWARNING\e[0m: ${@}" } message_error() { echo -e "\e[31mERROR\e[0m: ${@}" } message_info() { echo -e "\e[37mINFO\e[0m: ${@}" } message_suggestion() { echo -e "\e[33mSUGGESTION\e[0m: ${@}" } message_success() { echo -e "\e[32mSUCCESS\e[0m: ${@}" } django-structlog-8.1.0/compose/local/postgres/maintenance/_sourced/yes_no.sh000066400000000000000000000004451462421536000273360ustar00rootroot00000000000000#!/usr/bin/env bash yes_no() { declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." local arg1="${1}" local response= read -r -p "${arg1} (y/[n])? " response if [[ "${response}" =~ ^[Yy]$ ]] then exit 0 else exit 1 fi } django-structlog-8.1.0/compose/local/postgres/maintenance/backup000066400000000000000000000017611462421536000250750ustar00rootroot00000000000000#!/usr/bin/env bash ### Create a database backup. ### ### Usage: ### $ docker-compose -f .yml (exec |run --rm) postgres backup set -o errexit set -o pipefail set -o nounset working_dir="$(dirname ${0})" source "${working_dir}/_sourced/constants.sh" source "${working_dir}/_sourced/messages.sh" message_welcome "Backing up the '${POSTGRES_DB}' database..." if [[ "${POSTGRES_USER}" == "postgres" ]]; then message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." exit 1 fi export PGHOST="${POSTGRES_HOST}" export PGPORT="${POSTGRES_PORT}" export PGUSER="${POSTGRES_USER}" export PGPASSWORD="${POSTGRES_PASSWORD}" export PGDATABASE="${POSTGRES_DB}" backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." django-structlog-8.1.0/compose/local/postgres/maintenance/backups000066400000000000000000000006101462421536000252500ustar00rootroot00000000000000#!/usr/bin/env bash ### View backups. ### ### Usage: ### $ docker-compose -f .yml (exec |run --rm) postgres backups set -o errexit set -o pipefail set -o nounset working_dir="$(dirname ${0})" source "${working_dir}/_sourced/constants.sh" source "${working_dir}/_sourced/messages.sh" message_welcome "These are the backups you have got:" ls -lht "${BACKUP_DIR_PATH}" django-structlog-8.1.0/compose/local/postgres/maintenance/restore000066400000000000000000000031431462421536000253070ustar00rootroot00000000000000#!/usr/bin/env bash ### Restore database from a backup. ### ### Parameters: ### <1> filename of an existing backup. ### ### Usage: ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> set -o errexit set -o pipefail set -o nounset working_dir="$(dirname ${0})" source "${working_dir}/_sourced/constants.sh" source "${working_dir}/_sourced/messages.sh" if [[ -z ${1+x} ]]; then message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." exit 1 fi backup_filename="${BACKUP_DIR_PATH}/${1}" if [[ ! -f "${backup_filename}" ]]; then message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." exit 1 fi message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." if [[ "${POSTGRES_USER}" == "postgres" ]]; then message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." exit 1 fi export PGHOST="${POSTGRES_HOST}" export PGPORT="${POSTGRES_PORT}" export PGUSER="${POSTGRES_USER}" export PGPASSWORD="${POSTGRES_PASSWORD}" export PGDATABASE="${POSTGRES_DB}" message_info "Dropping the database..." dropdb "${PGDATABASE}" message_info "Creating a new database..." createdb --owner="${POSTGRES_USER}" message_info "Applying the backup to the new database..." gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." django-structlog-8.1.0/config/000077500000000000000000000000001462421536000162565ustar00rootroot00000000000000django-structlog-8.1.0/config/__init__.py000066400000000000000000000000001462421536000203550ustar00rootroot00000000000000django-structlog-8.1.0/config/asgi.py000066400000000000000000000021411462421536000175510ustar00rootroot00000000000000""" ASGI config for django_structlog_demo_project project. """ import os import sys from django.core.asgi import get_asgi_application # This allows easy placement of apps within the interior # django_structlog_demo_project directory. app_path = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) ) sys.path.append(os.path.join(app_path, "django_structlog_demo_project")) # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. application = get_asgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) django-structlog-8.1.0/config/settings/000077500000000000000000000000001462421536000201165ustar00rootroot00000000000000django-structlog-8.1.0/config/settings/__init__.py000066400000000000000000000000001462421536000222150ustar00rootroot00000000000000django-structlog-8.1.0/config/settings/base.py000066400000000000000000000263531462421536000214130ustar00rootroot00000000000000""" Base settings to build other settings files upon. """ import environ from django.core.exceptions import ImproperlyConfigured ROOT_DIR = ( environ.Path(__file__) - 3 ) # (django_structlog_demo_project/config/settings/base.py - 3 = django_structlog_demo_project/) APPS_DIR = ROOT_DIR.path("django_structlog_demo_project") env = environ.Env() READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) if READ_DOT_ENV_FILE: # OS environment variables take precedence over variables from .env env.read_env(str(ROOT_DIR.path(".env"))) # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug DEBUG = env.bool("DJANGO_DEBUG", False) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. # In Windows, this must be set to your system time zone. TIME_ZONE = "UTC" # https://docs.djangoproject.com/en/dev/ref/settings/#language-code LANGUAGE_CODE = "en-us" # https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n USE_I18N = True # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz USE_TZ = True # URLS # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf ROOT_URLCONF = "config.urls" # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application WSGI_APPLICATION = "config.wsgi.application" # APPS # ------------------------------------------------------------------------------ DJANGO_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", # "django.contrib.humanize", # Handy template tags "django.contrib.admin", ] THIRD_PARTY_APPS = [ "crispy_forms", "crispy_bootstrap5", "allauth", "allauth.account", "allauth.socialaccount", "rest_framework", ] LOCAL_APPS = [ "django_structlog_demo_project.command_examples", "django_structlog_demo_project.users", "django_structlog_demo_project.home", # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # MIGRATIONS # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules MIGRATION_MODULES = {"sites": "django_structlog_demo_project.contrib.sites.migrations"} # AUTHENTICATION # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ] # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model AUTH_USER_MODEL = "users.User" # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url LOGIN_REDIRECT_URL = "users:redirect" # https://docs.djangoproject.com/en/dev/ref/settings/#login-url LOGIN_URL = "account_login" # PASSWORDS # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers PASSWORD_HASHERS = [ "django.contrib.auth.hashers.PBKDF2PasswordHasher", "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", "django.contrib.auth.hashers.BCryptPasswordHasher", ] # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [] # MIDDLEWARE # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#middleware MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "allauth.account.middleware.AccountMiddleware", ] # STATIC # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#static-root STATIC_ROOT = str(ROOT_DIR("staticfiles")) # https://docs.djangoproject.com/en/dev/ref/settings/#static-url if env.bool("ASGI", default=False) or env.bool("WSGI", default=False): # asgi will use static files from the runserver_plus STATIC_URL = "http://127.0.0.1:8000/static/" else: # runserver_plus serve static files with cors disabled STATIC_URL = "/static/" # fix cors https://stackoverflow.com/a/66688806/2115513 from django.contrib.staticfiles import handlers # extend StaticFilesHandler to add "Access-Control-Allow-Origin" to every response class CORSStaticFilesHandler(handlers.StaticFilesHandler): def serve(self, request): response = super().serve(request) response["Access-Control-Allow-Origin"] = "*" return response # monkeypatch handlers to use our class instead of the original StaticFilesHandler handlers.StaticFilesHandler = CORSStaticFilesHandler # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS STATICFILES_DIRS = [str(APPS_DIR.path("static"))] # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders STATICFILES_FINDERS = [ "django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.AppDirectoriesFinder", ] # MEDIA # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#media-root MEDIA_ROOT = str(APPS_DIR("media")) # https://docs.djangoproject.com/en/dev/ref/settings/#media-url MEDIA_URL = "/media/" # TEMPLATES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#templates TEMPLATES = [ { # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND "BACKEND": "django.template.backends.django.DjangoTemplates", # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs "DIRS": [str(APPS_DIR.path("templates"))], "OPTIONS": { # https://docs.djangoproject.com/en/dev/ref/settings/#template-debug "debug": DEBUG, # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types "loaders": [ "django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader", ], # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors "context_processors": [ "django.template.context_processors.debug", "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ], }, } ] # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs CRISPY_TEMPLATE_PACK = "bootstrap5" # FIXTURES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) # SECURITY # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly SESSION_COOKIE_HTTPONLY = True # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly CSRF_COOKIE_HTTPONLY = True # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter SECURE_BROWSER_XSS_FILTER = True # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options X_FRAME_OPTIONS = "DENY" # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = env( "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" ) # ADMIN # ------------------------------------------------------------------------------ # Django Admin URL. ADMIN_URL = "admin/" # https://docs.djangoproject.com/en/dev/ref/settings/#admins ADMINS = [] # https://docs.djangoproject.com/en/dev/ref/settings/#managers MANAGERS = ADMINS # Celery # ------------------------------------------------------------------------------ INSTALLED_APPS += ["django_structlog_demo_project.taskapp.celery.CeleryAppConfig"] if USE_TZ: # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone CELERY_TIMEZONE = TIME_ZONE # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url try: CELERY_BROKER_URL = env("CELERY_BROKER_URL") CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND") # This breaks when running tests locally. except ImproperlyConfigured: CELERY_BROKER_URL = "redis://0.0.0.0:6379/0" CELERY_RESULT_BACKEND = CELERY_BROKER_URL BROKER_URL = CELERY_BROKER_URL # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content CELERY_ACCEPT_CONTENT = ["json"] # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer CELERY_TASK_SERIALIZER = "json" # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer CELERY_RESULT_SERIALIZER = "json" # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit # TODO: set to whatever value is adequate in your circumstances CELERYD_TASK_TIME_LIMIT = 5 * 60 # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit # TODO: set to whatever value is adequate in your circumstances CELERYD_TASK_SOFT_TIME_LIMIT = 60 # django-allauth # ------------------------------------------------------------------------------ ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_AUTHENTICATION_METHOD = "username" # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_EMAIL_REQUIRED = True # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_EMAIL_VERIFICATION = "mandatory" # https://django-allauth.readthedocs.io/en/latest/configuration.html ACCOUNT_ADAPTER = "django_structlog_demo_project.users.adapters.AccountAdapter" # Your stuff... # ------------------------------------------------------------------------------ INSTALLED_APPS += ["django_structlog"] django-structlog-8.1.0/config/settings/local.py000066400000000000000000000137351462421536000215730ustar00rootroot00000000000000import structlog from .base import * # noqa: F403 from .base import env, MIDDLEWARE # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug DEBUG = env.bool("DJANGO_DEBUG", default=True) # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", default="DXatocQyyroxzcpo0tDxK3v5Rm4fatD9U7UeuLWwnZMOIaCQdPWovuqp4rxOct1T", ) # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] IS_WORKER = env.bool("IS_WORKER", default=False) # CACHES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#caches CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "", } } # TEMPLATES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#templates TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405 # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = env( "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" ) # https://docs.djangoproject.com/en/dev/ref/settings/#email-host EMAIL_HOST = "localhost" # https://docs.djangoproject.com/en/dev/ref/settings/#email-port EMAIL_PORT = 1025 INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] if env("USE_DOCKER") == "yes": import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) INTERNAL_IPS += [ip[:-1] + "1" for ip in ips] # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration INSTALLED_APPS += ["django_extensions"] # noqa F405 # Celery # ------------------------------------------------------------------------------ # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates CELERY_TASK_EAGER_PROPAGATES = True CELERY_BEAT_SCHEDULE = { "example-scheduled-task": { "task": "django_structlog_demo_project.taskapp.celery.scheduled_task", "schedule": 30.0, }, } # DATABASES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = {"default": env.db("DATABASE_URL")} # Your stuff... # ------------------------------------------------------------------------------ LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "json_formatter": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(), "foreign_pre_chain": [ structlog.contextvars.merge_contextvars, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), ], }, "colored": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(colors=True), "foreign_pre_chain": [ structlog.contextvars.merge_contextvars, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), ], }, "key_value": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.KeyValueRenderer( key_order=["timestamp", "level", "event", "logger"] ), "foreign_pre_chain": [ structlog.contextvars.merge_contextvars, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), ], }, }, "handlers": { "colored_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, "json_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/json.log", "formatter": "json_formatter", }, "flat_line_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/flat_line.log", "formatter": "key_value", }, }, "loggers": { "django_structlog": { "handlers": ["colored_stream", "flat_line_file", "json_file"], "level": "INFO", }, "django_structlog_demo_project": { "handlers": ["colored_stream", "flat_line_file", "json_file"], "level": "INFO", }, "foreign_logger": { "handlers": ["colored_stream", "flat_line_file", "json_file"], "level": "INFO", }, }, } structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) MIDDLEWARE += [ "django_structlog.middlewares.RequestMiddleware", ] DJANGO_STRUCTLOG_CELERY_ENABLED = True DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True django-structlog-8.1.0/config/settings/test.py000066400000000000000000000067111462421536000214540ustar00rootroot00000000000000""" With these settings, tests run faster. """ import os import environ import structlog env = environ.Env() ROOT_DIR = ( environ.Path(__file__) - 3 ) # (test_app/config/settings/base.py - 3 = test_app/) APPS_DIR = ROOT_DIR.path("test_app") # APPS # ------------------------------------------------------------------------------ INSTALLED_APPS = [ "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", ] # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug DEBUG = False # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz USE_TZ = True # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", default="SqlHVcvZwwazrUrjtUiMJerENM8bU3k2p7WZu1WgA4yc8R1DcDc2Rh54m8dRvWcs", ) # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner TEST_RUNNER = "django.test.runner.DiscoverRunner" # CACHES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#caches CACHES = { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "", } } # EMAIL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # https://docs.djangoproject.com/en/dev/ref/settings/#email-host EMAIL_HOST = "localhost" # https://docs.djangoproject.com/en/dev/ref/settings/#email-port EMAIL_PORT = 1025 # Your stuff... # ------------------------------------------------------------------------------ LOGGING = { "version": 1, "disable_existing_loggers": True, "formatters": { "plain": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(colors=False), }, "colored": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(colors=True), }, }, "filters": {}, "handlers": { "structured_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, "structured_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "test.log", "formatter": "plain", }, }, "loggers": {"": {"handlers": ["structured_stream"], "level": "INFO"}}, } structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": os.path.join(str(ROOT_DIR), "db.sqlite3"), } } INSTALLED_APPS += ["django_structlog", "test_app"] DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True django-structlog-8.1.0/config/settings/test_demo_app.py000066400000000000000000000004331462421536000233130ustar00rootroot00000000000000""" With these settings, tests run faster. """ # noinspection PyUnresolvedReferences from .base import * # noqa: F401,F403 # noinspection PyUnresolvedReferences from .test import DATABASES, LOGGING # noqa: F401 DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True IS_WORKER = False django-structlog-8.1.0/config/urls.py000066400000000000000000000056561462421536000176310ustar00rootroot00000000000000from django.conf import settings from django.conf.urls import include from django.urls import re_path from django.conf.urls.static import static from django.contrib import admin from django.views.generic import TemplateView from django.views import defaults as default_views from django_structlog_demo_project.home import views, api_views, ninja_views def uncaught_exception_view(request): raise Exception("Uncaught Exception") urlpatterns = [ re_path(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"), re_path( r"^success_task$", views.enqueue_successful_task, name="enqueue_successful_task" ), re_path(r"^failing_task$", views.enqueue_failing_task, name="enqueue_failing_task"), re_path(r"^nesting_task$", views.enqueue_nesting_task, name="enqueue_nesting_task"), re_path(r"^unknown_task$", views.enqueue_unknown_task, name="enqueue_unknown_task"), re_path( r"^rejected_task$", views.enqueue_rejected_task, name="enqueue_rejected_task" ), re_path(r"^raise_exception", views.raise_exception, name="raise_exception"), re_path( r"^standard_logger", views.log_with_standard_logger, name="standard_logger" ), re_path(r"^async_view", views.async_view, name="async_view"), re_path(r"^api_view$", api_views.home_api_view, name="api_view"), re_path( r"^about/", TemplateView.as_view(template_name="pages/about.html"), name="about" ), re_path(r"^revoke_task", views.revoke_task, name="revoke_task"), re_path( r"^async_streaming_view", views.async_streaming_view, name="async_streaming_view", ), re_path( r"^sync_streaming_view", views.sync_streaming_view, name="sync_streaming_view" ), # Django Admin, use {% url 'admin:index' %} re_path(settings.ADMIN_URL, admin.site.urls), # User management re_path( r"^users/", include("django_structlog_demo_project.users.urls", namespace="users"), ), re_path(r"^accounts/", include("allauth.urls")), re_path("^ninja/", ninja_views.api.urls), # Your stuff: custom urls includes go here ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. urlpatterns += [ re_path( r"^400/", default_views.bad_request, kwargs={"exception": Exception("Bad Request!")}, ), re_path( r"^403/", default_views.permission_denied, kwargs={"exception": Exception("Permission Denied")}, ), re_path( r"^404/", default_views.page_not_found, kwargs={"exception": Exception("Page not Found")}, ), re_path(r"^500/", default_views.server_error), re_path(r"^uncaught_exception/", uncaught_exception_view), ] django-structlog-8.1.0/config/wsgi.py000066400000000000000000000033151462421536000176030ustar00rootroot00000000000000""" WSGI config for django_structlog_demo_project project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover this application via the ``WSGI_APPLICATION`` setting. Usually you will have the standard Django WSGI application here, but it also might make sense to replace the whole Django WSGI application with a custom one that later delegates to the Django one. For example, you could introduce WSGI middleware here, or combine a Django application with an application of another framework. """ import os import sys from django.core.wsgi import get_wsgi_application # This allows easy placement of apps within the interior # django_structlog_demo_project directory. app_path = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) ) sys.path.append(os.path.join(app_path, "django_structlog_demo_project")) # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. application = get_wsgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) django-structlog-8.1.0/django_structlog/000077500000000000000000000000001462421536000203615ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog/__init__.py000066400000000000000000000003161462421536000224720ustar00rootroot00000000000000""" ``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``. """ name = "django_structlog" VERSION = (8, 1, 0) __version__ = ".".join(str(v) for v in VERSION) django-structlog-8.1.0/django_structlog/app_settings.py000066400000000000000000000012331462421536000234320ustar00rootroot00000000000000import logging from django.conf import settings # noinspection PyPep8Naming class AppSettings: PREFIX = "DJANGO_STRUCTLOG_" @property def CELERY_ENABLED(self): return getattr(settings, self.PREFIX + "CELERY_ENABLED", False) @property def STATUS_4XX_LOG_LEVEL(self): return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING) @property def COMMAND_LOGGING_ENABLED(self): return getattr(settings, self.PREFIX + "COMMAND_LOGGING_ENABLED", False) @property def USER_ID_FIELD(self): return getattr(settings, self.PREFIX + "USER_ID_FIELD", "pk") app_settings = AppSettings() django-structlog-8.1.0/django_structlog/apps.py000066400000000000000000000011361462421536000216770ustar00rootroot00000000000000from django.apps import AppConfig from .app_settings import app_settings class DjangoStructLogConfig(AppConfig): name = "django_structlog" def ready(self): if app_settings.CELERY_ENABLED: from .celery.receivers import CeleryReceiver self._celery_receiver = CeleryReceiver() self._celery_receiver.connect_signals() if app_settings.COMMAND_LOGGING_ENABLED: from .commands import DjangoCommandReceiver self._django_command_receiver = DjangoCommandReceiver() self._django_command_receiver.connect_signals() django-structlog-8.1.0/django_structlog/celery/000077500000000000000000000000001462421536000216445ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog/celery/__init__.py000066400000000000000000000000711462421536000237530ustar00rootroot00000000000000""" ``celery`` integration for ``django_structlog``. """ django-structlog-8.1.0/django_structlog/celery/receivers.py000066400000000000000000000113731462421536000242120ustar00rootroot00000000000000import structlog from celery import current_app from celery.signals import ( before_task_publish, after_task_publish, task_prerun, task_retry, task_success, task_failure, task_revoked, task_unknown, task_rejected, ) from . import signals logger = structlog.getLogger(__name__) class CeleryReceiver: def __init__(self): self._priority = None def receiver_before_task_publish( self, sender=None, headers=None, body=None, properties=None, routing_key=None, **kwargs, ): if current_app.conf.task_protocol < 2: return context = structlog.contextvars.get_merged_contextvars(logger) if "task_id" in context: context["parent_task_id"] = context.pop("task_id") signals.modify_context_before_task_publish.send( sender=self.receiver_before_task_publish, context=context, task_routing_key=routing_key, task_properties=properties, ) if properties: self._priority = properties.get("priority", None) headers["__django_structlog__"] = context def receiver_after_task_publish( self, sender=None, headers=None, body=None, routing_key=None, **kwargs ): properties = {} if self._priority is not None: properties["priority"] = self._priority self._priority = None logger.info( "task_enqueued", child_task_id=headers.get("id") if headers else body.get("id"), child_task_name=headers.get("task") if headers else body.get("task"), routing_key=routing_key, **properties, ) def receiver_task_prerun(self, task_id, task, *args, **kwargs): structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars(task_id=task_id) metadata = getattr(task.request, "__django_structlog__", {}) structlog.contextvars.bind_contextvars(**metadata) signals.bind_extra_task_metadata.send( sender=self.receiver_task_prerun, task=task, logger=logger ) logger.info("task_started", task=task.name) def receiver_task_retry(self, request=None, reason=None, einfo=None, **kwargs): logger.warning("task_retrying", reason=reason) def receiver_task_success(self, result=None, **kwargs): signals.pre_task_succeeded.send( sender=self.receiver_task_success, logger=logger, result=result ) logger.info("task_succeeded") def receiver_task_failure( self, task_id=None, exception=None, traceback=None, einfo=None, sender=None, *args, **kwargs, ): throws = getattr(sender, "throws", ()) if isinstance(exception, throws): logger.info( "task_failed", error=str(exception), ) else: logger.exception( "task_failed", error=str(exception), exception=exception, ) def receiver_task_revoked( self, request=None, terminated=None, signum=None, expired=None, **kwargs ): metadata = getattr(request, "__django_structlog__", {}).copy() metadata["task_id"] = request.id metadata["task"] = request.task logger.warning( "task_revoked", terminated=terminated, signum=signum.value if signum is not None else None, signame=signum.name if signum is not None else None, expired=expired, **metadata, ) def receiver_task_unknown( self, message=None, exc=None, name=None, id=None, **kwargs ): logger.error( "task_not_found", task=name, task_id=id, ) def receiver_task_rejected(self, message=None, exc=None, **kwargs): logger.exception( "task_rejected", task_id=message.properties.get("correlation_id") ) def connect_signals(self): before_task_publish.connect(self.receiver_before_task_publish) after_task_publish.connect(self.receiver_after_task_publish) def connect_worker_signals(self): before_task_publish.connect(self.receiver_before_task_publish) after_task_publish.connect(self.receiver_after_task_publish) task_prerun.connect(self.receiver_task_prerun) task_retry.connect(self.receiver_task_retry) task_success.connect(self.receiver_task_success) task_failure.connect(self.receiver_task_failure) task_revoked.connect(self.receiver_task_revoked) task_unknown.connect(self.receiver_task_unknown) task_rejected.connect(self.receiver_task_rejected) django-structlog-8.1.0/django_structlog/celery/signals.py000066400000000000000000000041041462421536000236550ustar00rootroot00000000000000import django.dispatch bind_extra_task_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``celery``'s task. :param task: the celery task being run :param logger: the logger to bind more metadata or override existing bound metadata >>> from django.dispatch import receiver >>> from django_structlog.celery import signals >>> import structlog >>> >>> @receiver(signals.bind_extra_task_metadata) ... def receiver_bind_extra_task_metadata(sender, signal, task=None, logger=None, **kwargs): ... structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id) """ modify_context_before_task_publish = django.dispatch.Signal() """ Signal to modify context passed over to ``celery`` task's context. You must modify the ``context`` dict. :param context: the context dict that will be passed over to the task runner's logger :param task_routing_key: routing key of the task :param task_properties: task's message properties >>> from django.dispatch import receiver >>> from django_structlog.celery import signals >>> >>> @receiver(signals.modify_context_before_task_publish) ... def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs): ... keys_to_keep = {"request_id", "parent_task_id"} ... new_dict = { ... key_to_keep: context[key_to_keep] ... for key_to_keep in keys_to_keep ... if key_to_keep in context ... } ... context.clear() ... context.update(new_dict) """ pre_task_succeeded = django.dispatch.Signal() """ Signal to add ``structlog`` bindings from ``celery``'s successful task. :param logger: the logger to bind more metadata or override existing bound metadata :param result: result of the succeeding task >>> from django.dispatch import receiver >>> from django_structlog.celery import signals >>> import structlog >>> >>> @receiver(signals.pre_task_succeeded) ... def receiver_pre_task_succeeded(sender, signal, logger=None, result=None, **kwargs): ... structlog.contextvars.bind_contextvars(result=str(result)) """ django-structlog-8.1.0/django_structlog/celery/steps.py000066400000000000000000000011271462421536000233550ustar00rootroot00000000000000from celery import bootsteps from .receivers import CeleryReceiver class DjangoStructLogInitStep(bootsteps.Step): """``celery`` worker boot step to initialize ``django_structlog``. >>> from celery import Celery >>> from django_structlog.celery.steps import DjangoStructLogInitStep >>> >>> app = Celery("django_structlog_demo_project") >>> app.steps['worker'].add(DjangoStructLogInitStep) """ def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self.receiver = CeleryReceiver() self.receiver.connect_worker_signals() django-structlog-8.1.0/django_structlog/commands.py000066400000000000000000000023131462421536000225330ustar00rootroot00000000000000import structlog import uuid from django_extensions.management.signals import pre_command, post_command logger = structlog.getLogger(__name__) class DjangoCommandReceiver: def __init__(self): self.stack = [] def pre_receiver(self, sender, *args, **kwargs): command_id = str(uuid.uuid4()) if len(self.stack): parent_command_id, _ = self.stack[-1] tokens = structlog.contextvars.bind_contextvars( parent_command_id=parent_command_id, command_id=command_id ) else: tokens = structlog.contextvars.bind_contextvars(command_id=command_id) self.stack.append((command_id, tokens)) logger.info( "command_started", command_name=sender.__module__.replace(".management.commands", ""), ) def post_receiver(self, sender, outcome, *args, **kwargs): logger.info("command_finished") if len(self.stack): # pragma: no branch command_id, tokens = self.stack.pop() structlog.contextvars.reset_contextvars(**tokens) def connect_signals(self): pre_command.connect(self.pre_receiver) post_command.connect(self.post_receiver) django-structlog-8.1.0/django_structlog/middlewares/000077500000000000000000000000001462421536000226615ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog/middlewares/__init__.py000066400000000000000000000000641462421536000247720ustar00rootroot00000000000000from .request import RequestMiddleware # noqa F401 django-structlog-8.1.0/django_structlog/middlewares/request.py000066400000000000000000000154121462421536000247260ustar00rootroot00000000000000import asyncio import logging import uuid import structlog from asgiref.sync import iscoroutinefunction, markcoroutinefunction from django.core.exceptions import PermissionDenied from django.http import Http404, StreamingHttpResponse from asgiref import sync from .. import signals from ..app_settings import app_settings logger = structlog.getLogger(__name__) def get_request_header(request, header_key, meta_key): if hasattr(request, "headers"): return request.headers.get(header_key) return request.META.get(meta_key) def sync_streaming_content_wrapper(streaming_content, context): with structlog.contextvars.bound_contextvars(**context): logger.info("streaming_started") try: for chunk in streaming_content: yield chunk except Exception: logger.exception("streaming_failed") else: logger.info("streaming_finished") async def async_streaming_content_wrapper(streaming_content, context): with structlog.contextvars.bound_contextvars(**context): logger.info("streaming_started") try: async for chunk in streaming_content: yield chunk except asyncio.CancelledError: logger.warning("streaming_cancelled") raise except Exception: logger.exception("streaming_failed") else: logger.info("streaming_finished") class RequestMiddleware: """``RequestMiddleware`` adds request metadata to ``structlog``'s logger context automatically. >>> MIDDLEWARE = [ ... # ... ... 'django_structlog.middlewares.RequestMiddleware', ... ] """ sync_capable = True async_capable = True def __init__(self, get_response): self.get_response = get_response if iscoroutinefunction(self.get_response): markcoroutinefunction(self) def __call__(self, request): if iscoroutinefunction(self): return self.__acall__(request) self.prepare(request) response = self.get_response(request) self.handle_response(request, response) return response async def __acall__(self, request): await sync.sync_to_async(self.prepare)(request) try: response = await self.get_response(request) except asyncio.CancelledError: logger.warning("request_cancelled") raise await sync.sync_to_async(self.handle_response)(request, response) return response def handle_response(self, request, response): if not hasattr(request, "_raised_exception"): self.bind_user_id(request) context = structlog.contextvars.get_merged_contextvars(logger) log_kwargs = dict( code=response.status_code, request=self.format_request(request), ) signals.bind_extra_request_finished_metadata.send( sender=self.__class__, request=request, logger=logger, response=response, log_kwargs=log_kwargs, ) if response.status_code >= 500: level = logging.ERROR elif response.status_code >= 400: level = app_settings.STATUS_4XX_LOG_LEVEL else: level = logging.INFO logger.log( level, "request_finished", **log_kwargs, ) if isinstance(response, StreamingHttpResponse): streaming_content = response.streaming_content try: iter(streaming_content) except TypeError: response.streaming_content = async_streaming_content_wrapper( streaming_content, context ) else: response.streaming_content = sync_streaming_content_wrapper( streaming_content, context ) else: exception = getattr(request, "_raised_exception") delattr(request, "_raised_exception") signals.update_failure_response.send( sender=self.__class__, request=request, response=response, logger=logger, exception=exception, ) structlog.contextvars.clear_contextvars() def prepare(self, request): from ipware import get_client_ip request_id = get_request_header( request, "x-request-id", "HTTP_X_REQUEST_ID" ) or str(uuid.uuid4()) correlation_id = get_request_header( request, "x-correlation-id", "HTTP_X_CORRELATION_ID" ) structlog.contextvars.bind_contextvars(request_id=request_id) self.bind_user_id(request) if correlation_id: structlog.contextvars.bind_contextvars(correlation_id=correlation_id) ip, _ = get_client_ip(request) structlog.contextvars.bind_contextvars(ip=ip) log_kwargs = { "request": self.format_request(request), "user_agent": request.META.get("HTTP_USER_AGENT"), } signals.bind_extra_request_metadata.send( sender=self.__class__, request=request, logger=logger, log_kwargs=log_kwargs ) logger.info("request_started", **log_kwargs) @staticmethod def format_request(request): return f"{request.method} {request.get_full_path()}" @staticmethod def bind_user_id(request): user_id_field = app_settings.USER_ID_FIELD if hasattr(request, "user") and request.user is not None and user_id_field: user_id = None if hasattr(request.user, user_id_field): user_id = getattr(request.user, user_id_field) if isinstance(user_id, uuid.UUID): user_id = str(user_id) structlog.contextvars.bind_contextvars(user_id=user_id) def process_exception(self, request, exception): if isinstance(exception, (Http404, PermissionDenied)): # We don't log an exception here, and we don't set that we handled # an error as we want the standard `request_finished` log message # to be emitted. return setattr(request, "_raised_exception", exception) self.bind_user_id(request) log_kwargs = dict( code=500, request=self.format_request(request), ) signals.bind_extra_request_failed_metadata.send( sender=self.__class__, request=request, logger=logger, exception=exception, log_kwargs=log_kwargs, ) logger.exception( "request_failed", **log_kwargs, ) django-structlog-8.1.0/django_structlog/signals.py000066400000000000000000000061321462421536000223750ustar00rootroot00000000000000import django.dispatch bind_extra_request_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``django``'s request. :param request: the request returned by the view :param logger: the logger :param log_kwargs: dictionary of log metadata for the ``request_started`` event. It contains ``request`` and ``user_agent`` keys. You may modify it to add extra information. >>> from django.contrib.sites.shortcuts import get_current_site >>> from django.dispatch import receiver >>> from django_structlog import signals >>> import structlog >>> >>> @receiver(signals.bind_extra_request_metadata) ... def bind_domain(request, logger, log_kwargs, **kwargs): ... current_site = get_current_site(request) ... structlog.contextvars.bind_contextvars(domain=current_site.domain) """ bind_extra_request_finished_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``django``'s finished request and response. :param logger: the logger :param response: the response resulting of the request :param log_kwargs: dictionary of log metadata for the ``request_finished`` event. It contains ``request`` and ``code`` keys. You may modify it to add extra information. >>> from django.contrib.sites.shortcuts import get_current_site >>> from django.dispatch import receiver >>> from django_structlog import signals >>> import structlog >>> >>> @receiver(signals.bind_extra_request_finished_metadata) ... def bind_domain(request, logger, response, log_kwargs, **kwargs): ... current_site = get_current_site(request) ... structlog.contextvars.bind_contextvars(domain=current_site.domain) """ bind_extra_request_failed_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``django``'s failed request and exception. :param logger: the logger :param exception: the exception resulting of the request :param log_kwargs: dictionary of log metadata for the ``request_failed`` event. It contains ``request`` and ``code`` keys. You may modify it to add extra information. >>> from django.contrib.sites.shortcuts import get_current_site >>> from django.dispatch import receiver >>> from django_structlog import signals >>> import structlog >>> >>> @receiver(signals.bind_extra_request_failed_metadata) ... def bind_domain(request, logger, exception, log_kwargs, **kwargs): ... current_site = get_current_site(request) ... structlog.contextvars.bind_contextvars(domain=current_site.domain) """ update_failure_response = django.dispatch.Signal() """ Signal to update response failure response before it is returned. :param request: the request returned by the view :param response: the response resulting of the request :param logger: the logger :param exception: the exception >>> from django.dispatch import receiver >>> from django_structlog import signals >>> import structlog >>> >>> @receiver(signals.update_failure_response) ... def add_request_id_to_error_response(request, response, logger, exception, **kwargs): ... context = structlog.contextvars.get_merged_contextvars(logger) ... response['X-Request-ID'] = context["request_id"] """ django-structlog-8.1.0/django_structlog_demo_project/000077500000000000000000000000001462421536000231135ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/__init__.py000066400000000000000000000002501462421536000252210ustar00rootroot00000000000000__version__ = "0.1.0" __version_info__ = tuple( [ int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".") ] ) django-structlog-8.1.0/django_structlog_demo_project/command_examples/000077500000000000000000000000001462421536000264275ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/__init__.py000066400000000000000000000000001462421536000305260ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/apps.py000066400000000000000000000003011462421536000277360ustar00rootroot00000000000000from django.apps import AppConfig class CommandExamplesAppConfig(AppConfig): name = "django_structlog_demo_project.command_examples" default_auto_field = "django.db.models.AutoField" django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/000077500000000000000000000000001462421536000305435ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/__init__.py000066400000000000000000000000001462421536000326420ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/commands/000077500000000000000000000000001462421536000323445ustar00rootroot00000000000000__init__.py000066400000000000000000000000001462421536000343640ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/commandsexample_command.py000066400000000000000000000010161462421536000357660ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/commandsimport structlog from django.core import management from django.core.management import BaseCommand from django_extensions.management.utils import signalcommand logger = structlog.getLogger(__name__) class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("foo", type=str) @signalcommand def handle(self, foo, *args, **options): logger.info("my log", foo=foo) management.call_command("example_nested_command", "buz", verbosity=0) logger.info("my log 2") example_nested_command.py000066400000000000000000000006251462421536000373350ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/management/commandsimport structlog from django.core.management import BaseCommand from django_extensions.management.utils import signalcommand logger = structlog.getLogger(__name__) class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("baz", type=str) @signalcommand def handle(self, baz, *args, **options): logger.info("my nested log", baz=baz) return 0 django-structlog-8.1.0/django_structlog_demo_project/command_examples/tests/000077500000000000000000000000001462421536000275715ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/tests/__init__.py000066400000000000000000000000001462421536000316700ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/command_examples/tests/test_commands.py000066400000000000000000000005611462421536000330050ustar00rootroot00000000000000import pytest from django.core import management from django_structlog_demo_project.command_examples.management.commands import ( example_command, ) pytestmark = pytest.mark.django_db class TestCommand: def test_command(self): assert ( management.call_command(example_command.Command(), "bar", verbosity=0) is None ) django-structlog-8.1.0/django_structlog_demo_project/conftest.py000066400000000000000000000006731462421536000253200ustar00rootroot00000000000000import pytest from django.conf import settings from django.test import RequestFactory from django_structlog_demo_project.users.tests.factories import UserFactory @pytest.fixture(autouse=True) def media_storage(settings, tmpdir): settings.MEDIA_ROOT = tmpdir.strpath @pytest.fixture def user() -> settings.AUTH_USER_MODEL: return UserFactory() @pytest.fixture def request_factory() -> RequestFactory: return RequestFactory() django-structlog-8.1.0/django_structlog_demo_project/contrib/000077500000000000000000000000001462421536000245535ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/__init__.py000066400000000000000000000002751462421536000266700ustar00rootroot00000000000000""" To understand why this file is here, please read: http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/000077500000000000000000000000001462421536000257025ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/__init__.py000066400000000000000000000002751462421536000300170ustar00rootroot00000000000000""" To understand why this file is here, please read: http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrations/000077500000000000000000000000001462421536000300565ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrations/0001_initial.py000066400000000000000000000024621462421536000325250ustar00rootroot00000000000000import django.contrib.sites.models from django.contrib.sites.models import _simple_domain_name_validator from django.db import migrations, models class Migration(migrations.Migration): dependencies = [] operations = [ migrations.CreateModel( name="Site", fields=[ ( "id", models.AutoField( verbose_name="ID", serialize=False, auto_created=True, primary_key=True, ), ), ( "domain", models.CharField( max_length=100, verbose_name="domain name", validators=[_simple_domain_name_validator], ), ), ("name", models.CharField(max_length=50, verbose_name="display name")), ], options={ "ordering": ("domain",), "db_table": "django_site", "verbose_name": "site", "verbose_name_plural": "sites", }, bases=(models.Model,), managers=[("objects", django.contrib.sites.models.SiteManager())], ) ] 0002_alter_domain_unique.py000066400000000000000000000010251462421536000350340ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrationsimport django.contrib.sites.models from django.db import migrations, models class Migration(migrations.Migration): dependencies = [("sites", "0001_initial")] operations = [ migrations.AlterField( model_name="site", name="domain", field=models.CharField( max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], verbose_name="domain name", ), ) ] 0003_set_site_domain_and_name.py000066400000000000000000000013431462421536000360040ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrations""" To understand why this file is here, please read: http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ from django.conf import settings from django.db import migrations def update_site_forward(apps, schema_editor): """Set site domain and name.""" site_model = apps.get_model("sites", "Site") site_model.objects.update_or_create( id=settings.SITE_ID, defaults={"domain": "example.com", "name": "django_structlog_demo_project"}, ) class Migration(migrations.Migration): dependencies = [("sites", "0002_alter_domain_unique")] operations = [migrations.RunPython(update_site_forward, migrations.RunPython.noop)] 0004_alter_site_options.py000066400000000000000000000007241462421536000347250ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrations# Generated by Django 3.2.14 on 2022-08-02 17:18 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ("sites", "0003_set_site_domain_and_name"), ] operations = [ migrations.AlterModelOptions( name="site", options={ "ordering": ["domain"], "verbose_name": "site", "verbose_name_plural": "sites", }, ), ] django-structlog-8.1.0/django_structlog_demo_project/contrib/sites/migrations/__init__.py000066400000000000000000000002751462421536000321730ustar00rootroot00000000000000""" To understand why this file is here, please read: http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ django-structlog-8.1.0/django_structlog_demo_project/home/000077500000000000000000000000001462421536000240435ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/__init__.py000066400000000000000000000000001462421536000261420ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/api_views.py000066400000000000000000000004551462421536000264070ustar00rootroot00000000000000from rest_framework.decorators import api_view from rest_framework.response import Response import structlog logger = structlog.get_logger(__name__) @api_view() def home_api_view(request): logger.info("This is a rest-framework structured log") return Response({"message": "Hello, world!"}) django-structlog-8.1.0/django_structlog_demo_project/home/apps.py000066400000000000000000000002521462421536000253570ustar00rootroot00000000000000from django.apps import AppConfig class HomeAppConfig(AppConfig): name = "django_structlog_demo_project.home" default_auto_field = "django.db.models.AutoField" django-structlog-8.1.0/django_structlog_demo_project/home/ninja_views.py000066400000000000000000000011151462421536000267270ustar00rootroot00000000000000import structlog from ninja import NinjaAPI, Router from ninja.security import SessionAuth api = NinjaAPI(urls_namespace="ninja") router = Router() logger = structlog.get_logger(__name__) # OptionalSessionAuth is a custom authentication class that allows the user to be anonymous class OptionalSessionAuth(SessionAuth): def authenticate(self, request, key): return request.user @router.get("/ninja", url_name="add", auth=OptionalSessionAuth()) def ninja(request): logger.info("This is a ninja structured log") return {"result": "ok"} api.add_router("", router) django-structlog-8.1.0/django_structlog_demo_project/home/static/000077500000000000000000000000001462421536000253325ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/static/js/000077500000000000000000000000001462421536000257465ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/static/js/home.js000066400000000000000000000051661462421536000272440ustar00rootroot00000000000000const toastContainer = document.getElementById('toast-container') const toastTemplate = document.getElementById('toastTemplate'); let abortController = null; function log(title, url, body, isError, duration) { const newToast = toastTemplate.cloneNode(true) const text = body ? body.toString() : "" if (isError) { console.error(title, url, body, duration); } else { console.log(title, url, body, duration); } if (isError) { newToast.classList.add("border-danger") } else { newToast.classList.add("border-success") } newToast.removeAttribute('id'); const toastHeader = newToast.querySelector('.toast-header > .me-auto'); toastHeader.textContent = `${title} ${url}` if (duration) { const toastDuration = newToast.querySelector('.duration'); toastDuration.textContent = `${duration} ms` } const toastBody = newToast.querySelector('.toast-body'); if (body) { toastBody.textContent = text.slice(0, 400) } else { newToast.removeChild(toastBody); } toastContainer.appendChild(newToast); const toast = new bootstrap.Toast(newToast) toast.show() } async function fetchUrl(url) { abortController = new AbortController(); log("request_started", url); const start = new Date(); try { const response = await fetch(url, { method: 'get', headers: {"Content-Type": "application/json"}, signal: abortController.signal, }); const text = await response.text(); if (response.ok) { log("request_finished", url, text, false, new Date() - start); } else { log("request_failed", url, text, true, new Date() - start); } } catch (err) { log("request_failed", url, err, true, new Date() - start); } } async function fetchStreamingUrl(url) { const start = new Date(); try { abortController = new AbortController(); log("streaming_request_started", url); const response = await fetch(url, { method: 'get', signal: abortController.signal, }); log("streaming_request_finished", url, `Status code ${response.status}`, false, new Date() - start); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); log("streaming_response_started", url, undefined, false, new Date() - start); while (true) { const {value, done} = await reader.read(); if (done) break; log("received", url, value, false, new Date() - start); } log("streaming_response_finished", url, undefined, false, new Date() - start); } catch (err) { log("request_failed", url, err, true, new Date() - start); } } function cancelAsync() { if (abortController) abortController.abort(); } django-structlog-8.1.0/django_structlog_demo_project/home/tests/000077500000000000000000000000001462421536000252055ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/tests/__init__.py000066400000000000000000000000001462421536000273040ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/home/tests/test_api_views.py000066400000000000000000000006311462421536000306040ustar00rootroot00000000000000import pytest from .. import api_views pytestmark = pytest.mark.django_db class TestApiView: def test(self, caplog, request_factory): response = api_views.home_api_view(request_factory.get("/")) assert response.status_code == 200 assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a rest-framework structured log" django-structlog-8.1.0/django_structlog_demo_project/home/tests/test_ninja_views.py000066400000000000000000000007711462421536000311370ustar00rootroot00000000000000import pytest from ninja.testing import TestClient from ..ninja_views import router pytestmark = pytest.mark.django_db class TestNinjaView: def test(self, caplog, request_factory): client = TestClient(router) response = client.get("/ninja") assert response.status_code == 200 assert response.json() == {"result": "ok"} assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a ninja structured log" django-structlog-8.1.0/django_structlog_demo_project/home/tests/test_views.py000066400000000000000000000053011462421536000277520ustar00rootroot00000000000000import pytest from .. import views pytestmark = pytest.mark.django_db pytest_plugins = ("pytest_asyncio",) class TestEnqueueSuccessfulTask: def test(self): response = views.enqueue_successful_task(None) assert response.status_code == 201 class TestEnqueueFailingTask: def test(self): response = views.enqueue_failing_task(None) assert response.status_code == 201 class TestEnqueueNestingTask: def test(self): response = views.enqueue_nesting_task(None) assert response.status_code == 201 class TestRaiseException: def test(self): with pytest.raises(Exception) as e: views.raise_exception(None) assert str(e.value) == "This is a view raising an exception." class TestLogWithStandardLogger: def test(self): response = views.log_with_standard_logger(None) assert response.status_code == 200 @pytest.mark.asyncio class TestAsyncView: async def test(self, mocker): mocker.patch("asyncio.sleep") response = await views.async_view(None) assert response.status_code == 200 class TestRevokeTask: def test(self): response = views.revoke_task(None) assert response.status_code == 201 class TestEnqueueUnknownTask: def test(self): response = views.enqueue_unknown_task(None) assert response.status_code == 201 class TestEnqueueRejectedTask: def test(self): response = views.enqueue_rejected_task(None) assert response.status_code == 201 @pytest.mark.asyncio class TestAsyncStreamingViewView: async def test(self, mocker): response = await views.async_streaming_view(None) assert response.status_code == 200 mocker.patch("asyncio.sleep") assert b"0" == await anext(response.streaming_content) assert b"1" == await anext(response.streaming_content) assert b"2" == await anext(response.streaming_content) assert b"3" == await anext(response.streaming_content) assert b"4" == await anext(response.streaming_content) with pytest.raises(StopAsyncIteration): await anext(response.streaming_content) class TestSyncStreamingViewView: def test(self, mocker): response = views.sync_streaming_view(None) assert response.status_code == 200 mocker.patch("time.sleep") assert b"0" == next(response.streaming_content) assert b"1" == next(response.streaming_content) assert b"2" == next(response.streaming_content) assert b"3" == next(response.streaming_content) assert b"4" == next(response.streaming_content) with pytest.raises(StopIteration): next(response.streaming_content) django-structlog-8.1.0/django_structlog_demo_project/home/views.py000066400000000000000000000044151462421536000255560ustar00rootroot00000000000000import asyncio import logging import time import structlog from django.http import HttpResponse, StreamingHttpResponse from django_structlog_demo_project.taskapp.celery import ( successful_task, failing_task, nesting_task, rejected_task, ) logger = structlog.get_logger(__name__) def enqueue_successful_task(request): logger.info("Enqueuing successful task") successful_task.apply_async(foo="bar", priority=5) return HttpResponse(status=201) def enqueue_failing_task(request): logger.info("Enqueuing failing task") failing_task.delay(foo="bar") return HttpResponse(status=201) def enqueue_nesting_task(request): logger.info("Enqueuing nesting task") nesting_task.delay() return HttpResponse(status=201) def log_with_standard_logger(request): logging.getLogger("foreign_logger").info("This is a standard logger") return HttpResponse(status=200) def revoke_task(request): async_result = successful_task.apply_async(countdown=1) async_result.revoke() return HttpResponse(status=201) def enqueue_unknown_task(request): from django_structlog_demo_project.taskapp.celery import ( unknown_task, ) logger.info("Enqueuing unknown task") unknown_task.delay() return HttpResponse(status=201) def enqueue_rejected_task(request): rejected_task.delay() return HttpResponse(status=201) async def async_view(request): for num in range(1, 2): await asyncio.sleep(1) logger.info(f"This this is an async view {num}") return HttpResponse(status=200) async def async_streaming_response(): for chunk in range(0, 5): await asyncio.sleep(0.5) logger.info("streaming_chunk", chunk=chunk) yield chunk def sync_streaming_response(): for chunk in range(0, 5): time.sleep(0.5) logger.info("streaming_chunk", chunk=chunk) yield chunk def sync_streaming_view(request): logger.info("This this is a sync streaming view") return StreamingHttpResponse(sync_streaming_response()) async def async_streaming_view(request): logger.info("This this is an async streaming view") return StreamingHttpResponse(async_streaming_response()) def raise_exception(request): raise Exception("This is a view raising an exception.") django-structlog-8.1.0/django_structlog_demo_project/static/000077500000000000000000000000001462421536000244025ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/css/000077500000000000000000000000001462421536000251725ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/css/project.css000066400000000000000000000003451462421536000273540ustar00rootroot00000000000000/* These styles are generated from project.scss. */ .alert-debug { color: black; background-color: white; border-color: #d6e9c6; } .alert-error { color: #b94a48; background-color: #f2dede; border-color: #eed3d7; } django-structlog-8.1.0/django_structlog_demo_project/static/fonts/000077500000000000000000000000001462421536000255335ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/fonts/.gitkeep000066400000000000000000000000001462421536000271520ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/images/000077500000000000000000000000001462421536000256475ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/images/favicons/000077500000000000000000000000001462421536000274575ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/images/favicons/favicon.ico000066400000000000000000000202341462421536000316010ustar00rootroot00000000000000  (& (N( @ :U-'8R-­5R-ò8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ5R-ò8R-­:U-'9R,(7R*ï8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ7R*ï9R,(8R-­8R-ÿ=Y1ÿEd7ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ\xOÿÓÚÏÿš¬“ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿEd7ÿ=Y1ÿ8R-ÿ8R-­8Q-ñ8R-ÿEd7ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿz‘oÿñôðÿÿÿÿÿÿÿÿÿ®–ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿEd7ÿ8R-ÿ5O-ó8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ¡²šÿþþýÿÿÿÿÿÿÿÿÿÿÿÿÿüýüÿdYÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿdYÿÓÚÏÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÈÑÄÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿHh:ÿ½È¸ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿn‡cÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿMl@ÿëîéÿÿÿÿÿÿÿÿÿÿÿÿÿ±¾«ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿMk?ÿŒ „ÿÄÎÀÿèìæÿûüûÿÿÿÿÿóõòÿëîéÿÙàÖÿ¿Êºÿ¥µžÿFf8ÿ¥µžÿÿÿÿÿÿÿÿÿÿÿÿÿÛâÙÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿukÿëïêÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿwmÿÿÿÿÿÿÿÿÿÿÿÿÿïòîÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ•vÿýýýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿ`|Tÿÿÿÿÿÿÿÿÿÿÿÿÿøù÷ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿWtJÿôöóÿÿÿÿÿÿÿÿÿÿÿÿÿþþþÿ×ÞÔÿ¶Ã±ÿ®¼¨ÿºÆµÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿRpDÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿ¢²›ÿÿÿÿÿÿÿÿÿÿÿÿÿðòïÿl…aÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿJj=ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿÚà×ÿÿÿÿÿÿÿÿÿÿÿÿÿˆÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿö÷õÿÿÿÿÿÿÿÿÿÿÿÿÿRpEÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿKj=ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿîñíÿÿÿÿÿÿÿÿÿÿÿÿÿoˆdÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿÆÐÂÿÿÿÿÿÿÿÿÿÿÿÿÿÍÖÉÿIh;ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿ~”tÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÙßÖÿ†›}ÿb}Vÿ^zRÿoˆdÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÄÎÀÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿMl@ÿÀË»ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ|’rÿËÔÇÿøù÷ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿJi<ÿ`|Tÿk…`ÿj„_ÿVsIÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿGg9ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿQoDÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿ8Q-ñ8R-ÿEd7ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿEd7ÿ8R-ÿ5O-ó8R-­8R-ÿ=Y1ÿEd7ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿEd7ÿ=Y1ÿ8R-ÿ8R-­:U-'7R*ï8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ7R*ï:U-'7R0%8R-­8Q-ñ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8Q-ñ8R-­7R0%(  7R,J8O-ç8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ5P*ä7R,J8R-æB`5ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿTqGÿÈÑÄÿ¡±šÿFf8ÿFf8ÿB`5ÿ8O-ç8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ´Á¯ÿÿÿÿÿþþþÿm†bÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿGg9ÿÃ;ÿÿÿÿÿÈÑÄÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿ|’rÿÒÚÏÿöøöÿýýýÿïòîÿÙàÖÿoˆdÿÿÿÿÿðòïÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿƒ˜zÿþþþÿÿÿÿÿäéâÿÛáØÿÿÿÿÿÿÿÿÿRpDÿÿÿÿÿúûúÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿÞäÜÿÿÿÿÿŠžÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿJi<ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿûüûÿÿÿÿÿJi<ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿéíèÿÿÿÿÿr‹hÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿ¡²šÿÿÿÿÿøúøÿ»Ç¶ÿµÂ¯ÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿHh:ÿ °™ÿðóïÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿGg9ÿUsHÿOmAÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿÿÿÿÿÿÿÿÿFf8ÿFf8ÿ8R-ÿ8R-æB`5ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿFf8ÿB`5ÿ8R-æ7P-I8R-æ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-ÿ8R-æ7P-Idjango-structlog-8.1.0/django_structlog_demo_project/static/js/000077500000000000000000000000001462421536000250165ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/js/project.js000066400000000000000000000000551462421536000270220ustar00rootroot00000000000000/* Project specific Javascript goes here. */ django-structlog-8.1.0/django_structlog_demo_project/static/sass/000077500000000000000000000000001462421536000253535ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss000066400000000000000000000000001462421536000327000ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/static/sass/project.scss000066400000000000000000000011421462421536000277140ustar00rootroot00000000000000 // project specific CSS goes here //////////////////////////////// //Variables// //////////////////////////////// // Alert colors $white: #fff; $mint-green: #d6e9c6; $black: #000; $pink: #f2dede; $dark-pink: #eed3d7; $red: #b94a48; //////////////////////////////// //Alerts// //////////////////////////////// // bootstrap alert CSS, translated to the django-standard levels of // debug, info, success, warning, error .alert-debug { background-color: $white; border-color: $mint-green; color: $black; } .alert-error { background-color: $pink; border-color: $dark-pink; color: $red; } django-structlog-8.1.0/django_structlog_demo_project/taskapp/000077500000000000000000000000001462421536000245565ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/taskapp/__init__.py000066400000000000000000000000001462421536000266550ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/taskapp/celery.py000066400000000000000000000062311462421536000264150ustar00rootroot00000000000000import logging import logging.config import os import structlog from celery import Celery, shared_task, signals from django.apps import apps, AppConfig from django.conf import settings from django_structlog.celery.steps import DjangoStructLogInitStep if not settings.configured: # set the default Django settings module for the 'celery' program. os.environ.setdefault( "DJANGO_SETTINGS_MODULE", "config.settings.local" ) # pragma: no cover app = Celery("django_structlog_demo_project", namespace="CELERY") app.config_from_object("django.conf:settings") # A step to initialize django-structlog app.steps["worker"].add(DjangoStructLogInitStep) @signals.setup_logging.connect def receiver_setup_logging( loglevel, logfile, format, colorize, **kwargs ): # pragma: no cover logging.config.dictConfig(settings.LOGGING) structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) class CeleryAppConfig(AppConfig): name = "django_structlog_demo_project.taskapp" verbose_name = "Celery Config" def ready(self): installed_apps = [app_config.name for app_config in apps.get_app_configs()] app.autodiscover_tasks(lambda: installed_apps, force=True) @shared_task def successful_task(foo=None): import structlog logger = structlog.getLogger(__name__) logger.info("This is a successful task") @shared_task def failing_task(foo=None, **kwargs): raise Exception("This is a failed task") @shared_task def nesting_task(): logger = structlog.getLogger(__name__) structlog.contextvars.bind_contextvars(foo="Bar") logger.info("This is a nesting task") nested_task.delay() @shared_task def nested_task(): logger = structlog.getLogger(__name__) logger.info("This is a nested task") @shared_task def scheduled_task(): logger = structlog.getLogger(__name__) logger.info("This is a scheduled task") @shared_task def rejected_task(): pass if not settings.IS_WORKER: # pragma: no branch @shared_task def unknown_task(): """Simulate a task unavailable in the worker for demonstration purpose""" @signals.before_task_publish.connect def corrupt_rejected_task(sender=None, headers=None, body=None, **kwargs): """Simulate celery's task rejection mechanism by breaking up the message""" logger = structlog.getLogger(__name__) if headers.get("task") == f"{rejected_task.__module__}.{rejected_task.__name__}": logger.warn( f"corrupting {rejected_task.__name__}", task_id=headers.get("id"), ) del headers["task"] django-structlog-8.1.0/django_structlog_demo_project/taskapp/tests/000077500000000000000000000000001462421536000257205ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/taskapp/tests/__init__.py000066400000000000000000000000001462421536000300170ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/taskapp/tests/test_celery.py000066400000000000000000000043671462421536000306260ustar00rootroot00000000000000import pytest from .. import celery pytestmark = pytest.mark.django_db class TestSuccessfulTask: def test(self, caplog): celery.successful_task(foo="bar") assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a successful task" class TestFailingTask: def test(self): with pytest.raises(Exception) as e: celery.failing_task(foo="bar") assert str(e.value) == "This is a failed task" class TestNestingTask: def test(self, caplog): celery.nesting_task() assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a nesting task" class TestNestedTask: def test(self, caplog): celery.nested_task() assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a nested task" class TestScheduledTask: def test(self, caplog): celery.scheduled_task() assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "This is a scheduled task" class TestRejectedTask: def test(self): assert celery.rejected_task() is None class TestCorruptRejectedTask: def test(self, caplog): task_id = "11111111-1111-1111-1111-111111111111" headers = dict( id=task_id, task="django_structlog_demo_project.taskapp.celery.rejected_task", ) celery.corrupt_rejected_task(sender=None, headers=headers) assert len(caplog.records) == 1 record = caplog.records[0] assert record.msg["event"] == "corrupting rejected_task" assert record.msg["task_id"] == task_id assert "task" not in headers def test_other_tasks_not_corrupted(self, caplog): task_id = "11111111-1111-1111-1111-111111111111" headers = dict( id=task_id, task="django_structlog_demo_project.taskapp.celery.successful_task", ) celery.corrupt_rejected_task(sender=None, headers=headers) assert len(caplog.records) == 0 assert ( headers["task"] == "django_structlog_demo_project.taskapp.celery.successful_task" ) django-structlog-8.1.0/django_structlog_demo_project/templates/000077500000000000000000000000001462421536000251115ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/templates/403_csrf.html000066400000000000000000000003021462421536000273150ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Forbidden (403){% endblock %} {% block content %}

Forbidden (403)

CSRF verification failed. Request aborted.

{% endblock content %} django-structlog-8.1.0/django_structlog_demo_project/templates/404.html000066400000000000000000000003001462421536000262770ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Page not found{% endblock %} {% block content %}

Page not found

This is not the page you were looking for.

{% endblock content %} django-structlog-8.1.0/django_structlog_demo_project/templates/500.html000066400000000000000000000004711462421536000263050ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}Server Error{% endblock %} {% block content %}

Ooops!!! 500

Looks like something went wrong!

We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.

{% endblock content %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/000077500000000000000000000000001462421536000265455ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/templates/account/account_inactive.html000066400000000000000000000003621462421536000327520ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Account Inactive" %}{% endblock %} {% block inner %}

{% trans "Account Inactive" %}

{% trans "This account is inactive." %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/base.html000066400000000000000000000003761462421536000303530ustar00rootroot00000000000000{% extends "base.html" %} {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} {% block content %}
{% block inner %}{% endblock %}
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/email.html000066400000000000000000000050761462421536000305320ustar00rootroot00000000000000 {% extends "account/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Account" %}{% endblock %} {% block inner %}

{% trans "E-mail Addresses" %}

{% if user.emailaddress_set.all %}

{% trans 'The following e-mail addresses are associated with your account:' %}

{% else %}

{% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}

{% endif %}

{% trans "Add E-mail Address" %}

{% csrf_token %} {{ form|crispy }}
{% endblock %} {% block javascript %} {{ block.super }} {% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/email_confirm.html000066400000000000000000000016661462421536000322500ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load account %} {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} {% block inner %}

{% trans "Confirm E-mail Address" %}

{% if confirmation %} {% user_display confirmation.email_address.user as user_display %}

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

{% csrf_token %}
{% else %} {% url 'account_email' as email_url %}

{% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request.{% endblocktrans %}

{% endif %} {% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/login.html000066400000000000000000000026571462421536000305550ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load account socialaccount %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Sign In" %}{% endblock %} {% block inner %}

{% trans "Sign In" %}

{% get_providers as socialaccount_providers %} {% if socialaccount_providers %}

{% blocktrans with site.name as site_name %}Please sign in with one of your existing third party accounts. Or, sign up for a {{ site_name }} account and sign in below:{% endblocktrans %}

    {% include "socialaccount/snippets/provider_list.html" with process="login" %}
{% include "socialaccount/snippets/login_extra.html" %} {% else %}

{% blocktrans %}If you have not created an account yet, then please sign up first.{% endblocktrans %}

{% endif %} {% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/logout.html000066400000000000000000000010341462421536000307420ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Sign Out" %}{% endblock %} {% block inner %}

{% trans "Sign Out" %}

{% trans 'Are you sure you want to sign out?' %}

{% csrf_token %} {% if redirect_field_value %} {% endif %}
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/password_change.html000066400000000000000000000007521462421536000326060ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Change Password" %}{% endblock %} {% block inner %}

{% trans "Change Password" %}

{% csrf_token %} {{ form|crispy }}
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/password_reset.html000066400000000000000000000015331462421536000325010ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load account %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Password Reset" %}{% endblock %} {% block inner %}

{% trans "Password Reset" %}

{% if user.is_authenticated %} {% include "account/snippets/already_logged_in.html" %} {% endif %}

{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}

{% csrf_token %} {{ form|crispy }}

{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/password_reset_done.html000066400000000000000000000007311462421536000335050ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load account %} {% block head_title %}{% trans "Password Reset" %}{% endblock %} {% block inner %}

{% trans "Password Reset" %}

{% if user.is_authenticated %} {% include "account/snippets/already_logged_in.html" %} {% endif %}

{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/password_reset_from_key.html000066400000000000000000000016751462421536000344030ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Change Password" %}{% endblock %} {% block inner %}

{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}

{% if token_fail %} {% url 'account_reset_password' as passwd_reset_url %}

{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset.{% endblocktrans %}

{% else %} {% if form %}
{% csrf_token %} {{ form|crispy }}
{% else %}

{% trans 'Your password is now changed.' %}

{% endif %} {% endif %} {% endblock %} password_reset_from_key_done.html000066400000000000000000000003721462421536000353220ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/templates/account{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Change Password" %}{% endblock %} {% block inner %}

{% trans "Change Password" %}

{% trans 'Your password is now changed.' %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/password_set.html000066400000000000000000000007031462421536000321500ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Set Password" %}{% endblock %} {% block inner %}

{% trans "Set Password" %}

{% csrf_token %} {{ form|crispy }}
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/signup.html000066400000000000000000000012601462421536000307370ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block head_title %}{% trans "Signup" %}{% endblock %} {% block inner %}

{% trans "Sign Up" %}

{% blocktrans %}Already have an account? Then please sign in.{% endblocktrans %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/signup_closed.html000066400000000000000000000004071462421536000322720ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} {% block inner %}

{% trans "Sign Up Closed" %}

{% trans "We are sorry, but the sign up is currently closed." %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/verification_sent.html000066400000000000000000000006641462421536000331540ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} {% block inner %}

{% trans "Verify Your E-mail Address" %}

{% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/account/verified_email_required.html000066400000000000000000000014341462421536000343010ustar00rootroot00000000000000{% extends "account/base.html" %} {% load i18n %} {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} {% block inner %}

{% trans "Verify Your E-mail Address" %}

{% url 'account_email' as email_url %}

{% blocktrans %}This part of the site requires us to verify that you are who you claim to be. For this purpose, we require that you verify ownership of your e-mail address. {% endblocktrans %}

{% blocktrans %}We have sent an e-mail to you for verification. Please click on the link inside this e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}

{% blocktrans %}Note: you can still change your e-mail address.{% endblocktrans %}

{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/base.html000066400000000000000000000103731462421536000267150ustar00rootroot00000000000000{% load static i18n %} {% block title %}django_structlog_demo_project{% endblock title %} {% block css %} {% endblock %}
{% if messages %} {% for message in messages %}
{{ message }}
{% endfor %} {% endif %} {% block content %}

Use this document as a way to quick start any new project.

{% endblock content %}
{% block modal %}{% endblock modal %} {% block javascript %} {% endblock javascript %} django-structlog-8.1.0/django_structlog_demo_project/templates/pages/000077500000000000000000000000001462421536000262105ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/templates/pages/about.html000066400000000000000000000000311462421536000302020ustar00rootroot00000000000000{% extends "base.html" %}django-structlog-8.1.0/django_structlog_demo_project/templates/pages/home.html000066400000000000000000000102601462421536000300250ustar00rootroot00000000000000{% extends "base.html" %} {% load static i18n %} {% block javascript %} {{ block.super }} {% endblock %} {% block content %}
Base
Async
rest-framework
ninja
Celery
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/users/000077500000000000000000000000001462421536000262525ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/templates/users/user_detail.html000066400000000000000000000013631462421536000314430ustar00rootroot00000000000000{% extends "base.html" %} {% load static %} {% block title %}User: {{ object.username }}{% endblock %} {% block content %}

{{ object.username }}

{% if object.name %}

{{ object.name }}

{% endif %}
{% if object == request.user %} {% endif %}
{% endblock content %} django-structlog-8.1.0/django_structlog_demo_project/templates/users/user_form.html000066400000000000000000000007031462421536000311410ustar00rootroot00000000000000{% extends "base.html" %} {% load crispy_forms_tags %} {% block title %}{{ user.username }}{% endblock %} {% block content %}

{{ user.username }}

{% csrf_token %} {{ form|crispy }}
{% endblock %} django-structlog-8.1.0/django_structlog_demo_project/templates/users/user_list.html000066400000000000000000000006521462421536000311540ustar00rootroot00000000000000{% extends "base.html" %} {% load static i18n %} {% block title %}Members{% endblock %} {% block content %}

Users

{% for user in user_list %}

{{ user.username }}

{% endfor %}
{% endblock content %} django-structlog-8.1.0/django_structlog_demo_project/users/000077500000000000000000000000001462421536000242545ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/__init__.py000066400000000000000000000000001462421536000263530ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/adapters.py000066400000000000000000000004531462421536000264330ustar00rootroot00000000000000from allauth.account.adapter import DefaultAccountAdapter from django.conf import settings from django.http import HttpRequest class AccountAdapter(DefaultAccountAdapter): def is_open_for_signup(self, request: HttpRequest): return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) django-structlog-8.1.0/django_structlog_demo_project/users/admin.py000066400000000000000000000010271462421536000257160ustar00rootroot00000000000000from django.contrib import admin from django.contrib.auth import admin as auth_admin from django.contrib.auth import get_user_model from django_structlog_demo_project.users.forms import UserChangeForm, UserCreationForm User = get_user_model() @admin.register(User) class UserAdmin(auth_admin.UserAdmin): form = UserChangeForm add_form = UserCreationForm fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets list_display = ["username", "name", "is_superuser"] search_fields = ["name"] django-structlog-8.1.0/django_structlog_demo_project/users/apps.py000066400000000000000000000004661462421536000255770ustar00rootroot00000000000000from django.apps import AppConfig class UsersAppConfig(AppConfig): name = "django_structlog_demo_project.users" verbose_name = "Users" default_auto_field = "django.db.models.AutoField" def ready(self): # noinspection PyUnresolvedReferences from . import signals # noqa F401 django-structlog-8.1.0/django_structlog_demo_project/users/forms.py000066400000000000000000000015051462421536000257550ustar00rootroot00000000000000from django.contrib.auth import get_user_model, forms from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ User = get_user_model() class UserChangeForm(forms.UserChangeForm): class Meta(forms.UserChangeForm.Meta): model = User class UserCreationForm(forms.UserCreationForm): error_message = forms.UserCreationForm.error_messages.update( {"duplicate_username": _("This username has already been taken.")} ) class Meta(forms.UserCreationForm.Meta): model = User def clean_username(self): username = self.cleaned_data["username"] try: User.objects.get(username=username) except User.DoesNotExist: return username raise ValidationError(self.error_messages["duplicate_username"]) django-structlog-8.1.0/django_structlog_demo_project/users/migrations/000077500000000000000000000000001462421536000264305ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/migrations/0001_initial.py000066400000000000000000000114771462421536000311050ustar00rootroot00000000000000import django.contrib.auth.models import django.contrib.auth.validators from django.db import migrations, models import django.utils.timezone class Migration(migrations.Migration): initial = True dependencies = [("auth", "0008_alter_user_username_max_length")] operations = [ migrations.CreateModel( name="User", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("password", models.CharField(max_length=128, verbose_name="password")), ( "last_login", models.DateTimeField( blank=True, null=True, verbose_name="last login" ), ), ( "is_superuser", models.BooleanField( default=False, help_text="Designates that this user has all permissions without explicitly assigning them.", verbose_name="superuser status", ), ), ( "username", models.CharField( error_messages={ "unique": "A user with that username already exists." }, help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", max_length=150, unique=True, validators=[ django.contrib.auth.validators.UnicodeUsernameValidator() ], verbose_name="username", ), ), ( "first_name", models.CharField( blank=True, max_length=30, verbose_name="first name" ), ), ( "last_name", models.CharField( blank=True, max_length=150, verbose_name="last name" ), ), ( "email", models.EmailField( blank=True, max_length=254, verbose_name="email address" ), ), ( "is_staff", models.BooleanField( default=False, help_text="Designates whether the user can log into this admin site.", verbose_name="staff status", ), ), ( "is_active", models.BooleanField( default=True, help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", verbose_name="active", ), ), ( "date_joined", models.DateTimeField( default=django.utils.timezone.now, verbose_name="date joined" ), ), ( "name", models.CharField( blank=True, max_length=255, verbose_name="Name of User" ), ), ( "groups", models.ManyToManyField( blank=True, help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", related_name="user_set", related_query_name="user", to="auth.Group", verbose_name="groups", ), ), ( "user_permissions", models.ManyToManyField( blank=True, help_text="Specific permissions for this user.", related_name="user_set", related_query_name="user", to="auth.Permission", verbose_name="user permissions", ), ), ], options={ "verbose_name_plural": "users", "verbose_name": "user", "abstract": False, }, managers=[("objects", django.contrib.auth.models.UserManager())], ) ] django-structlog-8.1.0/django_structlog_demo_project/users/migrations/0002_alter_user_first_name.py000066400000000000000000000006751462421536000340270ustar00rootroot00000000000000# Generated by Django 3.2.14 on 2022-08-02 17:18 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("users", "0001_initial"), ] operations = [ migrations.AlterField( model_name="user", name="first_name", field=models.CharField( blank=True, max_length=150, verbose_name="first name" ), ), ] django-structlog-8.1.0/django_structlog_demo_project/users/migrations/__init__.py000066400000000000000000000000001462421536000305270ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/models.py000066400000000000000000000007201462421536000261100ustar00rootroot00000000000000from django.contrib.auth.models import AbstractUser from django.db.models import CharField from django.urls import reverse from django.utils.translation import gettext_lazy as _ class User(AbstractUser): # First Name and Last Name do not cover name patterns # around the globe. name = CharField(_("Name of User"), blank=True, max_length=255) def get_absolute_url(self): return reverse("users:detail", kwargs={"username": self.username}) django-structlog-8.1.0/django_structlog_demo_project/users/signals.py000066400000000000000000000000551462421536000262660ustar00rootroot00000000000000# Experiment with django worker signals here django-structlog-8.1.0/django_structlog_demo_project/users/tests/000077500000000000000000000000001462421536000254165ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/tests/__init__.py000066400000000000000000000000001462421536000275150ustar00rootroot00000000000000django-structlog-8.1.0/django_structlog_demo_project/users/tests/factories.py000066400000000000000000000013761462421536000277560ustar00rootroot00000000000000from typing import Any, Sequence from django.contrib.auth import get_user_model from factory import Faker, post_generation from factory.django import DjangoModelFactory class UserFactory(DjangoModelFactory): username = Faker("user_name") email = Faker("email") name = Faker("name") @post_generation def password(self, create: bool, extracted: Sequence[Any], **kwargs): password = Faker( "password", length=42, special_chars=True, digits=True, upper_case=True, lower_case=True, ).evaluate(None, None, extra={"locale": None}) self.set_password(password) class Meta: model = get_user_model() django_get_or_create = ["username"] django-structlog-8.1.0/django_structlog_demo_project/users/tests/test_adapters.py000066400000000000000000000003771462421536000306410ustar00rootroot00000000000000import pytest from django_structlog_demo_project.users.adapters import ( AccountAdapter, ) pytestmark = pytest.mark.django_db class TestUserCreationForm: def test_account_adapter(self): assert AccountAdapter().is_open_for_signup(None) django-structlog-8.1.0/django_structlog_demo_project/users/tests/test_forms.py000066400000000000000000000022051462421536000301540ustar00rootroot00000000000000import pytest from django_structlog_demo_project.users.forms import UserCreationForm from django_structlog_demo_project.users.tests.factories import UserFactory pytestmark = pytest.mark.django_db class TestUserCreationForm: def test_clean_username(self): # A user with proto_user params does not exist yet. proto_user = UserFactory.build() form = UserCreationForm( { "username": proto_user.username, "password1": proto_user._password, "password2": proto_user._password, } ) assert form.is_valid() assert form.clean_username() == proto_user.username # Creating a user. form.save() # The user with proto_user params already exists, # hence cannot be created. form = UserCreationForm( { "username": proto_user.username, "password1": proto_user._password, "password2": proto_user._password, } ) assert not form.is_valid() assert len(form.errors) == 1 assert "username" in form.errors django-structlog-8.1.0/django_structlog_demo_project/users/tests/test_models.py000066400000000000000000000003751462421536000303170ustar00rootroot00000000000000import pytest from django.conf import settings pytestmark = pytest.mark.django_db def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL): assert user.get_absolute_url() == "/users/{username}/".format( username=user.username ) django-structlog-8.1.0/django_structlog_demo_project/users/tests/test_urls.py000066400000000000000000000017041462421536000300160ustar00rootroot00000000000000import pytest from django.conf import settings from django.urls import reverse, resolve pytestmark = pytest.mark.django_db def test_detail(user: settings.AUTH_USER_MODEL): route = f"/users/{user.username}/" assert reverse("users:detail", kwargs={"username": user.username}) == route assert resolve(route).view_name == "users:detail" def test_detail_username_with_dot(): route = "/users/foo.bar/" assert reverse("users:detail", kwargs={"username": "foo.bar"}) == route assert resolve(route).view_name == "users:detail" def test_list(): assert reverse("users:list") == "/users/" assert resolve("/users/").view_name == "users:list" def test_update(): assert reverse("users:update") == "/users/~update/" assert resolve("/users/~update/").view_name == "users:update" def test_redirect(): assert reverse("users:redirect") == "/users/~redirect/" assert resolve("/users/~redirect/").view_name == "users:redirect" django-structlog-8.1.0/django_structlog_demo_project/users/tests/test_views.py000066400000000000000000000031161462421536000301650ustar00rootroot00000000000000import pytest from django.conf import settings from django.test import RequestFactory from django_structlog_demo_project.users.views import UserRedirectView, UserUpdateView pytestmark = pytest.mark.django_db class TestUserUpdateView: """ TODO: extracting view initialization code as class-scoped fixture would be great if only pytest-django supported non-function-scoped fixture db access -- this is a work-in-progress for now: https://github.com/pytest-dev/pytest-django/pull/258 """ def test_get_success_url( self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory ): view = UserUpdateView() request = request_factory.get("/fake-url/") request.user = user view.request = request assert view.get_success_url() == "/users/{username}/".format( username=user.username ) def test_get_object( self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory ): view = UserUpdateView() request = request_factory.get("/fake-url/") request.user = user view.request = request assert view.get_object() == user class TestUserRedirectView: def test_get_redirect_url( self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory ): view = UserRedirectView() request = request_factory.get("/fake-url") request.user = user view.request = request assert view.get_redirect_url() == "/users/{username}/".format( username=user.username ) django-structlog-8.1.0/django_structlog_demo_project/users/urls.py000066400000000000000000000007451462421536000256210ustar00rootroot00000000000000from django.urls import re_path from django_structlog_demo_project.users.views import ( user_list_view, user_redirect_view, user_update_view, user_detail_view, ) app_name = "users" urlpatterns = [ re_path(r"^$", view=user_list_view, name="list"), re_path(r"~redirect/", view=user_redirect_view, name="redirect"), re_path(r"~update/", view=user_update_view, name="update"), re_path(r"^(?P(\w|\.)+)/", view=user_detail_view, name="detail"), ] django-structlog-8.1.0/django_structlog_demo_project/users/views.py000066400000000000000000000023221462421536000257620ustar00rootroot00000000000000from django.contrib.auth import get_user_model from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from django.views.generic import DetailView, ListView, RedirectView, UpdateView User = get_user_model() class UserDetailView(LoginRequiredMixin, DetailView): model = User slug_field = "username" slug_url_kwarg = "username" user_detail_view = UserDetailView.as_view() class UserListView(LoginRequiredMixin, ListView): model = User slug_field = "username" slug_url_kwarg = "username" user_list_view = UserListView.as_view() class UserUpdateView(LoginRequiredMixin, UpdateView): model = User fields = ["name"] def get_success_url(self): return reverse("users:detail", kwargs={"username": self.request.user.username}) def get_object(self, queryset=None): return User.objects.get(username=self.request.user.username) user_update_view = UserUpdateView.as_view() class UserRedirectView(LoginRequiredMixin, RedirectView): permanent = False def get_redirect_url(self, *args, **kwargs): return reverse("users:detail", kwargs={"username": self.request.user.username}) user_redirect_view = UserRedirectView.as_view() django-structlog-8.1.0/docker-compose.amqp.yml000066400000000000000000000020361462421536000214040ustar00rootroot00000000000000volumes: local_postgres_data: {} local_postgres_data_backups: {} services: django: env_file: - ./.envs/.local/.django - ./.envs/.local/.amqp - ./.envs/.local/.postgres django_asgi: env_file: - ./.envs/.local/.django - ./.envs/.local/.amqp - ./.envs/.local/.postgres rabbitmq: hostname: rabbit image: rabbitmq:3.12-alpine environment: - RABBITMQ_DEFAULT_USER=admin - RABBITMQ_DEFAULT_PASS=unsecure-password ports: - "5672:5672" - "15672:15672" celeryworker: depends_on: - rabbitmq - postgres env_file: - ./.envs/.local/.django - ./.envs/.local/.amqp - ./.envs/.local/.postgres celerybeat: depends_on: - rabbitmq - postgres env_file: - ./.envs/.local/.django - ./.envs/.local/.amqp - ./.envs/.local/.postgres flower: depends_on: - rabbitmq - postgres env_file: - ./.envs/.local/.django - ./.envs/.local/.amqp - ./.envs/.local/.postgres django-structlog-8.1.0/docker-compose.docs.yml000066400000000000000000000007371462421536000214040ustar00rootroot00000000000000services: docs: build: context: . dockerfile: ./compose/local/docs/Dockerfile args: PYTHON_VERSION: 3.12 image: django_structlog_demo_project_docs volumes: - .:/app:cached command: /start environment: - SPHINX_COMMAND=html ports: - "5000:5000" docs-test: image: django_structlog_demo_project_docs volumes: - .:/app:cached command: /start environment: - SPHINX_COMMAND=doctest -E django-structlog-8.1.0/docker-compose.yml000066400000000000000000000041751462421536000204550ustar00rootroot00000000000000volumes: local_postgres_data: {} local_postgres_data_backups: {} services: django: &django build: context: . dockerfile: ./compose/local/django/Dockerfile args: PYTHON_VERSION: 3.12 image: django_structlog_demo_project_local_django depends_on: - postgres volumes: - .:/app:cached env_file: - ./.envs/.local/.django - ./.envs/.local/.redis - ./.envs/.local/.postgres tty: true # needed for colors to show in console logs ports: - "8000:8000" command: /start django_wsgi: <<: *django environment: DJANGO_DEBUG: False ports: - "8001:8000" command: /start_wsgi django_asgi: <<: *django environment: DJANGO_DEBUG: False ports: - "8002:8000" command: /start_asgi postgres: build: context: . dockerfile: ./compose/local/postgres/Dockerfile image: django_structlog_demo_project_local_postgres volumes: - local_postgres_data:/var/lib/postgresql/data:cached - local_postgres_data_backups:/backups:cached env_file: - ./.envs/.local/.postgres redis: image: redis:3.2 ports: - "6379:6379" celeryworker: image: django_structlog_demo_project_local_django depends_on: - redis - postgres volumes: - .:/app:cached env_file: - ./.envs/.local/.django - ./.envs/.local/.redis - ./.envs/.local/.postgres command: /start-celeryworker tty: true # needed for colors to show in console logs celerybeat: image: django_structlog_demo_project_local_django depends_on: - redis - postgres volumes: - .:/app:cached env_file: - ./.envs/.local/.django - ./.envs/.local/.redis - ./.envs/.local/.postgres command: /start-celerybeat tty: true # needed for colors to show in console logs flower: image: django_structlog_demo_project_local_django ports: - "5555:5555" volumes: - .:/app:cached env_file: - ./.envs/.local/.django - ./.envs/.local/.redis - ./.envs/.local/.postgres command: /start-flower django-structlog-8.1.0/docs/000077500000000000000000000000001462421536000157415ustar00rootroot00000000000000django-structlog-8.1.0/docs/Makefile000066400000000000000000000011041462421536000173750ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)django-structlog-8.1.0/docs/_static/000077500000000000000000000000001462421536000173675ustar00rootroot00000000000000django-structlog-8.1.0/docs/_static/.gitkeep000066400000000000000000000000001462421536000210060ustar00rootroot00000000000000django-structlog-8.1.0/docs/acknowledgements.rst000066400000000000000000000002101462421536000220160ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-acknowledgements-begin :end-before: inclusion-marker-acknowledgements-end django-structlog-8.1.0/docs/api_documentation.rst000066400000000000000000000015661462421536000222050ustar00rootroot00000000000000API documentation ================= django_structlog ^^^^^^^^^^^^^^^^ .. automodule:: django_structlog :members: :undoc-members: :show-inheritance: .. automodule:: django_structlog.middlewares :members: RequestMiddleware :undoc-members: :show-inheritance: .. automodule:: django_structlog.signals :members: bind_extra_request_metadata, bind_extra_request_finished_metadata, bind_extra_request_failed_metadata, update_failure_response django_structlog.celery ^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: django_structlog.celery :members: :undoc-members: :show-inheritance: .. automodule:: django_structlog.celery.steps :members: DjangoStructLogInitStep :undoc-members: :show-inheritance: .. automodule:: django_structlog.celery.signals :members: bind_extra_task_metadata, modify_context_before_task_publish, pre_task_succeeded django-structlog-8.1.0/docs/authors.rst000066400000000000000000000001661462421536000201630ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-authors-begin :end-before: inclusion-marker-authors-end django-structlog-8.1.0/docs/celery.rst000066400000000000000000000144101462421536000177560ustar00rootroot00000000000000.. _celery_integration: Celery Integration ================== Getting Started with Celery ^^^^^^^^^^^^^^^^^^^^^^^^^^^ In order to be able to support celery you need to configure both your webapp and your workers .. warning:: If json is used to serialize your celery tasks, the log context in use when executing a task (through ``apply_async`` or ``delay``) should only contain JSON-serializable data. You can use modify_context_before_task_publish_ to ensure this is the case. Replace your requirements ------------------------- First of all, make sure your ``django-structlog`` installation knows you use ``celery`` in order to validate compatibility with your installed version. See `Installing “Extras†`_ for more information. Replace ``django-structlog`` with ``django-structlog[celery]`` in your ``requirements.txt``. .. code-block:: python django-structlog[celery]==X.Y.Z Enable celery integration in your web app ----------------------------------------- In your settings.py .. code-block:: python MIDDLEWARE = [ # ... 'django_structlog.middlewares.RequestMiddleware', ] DJANGO_STRUCTLOG_CELERY_ENABLED = True Initialize Celery Worker with DjangoStructLogInitStep ----------------------------------------------------- In your celery AppConfig's module. .. code-block:: python import logging import structlog from celery import Celery from celery.signals import setup_logging from django_structlog.celery.steps import DjangoStructLogInitStep app = Celery("your_celery_project") # A step to initialize django-structlog app.steps['worker'].add(DjangoStructLogInitStep) .. warning:: If you use ``celery``'s `task_protocol v1 `_, ``django-structlog`` will not be able to transfer metadata to child task. Ex: .. code-block:: python app = Celery("your_celery_project", task_protocol=1) Configure celery's logger ------------------------- In the same file as before .. code-block:: python @setup_logging.connect def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover logging.config.dictConfig( { "version": 1, "disable_existing_loggers": False, "formatters": { "json_formatter": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.JSONRenderer(), }, "plain_console": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.dev.ConsoleRenderer(), }, "key_value": { "()": structlog.stdlib.ProcessorFormatter, "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']), }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "plain_console", }, "json_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/json.log", "formatter": "json_formatter", }, "flat_line_file": { "class": "logging.handlers.WatchedFileHandler", "filename": "logs/flat_line.log", "formatter": "key_value", }, }, "loggers": { "django_structlog": { "handlers": ["console", "flat_line_file", "json_file"], "level": "INFO", }, "django_structlog_demo_project": { "handlers": ["console", "flat_line_file", "json_file"], "level": "INFO", }, } } ) structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.stdlib.filter_by_level, structlog.processors.TimeStamper(fmt="iso"), structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.stdlib.PositionalArgumentsFormatter(), structlog.processors.StackInfoRenderer(), structlog.processors.format_exc_info, structlog.processors.UnicodeDecoder(), structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), cache_logger_on_first_use=True, ) .. _celery_signals: Signals ^^^^^^^ .. _modify_context_before_task_publish: modify_context_before_task_publish ---------------------------------- You can connect to ``modify_context_before_task_publish`` signal in order to modify the metadata before it is stored in the task's message. By example you can strip down the ``context`` to keep only some of the keys: .. code-block:: python @receiver(signals.modify_context_before_task_publish) def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs): keys_to_keep = {"request_id", "parent_task_id"} new_dict = {key_to_keep: context[key_to_keep] for key_to_keep in keys_to_keep if key_to_keep in context} context.clear() context.update(new_dict) bind_extra_task_metadata ------------------------ You can optionally connect to ``bind_extra_task_metadata`` signal in order to bind more metadata to the logger or override existing bound metadata. This is called in celery's ``receiver_task_pre_run``. .. code-block:: python from django_structlog.celery import signals import structlog @receiver(signals.bind_extra_task_metadata) def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None, **kwargs): structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id) django-structlog-8.1.0/docs/changelog.rst000066400000000000000000000501261462421536000204260ustar00rootroot00000000000000Change Log ========== 8.1.0 (May 24, 2024) -------------------- *New:* - Add a :ref:`setting ` ``DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'`` to customize what user field to use as ``user_id`` in the logs. `#546 `_ and `#545 `_. Special thanks to `@sshishov `_. *Changes:* - Drop support of python 3.7 - Drop support of django 3.2, and 4.1 8.0.0 (March 13, 2024) ---------------------- See: :ref:`upgrade_8.0` *New:* - add ``log_kwargs`` to :class:`django_structlog.signals.bind_extra_request_metadata`, :class:`django_structlog.signals.bind_extra_request_finished_metadata`, and :class:`django_structlog.signals.bind_extra_request_failed_metadata`. See `#484 `_. Special thanks to `@shtoltz `_. 7.1.0 (December 20, 2023) ------------------------- *New:* - add ``asgi``'s async view cancellation event ``request_cancelled``. See `#413 `_ and and :ref:`request_events`. 7.0.0 (December 13, 2023) ------------------------- See: :ref:`upgrade_7.0` *New:* - (celery) add ``priority`` (if available) and ``routing_key`` to ``task_enqueued``. See `#345 `_ and `#341 `_. Special thanks to `badziyoussef `_. - Django 5.0 support. See `#350 `_. Take note async view cancellation when using ``asgi`` does not work yet, see `#351 `_. - Add streaming response support (for both sync and async) and Django 5's async cancellation. See `#353 `_ and :ref:`streaming_response_events`. *Changes:* - Supports django-ipware 6+ only. See `#403 `_. Special thanks to the maintainer `@un33k `_. - Drop support of django 4.0. See `#338 `_. Special thanks to `@jairhenrique `_. *Other:* - Improved demo project - Better UI - Easy switch between ``runserver_plus``, ``wsgi`` an ``asgi`` - Front-end request feedbacks using ``toasts`` 6.0.1 (November 29, 2023) ------------------------- *Fixes:* - freeze ``django-ipware<6`` for now due to breaking changes. See `#388 `_ 6.0 (October 3, 2023) --------------------- See: :ref:`upgrade_6.0` *New:* - Python 3.12 support - Add support of logging :ref:`commands` - ``task_revoked`` has now a ``signame`` metadata. See `#323 `_. - ``task_not_found`` has now a ``task_id`` and a ``task`` metadata. See `#323 `_. *Fixes:* - Add missing metadata when a task is revoked. See `#317 `_. Special thanks to `@badziyoussef `_. *Changes:* - Drop support of python 3.7 - Drop legacy code still supporting celery < 4 - Removal of deprecated: - :class:`django_structlog.middlewares.CeleryMiddleware` - :class:`django_structlog.middlewares.SyncRequestMiddleware` - :class:`django_structlog.middlewares.AsyncRequestMiddleware` - :class:`django_structlog.middlewares.request_middleware_router` - ``4XX`` status codes now log by default as ``WARNING`` and ``5XX`` as ``ERROR``. The behaviour of ``4XX`` can be customized with :ref:`configuration`. See `#308 `_. Special thanks to `@adinhodovic `_. - ``task_revoked``'s ``signum`` is now an integer instead of an object. See `#323 `_. - ``task_not_found``'s ``message`` was removed and replaced by ``task_id`` and ``task``. See `#323 `_. - ``task_rejected``'s ``message`` was removed and replaced by ``task_id``. See `#323 `_. - Switched from ``git``'s ``master`` to ``main`` branch *Other:* - Add new :ref:`how_tos` section in the documentation. 5.3.0 (June 30, 2023) --------------------- *New:* - django setting ``DJANGO_STRUCTLOG_CELERY_ENABLED = True`` replacing :class:`django_structlog.middlewares.CeleryMiddleware`. See :ref:`upgrade_6.0` and `#265 `_. Also introduce new internal `app_settings` that may come handy for future configurations. *Deprecations:* - :class:`django_structlog.middlewares.CeleryMiddleware` (see above). 5.2.0 (June 29, 2023) --------------------- *New:* - Add new event ``task_started``. See `#260 `_. Special thanks to `@adrenaline681 `_. 5.1.0 (April 22, 2023) ---------------------- *New:* - Add new signal :class:`django_structlog.signals.update_failure_response` allowing to modify the response in case of failure. See `#231 `_. Special thanks to `@HMaker `_. 5.0.2 (April 16, 2023) ---------------------- See: :ref:`upgrade_5.0` *Fixes:* - Fix regression in 5.0.0 and 5.0.1 where exceptions were not logged as ``error`` but as ``info``. See `#226 `_. Special thanks to `@ntap-fge `_. *Rollbacks from 5.0.0:* - Rollback removal of ``django_structlog.signals.bind_extra_request_failed_metadata``. Relates the above fix. 5.0.1 (March 24, 2023) ---------------------- See: :ref:`upgrade_5.0` *Changes:* - minimum requirements change for ``asgiref`` to 3.6.0. See `#209 `_. Special thanks to `@adinsoon `_. 5.0.0 (March 23, 2023) ---------------------- See: :ref:`upgrade_5.0` *Changes:* - ``RequestMiddleware`` and ``CeleryMiddleware`` now properly support async views *Removed:* - *(Rolled back in 5.0.2)* ``django_structlog.signals.bind_extra_request_failed_metadata`` *Deprecates:* - :class:`django_structlog.middlewares.request_middleware_router` - :class:`django_structlog.middlewares.requests.AsyncRequestMiddleware` - :class:`django_structlog.middlewares.requests.SyncRequestMiddleware` 4.1.1 (February 7, 2023) ------------------------ *New:* - Add :class:`django_structlog.middlewares.request_middleware_router` to choose automatically between Async or Sync middleware *Rollbacks from 4.1.0:* - Rollback ``RequestMiddleware`` not being a class anymore, its an internal ``SyncRequestMiddleware`` *Others:* - Migrate project to ``pyproject.toml`` instead of ``setup.py`` - Add `asgi` server to demo project see :ref:`development`. 4.1.0 (February 4, 2023) ------------------------ *New:* - Add `async view `_ support. See `#180 `_. Special thanks to `@DamianMel `_. *Changes:* - ``RequestMiddleware`` is no longer a class but a function due to async view support. This should only affect projects using the middleware not as intended. If this cause you problems, please refer to this issue `#183 `_, `the documentation `_ or feel free to open a new issue. Special thanks to `@gvangool `_ for pointing that out. *Others:* - Add colours in log in the demo project. See `63bdb4d `_ to update your projects. Special thanks to `@RoscoeTheDog `_. - Upgrade or remove various development packages 4.0.1 (October 25, 2022) ------------------------ *New:* - Add support to ``python`` 3.11. See `#142 `_. Special thanks to `@jairhenrique `_. 4.0.0 (October 22, 2022) ------------------------ See: :ref:`upgrade_4.0` *Changes:* - ``django-structlog`` will now on follow LTS versions of Python, Django, and Celery. See `#110 `_. Special thanks to `@jairhenrique `_ for his convincing arguments. *New:* - You can now install ``django-structlog`` with ``celery`` extra. Specifying ``django-structlog[celery]==4.0.0`` in ``requirements.txt`` will make sure your ``celery``'s version is compatible. *Others:* - Upgrade or remove various development packages - Upgrade local development environment from python 3.7 to 3.10 and from django 3.2 to django 4.1 - Added a `gh-pages `_ 3.0.1 (August 2, 2022) ---------------------- *Fixes:* - ``AttributeError`` with custom User without ``pk``. See `#80 `_. Special thanks to `@mlegner `_. *Others:* - Add ``dependabot`` to manage dependencies. See `#83 `_. Special thanks to `@jairhenrique `_. - Upgrade various development packages 3.0.0 (August 1, 2022) ---------------------- See: :ref:`upgrade_3.0` *Changes:* - ``django-structlog`` now uses ``structlog.contextvars`` instead of ``structlog.threadlocal``. See the upgrade guide for more information (:ref:`upgrade_3.0`) and `#78 `_. Special thanks to `@AndrewGuenther `_ and `@shimizukawa `_. - removed ``django_structlog.processors.inject_context_dict`` - minimum requirements change to ``python`` 3.7+ - minimum requirements change to ``structlog`` 21.5 *New:* - Add python 3.10, celery 5.2 and django 4.0 to the test matrix. *Others:* - Remove ``wrapper_class`` from the configuration 2.2.0 (November 18, 2021) ------------------------- *Changes:* - Requests were logged as ```` (as an object) and now they are logged like this ``GET /`` (as a string). See `#72 `_. Special thanks to `@humitos `_. 2.1.3 (September 28, 2021) -------------------------- *Fixes:* - Implement `Celery Task.throws `_' behaviour of logging expected exception as ``INFO`` with no tracebacks. See `#62 `_ and `#70 `_. Special thanks to `@meunomemauricio `_. 2.1.2 (August 31, 2021) ----------------------- *Fixes:* - ``django.core.exceptions.PermissionDenied`` is no longer logged as 500 but 403. See `#68 `_. Special thanks to `@rabbit-aaron `_. 2.1.1 (June 22, 2021) ------------------------- *Others:* - Add ``django`` 3.2 and ``python`` 3.9 to the test matrix and ``pypi`` metadata. See `#65 `_. Special thanks to `@kashewnuts `_. 2.1.0 (November 26, 2020) ------------------------- *New:* - :class:`django_structlog.processors.inject_context_dict` for standard python loggers. See `#24 `_. Special thanks to `@debfx `_. 2.0.0 (November 25, 2020) ------------------------- *Upgrade:* - There are necessary configuration changes needed. See :ref:`upgrade_2.0` for the details. *Changes:* - No longer add ``error`` and ``error_traceback``. See `#55 `_ and :ref:`upgrade_2.0`. Special thanks to `@debfx `_. *Fixes:* - Fix crash when request's user is ``None`` for `django-oauth-toolkit `_. See `#56 `_. Special thanks to `@nicholasamorim `_. 1.6.3 (November 11, 2020) ------------------------- *Improvements:* - Call stack of exception in log is now an appropriate string. See `#54 `_. Special thanks to `@debfx `_. 1.6.2 (October 4, 2020) ----------------------- *Fixes:* - Fix UUID as User pk causing issues. See `#52 `_ `#45 `_ and `#51 `_. Special thanks to `@fadedDexofan `_. 1.6.1 (August 13, 2020) ----------------------- *Fixes:* - Removed ``providing_args`` from signals to fix django 4.0 deprecation warnings introduced by django 3.1. See `#44 `_. Special thanks to `@ticosax `_. - Fix ``sender`` of ``signals.pre_task_succeeded`` - Documented signal parameters in doc strings and ``API documentation`` to replace ``providing_args`` *Others:* - Add ``django`` 3.0 and 3.1 to the test matrix and ``pypi`` supported frameworks metadata - Fix reference of the previous ci in the documentation 1.6.0 (June 17, 2020) --------------------- *Changes:* - ``task_succeed`` is now ``task_succeeded``. Special thanks to `@PawelMorawian `_. - Remove ``result`` from ``task_succeeded`` log (may be added back, see below). Special thanks to `@PawelMorawian `_ as well. - Add ``django_structlog.celery.signals.pre_task_succeeded``. To be able to bind ``result`` if someone really needs it. 1.5.5 (June 16, 2020) --------------------- *New:* - Add ``bind_extra_request_finished_metadata`` and ``bind_extra_request_failed_metadata``. See `#39 `_. Special thanks to `@prik2693 `_. 1.5.4 (June 15, 2020) --------------------- *Improvements:* - Remove redundant ``DJANGO_STRUCTLOG_LOG_USER_IN_REQUEST_FINISHED`` setting and just always make sure ``user_id`` is in ``request_finished`` and ``request_failed`` instead. See `#37 `_. 1.5.3 (June 15, 2020) --------------------- *New:* - Add ``DJANGO_STRUCTLOG_LOG_USER_IN_REQUEST_FINISHED`` setting to support `Django REST framework `_. See `#37 `_. Special thanks to `@immortaleeb `_. 1.5.2 (April 2, 2020) --------------------- *New:* - Add ``modify_context_before_task_publish`` signal. 1.5.1 (March 18, 2020) ---------------------- *Improvements:* - Allow to override celery task metadata from binding. See `#32 `_ and `#33 `_. Special thanks to `@chiragjn `_ 1.5.0 (March 6, 2020) --------------------- *Improvements:* - Add support for celery 3. See `#26 `_ and `#31 `_. Special thanks to `@chiragjn `_ and `@prik2693 `_ 1.4.1 (February 8, 2020) ------------------------ *New:* - Bind ``X-Correlation-ID`` HTTP header's value as ``correlation_id`` when provided in request. 1.4.0 (February 7, 2020) ------------------------ *New:* - Use ``X-Request-ID`` HTTP header's value as ``request_id`` when provided in request. See `#22 `_. Special thanks to `@jairhenrique `_ 1.3.5 (December 23, 2019) ------------------------- *New:* - Add python 3.8, celery 4.4 and django 3.0 to the test matrix. *Improvements:* - Extract ``test_app`` from ``django_structlog_demo_app`` in order to test ``django_structlog`` all by itself - Improve CI execution speed by merging stages - Upgrade a few development depencencies 1.3.4 (November 27, 2019) ------------------------- *Bugfix:* - Exception logging not working properly with ``DEBUG = False``. See `#19 `_. Special thanks to `@danpalmer `_ 1.3.3 (October 6, 2019) ----------------------- *Bugfix:* - Fix support of different primary key for ``User`` model. See `#13 `_. Special thanks to `@dhararon `_ 1.3.2 (September 21, 2019) -------------------------- *Improvements:* - Add support of projects without ``AuthenticationMiddleware``. See `#9 `_. Special thanks to `@dhararon `_ 1.3.1 (September 4, 2019) ------------------------- *Bugfixes:* - Remove extraneous ``rest-framework`` dependency introduced by `#7 `_. See `#8 `_ . Special thanks to `@ghickman `_ 1.3.0 (September 3, 2019) ------------------------- *Improvements:* - Improve django uncaught exception formatting. See `#7 `_. Special thanks to `@paulstuartparker `_ 1.2.3 (May 18, 2019) -------------------- *Bugfixes:* - Fix ``structlog`` dependency not being installed *Improvements:* - Use `black `_ code formatter 1.2.2 (May 13, 2019) -------------------- *Improvements:* - Use appropriate packaging 1.2.1 (May 8, 2019) ------------------- *Bugfixes:* - Fix missing license file to be included in distribution 1.2.0 (May 8, 2019) ------------------- *Changes:* - In the event ``task_enqueued``, ``task_id`` and ``task_name`` are renamed ``child_task_id`` and ``child_task_name`` respectively to avoid override of ``task_id`` in nested tasks. 1.1.6 (May 8, 2019) ------------------- *New:* - Add ``task_name`` when a task is enqueued 1.1.5 (May 8, 2019) ------------------- *New:* - Add support of tasks calling other tasks (introducing ``parent_task_id``) *Bugfixes:* - Fix missing packages 1.1.4 (April 22, 2019) ---------------------- *Improvements:* - Wheel distribution 1.1.3 (April 22, 2019) ---------------------- *Improvements:* - api documentation - code documentation 1.1.2 (April 19, 2019) ---------------------- *Changes:* - Rewrite the log texts as events 1.1.1 (April 18, 2019) ---------------------- *New:* - Add ``celery`` signal ``signals.bind_extra_task_metadata`` 1.1 (April 16, 2019) -------------------- *New:* - Add ``celery`` tasks support 1.0.4 to 1.0.7 (April 14, 2019) ------------------------------- *New:* - Automated releases with tags on ``travis`` 1.0.3 (April 14, 2019) ---------------------- *Bugfixes:* - Add ``bind_extra_request_metadata`` documentation 1.0.2 (April 13, 2019) ---------------------- *Bugfixes:* - Tweaked documentation. 1.0.0 (April 13, 2019) ---------------------- *New*: - Fist public release. django-structlog-8.1.0/docs/commands.rst000066400000000000000000000071231462421536000202770ustar00rootroot00000000000000.. _commands: Commands ======== Prerequisites ^^^^^^^^^^^^^ Install ``django-structlog`` with command support (it will install `django-extensions `_). .. code-block:: bash pip install django-structlog[commands] Alternatively install `django-extensions `_ directly: .. code-block:: bash pip install django-extensions Configuration ^^^^^^^^^^^^^ Enable ``django-structlog``'s command logging: .. code-block:: python DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True Add ``django-extensions``'s `@signalcommand `_ to your commands .. code-block:: python import structlog from django.core.management import BaseCommand from django_extensions.management.utils import signalcommand # <- add this logger = structlog.getLogger(__name__) class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument("foo", type=str) @signalcommand # <- add this def handle(self, foo, *args, **options): logger.info("my log", foo=foo) return 0 Results ^^^^^^^ Log will add ``command_name`` and ``command_id`` to the logs: .. code-block:: bash $ python manage.py example_command bar 2023-09-13T21:10:50.084368Z [info ] command_started [django_structlog.commands] command_name=django_structlog_demo_project.users.example_command command_id=be723d34-59f5-468e-9258-24232aa4cedd 2023-09-13T21:10:50.085325Z [info ] my log [django_structlog_demo_project.users.management.commands.example_command] command_id=be723d34-59f5-468e-9258-24232aa4cedd foo=bar 2023-09-13T21:10:50.085877Z [info ] command_finished [django_structlog.commands] command_id=be723d34-59f5-468e-9258-24232aa4cedd It also supports nested commands which will keep track of parent commands through ``parent_id``: .. code-block:: bash $ python manage.py example_command bar 2023-09-15T00:10:10.466616Z [info ] command_started [django_structlog.commands] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed command_name=django_structlog_demo_project.users.example_command 2023-09-15T00:10:10.467250Z [info ] my log [django_structlog_demo_project.users.management.commands.example_command] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed foo=bar 2023-09-15T00:10:10.468176Z [info ] command_started [django_structlog.commands] baz=2 command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 command_name=django_structlog_demo_project.users.example_nested_command parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed 2023-09-15T00:10:10.468871Z [info ] my nested log [django_structlog_demo_project.users.management.commands.example_nested_command] command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed 2023-09-15T00:10:10.469418Z [info ] command_finished [django_structlog.commands] command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed 2023-09-15T00:10:10.469964Z [info ] my log 2 [django_structlog_demo_project.users.management.commands.example_command] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed 2023-09-15T00:10:10.470585Z [info ] command_finished [django_structlog.commands] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed django-structlog-8.1.0/docs/conf.py000066400000000000000000000043361462421536000172460ustar00rootroot00000000000000import sys import os sys.path.append(os.path.join(os.path.dirname(__file__), "..")) # Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # The master toctree document. master_doc = "index" # -- Project information ----------------------------------------------------- project = "django-structlog" copyright = "2019, Jules Robichaud-Gagnon" author = "Jules Robichaud-Gagnon" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] def get_version(precision): import django_structlog return ".".join(str(v) for v in django_structlog.VERSION[:precision]) # Full version release = get_version(3) # Minor version version = get_version(2) # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] django-structlog-8.1.0/docs/configuration.rst000066400000000000000000000036261462421536000213510ustar00rootroot00000000000000.. _configuration: Configuration ============= In your ``settings.py`` you can customize ``django-structlog``. Example: .. code-block:: python import logging DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL = logging.INFO .. _settings: Settings -------- +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ | Key | Type | Default | Description | +==========================================+=========+=================+===============================================================================+ | DJANGO_STRUCTLOG_CELERY_ENABLED | boolean | False | See :ref:`celery_integration` | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL | int | logging.WARNING | Log level of 4XX status codes | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False | See :ref:`commands` | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ | DJANGO_STRUCTLOG_USER_ID_FIELD | boolean | ``"pk"`` | Change field used to identify user in logs, ``None`` to disable user binding | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ django-structlog-8.1.0/docs/demo.rst000066400000000000000000000001601462421536000174140ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-demo-begin :end-before: inclusion-marker-demo-end django-structlog-8.1.0/docs/development.rst000066400000000000000000000017431462421536000210220ustar00rootroot00000000000000.. _development: Development =========== Prerequisites ------------- - `docker `_ Installation ------------ .. code-block:: bash $ git clone https://github.com/jrobichaud/django-structlog.git $ cd django-structlog $ pip install -r requirements.txt $ pre-commit install Start Demo App -------------- .. code-block:: bash $ docker compose up --build - ``runserver_plus`` server: http://127.0.0.1:8000/ - ``WSGI`` server: http://127.0.0.1:8001/ - ``ASGI`` server: http://127.0.0.1:8002/ Use ``RabbitMQ`` broker instead of ``redis`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: bash $ docker compose -f ./docker-compose.yml -f ./docker-compose.amqp.yml up --build Building, Serving and Testing the Documentation Locally ------------------------------------------------------- .. code-block:: bash $ docker compose -p django-structlog-docs -f docker-compose.docs.yml up --build Serving on http://127.0.0.1:5000 django-structlog-8.1.0/docs/events.rst000066400000000000000000000306501462421536000200030ustar00rootroot00000000000000Events and Metadata =================== Django's RequestMiddleware -------------------------- .. _request_events: Request Events ^^^^^^^^^^^^^^ +-------------------+--------------------+-----------------------------------------------------+ | Event | Type | Description | +===================+====================+=====================================================+ | request_started | INFO | Django received a request | +-------------------+--------------------+-----------------------------------------------------+ | request_finished | INFO/WARNING/ERROR | request completed with status (2XX or 3XX)/4XX/5XX | +-------------------+--------------------+-----------------------------------------------------+ | request_cancelled | WARNING | request cancelled during an async request with asgi | +-------------------+--------------------+-----------------------------------------------------+ | request_failed | ERROR | unhandled exception occurred | +-------------------+--------------------+-----------------------------------------------------+ .. _streaming_response_events: StreamingHttpResponse Events ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Specific to `StreamingHttpResponse `_ +---------------------+--------------------+-------------------------------------+ | Event | Type | Description | +=====================+====================+=====================================+ | streaming_started | INFO | Streaming of response started | +---------------------+--------------------+-------------------------------------+ | streaming_finished | INFO | Streaming of response finished | +---------------------+--------------------+-------------------------------------+ | streaming_cancelled | WARNING | Streaming of response was cancelled | +---------------------+--------------------+-------------------------------------+ | streaming_failed | ERROR | Streaming of response failed | +---------------------+--------------------+-------------------------------------+ Request Bound Metadata ^^^^^^^^^^^^^^^^^^^^^^ These metadata are repeated on each log of the current request and will be also be repeated in all children Celery tasks. +------------------+---------------------------------------------------------------------------------------------------------------------------------+ | Key | Value | +==================+=================================================================================================================================+ | request_id | UUID for the request or value of ``X-Request-ID`` HTTP header when provided | +------------------+---------------------------------------------------------------------------------------------------------------------------------+ | correlation_id | value of ``X-Correlation-ID`` HTTP header when provided | +------------------+---------------------------------------------------------------------------------------------------------------------------------+ | user_id | user's id or None (requires `django.contrib.auth.middleware.AuthenticationMiddleware`_) | | | | | | `DRF `_: it will only be in ``request_finished`` and ``request_failed`` events | | | | | | If you need to override the bound ``user_id``, it has to be done in all three signals: | | | - :attr:`django_structlog.signals.bind_extra_request_metadata` | | | - :attr:`django_structlog.signals.bind_extra_request_finished_metadata` | | | - :attr:`django_structlog.signals.bind_extra_request_failed_metadata` | +------------------+---------------------------------------------------------------------------------------------------------------------------------+ | ip | request's ip | +------------------+---------------------------------------------------------------------------------------------------------------------------------+ To bind more metadata or override existing metadata from request see :ref:`django_signals` .. _`django.contrib.auth.middleware.AuthenticationMiddleware`: https://docs.djangoproject.com/en/dev/ref/middleware/#module-django.contrib.auth.middleware Request Events Metadata ^^^^^^^^^^^^^^^^^^^^^^^ These metadata appear once along with their associated event +------------------+------------------+--------------------------------------------------------------+ | Event | Key | Value | +==================+==================+==============================================================+ | request_started | request | request as string | +------------------+------------------+--------------------------------------------------------------+ | request_started | user_agent | request's user agent | +------------------+------------------+--------------------------------------------------------------+ | request_finished | code | request's status code | +------------------+------------------+--------------------------------------------------------------+ | request_failed | exception | exception traceback (requires format_exc_info_) | +------------------+------------------+--------------------------------------------------------------+ .. _format_exc_info: https://www.structlog.org/en/stable/api.html#structlog.processors.format_exc_info Celery ------ Task Events ^^^^^^^^^^^ +--------------------+-------------+------------------------------------------------+ | Event | Type | Description | +====================+=============+================================================+ | task_enqueued | INFO | A task was enqueued by request or another task | +--------------------+-------------+------------------------------------------------+ | task_retrying | WARNING | Worker retry task | +--------------------+-------------+------------------------------------------------+ | task_started | INFO | task just started executing | +--------------------+-------------+------------------------------------------------+ | task_succeeded | INFO | Task completed successfully | +--------------------+-------------+------------------------------------------------+ | task_failed | ERROR/INFO* | Task failed | +--------------------+-------------+------------------------------------------------+ | task_revoked | WARNING | Task was canceled | +--------------------+-------------+------------------------------------------------+ | task_not_found | ERROR | Celery app did not discover the requested task | +--------------------+-------------+------------------------------------------------+ | task_rejected | ERROR | Task could not be enqueued | +--------------------+-------------+------------------------------------------------+ \* if task threw an expected exception, it will logged as ``INFO``. See `Celery's Task.throws `_ Task Bound Metadata ^^^^^^^^^^^^^^^^^^^ These metadata are repeated on each log of the current task and will be also be repeated in all children Celery tasks. Take note that all the caller's logger bound metadata are also bound to the task's logger. +------------------+------------------------------------+ | Key | Value | +==================+====================================+ | task_id | UUID of the current task | +------------------+------------------------------------+ | parent_task_id | UUID of the parent's task (if any) | +------------------+------------------------------------+ To bind more metadata or override existing metadata from task see :ref:`celery_signals` Task Event Metadata ^^^^^^^^^^^^^^^^^^^ These metadata appear once along with their associated event +------------------+------------------+----------------------------------------+ | Event | Key | Value | +==================+==================+========================================+ | task_enqueued | child_task_id | id of the task being enqueued | +------------------+------------------+----------------------------------------+ | task_enqueued | child_task_name | name of the task being enqueued | +------------------+------------------+----------------------------------------+ | task_enqueued | routing_key | task's routing key | +------------------+------------------+----------------------------------------+ | task_enqueued | priority | priority of task (if any) | +------------------+------------------+----------------------------------------+ | task_retrying | reason | reason for retry | +------------------+------------------+----------------------------------------+ | task_started | task | name of the task | +------------------+------------------+----------------------------------------+ | task_failed | error | exception as string | +------------------+------------------+----------------------------------------+ | task_failed | exception* | exception's traceback | +------------------+------------------+----------------------------------------+ | task_revoked | terminated | Set to True if the task was terminated | +------------------+------------------+----------------------------------------+ | task_revoked | signum | python termination signal's number | +------------------+------------------+----------------------------------------+ | task_revoked | signame | python termination signal's name | +------------------+------------------+----------------------------------------+ | task_revoked | expired | see Celery's documentation | +------------------+------------------+----------------------------------------+ | task_revoked | task_id | id of the task being revoked | +------------------+------------------+----------------------------------------+ | task_revoked | task | name of the task being revoked | +------------------+------------------+----------------------------------------+ | task_not_found | task_id | id of the task not found | +------------------+------------------+----------------------------------------+ | task_not_found | task | name of the task not found | +------------------+------------------+----------------------------------------+ | task_rejected | task_id | id of the task being rejected | +------------------+------------------+----------------------------------------+ \* if task threw an expected exception, ``exception`` will be omitted. See `Celery's Task.throws `_ django-structlog-8.1.0/docs/example_outputs.rst000066400000000000000000000002061462421536000217270ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-example-outputs-begin :end-before: inclusion-marker-example-outputs-end django-structlog-8.1.0/docs/getting_started.rst000066400000000000000000000002061462421536000216600ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-getting-started-begin :end-before: inclusion-marker-getting-started-end django-structlog-8.1.0/docs/how_tos.rst000066400000000000000000000111741462421536000201610ustar00rootroot00000000000000.. _how_tos: How Tos ======= These are code snippets on how to achieve some specific use cases. .. warning:: Be aware they are untested. Please `open an issue `_ if there are bugs in these examples or if you want to share some great examples that should be there. Bind ``request_id`` to response's header ---------------------------------------- You can add the ``request_id`` to a custom response header ``X-Request-ID`` in order to trace the request by the caller. Origin: `#231 `_ .. code-block:: python from django.dispatch import receiver from django_structlog import signals import structlog @receiver(signals.update_failure_response) @receiver(signals.bind_extra_request_finished_metadata) def add_request_id_to_error_response(response, logger, **kwargs): context = structlog.contextvars.get_merged_contextvars(logger) response['X-Request-ID'] = context["request_id"] Bind ``rest_framework_simplejwt`` token's user id ------------------------------------------------- Bind token's user_id from `rest_framework_simplejwt `_ to the request. It is a workaround for ``restframework``'s non-standard authentication system. It prevents access of the user in middlewares, therefore ``django-structlog`` cannot bind the ``user_id`` by default. .. code-block:: python import structlog from django.dispatch import receiver from django_structlog.signals import bind_extra_request_metadata from rest_framework_simplejwt.tokens import UntypedToken @receiver(bind_extra_request_metadata) def bind_token_user_id(request, logger, **kwargs): try: header = request.META.get("HTTP_AUTHORIZATION") if header: raw_token = header.split()[1] token = UntypedToken(raw_token) user_id = token["user_id"] structlog.contextvars.bind_contextvars(user_id=user_id) except Exception: pass Bind AWS's ``X-Amzn-Trace-Id`` ------------------------------ See `Request tracing for your Application Load Balancer `_ Origin: `#324 `_ .. code-block:: python from django.dispatch import receiver from django_structlog import signals from django_structlog.middlewares.request import get_request_header import structlog @receiver(signals.bind_extra_request_metadata) def bind_trace_id(request, logger, **kwargs): trace_id = get_request_header( request, "x-amzn-trace-id", "HTTP_X_AMZN_TRACE_ID" ) if trace_id: structlog.contextvars.bind_contextvars(trace_id=trace_id) Filter logs from being recorded ------------------------------- You can add a custom filter to prevent some specific logs from being recorded, based on your criteria See `Django logging documentation `_ Origin: `#412 `_ .. code-block:: python # your_project/logging/filters.py import logging class ExcludeEventsFilter(logging.Filter): def __init__(self, excluded_event_type=None): super().__init__() self.excluded_event_type = excluded_event_type def filter(self, record): if not isinstance(msg, dict) or self.excluded_event_type is None: return True # Include the log message if msg is not a dictionary or excluded_event_type is not provided if record.msg.get('event') in self.excluded_event_type: return False # Exclude the log message return True # Include the log message # in your settings.py LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'filters': ['exclude_request_started'] }, }, 'filters': { 'exclude_request_started': { '()': 'your_project.logging.filters.ExcludeEventsFilter', 'excluded_event_type': ['request_started'] # Example excluding request_started event }, }, 'loggers': { 'django': { 'handlers': ['console'], 'level': 'DEBUG', }, }, } django-structlog-8.1.0/docs/index.rst000066400000000000000000000007041462421536000176030ustar00rootroot00000000000000.. include:: ../README.rst :start-after: introduction-begin :end-before: introduction-end Contents, indices and tables ============================ .. toctree:: :maxdepth: 2 getting_started configuration celery commands api_documentation events example_outputs how_tos running_tests development demo changelog upgrade_guide authors acknowledgements licence * :ref:`genindex` * :ref:`modindex` * :ref:`search` django-structlog-8.1.0/docs/licence.rst000066400000000000000000000000551462421536000200750ustar00rootroot00000000000000Licence ======= .. include:: ../LICENSE.rst django-structlog-8.1.0/docs/make.bat000066400000000000000000000013601462421536000173460ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd django-structlog-8.1.0/docs/requirements.txt000066400000000000000000000002201462421536000212170ustar00rootroot00000000000000sphinx==7.3.7 sphinx_rtd_theme==2.0.0 celery==5.4.0 django>=4.2,<6 structlog sphinx-autobuild==2024.4.16 Jinja2==3.1.4 importlib-metadata>=1,<8 django-structlog-8.1.0/docs/running_tests.rst000066400000000000000000000002021462421536000213670ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-running-tests-begin :end-before: inclusion-marker-running-tests-end django-structlog-8.1.0/docs/upgrade_guide.rst000066400000000000000000000002021462421536000212710ustar00rootroot00000000000000.. include:: ../README.rst :start-after: inclusion-marker-upgrade-guide-begin :end-before: inclusion-marker-upgrade-guide-end django-structlog-8.1.0/logs/000077500000000000000000000000001462421536000157555ustar00rootroot00000000000000django-structlog-8.1.0/logs/.gitkeep000066400000000000000000000000001462421536000173740ustar00rootroot00000000000000django-structlog-8.1.0/manage.py000077500000000000000000000020611462421536000166150ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") try: from django.core.management import execute_from_command_line except ImportError: # The above import may fail for some other reason. Ensure that the # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: import django # noqa except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) raise # This allows easy placement of apps within the interior # django_structlog_demo_project directory. current_path = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.join(current_path, "django_structlog_demo_project")) execute_from_command_line(sys.argv) django-structlog-8.1.0/pyproject.toml000066400000000000000000000071241462421536000177310ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "django-structlog" description = "Structured Logging for Django" authors = [ { name = "Jules Robichaud-Gagnon", email = "j.robichaudg+pypi@gmail.com" }, ] readme = "README.rst" dynamic = ["version"] requires-python = ">=3.8" license = { text = "MIT" } dependencies = [ "django>=4.2", "structlog>=21.4.0", "asgiref>=3.6.0", "django-ipware>=6.0.2", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: System :: Logging", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] [project.urls] homepage = "https://github.com/jrobichaud/django-structlog" repository = "https://github.com/jrobichaud/django-structlog" documentation = "https://django-structlog.readthedocs.io" tracker = "https://github.com/jrobichaud/django-structlog/issues" changelog = "https://django-structlog.readthedocs.io/en/latest/changelog.html" [project.optional-dependencies] celery = [ "celery>=5.1" ] commands = [ "django-extensions>=1.4.9" ] [tool.setuptools.dynamic] version = { attr = "django_structlog.__version__" } [tool.setuptools.packages.find] include = [ "django_structlog", "django_structlog.*", ] [tool.black] line-length = 88 target-version = [ 'py38', 'py39', 'py310', 'py311', 'py312', ] include = '\.pyi?$' exclude = ''' /( \.git | \.hg | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [tool.ruff] line-length = 88 target-version = "py312" lint.ignore = [ 'E501', ] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings.test_demo_app" [tool.tox] legacy_tox_ini = """ [tox] # Test against latest supported version of each of python for each Django version. # # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml envlist = py{38,39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5, py31{0,1}-django50-celery53-redis4-kombu5, py312-django{42,50}-celery53-redis4-kombu5, [gh-actions] python = 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 [testenv] setenv = PYTHONPATH={toxinidir} CELERY_BROKER_URL=redis://0.0.0.0:6379 CELERY_RESULT_BACKEND=redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE=config.settings.test pip_pre = True deps = redis3: redis>=3, <4 redis4: redis>=4, <5 kombu5: kombu<6 celery51: Celery >=5.1, <5.2 celery52: Celery >=5.2, <5.3 celery53: Celery >=5.3, <5.4 django42: Django >=4.2, <5.0 django50: Django >=5.0, <5.1 -r{toxinidir}/requirements/ci.txt commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app """ [tool.coverage.run] branch = true [tool.coverage.report] precision = 2 skip_covered = true show_missing = true exclude_lines = [ "pragma: no cover", "raise NotImplementedError" ] include = [ "./django_structlog/*", "./django_structlog_demo_project/*", "./test_app/*", ] django-structlog-8.1.0/requirements.txt000066400000000000000000000000321462421536000202700ustar00rootroot00000000000000-r requirements/local.txt django-structlog-8.1.0/requirements/000077500000000000000000000000001462421536000175345ustar00rootroot00000000000000django-structlog-8.1.0/requirements/black.txt000066400000000000000000000000571462421536000213530ustar00rootroot00000000000000black==24.4.2 # https://github.com/ambv/black django-structlog-8.1.0/requirements/ci.txt000066400000000000000000000023671462421536000207000ustar00rootroot00000000000000# Django # ------------------------------------------------------------------------------ django-environ==0.11.2 # https://github.com/joke2k/django-environ django-redis==5.4.0 # https://github.com/niwinz/django-redis django-extensions==3.2.3 structlog>=21.4.0 colorama>=0.4.3 psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 # Testing # ------------------------------------------------------------------------------ pytest==8.2.1 # https://github.com/pytest-dev/pytest pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar pytest-cov==5.0.0 # Code quality # ------------------------------------------------------------------------------ coverage==7.5.1 # https://github.com/nedbat/coveragepy pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery # Django # ------------------------------------------------------------------------------ factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django # Setup tools # ------------------------------------------------------------------------------ setuptools>=41.0.1 django-structlog-8.1.0/requirements/deployment.txt000066400000000000000000000000311462421536000224470ustar00rootroot00000000000000importlib-metadata>=1,<8 django-structlog-8.1.0/requirements/local-base.txt000066400000000000000000000045711462421536000223060ustar00rootroot00000000000000pytz==2024.1 # https://github.com/stub42/pytz python-slugify==8.0.4 # https://github.com/un33k/python-slugify # Django # ------------------------------------------------------------------------------ django==5.0.6 # https://www.djangoproject.com/ django-environ==0.11.2 # https://github.com/joke2k/django-environ django-model-utils==4.5.1 # https://github.com/jazzband/django-model-utils django-allauth==0.63.1 # https://github.com/pennersr/django-allauth django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms crispy-bootstrap5==2024.2 # https://github.com/django-crispy-forms/crispy-bootstrap5 django-redis==5.4.0 # https://github.com/niwinz/django-redis asgiref==3.8.1 # https://github.com/django/asgiref # Django REST Framework djangorestframework==3.15.1 # https://github.com/encode/django-rest-framework coreapi==2.3.3 # https://github.com/core-api/python-client # django-ninja django-ninja==1.1.0 # https://github.com/vitalik/django-ninja structlog==24.1.0 colorama==0.4.6 django-ipware==7.0.1 Werkzeug==3.0.3 # https://github.com/pallets/werkzeug ipdb==0.13.13 # https://github.com/gotcha/ipdb psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 # Testing # ------------------------------------------------------------------------------ pytest==8.2.1 # https://github.com/pytest-dev/pytest pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar pytest-cov==5.0.0 pytest-asyncio==0.23.7 # https://github.com/pytest-dev/pytest-asyncio pytest-mock==3.14.0 # https://github.com/pytest-dev/pytest-mock # Code quality # ------------------------------------------------------------------------------ -r ruff.txt coverage==7.5.1 # https://github.com/nedbat/coveragepy -r black.txt pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery # Django # ------------------------------------------------------------------------------ factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy django-extensions==3.2.3 # https://github.com/django-extensions/django-extensions django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django # pre-commit # ------------------------------------------------------------------------------ pre-commit==3.7.1 # https://github.com/pre-commit/pre-commit django-structlog-8.1.0/requirements/local.txt000066400000000000000000000005351462421536000213720ustar00rootroot00000000000000-r local-base.txt redis==5.0.4 # https://github.com/antirez/redis celery==5.4.0 # pyup: < 5.0 # https://github.com/celery/celery kombu==5.3.7 flower==2.0.1 # https://github.com/mher/flower uvicorn==0.29.0 # https://github.com/encode/uvicorn gunicorn==22.0.0 # https://github.com/benoitc/gunicorn amqp==5.2.0 # https://github.com/celery/py-amqp django-structlog-8.1.0/requirements/ruff.txt000066400000000000000000000000611462421536000212340ustar00rootroot00000000000000ruff==0.4.4 # https://github.com/astral-sh/ruff django-structlog-8.1.0/test_app/000077500000000000000000000000001462421536000166305ustar00rootroot00000000000000django-structlog-8.1.0/test_app/__init__.py000066400000000000000000000000001462421536000207270ustar00rootroot00000000000000django-structlog-8.1.0/test_app/apps.py000066400000000000000000000001321462421536000201410ustar00rootroot00000000000000from django.apps import AppConfig class TestAppConfig(AppConfig): name = "test_app" django-structlog-8.1.0/test_app/migrations/000077500000000000000000000000001462421536000210045ustar00rootroot00000000000000django-structlog-8.1.0/test_app/migrations/__init__.py000066400000000000000000000000001462421536000231030ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/000077500000000000000000000000001462421536000177725ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/__init__.py000066400000000000000000000000001462421536000220710ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/celery/000077500000000000000000000000001462421536000212555ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/celery/__init__.py000066400000000000000000000000001462421536000233540ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/celery/test_receivers.py000066400000000000000000000535331462421536000246660ustar00rootroot00000000000000import logging from signal import SIGTERM from unittest.mock import Mock, patch, call, MagicMock import structlog from celery import shared_task from django.contrib.auth.models import AnonymousUser from django.dispatch import receiver as django_receiver from django.test import TestCase, RequestFactory from django_structlog.celery import receivers, signals class TestReceivers(TestCase): def setUp(self): self.factory = RequestFactory() self.logger = structlog.getLogger(__name__) def tearDown(self): structlog.contextvars.clear_contextvars() def test_defer_task(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() @shared_task def test_task(value): # pragma: no cover pass receiver = receivers.CeleryReceiver() receiver.connect_signals() structlog.contextvars.bind_contextvars(request_id=expected_uuid) with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: test_task.delay("foo") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) def test_receiver_before_task_publish_celery_protocol_v2(self): expected_uuid = "00000000-0000-0000-0000-000000000000" expected_user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" headers = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=expected_user_id, task_id=expected_parent_task_uuid, ) receiver = receivers.CeleryReceiver() receiver.receiver_before_task_publish(headers=headers) self.assertDictEqual( { "__django_structlog__": { "request_id": expected_uuid, "user_id": expected_user_id, "parent_task_id": expected_parent_task_uuid, } }, headers, ) def test_receiver_before_task_publish_celery_protocol_v1(self): """Protocol v1 does not allow to store metadata""" expected_uuid = "00000000-0000-0000-0000-000000000000" expected_user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" headers = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=expected_user_id, task_id=expected_parent_task_uuid, ) mock_current_app = MagicMock() mock_conf = MagicMock() mock_current_app.conf = mock_conf mock_conf.task_protocol = 1 receiver = receivers.CeleryReceiver() with patch("django_structlog.celery.receivers.current_app", mock_current_app): receiver.receiver_before_task_publish(headers=headers) self.assertDictEqual( {}, headers, ) def test_signal_modify_context_before_task_publish_celery_protocol_v2(self): expected_uuid = "00000000-0000-0000-0000-000000000000" user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" routing_key = "foo" properties = {"correlation_id": "22222222-2222-2222-2222-222222222222"} received_properties = None received_routing_key = None @django_receiver(signals.modify_context_before_task_publish) def receiver_modify_context_before_task_publish( sender, signal, context, task_properties, task_routing_key, **kwargs ): keys_to_keep = {"request_id", "parent_task_id"} new_dict = { key_to_keep: context[key_to_keep] for key_to_keep in keys_to_keep if key_to_keep in context } context.clear() context.update(new_dict) nonlocal received_properties received_properties = task_properties nonlocal received_routing_key received_routing_key = task_routing_key headers = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=user_id, task_id=expected_parent_task_uuid, ) receiver = receivers.CeleryReceiver() receiver.receiver_before_task_publish( headers=headers, routing_key=routing_key, properties=properties, ) self.assertDictEqual( { "__django_structlog__": { "request_id": expected_uuid, "parent_task_id": expected_parent_task_uuid, } }, headers, "Only `request_id` and `parent_task_id` are preserved", ) self.assertDictEqual( {"correlation_id": "22222222-2222-2222-2222-222222222222"}, received_properties, ) self.assertEqual("foo", received_routing_key) def test_receiver_after_task_publish(self): expected_task_id = "00000000-0000-0000-0000-000000000000" expected_task_name = "Foo" headers = {"id": expected_task_id, "task": expected_task_name} receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_after_task_publish(headers=headers) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) self.assertEqual(expected_task_id, record.msg["child_task_id"]) self.assertIn("child_task_name", record.msg) self.assertEqual(expected_task_name, record.msg["child_task_name"]) def test_receiver_after_task_publish_protocol_v1(self): expected_task_id = "00000000-0000-0000-0000-000000000000" expected_task_name = "Foo" body = {"id": expected_task_id, "task": expected_task_name} receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_after_task_publish(body=body) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) self.assertEqual(expected_task_id, record.msg["child_task_id"]) self.assertIn("child_task_name", record.msg) self.assertEqual(expected_task_name, record.msg["child_task_name"]) def test_receiver_task_pre_run(self): expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" task = Mock() task.request = Mock() task.request.__django_structlog__ = { "request_id": expected_request_uuid, "user_id": expected_user_id, } task.name = "task_name" structlog.contextvars.bind_contextvars(foo="bar") context = structlog.contextvars.get_merged_contextvars(self.logger) self.assertDictEqual({"foo": "bar"}, context) receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_task_prerun(task_id, task) context = structlog.contextvars.get_merged_contextvars(self.logger) self.assertDictEqual( { "task_id": task_id, "request_id": expected_request_uuid, "user_id": expected_user_id, }, context, ) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_started", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("task", record.msg) self.assertEqual("task_name", record.msg["task"]) def test_signal_bind_extra_task_metadata(self): @django_receiver(signals.bind_extra_task_metadata) def receiver_bind_extra_request_metadata( sender, signal, task=None, logger=None ): structlog.contextvars.bind_contextvars( correlation_id=task.request.correlation_id ) expected_correlation_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" task = Mock() task.request = Mock() task.request.correlation_id = expected_correlation_uuid structlog.contextvars.bind_contextvars(foo="bar") context = structlog.contextvars.get_merged_contextvars(self.logger) self.assertDictEqual({"foo": "bar"}, context) receiver = receivers.CeleryReceiver() receiver.receiver_task_prerun(task_id, task) context = structlog.contextvars.get_merged_contextvars(self.logger) self.assertEqual(context["correlation_id"], expected_correlation_uuid) self.assertEqual(context["task_id"], task_id) def test_receiver_task_retry(self): expected_reason = "foo" receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.WARNING ) as log_results: receiver.receiver_task_retry(reason=expected_reason) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_retrying", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("reason", record.msg) self.assertEqual(expected_reason, record.msg["reason"]) def test_receiver_task_success(self): expected_result = "foo" @django_receiver(signals.pre_task_succeeded) def receiver_pre_task_succeeded( sender, signal, task=None, logger=None, result=None ): structlog.contextvars.bind_contextvars(result=result) receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_task_success(result=expected_result) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_succeeded", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("result", record.msg) self.assertEqual(expected_result, record.msg["result"]) def test_receiver_task_failure(self): expected_exception = "foo" receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.ERROR ) as log_results: receiver.receiver_task_failure(exception=Exception("foo")) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_failed", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("error", record.msg) self.assertIn("exception", record.msg) self.assertEqual(expected_exception, record.msg["error"]) def test_receiver_task_failure_with_throws(self): expected_exception = "foo" mock_sender = Mock() mock_sender.throws = (Exception,) receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_task_failure( exception=Exception("foo"), sender=mock_sender ) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_failed", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("error", record.msg) self.assertNotIn("exception", record.msg) self.assertEqual(expected_exception, record.msg["error"]) def test_receiver_task_revoked(self): expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" expected_task_name = "task_name" request = Mock() request.__django_structlog__ = { "request_id": expected_request_uuid, "user_id": expected_user_id, } request.task = expected_task_name request.id = task_id receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.WARNING ) as log_results: receiver.receiver_task_revoked( request=request, terminated=False, signum=None, expired=False ) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_revoked", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("terminated", record.msg) self.assertFalse(record.msg["terminated"]) self.assertIn("signum", record.msg) self.assertIsNone(record.msg["signum"]) self.assertIn("signame", record.msg) self.assertIsNone(record.msg["signame"]) self.assertIn("expired", record.msg) self.assertFalse(record.msg["expired"]) self.assertIn("task_id", record.msg) self.assertEqual(task_id, record.msg["task_id"]) self.assertIn("task", record.msg) self.assertEqual(expected_task_name, record.msg["task"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_request_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertEqual(expected_user_id, record.msg["user_id"]) def test_receiver_task_revoked_terminated(self): expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" expected_task_name = "task_name" request = Mock() request.__django_structlog__ = { "request_id": expected_request_uuid, "user_id": expected_user_id, } request.task = expected_task_name request.id = task_id receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.WARNING ) as log_results: receiver.receiver_task_revoked( request=request, terminated=True, signum=SIGTERM, expired=False ) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_revoked", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("terminated", record.msg) self.assertTrue(record.msg["terminated"]) self.assertIn("signum", record.msg) self.assertEqual(15, record.msg["signum"]) self.assertIn("signame", record.msg) self.assertEqual("SIGTERM", record.msg["signame"]) self.assertIn("expired", record.msg) self.assertFalse(record.msg["expired"]) self.assertIn("task_id", record.msg) self.assertEqual(task_id, record.msg["task_id"]) self.assertIn("task", record.msg) self.assertEqual(expected_task_name, record.msg["task"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_request_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertEqual(expected_user_id, record.msg["user_id"]) def test_receiver_task_unknown(self): task_id = "11111111-1111-1111-1111-111111111111" expected_task_name = "task_name" receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.ERROR ) as log_results: receiver.receiver_task_unknown(id=task_id, name=expected_task_name) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_not_found", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("task_id", record.msg) self.assertEqual(task_id, record.msg["task_id"]) self.assertIn("task", record.msg) self.assertEqual(expected_task_name, record.msg["task"]) def test_receiver_task_rejected(self): task_id = "11111111-1111-1111-1111-111111111111" message = Mock(name="message") message.properties = dict(correlation_id=task_id) receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.ERROR ) as log_results: receiver.receiver_task_rejected(message=message) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_rejected", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("task_id", record.msg) self.assertEqual(task_id, record.msg["task_id"]) def test_priority(self): expected_uuid = "00000000-0000-0000-0000-000000000000" user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" expected_routing_key = "foo" expected_priority = 6 properties = {"priority": expected_priority} headers = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=user_id, task_id=expected_parent_task_uuid, ) receiver = receivers.CeleryReceiver() receiver.receiver_before_task_publish( headers=headers, routing_key=expected_routing_key, properties=properties, ) self.assertDictEqual( { "__django_structlog__": { "user_id": user_id, "request_id": expected_uuid, "parent_task_id": expected_parent_task_uuid, } }, headers, "Only `request_id` and `parent_task_id` are preserved", ) expected_task_id = "00000000-0000-0000-0000-000000000000" expected_task_name = "Foo" headers = {"id": expected_task_id, "task": expected_task_name} with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO ) as log_results: receiver.receiver_after_task_publish( headers=headers, routing_key=expected_routing_key ) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) self.assertEqual(expected_task_id, record.msg["child_task_id"]) self.assertIn("child_task_name", record.msg) self.assertEqual(expected_task_name, record.msg["child_task_name"]) self.assertIn("priority", record.msg) self.assertEqual(expected_priority, record.msg["priority"]) self.assertIn("routing_key", record.msg) self.assertEqual(expected_routing_key, record.msg["routing_key"]) class TestConnectCeleryTaskSignals(TestCase): def test_call(self): from celery.signals import ( before_task_publish, after_task_publish, task_prerun, task_retry, task_success, task_failure, task_revoked, task_unknown, task_rejected, ) from django_structlog.celery.receivers import CeleryReceiver receiver = CeleryReceiver() with patch( "celery.utils.dispatch.signal.Signal.connect", autospec=True ) as mock_connect: receiver.connect_worker_signals() mock_connect.assert_has_calls( [ call(before_task_publish, receiver.receiver_before_task_publish), call(after_task_publish, receiver.receiver_after_task_publish), call(task_prerun, receiver.receiver_task_prerun), call(task_retry, receiver.receiver_task_retry), call(task_success, receiver.receiver_task_success), call(task_failure, receiver.receiver_task_failure), call(task_revoked, receiver.receiver_task_revoked), call(task_unknown, receiver.receiver_task_unknown), call(task_rejected, receiver.receiver_task_rejected), ] ) class TestConnectCelerySignals(TestCase): def test_call(self): from celery.signals import before_task_publish, after_task_publish from django_structlog.celery.receivers import CeleryReceiver receiver = CeleryReceiver() with patch( "celery.utils.dispatch.signal.Signal.connect", autospec=True ) as mock_connect: receiver.connect_signals() mock_connect.assert_has_calls( [ call(before_task_publish, receiver.receiver_before_task_publish), call(after_task_publish, receiver.receiver_after_task_publish), ] ) django-structlog-8.1.0/test_app/tests/celery/test_steps.py000066400000000000000000000007451462421536000240320ustar00rootroot00000000000000from unittest.mock import patch from django.test import TestCase from django_structlog.celery import steps class TestDjangoStructLogInitStep(TestCase): def test_call(self): with patch( "django_structlog.celery.receivers.CeleryReceiver.connect_worker_signals", autospec=True, ) as mock_connect: step = steps.DjangoStructLogInitStep(None) mock_connect.assert_called_once() self.assertIsNotNone(step.receiver) django-structlog-8.1.0/test_app/tests/middlewares/000077500000000000000000000000001462421536000222725ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/middlewares/__init__.py000066400000000000000000000000001462421536000243710ustar00rootroot00000000000000django-structlog-8.1.0/test_app/tests/middlewares/test_request.py000066400000000000000000001237221462421536000254020ustar00rootroot00000000000000import asyncio import logging import traceback import uuid from unittest import mock from unittest.mock import patch, Mock, AsyncMock from django.contrib.auth.models import AnonymousUser, User from django.contrib.sites.models import Site from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import PermissionDenied from django.dispatch import receiver from django.http import ( Http404, HttpResponseNotFound, HttpResponseForbidden, HttpResponseServerError, StreamingHttpResponse, ) from django.test import TestCase, RequestFactory, override_settings import structlog from django_structlog import middlewares from django_structlog.middlewares.request import ( get_request_header, sync_streaming_content_wrapper, async_streaming_content_wrapper, ) from django_structlog.signals import ( bind_extra_request_metadata, bind_extra_request_finished_metadata, bind_extra_request_failed_metadata, ) class TestRequestMiddleware(TestCase): def setUp(self): self.factory = RequestFactory() self.logger = structlog.getLogger(__name__) self.log_results = None Site.objects.update_or_create( id=1, defaults={"domain": "testserver", "name": "django_structlog_demo_project"}, ) def tearDown(self): structlog.contextvars.clear_contextvars() def test_process_request_without_user(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") middleware = middlewares.RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) def test_process_request_with_null_user(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") request.user = None middleware = middlewares.RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) def test_process_request_anonymous(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) def test_process_request_user(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") mock_user = User.objects.create() request.user = mock_user middleware = middlewares.RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsInstance(record.msg["user_id"], int) self.assertEqual(mock_user.id, record.msg["user_id"]) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) def test_process_request_user_uuid(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") mock_user = mock.Mock() mock_user.pk = uuid.UUID(expected_uuid) request.user = mock_user middleware = middlewares.RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("user_id", record.msg) self.assertIsInstance(record.msg["user_id"], str) self.assertEqual(expected_uuid, record.msg["user_id"]) def test_process_request_user_without_id(self): mock_response = Mock() mock_response.status_code = 200 def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") class SimpleUser: pass request.user = SimpleUser() middleware = middlewares.RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) def test_log_user_in_request_finished(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" mock_user = User.objects.create() request = self.factory.get("/foo") def get_response(_response): request.user = mock_user return mock_response middleware = middlewares.RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("request_started", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertNotIn("user_id", record.msg) record = log_results.records[1] self.assertEqual("INFO", record.levelname) self.assertEqual("request_finished", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) def test_log_user_in_request_finished_with_exception(self): mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" mock_user = User.objects.create() request = self.factory.get("/foo") middleware = middlewares.RequestMiddleware(None) exception = Exception() def get_response(_response): request.user = mock_user try: raise exception except Exception as e: middleware.process_exception(request, e) self.exception_traceback = traceback.format_exc() return mock_response middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("request_started", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertNotIn("user_id", record.msg) record = log_results.records[1] self.assertEqual("ERROR", record.levelname) self.assertEqual("request_failed", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("exception", record.msg) self.assertEqual(self.exception_traceback.strip(), record.msg["exception"]) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) def test_signal_bind_extra_request_metadata(self): @receiver(bind_extra_request_metadata) def receiver_bind_extra_request_metadata( sender, signal, request=None, logger=None, log_kwargs=None, **kwargs, ): current_site = get_current_site(request) log_kwargs["request_started_log"] = "foo" structlog.contextvars.bind_contextvars(domain=current_site.domain) mock_response = Mock() mock_response.status_code = 200 def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") mock_user = User.objects.create(email="foo@example.com") request.user = mock_user middleware = middlewares.RequestMiddleware(get_response) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as django_structlog_results: middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual("testserver", record.msg["domain"]) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) self.assertEqual(2, len(django_structlog_results.records)) record = django_structlog_results.records[0] self.assertEqual("request_started", record.msg["event"]) self.assertEqual("foo", record.msg["request_started_log"]) record = django_structlog_results.records[1] self.assertEqual("request_finished", record.msg["event"]) self.assertNotIn("request_started_log", record.msg) def test_signal_bind_extra_request_finished_metadata(self): mock_response = Mock() mock_response.status_code = 200 @receiver(bind_extra_request_finished_metadata) def receiver_bind_extra_request_finished_metadata( sender, signal, request=None, logger=None, response=None, log_kwargs=None ): self.assertEqual(response, mock_response) current_site = get_current_site(request) log_kwargs["request_finished_log"] = "foo" structlog.contextvars.bind_contextvars(domain=current_site.domain) def get_response(_response): return mock_response request = self.factory.get("/foo") mock_user = User.objects.create(email="foo@example.com") request.user = mock_user middleware = middlewares.RequestMiddleware(get_response) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("event", record.msg) self.assertEqual("request_started", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertNotIn("domain", record.msg) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) record = log_results.records[1] self.assertEqual("INFO", record.levelname) self.assertIn("event", record.msg) self.assertEqual("request_finished", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertIn("domain", record.msg) self.assertEqual("testserver", record.msg["domain"]) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) self.assertIn("request_finished_log", record.msg) self.assertEqual("foo", record.msg["request_finished_log"]) def test_signal_bind_extra_request_failed_metadata(self): expected_exception = Exception() @receiver(bind_extra_request_failed_metadata) def receiver_bind_extra_request_failed_metadata( sender, signal, request=None, response=None, logger=None, exception=None, log_kwargs=None, ): self.assertEqual(exception, expected_exception) current_site = get_current_site(request) log_kwargs["request_failed_log"] = "foo" structlog.contextvars.bind_contextvars(domain=current_site.domain) request = self.factory.get("/foo") mock_user = User.objects.create(email="foo@example.com") request.user = mock_user middleware = middlewares.RequestMiddleware(None) mock_response = Mock() def get_response(_response): middleware.process_exception(request, expected_exception) return mock_response middleware.get_response = get_response with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("event", record.msg) self.assertEqual("request_started", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertNotIn("domain", record.msg) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) record = log_results.records[1] self.assertEqual("ERROR", record.levelname) self.assertIn("event", record.msg) self.assertEqual("request_failed", record.msg["event"]) self.assertIn("request_id", record.msg) self.assertIn("domain", record.msg) self.assertEqual("testserver", record.msg["domain"]) self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) self.assertIn("request_failed_log", record.msg) self.assertEqual("foo", record.msg["request_failed_log"]) def test_process_request_error(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(None) exception = Exception("This is an exception") def get_response(_response): """Simulate an exception""" try: raise exception except Exception as e: middleware.process_exception(request, e) self.exception_traceback = traceback.format_exc() middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( logging.getLogger("django_structlog"), logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) record = log_results.records[1] self.assertEqual("ERROR", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) self.assertIn("code", record.msg) self.assertEqual(record.msg["code"], 500) self.assertIn("exception", record.msg) self.assertEqual(self.exception_traceback.strip(), record.msg["exception"]) self.assertIn("request", record.msg) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) def test_process_request_403_are_processed_as_regular_requests(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(None) exception = PermissionDenied() def get_response(_response): """Simulate an exception""" middleware.process_exception(request, exception) return HttpResponseForbidden() middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( logging.getLogger("django_structlog"), logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) record = log_results.records[1] self.assertEqual("WARNING", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) self.assertIn("code", record.msg) self.assertEqual(record.msg["code"], 403) self.assertNotIn("exception", record.msg) self.assertIn("request", record.msg) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) def test_process_request_404_are_processed_as_regular_requests(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(None) exception = Http404() def get_response(_response): """Simulate an exception""" middleware.process_exception(request, exception) return HttpResponseNotFound() middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( logging.getLogger("django_structlog"), logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) record = log_results.records[1] self.assertEqual("WARNING", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) self.assertIn("code", record.msg) self.assertEqual(record.msg["code"], 404) self.assertNotIn("exception", record.msg) self.assertIn("request", record.msg) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) @override_settings(DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL=logging.INFO) def test_process_request_4XX_can_be_personalized(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(None) exception = Http404() def get_response(_response): """Simulate an exception""" middleware.process_exception(request, exception) return HttpResponseNotFound() middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( logging.getLogger("django_structlog"), logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) record = log_results.records[1] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) self.assertIn("code", record.msg) self.assertEqual(record.msg["code"], 404) self.assertNotIn("exception", record.msg) self.assertIn("request", record.msg) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) def test_process_request_500_are_processed_as_errors(self): expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() middleware = middlewares.RequestMiddleware(None) def get_response(_response): return HttpResponseServerError() middleware.get_response = get_response with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( logging.getLogger("django_structlog"), logging.INFO ) as log_results: middleware(request) self.assertEqual(2, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) record = log_results.records[1] self.assertEqual("ERROR", record.levelname) self.assertIn("request_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) self.assertIn("code", record.msg) self.assertEqual(record.msg["code"], 500) self.assertNotIn("exception", record.msg) self.assertIn("request", record.msg) with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) def test_should_log_request_id_from_request_x_request_id_header(self): mock_response = Mock() mock_response.status_code = 200 x_request_id = "my-fake-request-id" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = RequestFactory(HTTP_X_REQUEST_ID=x_request_id).get("/foo") middleware = middlewares.RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertEqual(x_request_id, record.msg["request_id"]) def test_should_log_correlation_id_from_request_x_correlation_id_header(self): mock_response = Mock() mock_response.status_code = 200 x_correlation_id = "my-fake-correlation-id" def get_response(_response): with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = RequestFactory(HTTP_X_CORRELATION_ID=x_correlation_id).get("/foo") middleware = middlewares.RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) self.assertEqual(x_correlation_id, record.msg["correlation_id"]) def test_sync_streaming_response(self): def streaming_content(): yield mock_response = mock.create_autospec(StreamingHttpResponse) mock_response.streaming_content = streaming_content() mock_response.status_code = 200 def get_response(_response): return mock_response request = RequestFactory().get("/foo") middleware = middlewares.RequestMiddleware(get_response) mock_wrapper = AsyncMock() with patch( "django_structlog.middlewares.request.sync_streaming_content_wrapper", return_value=mock_wrapper, ) as mock_sync_streaming_response_wrapper: response = middleware(request) mock_sync_streaming_response_wrapper.assert_called_once() self.assertEqual(response.streaming_content, mock_wrapper) def test_async_streaming_response(self): async def streaming_content(): yield mock_response = mock.create_autospec(StreamingHttpResponse) mock_response.streaming_content = streaming_content() mock_response.status_code = 200 def get_response(_response): return mock_response request = RequestFactory().get("/foo") middleware = middlewares.RequestMiddleware(get_response) mock_wrapper = AsyncMock() with patch( "django_structlog.middlewares.request.async_streaming_content_wrapper", return_value=mock_wrapper, ) as mock_sync_streaming_response_wrapper: response = middleware(request) mock_sync_streaming_response_wrapper.assert_called_once() self.assertEqual(response.streaming_content, mock_wrapper) async def test_async_cancel(self): async def async_get_response(request): raise asyncio.CancelledError middleware = middlewares.RequestMiddleware(async_get_response) mock_request = Mock() with patch( "django_structlog.middlewares.request.RequestMiddleware.prepare" ) as mock_prepare, patch( "django_structlog.middlewares.request.RequestMiddleware.handle_response" ) as mock_handle_response, self.assertLogs( "django_structlog.middlewares.request", logging.WARNING ) as log_results: with self.assertRaises(asyncio.CancelledError): await middleware(mock_request) mock_prepare.assert_called_once_with(mock_request) mock_handle_response.assert_not_called() self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("request_cancelled", record.msg["event"]) class TestRequestMiddlewareRouter(TestCase): async def test_async(self): mock_response = Mock() async def async_get_response(request): return mock_response middleware = middlewares.RequestMiddleware(async_get_response) mock_request = Mock() with patch( "django_structlog.middlewares.request.RequestMiddleware.prepare" ) as mock_prepare, patch( "django_structlog.middlewares.request.RequestMiddleware.handle_response" ) as mock_handle_response: response = await middleware(mock_request) self.assertEqual(response, mock_response) mock_prepare.assert_called_once_with(mock_request) mock_handle_response.assert_called_once_with(mock_request, mock_response) def test_sync(self): mock_response = Mock() def get_response(request): return mock_response middleware = middlewares.RequestMiddleware(get_response) mock_request = Mock() with patch( "django_structlog.middlewares.request.RequestMiddleware.prepare" ) as mock_prepare, patch( "django_structlog.middlewares.request.RequestMiddleware.handle_response" ) as mock_handle_response: response = middleware(mock_request) self.assertEqual(response, mock_response) mock_prepare.assert_called_once_with(mock_request) mock_handle_response.assert_called_once_with(mock_request, mock_response) class TestGetRequestHeader(TestCase): def test_django_22_or_higher(self): mock_request = mock.MagicMock(spec=["headers"]) get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") mock_request.headers.get.assert_called_once_with("x-foo-bar") def test_django_prior_to_22(self): mock_request = mock.MagicMock(spec=["META"]) get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") mock_request.META.get.assert_called_once_with("HTTP_X_FOO_BAR") class TestSyncStreamingContentWrapper(TestCase): def setUp(self): self.logger = structlog.getLogger(__name__) def test_success(self): result = Mock() def streaming_content(): self.logger.info("streaming_content") yield result wrapped_streaming_content = sync_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) with self.assertLogs( __name__, logging.INFO ) as streaming_content_log_results, self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertEqual(result, next(wrapped_streaming_content)) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertRaises(StopIteration, next, wrapped_streaming_content) self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) self.assertEqual(1, len(streaming_content_log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_finished", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) def test_failure(self): result = Mock() exception = Exception() def streaming_content(): self.logger.info("streaming_content") yield result raise exception wrapped_streaming_content = sync_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) with self.assertLogs( __name__, logging.INFO ) as streaming_content_log_results, self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertEqual(result, next(wrapped_streaming_content)) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertRaises(Exception, next, wrapped_streaming_content) self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) self.assertEqual(1, len(streaming_content_log_results.records)) record = log_results.records[0] self.assertEqual("ERROR", record.levelname) self.assertEqual("streaming_failed", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) class TestASyncStreamingContentWrapper(TestCase): def setUp(self): self.logger = structlog.getLogger(__name__) async def test_success(self): result = Mock() async def streaming_content(): self.logger.info("streaming_content") yield result wrapped_streaming_content = async_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) with self.assertLogs( __name__, logging.INFO ) as streaming_content_log_results, self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertEqual(result, await wrapped_streaming_content.__anext__()) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: with self.assertRaises(StopAsyncIteration): await wrapped_streaming_content.__anext__() self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) self.assertEqual(1, len(streaming_content_log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_finished", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) async def test_failure(self): result = Mock() exception = Exception() async def streaming_content(): self.logger.info("streaming_content") yield result raise exception wrapped_streaming_content = async_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) with self.assertLogs( __name__, logging.INFO ) as streaming_content_log_results, self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: self.assertEqual(result, await wrapped_streaming_content.__anext__()) self.assertEqual(1, len(log_results.records)) record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: with self.assertRaises(StopAsyncIteration): await wrapped_streaming_content.__anext__() self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) self.assertEqual(1, len(streaming_content_log_results.records)) record = log_results.records[0] self.assertEqual("ERROR", record.levelname) self.assertEqual("streaming_failed", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) async def test_cancel(self): result = Mock() exception = asyncio.CancelledError() async def streaming_content(): self.logger.info("streaming_content") yield result raise exception wrapped_streaming_content = async_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) with self.assertLogs(__name__, logging.INFO) as streaming_content_log_results: self.assertEqual(result, await wrapped_streaming_content.__anext__()) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: with self.assertRaises(asyncio.CancelledError): await wrapped_streaming_content.__anext__() self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) self.assertEqual(1, len(streaming_content_log_results.records)) record = log_results.records[0] self.assertEqual("WARNING", record.levelname) self.assertEqual("streaming_cancelled", record.msg["event"]) self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) django-structlog-8.1.0/test_app/tests/test_app_settings.py000066400000000000000000000010051462421536000240770ustar00rootroot00000000000000from django.test import TestCase from django_structlog import app_settings class TestAppSettings(TestCase): def test_celery_enabled(self): settings = app_settings.AppSettings() with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True): self.assertTrue(settings.CELERY_ENABLED) def test_celery_disabled(self): settings = app_settings.AppSettings() with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False): self.assertFalse(settings.CELERY_ENABLED) django-structlog-8.1.0/test_app/tests/test_apps.py000066400000000000000000000051441462421536000223520ustar00rootroot00000000000000from unittest.mock import patch, create_autospec from django.test import TestCase from django_structlog import apps, commands from django_structlog.celery import receivers class TestAppConfig(TestCase): def test_celery_enabled(self): app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) mock_receiver = create_autospec(spec=receivers.CeleryReceiver) with patch( "django_structlog.celery.receivers.CeleryReceiver", return_value=mock_receiver, ): with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True): app.ready() mock_receiver.connect_signals.assert_called_once() self.assertTrue(hasattr(app, "_celery_receiver")) self.assertIsNotNone(app._celery_receiver) def test_celery_disabled(self): app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) mock_receiver = create_autospec(spec=receivers.CeleryReceiver) with patch( "django_structlog.celery.receivers.CeleryReceiver", return_value=mock_receiver, ): with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False): app.ready() mock_receiver.connect_signals.assert_not_called() self.assertFalse(hasattr(app, "_celery_receiver")) def test_command_enabled(self): app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver) with patch( "django_structlog.commands.DjangoCommandReceiver", return_value=mock_receiver, ): with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=True): app.ready() mock_receiver.connect_signals.assert_called_once() self.assertTrue(hasattr(app, "_django_command_receiver")) self.assertIsNotNone(app._django_command_receiver) def test_command_disabled(self): app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver) with patch( "django_structlog.commands.DjangoCommandReceiver", return_value=mock_receiver, ): with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=False): app.ready() mock_receiver.connect_signals.assert_not_called() self.assertFalse(hasattr(app, "_django_command_receiver")) django-structlog-8.1.0/test_app/tests/test_commands.py000066400000000000000000000103471462421536000232110ustar00rootroot00000000000000import logging import structlog from django.test import TestCase from django.core.management import BaseCommand, call_command from django_extensions.management.utils import signalcommand class TestCommands(TestCase): def test_command(self): class Command(BaseCommand): @signalcommand def handle(self, *args, **options): structlog.getLogger("command").info("command_event") with self.assertLogs( "command", logging.INFO ) as command_log_results, self.assertLogs( "django_structlog.commands", logging.INFO ) as django_structlog_commands_log_results: call_command(Command()) self.assertEqual(1, len(command_log_results.records)) record = command_log_results.records[0] self.assertEqual("command_event", record.msg["event"]) self.assertIn("command_id", record.msg) self.assertEqual(2, len(django_structlog_commands_log_results.records)) record = django_structlog_commands_log_results.records[0] self.assertEqual("command_started", record.msg["event"]) self.assertIn("command_id", record.msg) record = django_structlog_commands_log_results.records[1] self.assertEqual("command_finished", record.msg["event"]) self.assertIn("command_id", record.msg) def test_nested_command(self): class Command(BaseCommand): @signalcommand def handle(self, *args, **options): logger = structlog.getLogger("command") logger.info("command_event_1") call_command(NestedCommand()) logger.info("command_event_2") class NestedCommand(BaseCommand): @signalcommand def handle(self, *args, **options): structlog.getLogger("nested_command").info("nested_command_event") with self.assertLogs( "command", logging.INFO ) as command_log_results, self.assertLogs( "nested_command", logging.INFO ), self.assertLogs( "django_structlog.commands", logging.INFO ) as django_structlog_commands_log_results: call_command(Command()) self.assertEqual(2, len(command_log_results.records)) command_event_1 = command_log_results.records[0] self.assertEqual("command_event_1", command_event_1.msg["event"]) self.assertIn("command_id", command_event_1.msg) command_event_2 = command_log_results.records[1] self.assertEqual("command_event_2", command_event_2.msg["event"]) self.assertIn("command_id", command_event_2.msg) self.assertEqual( command_event_1.msg["command_id"], command_event_2.msg["command_id"] ) self.assertEqual(4, len(django_structlog_commands_log_results.records)) command_started_1 = django_structlog_commands_log_results.records[0] self.assertEqual("command_started", command_started_1.msg["event"]) self.assertIn("command_id", command_started_1.msg) command_started_2 = django_structlog_commands_log_results.records[1] self.assertEqual("command_started", command_started_2.msg["event"]) self.assertIn("command_id", command_started_2.msg) self.assertIn("parent_command_id", command_started_2.msg) self.assertEqual( command_started_1.msg["command_id"], command_started_2.msg["parent_command_id"], ) command_finished_1 = django_structlog_commands_log_results.records[2] self.assertEqual("command_finished", command_finished_1.msg["event"]) self.assertIn("command_id", command_finished_1.msg) self.assertIn("parent_command_id", command_finished_1.msg) self.assertEqual( command_started_1.msg["command_id"], command_finished_1.msg["parent_command_id"], ) command_finished_2 = django_structlog_commands_log_results.records[3] self.assertEqual("command_finished", command_finished_2.msg["event"]) self.assertIn("command_id", command_finished_2.msg) self.assertNotIn("parent_command_id", command_finished_2.msg) self.assertEqual( command_event_1.msg["command_id"], command_finished_2.msg["command_id"] )