├── .dockerignore ├── .editorconfig ├── .envs └── .local │ ├── .amqp │ ├── .django │ ├── .postgres │ └── .redis ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── gh.yml │ ├── main.yml │ └── pre-commit.yml ├── .gitignore ├── .idea ├── django-structlog.iml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── modules.xml └── vcs.xml ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── compose └── local │ ├── django │ ├── Dockerfile │ ├── celery │ │ ├── beat │ │ │ └── start │ │ ├── flower │ │ │ └── start │ │ └── worker │ │ │ └── start │ ├── entrypoint │ ├── start │ ├── start_asgi │ └── start_wsgi │ ├── docs │ ├── Dockerfile │ └── start │ └── postgres │ ├── Dockerfile │ └── maintenance │ ├── _sourced │ ├── constants.sh │ ├── countdown.sh │ ├── messages.sh │ └── yes_no.sh │ ├── backup │ ├── backups │ └── restore ├── config ├── __init__.py ├── asgi.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── local.py │ ├── test.py │ └── test_demo_app.py ├── urls.py └── wsgi.py ├── django_structlog ├── __init__.py ├── app_settings.py ├── apps.py ├── celery │ ├── __init__.py │ ├── receivers.py │ ├── signals.py │ └── steps.py ├── commands.py ├── middlewares │ ├── __init__.py │ └── request.py ├── py.typed └── signals.py ├── django_structlog_demo_project ├── __init__.py ├── command_examples │ ├── __init__.py │ ├── apps.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── example_command.py │ │ │ └── example_nested_command.py │ └── tests │ │ ├── __init__.py │ │ └── test_commands.py ├── conftest.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ ├── 0004_alter_site_options.py │ │ └── __init__.py ├── home │ ├── __init__.py │ ├── api_views.py │ ├── apps.py │ ├── ninja_views.py │ ├── static │ │ └── js │ │ │ └── home.js │ ├── tests │ │ ├── __init__.py │ │ ├── test_api_views.py │ │ ├── test_ninja_views.py │ │ └── test_views.py │ └── views.py ├── static │ ├── css │ │ └── project.css │ ├── fonts │ │ └── .gitkeep │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ ├── js │ │ └── project.js │ └── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss ├── taskapp │ ├── __init__.py │ ├── celery.py │ └── tests │ │ ├── __init__.py │ │ └── test_celery.py ├── templates │ ├── 403_csrf.html │ ├── 404.html │ ├── 500.html │ ├── account │ │ ├── account_inactive.html │ │ ├── base.html │ │ ├── email.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ ├── logout.html │ │ ├── password_change.html │ │ ├── password_reset.html │ │ ├── password_reset_done.html │ │ ├── password_reset_from_key.html │ │ ├── password_reset_from_key_done.html │ │ ├── password_set.html │ │ ├── signup.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ └── verified_email_required.html │ ├── base.html │ ├── pages │ │ ├── about.html │ │ └── home.html │ └── users │ │ ├── user_detail.html │ │ ├── user_form.html │ │ └── user_list.html └── users │ ├── __init__.py │ ├── adapters.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_user_first_name.py │ └── __init__.py │ ├── models.py │ ├── signals.py │ ├── tests │ ├── __init__.py │ ├── factories.py │ ├── test_adapters.py │ ├── test_forms.py │ ├── test_models.py │ ├── test_urls.py │ └── test_views.py │ ├── urls.py │ └── views.py ├── docker-compose.amqp.yml ├── docker-compose.docs.yml ├── docker-compose.yml ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── acknowledgements.rst ├── api_documentation.rst ├── authors.rst ├── celery.rst ├── changelog.rst ├── commands.rst ├── conf.py ├── configuration.rst ├── demo.rst ├── development.rst ├── events.rst ├── example_outputs.rst ├── getting_started.rst ├── how_tos.rst ├── index.rst ├── licence.rst ├── make.bat ├── requirements.txt ├── running_tests.rst └── upgrade_guide.rst ├── logs └── .gitkeep ├── manage.py ├── pyproject.toml ├── requirements.txt ├── requirements ├── black.txt ├── ci.txt ├── coverage.txt ├── deployment.txt ├── isort.txt ├── local-base.txt ├── local.txt ├── mypy.txt └── ruff.txt └── test_app ├── __init__.py ├── apps.py ├── migrations └── __init__.py └── tests ├── __init__.py ├── celery ├── __init__.py ├── test_receivers.py └── test_steps.py ├── middlewares ├── __init__.py └── test_request.py ├── test_app_settings.py ├── test_apps.py └── test_commands.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.env 3 | !.pylintrc 4 | /logs/ 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{html,css,js}] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.envs/.local/.amqp: -------------------------------------------------------------------------------- 1 | # RabbitMQ 2 | # ------------------------------------------------------------------------------ 3 | CELERY_BROKER_URL=amqp://admin:unsecure-password@rabbit:5672 4 | -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | 6 | # Celery 7 | # ------------------------------------------------------------------------------ 8 | CELERY_RESULT_BACKEND="redis://redis:6379/0" 9 | 10 | # Flower 11 | CELERY_FLOWER_USER=debug 12 | CELERY_FLOWER_PASSWORD=debug 13 | 14 | -------------------------------------------------------------------------------- /.envs/.local/.postgres: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | # ------------------------------------------------------------------------------ 3 | POSTGRES_HOST=postgres 4 | POSTGRES_PORT=5432 5 | POSTGRES_DB=django_structlog_demo_project 6 | POSTGRES_USER=debug 7 | POSTGRES_PASSWORD=debug 8 | -------------------------------------------------------------------------------- /.envs/.local/.redis: -------------------------------------------------------------------------------- 1 | # Redis 2 | # ------------------------------------------------------------------------------ 3 | CELERY_BROKER_URL=redis://redis:6379/0 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: pip 5 | directory: "/requirements" 6 | schedule: 7 | interval: weekly 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | - package-ecosystem: pip 13 | directory: "/docs" 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.github/workflows/gh.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | on: 3 | workflow_run: 4 | workflows: ["ci"] 5 | branches: [main] 6 | types: 7 | - completed 8 | jobs: 9 | deploy-gh-pages: 10 | concurrency: ci-${{ github.ref }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -r docs/requirements.txt 19 | pip install ghp-import 20 | - name: make html 21 | working-directory: docs 22 | run: make html 23 | - name: Deploy Documentation 24 | run: | 25 | pip install ghp-import 26 | ghp-import -n -p -m "Documentation git hash $(git rev-parse --short HEAD)" -f docs/_build/html 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - release/** 7 | tags: 8 | - '[0-9]+.[0-9]+.[0-9]+' 9 | - '[0-9]+.[0-9]+.[0-9]+.dev[0-9]+' 10 | pull_request: {} 11 | 12 | jobs: 13 | test-demo-app: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: 18 | - '3.13' 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | cache: pip 26 | - name: Install dependencies 27 | run: | 28 | pip install --upgrade pip 29 | pip install -r requirements.txt 30 | - name: Start Redis 31 | uses: supercharge/redis-github-action@1.8.0 32 | - name: Test demo app 33 | env: 34 | CELERY_BROKER_URL: redis://0.0.0.0:6379 35 | DJANGO_SETTINGS_MODULE: config.settings.test_demo_app 36 | run: pytest --cov=./django_structlog_demo_project --cov-append django_structlog_demo_project 37 | - uses: codecov/codecov-action@v5 38 | with: 39 | token: ${{ secrets.CODECOV_TOKEN }} 40 | fail_ci_if_error: false # disable for the moment because it prevents PR to succeed 41 | test: 42 | runs-on: ubuntu-latest 43 | strategy: 44 | matrix: 45 | python-version: 46 | - '3.9' 47 | - '3.10' 48 | - '3.11' 49 | - '3.12' 50 | - '3.13' 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Python ${{ matrix.python-version }} 54 | uses: actions/setup-python@v5 55 | with: 56 | python-version: ${{ matrix.python-version }} 57 | cache: pip 58 | - name: Install dependencies 59 | run: | 60 | python -m pip install --upgrade pip 61 | python -m pip install -U setuptools 62 | python -m pip install tox tox-gh-actions -r requirements/coverage.txt 63 | - name: Start Redis 64 | uses: supercharge/redis-github-action@1.8.0 65 | - name: Test with tox 66 | run: tox 67 | - uses: codecov/codecov-action@v5 68 | with: 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | fail_ci_if_error: false # disable for the moment because it prevents PR to succeed 71 | 72 | test-docs: 73 | needs: 74 | - test-demo-app 75 | - test 76 | runs-on: ubuntu-latest 77 | strategy: 78 | matrix: 79 | python-version: 80 | - '3.13' 81 | steps: 82 | - uses: actions/checkout@v4 83 | - name: Install dependencies 84 | run: | 85 | python -m pip install --upgrade pip 86 | pip install -r docs/requirements.txt 87 | - name: make html 88 | working-directory: docs 89 | run: make html 90 | - name: make doctest 91 | working-directory: docs 92 | run: make doctest 93 | 94 | black: 95 | needs: 96 | - test-demo-app 97 | - test 98 | runs-on: ubuntu-latest 99 | strategy: 100 | matrix: 101 | python-version: 102 | - '3.13' 103 | steps: 104 | - uses: actions/checkout@v4 105 | - name: Install dependencies 106 | run: | 107 | python -m pip install --upgrade pip 108 | pip install -r requirements/black.txt 109 | - name: run black 110 | run: black --check --verbose . 111 | 112 | ruff: 113 | needs: 114 | - test-demo-app 115 | - test 116 | runs-on: ubuntu-latest 117 | strategy: 118 | matrix: 119 | python-version: 120 | - '3.13' 121 | steps: 122 | - uses: actions/checkout@v4 123 | - name: Install dependencies 124 | run: | 125 | python -m pip install --upgrade pip 126 | pip install -r requirements/ruff.txt 127 | - name: run ruff 128 | run: ruff check . 129 | 130 | mypy: 131 | needs: 132 | - test-demo-app 133 | - test 134 | runs-on: ubuntu-latest 135 | strategy: 136 | matrix: 137 | python-version: 138 | - '3.13' 139 | steps: 140 | - uses: actions/checkout@v4 141 | - name: Install dependencies 142 | run: | 143 | python -m pip install --upgrade pip 144 | pip install -r requirements/local.txt -r requirements/mypy.txt 145 | - name: run mypy 146 | run: mypy 147 | 148 | isort: 149 | needs: 150 | - test-demo-app 151 | - test 152 | runs-on: ubuntu-latest 153 | steps: 154 | - uses: actions/checkout@v4 155 | - uses: isort/isort-action@v1 156 | with: 157 | requirements-files: "requirements/local-base.txt requirements/isort.txt" 158 | 159 | test-install-base: 160 | runs-on: ubuntu-latest 161 | strategy: 162 | matrix: 163 | python-version: 164 | - '3.13' 165 | steps: 166 | - uses: actions/checkout@v4 167 | - name: Set up Python ${{ matrix.python-version }} 168 | uses: actions/setup-python@v5 169 | with: 170 | python-version: ${{ matrix.python-version }} 171 | cache: pip 172 | - name: Install dependencies 173 | run: | 174 | python -m pip install --upgrade pip 175 | python -m pip install -U setuptools 176 | python -m pip install . 177 | - name: Import django_structlog modules 178 | run: | 179 | python -c "import django_structlog" 180 | python -c "import django_structlog.apps" 181 | python -c "import django_structlog.signals" 182 | python -c "import django_structlog.app_settings" 183 | python -c "import django_structlog.middlewares.request" 184 | 185 | test-install-celery: 186 | runs-on: ubuntu-latest 187 | strategy: 188 | matrix: 189 | python-version: 190 | - '3.13' 191 | steps: 192 | - uses: actions/checkout@v4 193 | - name: Set up Python ${{ matrix.python-version }} 194 | uses: actions/setup-python@v5 195 | with: 196 | python-version: ${{ matrix.python-version }} 197 | cache: pip 198 | - name: Install dependencies 199 | run: | 200 | python -m pip install --upgrade pip 201 | python -m pip install -U setuptools 202 | python -m pip install .[celery] 203 | - name: Import django_structlog modules 204 | run: | 205 | python -c "import django_structlog" 206 | python -c "import django_structlog.apps" 207 | python -c "import django_structlog.signals" 208 | python -c "import django_structlog.app_settings" 209 | python -c "import django_structlog.middlewares.request" 210 | - name: Import django_structlog celery modules 211 | run: | 212 | python -c "import django_structlog.celery.receivers" 213 | python -c "import django_structlog.celery.steps" 214 | python -c "import django_structlog.celery.signals" 215 | 216 | test-install-commands: 217 | runs-on: ubuntu-latest 218 | strategy: 219 | matrix: 220 | python-version: 221 | - '3.13' 222 | steps: 223 | - uses: actions/checkout@v4 224 | - name: Set up Python ${{ matrix.python-version }} 225 | uses: actions/setup-python@v5 226 | with: 227 | python-version: ${{ matrix.python-version }} 228 | cache: pip 229 | - name: Install dependencies 230 | run: | 231 | python -m pip install --upgrade pip 232 | python -m pip install -U setuptools 233 | python -m pip install .[commands] 234 | - name: Import django_structlog modules 235 | run: | 236 | python -c "import django_structlog" 237 | python -c "import django_structlog.apps" 238 | python -c "import django_structlog.signals" 239 | python -c "import django_structlog.app_settings" 240 | python -c "import django_structlog.middlewares.request" 241 | - name: Import django_structlog commands modules 242 | run: | 243 | python -c "import django_structlog.commands" 244 | 245 | pypi-deployment: 246 | needs: 247 | - test-docs 248 | - test-install-base 249 | - test-install-celery 250 | - test-install-commands 251 | - black 252 | - ruff 253 | - mypy 254 | - isort 255 | runs-on: ubuntu-latest 256 | environment: 257 | name: pypi 258 | url: https://pypi.org/p/django-structlog 259 | permissions: 260 | id-token: write 261 | steps: 262 | - uses: actions/checkout@v4 263 | - name: Install dependencies 264 | run: | 265 | python -m pip install --upgrade pip 266 | python -m pip install -U -q build 267 | - name: build sdist 268 | run: python -m build --sdist 269 | - name: build wheel 270 | run: python -m build --wheel 271 | - name: Publish package 272 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 273 | uses: pypa/gh-action-pypi-publish@release/v1 274 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit auto-update 2 | 3 | on: 4 | # every monday at 11am utc 5 | schedule: 6 | - cron: "0 11 * * 1" 7 | # on demand 8 | workflow_dispatch: 9 | 10 | jobs: 11 | auto-update: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.13 19 | 20 | - uses: browniebroke/pre-commit-autoupdate-action@v1.0.0 21 | 22 | - uses: peter-evans/create-pull-request@v7.0.8 23 | with: 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | branch: update/pre-commit-hooks 26 | title: Update pre-commit hooks 27 | commit-message: "chore: update pre-commit hooks" 28 | body: Update versions of pre-commit hooks to latest version. 29 | #base: ${{ github.head_ref }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | # celery beat schedule file 66 | celerybeat-schedule 67 | 68 | # Environments 69 | .venv 70 | venv/ 71 | ENV/ 72 | 73 | # Rope project settings 74 | .ropeproject 75 | 76 | # mkdocs documentation 77 | /site 78 | 79 | ### Node template 80 | # Logs 81 | logs 82 | *.log 83 | npm-debug.log* 84 | yarn-debug.log* 85 | yarn-error.log* 86 | 87 | # Runtime data 88 | pids 89 | *.pid 90 | *.seed 91 | *.pid.lock 92 | 93 | # Directory for instrumented libs generated by jscoverage/JSCover 94 | lib-cov 95 | 96 | # Coverage directory used by tools like istanbul 97 | coverage 98 | 99 | # nyc test coverage 100 | .nyc_output 101 | 102 | # Bower dependency directory (https://bower.io/) 103 | bower_components 104 | 105 | # node-waf configuration 106 | .lock-wscript 107 | 108 | # Compiled binary addons (http://nodejs.org/api/addons.html) 109 | build/Release 110 | 111 | # Dependency directories 112 | node_modules/ 113 | jspm_packages/ 114 | 115 | # Typescript v1 declaration files 116 | typings/ 117 | 118 | # Optional npm cache directory 119 | .npm 120 | 121 | # Optional eslint cache 122 | .eslintcache 123 | 124 | # Optional REPL history 125 | .node_repl_history 126 | 127 | # Output of 'npm pack' 128 | *.tgz 129 | 130 | # Yarn Integrity file 131 | .yarn-integrity 132 | 133 | 134 | ### Linux template 135 | *~ 136 | 137 | # temporary files which can be created if a process still has a handle open of a deleted file 138 | .fuse_hidden* 139 | 140 | # KDE directory preferences 141 | .directory 142 | 143 | # Linux trash folder which might appear on any partition or disk 144 | .Trash-* 145 | 146 | # .nfs files are created when an open file is removed but is still being accessed 147 | .nfs* 148 | 149 | 150 | ### VisualStudioCode template 151 | .vscode/* 152 | !.vscode/settings.json 153 | !.vscode/tasks.json 154 | !.vscode/launch.json 155 | !.vscode/extensions.json 156 | 157 | 158 | # Provided default Pycharm Run/Debug Configurations should be tracked by git 159 | # In case of local modifications made by Pycharm, use update-index command 160 | # for each changed file, like this: 161 | # git update-index --assume-unchanged .idea/django_structlog_demo_project.iml 162 | ### JetBrains template 163 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 164 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 165 | 166 | # User-specific stuff: 167 | .idea/misc.xml 168 | .idea/google-java-format.xml 169 | .idea/**/workspace.xml 170 | .idea/**/tasks.xml 171 | .idea/dictionaries 172 | 173 | # Sensitive or high-churn files: 174 | .idea/**/dataSources/ 175 | .idea/**/dataSources.ids 176 | .idea/**/dataSources.xml 177 | .idea/**/dataSources.local.xml 178 | .idea/**/sqlDataSources.xml 179 | .idea/**/dynamic.xml 180 | .idea/**/uiDesigner.xml 181 | 182 | # Gradle: 183 | .idea/**/gradle.xml 184 | .idea/**/libraries 185 | 186 | # CMake 187 | cmake-build-debug/ 188 | 189 | # Mongo Explorer plugin: 190 | .idea/**/mongoSettings.xml 191 | 192 | ## File-based project format: 193 | *.iws 194 | 195 | ## Plugin-specific files: 196 | 197 | # IntelliJ 198 | out/ 199 | 200 | # mpeltonen/sbt-idea plugin 201 | .idea_modules/ 202 | 203 | # JIRA plugin 204 | atlassian-ide-plugin.xml 205 | 206 | # Cursive Clojure plugin 207 | .idea/replstate.xml 208 | 209 | # Crashlytics plugin (for Android Studio and IntelliJ) 210 | com_crashlytics_export_strings.xml 211 | crashlytics.properties 212 | crashlytics-build.properties 213 | fabric.properties 214 | 215 | 216 | 217 | ### Windows template 218 | # Windows thumbnail cache files 219 | Thumbs.db 220 | ehthumbs.db 221 | ehthumbs_vista.db 222 | 223 | # Dump file 224 | *.stackdump 225 | 226 | # Folder config file 227 | Desktop.ini 228 | 229 | # Recycle Bin used on file shares 230 | $RECYCLE.BIN/ 231 | 232 | # Windows Installer files 233 | *.cab 234 | *.msi 235 | *.msm 236 | *.msp 237 | 238 | # Windows shortcuts 239 | *.lnk 240 | 241 | 242 | ### macOS template 243 | # General 244 | *.DS_Store 245 | .AppleDouble 246 | .LSOverride 247 | 248 | # Icon must end with two \r 249 | Icon 250 | 251 | # Thumbnails 252 | ._* 253 | 254 | # Files that might appear in the root of a volume 255 | .DocumentRevisions-V100 256 | .fseventsd 257 | .Spotlight-V100 258 | .TemporaryItems 259 | .Trashes 260 | .VolumeIcon.icns 261 | .com.apple.timemachine.donotpresent 262 | 263 | # Directories potentially created on remote AFP share 264 | .AppleDB 265 | .AppleDesktop 266 | Network Trash Folder 267 | Temporary Items 268 | .apdisk 269 | 270 | 271 | ### SublimeText template 272 | # Cache files for Sublime Text 273 | *.tmlanguage.cache 274 | *.tmPreferences.cache 275 | *.stTheme.cache 276 | 277 | # Workspace files are user-specific 278 | *.sublime-workspace 279 | 280 | # Project files should be checked into the repository, unless a significant 281 | # proportion of contributors will probably not be using Sublime Text 282 | # *.sublime-project 283 | 284 | # SFTP configuration file 285 | sftp-config.json 286 | 287 | # Package control specific files 288 | Package Control.last-run 289 | Package Control.ca-list 290 | Package Control.ca-bundle 291 | Package Control.system-ca-bundle 292 | Package Control.cache/ 293 | Package Control.ca-certs/ 294 | Package Control.merged-ca-bundle 295 | Package Control.user-ca-bundle 296 | oscrypto-ca-bundle.crt 297 | bh_unicode_properties.cache 298 | 299 | # Sublime-github package stores a github token in this file 300 | # https://packagecontrol.io/packages/sublime-github 301 | GitHub.sublime-settings 302 | 303 | 304 | ### Vim template 305 | # Swap 306 | [._]*.s[a-v][a-z] 307 | [._]*.sw[a-p] 308 | [._]s[a-v][a-z] 309 | [._]sw[a-p] 310 | 311 | # Session 312 | Session.vim 313 | 314 | # Temporary 315 | .netrwhist 316 | 317 | # Auto-generated tag files 318 | tags 319 | 320 | 321 | ### Project template 322 | 323 | django_structlog_demo_project/media/ 324 | 325 | .pytest_cache/ 326 | 327 | 328 | .ipython/ 329 | .env 330 | .envs/* 331 | !.envs/.local/ 332 | .vscode/ 333 | -------------------------------------------------------------------------------- /.idea/django-structlog.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 39 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.11.11 4 | hooks: 5 | - id: ruff 6 | args: [--fix, --exit-non-zero-on-fix] 7 | - repo: https://github.com/pycqa/isort 8 | rev: 6.0.1 9 | hooks: 10 | - id: isort 11 | - repo: https://github.com/ambv/black 12 | rev: 25.1.0 13 | hooks: 14 | - id: black 15 | language_version: python3 16 | - repo: https://github.com/pre-commit/mirrors-mypy 17 | rev: v1.15.0 18 | hooks: 19 | - id: mypy 20 | args: [--no-incremental] 21 | additional_dependencies: [ 22 | celery-types==0.23.0, 23 | "django-stubs[compatible-mypy]==5.1.3", 24 | structlog==25.2.0, 25 | django-extensions==4.1, 26 | django-ipware==7.0.1, 27 | ] 28 | exclude: | 29 | (?x)( 30 | ^django_structlog_demo_project/| 31 | ^config/| 32 | ^docs/| 33 | ^manage.py 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-24.04 11 | tools: 12 | python: "3.13" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jules Robichaud-Gagnon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.rst 2 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | #syntax=docker/dockerfile:1.4 2 | ARG PYTHON_VERSION 3 | 4 | FROM python:${PYTHON_VERSION}-alpine as python 5 | 6 | FROM python AS python-build-stage 7 | 8 | ARG REQUIREMENTS_FILE=local.txt 9 | 10 | 11 | RUN <&2 echo 'Waiting for PostgreSQL to become available...' 41 | sleep 1 42 | done 43 | >&2 echo 'PostgreSQL is available' 44 | 45 | exec "$@" 46 | -------------------------------------------------------------------------------- /compose/local/django/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | python manage.py migrate 9 | python manage.py runserver_plus 0.0.0.0:8000 10 | -------------------------------------------------------------------------------- /compose/local/django/start_asgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | ASGI=true python -m uvicorn config.asgi:application --host=0.0.0.0 --reload 8 | -------------------------------------------------------------------------------- /compose/local/django/start_wsgi: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | WSGI=true gunicorn config.wsgi -b 0.0.0.0:8000 --reload 8 | -------------------------------------------------------------------------------- /compose/local/docs/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PYTHON_VERSION 2 | 3 | FROM python:${PYTHON_VERSION}-alpine as python 4 | 5 | FROM python AS python-build-stage 6 | 7 | RUN apk update \ 8 | && apk add --virtual build-dependencies \ 9 | build-base 10 | 11 | COPY ./docs/requirements.txt /requirements.txt 12 | 13 | RUN pip wheel --wheel-dir /usr/src/app/wheels \ 14 | -r /requirements.txt 15 | 16 | FROM python AS python-run-stage 17 | 18 | ENV PYTHONUNBUFFERED 1 19 | 20 | WORKDIR /app 21 | 22 | COPY --from=python-build-stage /usr/src/app/wheels /wheels/ 23 | 24 | RUN pip install --no-cache-dir --no-index --find-links=/wheels/ /wheels/* \ 25 | && rm -rf /wheels/ 26 | 27 | COPY ./compose/local/docs/start /start 28 | RUN sed -i 's/\r//' /start 29 | RUN chmod +x /start 30 | 31 | RUN addgroup --system django \ 32 | && adduser --system --ingroup django django 33 | RUN mkdir /docs 34 | RUN chown django:django /app 35 | RUN chown django:django /docs 36 | 37 | USER django 38 | -------------------------------------------------------------------------------- /compose/local/docs/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | # Basically we watch only README.rst, LICENCE.rst and everything under django_structlog 8 | sphinx-autobuild docs /docs/_build/html \ 9 | -b ${SPHINX_COMMAND} \ 10 | --port 8080 \ 11 | --host 0.0.0.0 \ 12 | --watch . \ 13 | --ignore "*___jb_*" \ 14 | --ignore ".*/*" \ 15 | --ignore ".*" \ 16 | --ignore "build/*" \ 17 | --ignore "compose/*" \ 18 | --ignore "config/*" \ 19 | --ignore "dist/*" \ 20 | --ignore "django_structlog.egg-info/*" \ 21 | --ignore "django_structlog_demo_project/*" \ 22 | --ignore "logs/*" \ 23 | --ignore "requirements/*" \ 24 | --ignore "requirements.txt" \ 25 | --ignore "docker*.yml" \ 26 | --ignore "manage.py" \ 27 | --ignore "MANIFEST.in" \ 28 | --ignore "*.toml" \ 29 | --ignore "*.log" \ 30 | --ignore "*.ini" 31 | -------------------------------------------------------------------------------- /compose/local/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:17 2 | 3 | COPY ./compose/local/postgres/maintenance /usr/local/bin/maintenance 4 | RUN chmod +x /usr/local/bin/maintenance/* 5 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ 6 | && rmdir /usr/local/bin/maintenance 7 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/constants.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | BACKUP_DIR_PATH='/backups' 5 | BACKUP_FILE_PREFIX='backup' 6 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/countdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | countdown() { 5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582" 6 | local seconds="${1}" 7 | local d=$(($(date +%s) + "${seconds}")) 8 | while [ "$d" -ge `date +%s` ]; do 9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; 10 | sleep 0.1 11 | done 12 | } 13 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/messages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | message_newline() { 5 | echo 6 | } 7 | 8 | message_debug() 9 | { 10 | echo -e "DEBUG: ${@}" 11 | } 12 | 13 | message_welcome() 14 | { 15 | echo -e "\e[1m${@}\e[0m" 16 | } 17 | 18 | message_warning() 19 | { 20 | echo -e "\e[33mWARNING\e[0m: ${@}" 21 | } 22 | 23 | message_error() 24 | { 25 | echo -e "\e[31mERROR\e[0m: ${@}" 26 | } 27 | 28 | message_info() 29 | { 30 | echo -e "\e[37mINFO\e[0m: ${@}" 31 | } 32 | 33 | message_suggestion() 34 | { 35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}" 36 | } 37 | 38 | message_success() 39 | { 40 | echo -e "\e[32mSUCCESS\e[0m: ${@}" 41 | } 42 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/_sourced/yes_no.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | yes_no() { 5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." 6 | local arg1="${1}" 7 | 8 | local response= 9 | read -r -p "${arg1} (y/[n])? " response 10 | if [[ "${response}" =~ ^[Yy]$ ]] 11 | then 12 | exit 0 13 | else 14 | exit 1 15 | fi 16 | } 17 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/backup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Create a database backup. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backup 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "Backing up the '${POSTGRES_DB}' database..." 21 | 22 | 23 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 24 | message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 25 | exit 1 26 | fi 27 | 28 | export PGHOST="${POSTGRES_HOST}" 29 | export PGPORT="${POSTGRES_PORT}" 30 | export PGUSER="${POSTGRES_USER}" 31 | export PGPASSWORD="${POSTGRES_PASSWORD}" 32 | export PGDATABASE="${POSTGRES_DB}" 33 | 34 | backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" 35 | pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" 36 | 37 | 38 | message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." 39 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/backups: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### View backups. 5 | ### 6 | ### Usage: 7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups 8 | 9 | 10 | set -o errexit 11 | set -o pipefail 12 | set -o nounset 13 | 14 | 15 | working_dir="$(dirname ${0})" 16 | source "${working_dir}/_sourced/constants.sh" 17 | source "${working_dir}/_sourced/messages.sh" 18 | 19 | 20 | message_welcome "These are the backups you have got:" 21 | 22 | ls -lht "${BACKUP_DIR_PATH}" 23 | -------------------------------------------------------------------------------- /compose/local/postgres/maintenance/restore: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | 4 | ### Restore database from a backup. 5 | ### 6 | ### Parameters: 7 | ### <1> filename of an existing backup. 8 | ### 9 | ### Usage: 10 | ### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> 11 | 12 | 13 | set -o errexit 14 | set -o pipefail 15 | set -o nounset 16 | 17 | 18 | working_dir="$(dirname ${0})" 19 | source "${working_dir}/_sourced/constants.sh" 20 | source "${working_dir}/_sourced/messages.sh" 21 | 22 | 23 | if [[ -z ${1+x} ]]; then 24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 25 | exit 1 26 | fi 27 | backup_filename="${BACKUP_DIR_PATH}/${1}" 28 | if [[ ! -f "${backup_filename}" ]]; then 29 | 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." 30 | exit 1 31 | fi 32 | 33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." 34 | 35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then 36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." 37 | exit 1 38 | fi 39 | 40 | export PGHOST="${POSTGRES_HOST}" 41 | export PGPORT="${POSTGRES_PORT}" 42 | export PGUSER="${POSTGRES_USER}" 43 | export PGPASSWORD="${POSTGRES_PASSWORD}" 44 | export PGDATABASE="${POSTGRES_DB}" 45 | 46 | message_info "Dropping the database..." 47 | dropdb "${PGDATABASE}" 48 | 49 | message_info "Creating a new database..." 50 | createdb --owner="${POSTGRES_USER}" 51 | 52 | message_info "Applying the backup to the new database..." 53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" 54 | 55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." 56 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/config/__init__.py -------------------------------------------------------------------------------- /config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_structlog_demo_project project. 3 | """ 4 | 5 | import os 6 | import sys 7 | 8 | from django.core.asgi import get_asgi_application 9 | 10 | # This allows easy placement of apps within the interior 11 | # django_structlog_demo_project directory. 12 | app_path = os.path.abspath( 13 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 14 | ) 15 | sys.path.append(os.path.join(app_path, "django_structlog_demo_project")) 16 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 17 | # if running multiple sites in the same mod_wsgi process. To fix this, use 18 | # mod_wsgi daemon mode with each site in its own daemon process, or use 19 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local" 20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 21 | 22 | # This application object is used by any WSGI server configured to use this 23 | # file. This includes Django's development server, if the WSGI_APPLICATION 24 | # setting points here. 25 | application = get_asgi_application() 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/config/settings/__init__.py -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | 3 | from .base import * # noqa: F403 4 | from .base import MIDDLEWARE, env 5 | 6 | # GENERAL 7 | # ------------------------------------------------------------------------------ 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 9 | DEBUG = env.bool("DJANGO_DEBUG", default=True) 10 | 11 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 12 | SECRET_KEY = env( 13 | "DJANGO_SECRET_KEY", 14 | default="DXatocQyyroxzcpo0tDxK3v5Rm4fatD9U7UeuLWwnZMOIaCQdPWovuqp4rxOct1T", 15 | ) 16 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 17 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] 18 | 19 | IS_WORKER = env.bool("IS_WORKER", default=False) 20 | 21 | # CACHES 22 | # ------------------------------------------------------------------------------ 23 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 24 | CACHES = { 25 | "default": { 26 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 27 | "LOCATION": "", 28 | } 29 | } 30 | 31 | # TEMPLATES 32 | # ------------------------------------------------------------------------------ 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 34 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405 35 | 36 | # EMAIL 37 | # ------------------------------------------------------------------------------ 38 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 39 | EMAIL_BACKEND = env( 40 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 41 | ) 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 43 | EMAIL_HOST = "localhost" 44 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 45 | EMAIL_PORT = 1025 46 | 47 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 48 | if env("USE_DOCKER") == "yes": 49 | import socket 50 | 51 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 52 | INTERNAL_IPS += [ip[:-1] + "1" for ip in ips] 53 | 54 | # django-extensions 55 | # ------------------------------------------------------------------------------ 56 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 57 | INSTALLED_APPS += ["django_extensions"] # noqa F405 58 | # Celery 59 | # ------------------------------------------------------------------------------ 60 | 61 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates 62 | CELERY_TASK_EAGER_PROPAGATES = True 63 | 64 | CELERY_BEAT_SCHEDULE = { 65 | "example-scheduled-task": { 66 | "task": "django_structlog_demo_project.taskapp.celery.scheduled_task", 67 | "schedule": 30.0, 68 | }, 69 | } 70 | 71 | # DATABASES 72 | # ------------------------------------------------------------------------------ 73 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 74 | DATABASES = {"default": env.db("DATABASE_URL")} 75 | 76 | # Your stuff... 77 | # ------------------------------------------------------------------------------ 78 | 79 | LOGGING = { 80 | "version": 1, 81 | "disable_existing_loggers": False, 82 | "formatters": { 83 | "json_formatter": { 84 | "()": structlog.stdlib.ProcessorFormatter, 85 | "processor": structlog.processors.JSONRenderer(), 86 | "foreign_pre_chain": [ 87 | structlog.contextvars.merge_contextvars, 88 | structlog.processors.TimeStamper(fmt="iso"), 89 | structlog.stdlib.add_logger_name, 90 | structlog.stdlib.add_log_level, 91 | structlog.stdlib.PositionalArgumentsFormatter(), 92 | ], 93 | }, 94 | "colored": { 95 | "()": structlog.stdlib.ProcessorFormatter, 96 | "processor": structlog.dev.ConsoleRenderer(colors=True), 97 | "foreign_pre_chain": [ 98 | structlog.contextvars.merge_contextvars, 99 | structlog.processors.TimeStamper(fmt="iso"), 100 | structlog.stdlib.add_logger_name, 101 | structlog.stdlib.add_log_level, 102 | structlog.stdlib.PositionalArgumentsFormatter(), 103 | ], 104 | }, 105 | "key_value": { 106 | "()": structlog.stdlib.ProcessorFormatter, 107 | "processor": structlog.processors.KeyValueRenderer( 108 | key_order=["timestamp", "level", "event", "logger"] 109 | ), 110 | "foreign_pre_chain": [ 111 | structlog.contextvars.merge_contextvars, 112 | structlog.processors.TimeStamper(fmt="iso"), 113 | structlog.stdlib.add_logger_name, 114 | structlog.stdlib.add_log_level, 115 | structlog.stdlib.PositionalArgumentsFormatter(), 116 | ], 117 | }, 118 | }, 119 | "handlers": { 120 | "colored_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, 121 | "json_file": { 122 | "class": "logging.handlers.WatchedFileHandler", 123 | "filename": "logs/json.log", 124 | "formatter": "json_formatter", 125 | }, 126 | "flat_line_file": { 127 | "class": "logging.handlers.WatchedFileHandler", 128 | "filename": "logs/flat_line.log", 129 | "formatter": "key_value", 130 | }, 131 | }, 132 | "loggers": { 133 | "django_structlog": { 134 | "handlers": ["colored_stream", "flat_line_file", "json_file"], 135 | "level": "INFO", 136 | }, 137 | "django_structlog_demo_project": { 138 | "handlers": ["colored_stream", "flat_line_file", "json_file"], 139 | "level": "INFO", 140 | }, 141 | "foreign_logger": { 142 | "handlers": ["colored_stream", "flat_line_file", "json_file"], 143 | "level": "INFO", 144 | }, 145 | }, 146 | } 147 | 148 | structlog.configure( 149 | processors=[ 150 | structlog.contextvars.merge_contextvars, 151 | structlog.stdlib.filter_by_level, 152 | structlog.processors.TimeStamper(fmt="iso"), 153 | structlog.stdlib.add_logger_name, 154 | structlog.stdlib.add_log_level, 155 | structlog.stdlib.PositionalArgumentsFormatter(), 156 | structlog.processors.StackInfoRenderer(), 157 | structlog.processors.format_exc_info, 158 | structlog.processors.UnicodeDecoder(), 159 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 160 | ], 161 | logger_factory=structlog.stdlib.LoggerFactory(), 162 | cache_logger_on_first_use=True, 163 | ) 164 | 165 | MIDDLEWARE += [ 166 | "django_structlog.middlewares.RequestMiddleware", 167 | ] 168 | 169 | DJANGO_STRUCTLOG_CELERY_ENABLED = True 170 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True 171 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | import os 6 | 7 | import environ 8 | import structlog 9 | 10 | env = environ.Env() 11 | 12 | ROOT_DIR = ( 13 | environ.Path(__file__) - 3 14 | ) # (test_app/config/settings/base.py - 3 = test_app/) 15 | APPS_DIR = ROOT_DIR.path("test_app") 16 | 17 | # APPS 18 | # ------------------------------------------------------------------------------ 19 | INSTALLED_APPS = [ 20 | "django.contrib.auth", 21 | "django.contrib.contenttypes", 22 | "django.contrib.sessions", 23 | "django.contrib.sites", 24 | "django.contrib.messages", 25 | "django.contrib.staticfiles", 26 | ] 27 | 28 | 29 | # GENERAL 30 | # ------------------------------------------------------------------------------ 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 32 | DEBUG = False 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 34 | USE_TZ = True 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 36 | SECRET_KEY = env( 37 | "DJANGO_SECRET_KEY", 38 | default="SqlHVcvZwwazrUrjtUiMJerENM8bU3k2p7WZu1WgA4yc8R1DcDc2Rh54m8dRvWcs", 39 | ) 40 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 41 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 42 | 43 | # CACHES 44 | # ------------------------------------------------------------------------------ 45 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 46 | CACHES = { 47 | "default": { 48 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 49 | "LOCATION": "", 50 | } 51 | } 52 | 53 | # EMAIL 54 | # ------------------------------------------------------------------------------ 55 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 56 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 57 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 58 | EMAIL_HOST = "localhost" 59 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 60 | EMAIL_PORT = 1025 61 | 62 | # Your stuff... 63 | # ------------------------------------------------------------------------------ 64 | 65 | LOGGING = { 66 | "version": 1, 67 | "disable_existing_loggers": True, 68 | "formatters": { 69 | "plain": { 70 | "()": structlog.stdlib.ProcessorFormatter, 71 | "processor": structlog.dev.ConsoleRenderer(colors=False), 72 | }, 73 | "colored": { 74 | "()": structlog.stdlib.ProcessorFormatter, 75 | "processor": structlog.dev.ConsoleRenderer(colors=True), 76 | }, 77 | }, 78 | "filters": {}, 79 | "handlers": { 80 | "structured_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, 81 | "structured_file": { 82 | "class": "logging.handlers.WatchedFileHandler", 83 | "filename": "test.log", 84 | "formatter": "plain", 85 | }, 86 | }, 87 | "loggers": {"": {"handlers": ["structured_stream"], "level": "INFO"}}, 88 | } 89 | 90 | structlog.configure( 91 | processors=[ 92 | structlog.contextvars.merge_contextvars, 93 | structlog.stdlib.filter_by_level, 94 | structlog.processors.TimeStamper(fmt="iso"), 95 | structlog.stdlib.add_logger_name, 96 | structlog.stdlib.add_log_level, 97 | structlog.stdlib.PositionalArgumentsFormatter(), 98 | structlog.processors.StackInfoRenderer(), 99 | structlog.processors.format_exc_info, 100 | structlog.processors.UnicodeDecoder(), 101 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 102 | ], 103 | logger_factory=structlog.stdlib.LoggerFactory(), 104 | cache_logger_on_first_use=True, 105 | ) 106 | 107 | DATABASES = { 108 | "default": { 109 | "ENGINE": "django.db.backends.sqlite3", 110 | "NAME": os.path.join(str(ROOT_DIR), "db.sqlite3"), 111 | } 112 | } 113 | 114 | INSTALLED_APPS += ["django_structlog", "test_app"] 115 | 116 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True 117 | 118 | IS_WORKER = False 119 | -------------------------------------------------------------------------------- /config/settings/test_demo_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | 5 | # noinspection PyUnresolvedReferences 6 | from .base import * # noqa: F401,F403 7 | 8 | # noinspection PyUnresolvedReferences 9 | from .test import DATABASES, LOGGING # noqa: F401 10 | 11 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True 12 | 13 | IS_WORKER = False 14 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.urls import re_path 6 | from django.views import defaults as default_views 7 | from django.views.generic import TemplateView 8 | 9 | from django_structlog_demo_project.home import api_views, ninja_views, views 10 | 11 | 12 | def uncaught_exception_view(request): 13 | raise Exception("Uncaught Exception") 14 | 15 | 16 | urlpatterns = [ 17 | re_path(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"), 18 | re_path( 19 | r"^success_task$", views.enqueue_successful_task, name="enqueue_successful_task" 20 | ), 21 | re_path(r"^failing_task$", views.enqueue_failing_task, name="enqueue_failing_task"), 22 | re_path(r"^nesting_task$", views.enqueue_nesting_task, name="enqueue_nesting_task"), 23 | re_path(r"^unknown_task$", views.enqueue_unknown_task, name="enqueue_unknown_task"), 24 | re_path( 25 | r"^rejected_task$", views.enqueue_rejected_task, name="enqueue_rejected_task" 26 | ), 27 | re_path(r"^raise_exception", views.raise_exception, name="raise_exception"), 28 | re_path( 29 | r"^standard_logger", views.log_with_standard_logger, name="standard_logger" 30 | ), 31 | re_path(r"^async_view", views.async_view, name="async_view"), 32 | re_path(r"^api_view$", api_views.home_api_view, name="api_view"), 33 | re_path( 34 | r"^about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 35 | ), 36 | re_path(r"^revoke_task", views.revoke_task, name="revoke_task"), 37 | re_path( 38 | r"^async_streaming_view", 39 | views.async_streaming_view, 40 | name="async_streaming_view", 41 | ), 42 | re_path( 43 | r"^sync_streaming_view", views.sync_streaming_view, name="sync_streaming_view" 44 | ), 45 | # Django Admin, use {% url 'admin:index' %} 46 | re_path(settings.ADMIN_URL, admin.site.urls), 47 | # User management 48 | re_path( 49 | r"^users/", 50 | include("django_structlog_demo_project.users.urls", namespace="users"), 51 | ), 52 | re_path(r"^accounts/", include("allauth.urls")), 53 | re_path("^ninja/", ninja_views.api.urls), 54 | # Your stuff: custom urls includes go here 55 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 56 | 57 | if settings.DEBUG: 58 | # This allows the error pages to be debugged during development, just visit 59 | # these url in browser to see how these error pages look like. 60 | urlpatterns += [ 61 | re_path( 62 | r"^400/", 63 | default_views.bad_request, 64 | kwargs={"exception": Exception("Bad Request!")}, 65 | ), 66 | re_path( 67 | r"^403/", 68 | default_views.permission_denied, 69 | kwargs={"exception": Exception("Permission Denied")}, 70 | ), 71 | re_path( 72 | r"^404/", 73 | default_views.page_not_found, 74 | kwargs={"exception": Exception("Page not Found")}, 75 | ), 76 | re_path(r"^500/", default_views.server_error), 77 | re_path(r"^uncaught_exception/", uncaught_exception_view), 78 | ] 79 | -------------------------------------------------------------------------------- /config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_structlog_demo_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | 17 | import os 18 | import sys 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | # This allows easy placement of apps within the interior 23 | # django_structlog_demo_project directory. 24 | app_path = os.path.abspath( 25 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 26 | ) 27 | sys.path.append(os.path.join(app_path, "django_structlog_demo_project")) 28 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 29 | # if running multiple sites in the same mod_wsgi process. To fix this, use 30 | # mod_wsgi daemon mode with each site in its own daemon process, or use 31 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local" 32 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 33 | 34 | # This application object is used by any WSGI server configured to use this 35 | # file. This includes Django's development server, if the WSGI_APPLICATION 36 | # setting points here. 37 | application = get_wsgi_application() 38 | # Apply WSGI middleware here. 39 | # from helloworld.wsgi import HelloWorldApplication 40 | # application = HelloWorldApplication(application) 41 | -------------------------------------------------------------------------------- /django_structlog/__init__.py: -------------------------------------------------------------------------------- 1 | """``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``.""" 2 | 3 | name = "django_structlog" 4 | 5 | VERSION = (9, 1, 1) 6 | 7 | __version__ = ".".join(str(v) for v in VERSION) 8 | -------------------------------------------------------------------------------- /django_structlog/app_settings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from django.conf import settings 4 | 5 | 6 | # noinspection PyPep8Naming 7 | class AppSettings: 8 | PREFIX = "DJANGO_STRUCTLOG_" 9 | 10 | @property 11 | def CELERY_ENABLED(self) -> bool: 12 | return getattr(settings, self.PREFIX + "CELERY_ENABLED", False) 13 | 14 | @property 15 | def IP_LOGGING_ENABLED(self) -> bool: 16 | return getattr(settings, self.PREFIX + "IP_LOGGING_ENABLED", True) 17 | 18 | @property 19 | def STATUS_4XX_LOG_LEVEL(self) -> int: 20 | return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING) 21 | 22 | @property 23 | def COMMAND_LOGGING_ENABLED(self) -> bool: 24 | return getattr(settings, self.PREFIX + "COMMAND_LOGGING_ENABLED", False) 25 | 26 | @property 27 | def USER_ID_FIELD(self) -> str: 28 | return getattr(settings, self.PREFIX + "USER_ID_FIELD", "pk") 29 | 30 | 31 | app_settings = AppSettings() 32 | -------------------------------------------------------------------------------- /django_structlog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from .app_settings import app_settings 4 | 5 | 6 | class DjangoStructLogConfig(AppConfig): 7 | name = "django_structlog" 8 | 9 | def ready(self) -> None: 10 | if app_settings.CELERY_ENABLED: 11 | from .celery.receivers import CeleryReceiver 12 | 13 | self._celery_receiver = CeleryReceiver() 14 | self._celery_receiver.connect_signals() 15 | 16 | if app_settings.COMMAND_LOGGING_ENABLED: 17 | from .commands import DjangoCommandReceiver 18 | 19 | self._django_command_receiver = DjangoCommandReceiver() 20 | self._django_command_receiver.connect_signals() 21 | -------------------------------------------------------------------------------- /django_structlog/celery/__init__.py: -------------------------------------------------------------------------------- 1 | """``celery`` integration for ``django_structlog``.""" 2 | -------------------------------------------------------------------------------- /django_structlog/celery/receivers.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import TYPE_CHECKING, Any, Optional, Type, cast 3 | 4 | import structlog 5 | from celery import current_app 6 | from celery.signals import ( 7 | after_task_publish, 8 | before_task_publish, 9 | task_failure, 10 | task_prerun, 11 | task_rejected, 12 | task_retry, 13 | task_revoked, 14 | task_success, 15 | task_unknown, 16 | ) 17 | 18 | from . import signals 19 | 20 | if TYPE_CHECKING: # pragma: no cover 21 | from types import TracebackType 22 | 23 | logger = structlog.getLogger(__name__) 24 | 25 | 26 | class CeleryReceiver: 27 | _priority: Optional[str] 28 | 29 | def __init__(self) -> None: 30 | self._priority = None 31 | 32 | def receiver_before_task_publish( 33 | self, 34 | sender: Optional[Type[Any]] = None, 35 | headers: Optional[dict[str, Any]] = None, 36 | body: Optional[dict[str, str]] = None, 37 | properties: Optional[dict[str, Any]] = None, 38 | routing_key: Optional[str] = None, 39 | **kwargs: dict[str, str], 40 | ) -> None: 41 | if current_app.conf.task_protocol < 2: 42 | return 43 | 44 | context = structlog.contextvars.get_merged_contextvars(logger) 45 | if "task_id" in context: 46 | context["parent_task_id"] = context.pop("task_id") 47 | 48 | signals.modify_context_before_task_publish.send( 49 | sender=self.receiver_before_task_publish, 50 | context=context, 51 | task_routing_key=routing_key, 52 | task_properties=properties, 53 | ) 54 | if properties: 55 | self._priority = properties.get("priority", None) 56 | cast(dict[str, Any], headers)["__django_structlog__"] = context 57 | 58 | def receiver_after_task_publish( 59 | self, 60 | sender: Optional[Type[Any]] = None, 61 | headers: Optional[dict[str, Optional[str]]] = None, 62 | body: Optional[dict[str, Optional[str]]] = None, 63 | routing_key: Optional[str] = None, 64 | **kwargs: Any, 65 | ) -> None: 66 | properties = {} 67 | if self._priority is not None: 68 | properties["priority"] = self._priority 69 | self._priority = None 70 | 71 | logger.info( 72 | "task_enqueued", 73 | child_task_id=( 74 | headers.get("id") 75 | if headers 76 | else cast(dict[str, Optional[str]], body).get("id") 77 | ), 78 | child_task_name=( 79 | headers.get("task") 80 | if headers 81 | else cast(dict[str, Optional[str]], body).get("task") 82 | ), 83 | routing_key=routing_key, 84 | **properties, 85 | ) 86 | 87 | def receiver_task_prerun( 88 | self, task_id: str, task: Any, *args: Any, **kwargs: Any 89 | ) -> None: 90 | structlog.contextvars.clear_contextvars() 91 | structlog.contextvars.bind_contextvars(task_id=task_id) 92 | metadata = getattr(task.request, "__django_structlog__", {}) 93 | structlog.contextvars.bind_contextvars(**metadata) 94 | signals.bind_extra_task_metadata.send( 95 | sender=self.receiver_task_prerun, task=task, logger=logger 96 | ) 97 | # Record the start time so we can log the task duration later. 98 | task.request._django_structlog_started_at = time.monotonic_ns() 99 | logger.info("task_started", task=task.name) 100 | 101 | def receiver_task_retry( 102 | self, 103 | request: Optional[Any] = None, 104 | reason: Optional[str] = None, 105 | einfo: Optional[Any] = None, 106 | **kwargs: Any, 107 | ) -> None: 108 | logger.warning("task_retrying", reason=reason) 109 | 110 | def receiver_task_success( 111 | self, result: Optional[str] = None, sender: Optional[Any] = None, **kwargs: Any 112 | ) -> None: 113 | signals.pre_task_succeeded.send( 114 | sender=self.receiver_task_success, logger=logger, result=result 115 | ) 116 | 117 | log_vars: dict[str, Any] = {} 118 | self.add_duration_ms(sender, log_vars) 119 | logger.info("task_succeeded", **log_vars) 120 | 121 | def receiver_task_failure( 122 | self, 123 | task_id: Optional[str] = None, 124 | exception: Optional[Exception] = None, 125 | traceback: Optional["TracebackType"] = None, 126 | einfo: Optional[Any] = None, 127 | sender: Optional[Type[Any]] = None, 128 | *args: Any, 129 | **kwargs: Any, 130 | ) -> None: 131 | log_vars: dict[str, Any] = {} 132 | self.add_duration_ms(sender, log_vars) 133 | throws = getattr(sender, "throws", ()) 134 | if isinstance(exception, throws): 135 | logger.info( 136 | "task_failed", 137 | error=str(exception), 138 | **log_vars, 139 | ) 140 | else: 141 | logger.exception( 142 | "task_failed", 143 | error=str(exception), 144 | exception=exception, 145 | **log_vars, 146 | ) 147 | 148 | @classmethod 149 | def add_duration_ms( 150 | cls, task: Optional[Type[Any]], log_vars: dict[str, Any] 151 | ) -> None: 152 | if task and hasattr(task.request, "_django_structlog_started_at"): 153 | started_at: int = task.request._django_structlog_started_at 154 | log_vars["duration_ms"] = round( 155 | (time.monotonic_ns() - started_at) / 1_000_000 156 | ) 157 | 158 | def receiver_task_revoked( 159 | self, 160 | request: Any, 161 | terminated: Optional[bool] = None, 162 | signum: Optional[Any] = None, 163 | expired: Optional[Any] = None, 164 | **kwargs: Any, 165 | ) -> None: 166 | metadata = getattr(request, "__django_structlog__", {}).copy() 167 | metadata["task_id"] = request.id 168 | metadata["task"] = request.task 169 | 170 | logger.warning( 171 | "task_revoked", 172 | terminated=terminated, 173 | signum=signum.value if signum is not None else None, 174 | signame=signum.name if signum is not None else None, 175 | expired=expired, 176 | **metadata, 177 | ) 178 | 179 | def receiver_task_unknown( 180 | self, 181 | message: Optional[str] = None, 182 | exc: Optional[Exception] = None, 183 | name: Optional[str] = None, 184 | id: Optional[str] = None, 185 | **kwargs: Any, 186 | ) -> None: 187 | logger.error( 188 | "task_not_found", 189 | task=name, 190 | task_id=id, 191 | ) 192 | 193 | def receiver_task_rejected( 194 | self, message: Any, exc: Optional[Exception] = None, **kwargs: Any 195 | ) -> None: 196 | logger.exception( 197 | "task_rejected", task_id=message.properties.get("correlation_id") 198 | ) 199 | 200 | def connect_signals(self) -> None: 201 | before_task_publish.connect(self.receiver_before_task_publish) 202 | after_task_publish.connect(self.receiver_after_task_publish) 203 | 204 | def connect_worker_signals(self) -> None: 205 | before_task_publish.connect(self.receiver_before_task_publish) 206 | after_task_publish.connect(self.receiver_after_task_publish) 207 | task_prerun.connect(self.receiver_task_prerun) 208 | task_retry.connect(self.receiver_task_retry) 209 | task_success.connect(self.receiver_task_success) 210 | task_failure.connect(self.receiver_task_failure) 211 | task_revoked.connect(self.receiver_task_revoked) 212 | task_unknown.connect(self.receiver_task_unknown) 213 | task_rejected.connect(self.receiver_task_rejected) 214 | -------------------------------------------------------------------------------- /django_structlog/celery/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | bind_extra_task_metadata = django.dispatch.Signal() 4 | """ Signal to add extra ``structlog`` bindings from ``celery``'s task. 5 | 6 | :param task: the celery task being run 7 | :param logger: the logger to bind more metadata or override existing bound metadata 8 | 9 | >>> from django.dispatch import receiver 10 | >>> from django_structlog.celery import signals 11 | >>> import structlog 12 | >>> 13 | >>> @receiver(signals.bind_extra_task_metadata) 14 | ... def receiver_bind_extra_task_metadata(sender, signal, task=None, logger=None, **kwargs): 15 | ... structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id) 16 | 17 | """ 18 | 19 | 20 | modify_context_before_task_publish = django.dispatch.Signal() 21 | """ Signal to modify context passed over to ``celery`` task's context. You must modify the ``context`` dict. 22 | 23 | :param context: the context dict that will be passed over to the task runner's logger 24 | :param task_routing_key: routing key of the task 25 | :param task_properties: task's message properties 26 | 27 | >>> from django.dispatch import receiver 28 | >>> from django_structlog.celery import signals 29 | >>> 30 | >>> @receiver(signals.modify_context_before_task_publish) 31 | ... def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs): 32 | ... keys_to_keep = {"request_id", "parent_task_id"} 33 | ... new_dict = { 34 | ... key_to_keep: context[key_to_keep] 35 | ... for key_to_keep in keys_to_keep 36 | ... if key_to_keep in context 37 | ... } 38 | ... context.clear() 39 | ... context.update(new_dict) 40 | 41 | """ 42 | 43 | pre_task_succeeded = django.dispatch.Signal() 44 | """ Signal to add ``structlog`` bindings from ``celery``'s successful task. 45 | 46 | :param logger: the logger to bind more metadata or override existing bound metadata 47 | :param result: result of the succeeding task 48 | 49 | >>> from django.dispatch import receiver 50 | >>> from django_structlog.celery import signals 51 | >>> import structlog 52 | >>> 53 | >>> @receiver(signals.pre_task_succeeded) 54 | ... def receiver_pre_task_succeeded(sender, signal, logger=None, result=None, **kwargs): 55 | ... structlog.contextvars.bind_contextvars(result=str(result)) 56 | 57 | """ 58 | -------------------------------------------------------------------------------- /django_structlog/celery/steps.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from celery import bootsteps 4 | 5 | from .receivers import CeleryReceiver 6 | 7 | 8 | class DjangoStructLogInitStep(bootsteps.Step): 9 | """``celery`` worker boot step to initialize ``django_structlog``. 10 | 11 | >>> from celery import Celery 12 | >>> from django_structlog.celery.steps import DjangoStructLogInitStep 13 | >>> 14 | >>> app = Celery("django_structlog_demo_project") 15 | >>> app.steps['worker'].add(DjangoStructLogInitStep) 16 | 17 | """ 18 | 19 | def __init__(self, parent: Any, **kwargs: Any) -> None: 20 | super().__init__(parent, **kwargs) 21 | self.receiver = CeleryReceiver() 22 | self.receiver.connect_worker_signals() 23 | -------------------------------------------------------------------------------- /django_structlog/commands.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from typing import TYPE_CHECKING, Any, List, Mapping, Tuple, Type 3 | 4 | import structlog 5 | from django_extensions.management.signals import ( 6 | post_command, 7 | pre_command, 8 | ) 9 | 10 | if TYPE_CHECKING: # pragma: no cover 11 | import contextvars 12 | 13 | logger = structlog.getLogger(__name__) 14 | 15 | 16 | class DjangoCommandReceiver: 17 | stack: List[Tuple[str, Mapping[str, "contextvars.Token[Any]"]]] 18 | 19 | def __init__(self) -> None: 20 | self.stack = [] 21 | 22 | def pre_receiver(self, sender: Type[Any], *args: Any, **kwargs: Any) -> None: 23 | command_id = str(uuid.uuid4()) 24 | if len(self.stack): 25 | parent_command_id, _ = self.stack[-1] 26 | tokens = structlog.contextvars.bind_contextvars( 27 | parent_command_id=parent_command_id, command_id=command_id 28 | ) 29 | else: 30 | tokens = structlog.contextvars.bind_contextvars(command_id=command_id) 31 | self.stack.append((command_id, tokens)) 32 | 33 | logger.info( 34 | "command_started", 35 | command_name=sender.__module__.replace(".management.commands", ""), 36 | ) 37 | 38 | def post_receiver( 39 | self, sender: Type[Any], outcome: str, *args: Any, **kwargs: Any 40 | ) -> None: 41 | logger.info("command_finished") 42 | 43 | if len(self.stack): # pragma: no branch 44 | command_id, tokens = self.stack.pop() 45 | structlog.contextvars.reset_contextvars(**tokens) 46 | 47 | def connect_signals(self) -> None: 48 | pre_command.connect(self.pre_receiver) 49 | post_command.connect(self.post_receiver) 50 | -------------------------------------------------------------------------------- /django_structlog/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .request import RequestMiddleware # noqa F401 2 | 3 | __all__ = [ 4 | "RequestMiddleware", 5 | ] 6 | -------------------------------------------------------------------------------- /django_structlog/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog/py.typed -------------------------------------------------------------------------------- /django_structlog/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | bind_extra_request_metadata = django.dispatch.Signal() 4 | """ Signal to add extra ``structlog`` bindings from ``django``'s request. 5 | 6 | :param request: the request returned by the view 7 | :param logger: the logger 8 | :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. 9 | 10 | >>> from django.contrib.sites.shortcuts import get_current_site 11 | >>> from django.dispatch import receiver 12 | >>> from django_structlog import signals 13 | >>> import structlog 14 | >>> 15 | >>> @receiver(signals.bind_extra_request_metadata) 16 | ... def bind_domain(request, logger, log_kwargs, **kwargs): 17 | ... current_site = get_current_site(request) 18 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain) 19 | 20 | """ 21 | 22 | bind_extra_request_finished_metadata = django.dispatch.Signal() 23 | """ Signal to add extra ``structlog`` bindings from ``django``'s finished request and response. 24 | 25 | :param logger: the logger 26 | :param response: the response resulting of the request 27 | :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. 28 | 29 | >>> from django.contrib.sites.shortcuts import get_current_site 30 | >>> from django.dispatch import receiver 31 | >>> from django_structlog import signals 32 | >>> import structlog 33 | >>> 34 | >>> @receiver(signals.bind_extra_request_finished_metadata) 35 | ... def bind_domain(request, logger, response, log_kwargs, **kwargs): 36 | ... current_site = get_current_site(request) 37 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain) 38 | 39 | """ 40 | 41 | bind_extra_request_failed_metadata = django.dispatch.Signal() 42 | """ Signal to add extra ``structlog`` bindings from ``django``'s failed request and exception. 43 | 44 | :param logger: the logger 45 | :param exception: the exception resulting of the request 46 | :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. 47 | 48 | >>> from django.contrib.sites.shortcuts import get_current_site 49 | >>> from django.dispatch import receiver 50 | >>> from django_structlog import signals 51 | >>> import structlog 52 | >>> 53 | >>> @receiver(signals.bind_extra_request_failed_metadata) 54 | ... def bind_domain(request, logger, exception, log_kwargs, **kwargs): 55 | ... current_site = get_current_site(request) 56 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain) 57 | 58 | """ 59 | 60 | update_failure_response = django.dispatch.Signal() 61 | """ Signal to update response failure response before it is returned. 62 | 63 | :param request: the request returned by the view 64 | :param response: the response resulting of the request 65 | :param logger: the logger 66 | :param exception: the exception 67 | 68 | >>> from django.dispatch import receiver 69 | >>> from django_structlog import signals 70 | >>> import structlog 71 | >>> 72 | >>> @receiver(signals.update_failure_response) 73 | ... def add_request_id_to_error_response(request, response, logger, exception, **kwargs): 74 | ... context = structlog.contextvars.get_merged_contextvars(logger) 75 | ... response['X-Request-ID'] = context["request_id"] 76 | 77 | """ 78 | -------------------------------------------------------------------------------- /django_structlog_demo_project/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | __version_info__ = tuple( 3 | [ 4 | int(num) if num.isdigit() else num 5 | for num in __version__.replace("-", ".", 1).split(".") 6 | ] 7 | ) 8 | -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommandExamplesAppConfig(AppConfig): 5 | name = "django_structlog_demo_project.command_examples" 6 | default_auto_field = "django.db.models.AutoField" 7 | -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/management/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/management/commands/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/management/commands/example_command.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from django.core import management 3 | from django.core.management import BaseCommand 4 | from django_extensions.management.utils import signalcommand 5 | 6 | logger = structlog.getLogger(__name__) 7 | 8 | 9 | class Command(BaseCommand): 10 | def add_arguments(self, parser): 11 | parser.add_argument("foo", type=str) 12 | 13 | @signalcommand 14 | def handle(self, foo, *args, **options): 15 | logger.info("my log", foo=foo) 16 | management.call_command("example_nested_command", "buz", verbosity=0) 17 | logger.info("my log 2") 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/management/commands/example_nested_command.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from django.core.management import BaseCommand 3 | from django_extensions.management.utils import signalcommand 4 | 5 | logger = structlog.getLogger(__name__) 6 | 7 | 8 | class Command(BaseCommand): 9 | def add_arguments(self, parser): 10 | parser.add_argument("baz", type=str) 11 | 12 | @signalcommand 13 | def handle(self, baz, *args, **options): 14 | logger.info("my nested log", baz=baz) 15 | return 0 16 | -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/tests/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/command_examples/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.core import management 3 | 4 | from django_structlog_demo_project.command_examples.management.commands import ( 5 | example_command, 6 | ) 7 | 8 | pytestmark = pytest.mark.django_db 9 | 10 | 11 | class TestCommand: 12 | def test_command(self): 13 | assert ( 14 | management.call_command(example_command.Command(), "bar", verbosity=0) 15 | is None 16 | ) 17 | -------------------------------------------------------------------------------- /django_structlog_demo_project/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.test import RequestFactory 4 | 5 | from django_structlog_demo_project.users.tests.factories import UserFactory 6 | 7 | 8 | @pytest.fixture(autouse=True) 9 | def media_storage(settings, tmpdir): 10 | settings.MEDIA_ROOT = tmpdir.strpath 11 | 12 | 13 | @pytest.fixture 14 | def user() -> settings.AUTH_USER_MODEL: 15 | return UserFactory() 16 | 17 | 18 | @pytest.fixture 19 | def request_factory() -> RequestFactory: 20 | return RequestFactory() 21 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.contrib.sites.models import _simple_domain_name_validator 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name="Site", 12 | fields=[ 13 | ( 14 | "id", 15 | models.AutoField( 16 | verbose_name="ID", 17 | serialize=False, 18 | auto_created=True, 19 | primary_key=True, 20 | ), 21 | ), 22 | ( 23 | "domain", 24 | models.CharField( 25 | max_length=100, 26 | verbose_name="domain name", 27 | validators=[_simple_domain_name_validator], 28 | ), 29 | ), 30 | ("name", models.CharField(max_length=50, verbose_name="display name")), 31 | ], 32 | options={ 33 | "ordering": ("domain",), 34 | "db_table": "django_site", 35 | "verbose_name": "site", 36 | "verbose_name_plural": "sites", 37 | }, 38 | bases=(models.Model,), 39 | managers=[("objects", django.contrib.sites.models.SiteManager())], 40 | ) 41 | ] 42 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/migrations/0002_alter_domain_unique.py: -------------------------------------------------------------------------------- 1 | import django.contrib.sites.models 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | dependencies = [("sites", "0001_initial")] 7 | 8 | operations = [ 9 | migrations.AlterField( 10 | model_name="site", 11 | name="domain", 12 | field=models.CharField( 13 | max_length=100, 14 | unique=True, 15 | validators=[django.contrib.sites.models._simple_domain_name_validator], 16 | verbose_name="domain name", 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/migrations/0003_set_site_domain_and_name.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | 7 | from django.conf import settings 8 | from django.db import migrations 9 | 10 | 11 | def update_site_forward(apps, schema_editor): 12 | """Set site domain and name.""" 13 | site_model = apps.get_model("sites", "Site") 14 | site_model.objects.update_or_create( 15 | id=settings.SITE_ID, 16 | defaults={"domain": "example.com", "name": "django_structlog_demo_project"}, 17 | ) 18 | 19 | 20 | class Migration(migrations.Migration): 21 | dependencies = [("sites", "0002_alter_domain_unique")] 22 | 23 | operations = [migrations.RunPython(update_site_forward, migrations.RunPython.noop)] 24 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/migrations/0004_alter_site_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-08-02 17:18 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("sites", "0003_set_site_domain_and_name"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="site", 14 | options={ 15 | "ordering": ["domain"], 16 | "verbose_name": "site", 17 | "verbose_name_plural": "sites", 18 | }, 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /django_structlog_demo_project/contrib/sites/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | To understand why this file is here, please read: 3 | 4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django 5 | """ 6 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/home/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/home/api_views.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from rest_framework.decorators import api_view 3 | from rest_framework.response import Response 4 | 5 | logger = structlog.get_logger(__name__) 6 | 7 | 8 | @api_view() 9 | def home_api_view(request): 10 | logger.info("This is a rest-framework structured log") 11 | return Response({"message": "Hello, world!"}) 12 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomeAppConfig(AppConfig): 5 | name = "django_structlog_demo_project.home" 6 | default_auto_field = "django.db.models.AutoField" 7 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/ninja_views.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from ninja import NinjaAPI, Router 3 | from ninja.security import SessionAuth 4 | 5 | api = NinjaAPI(urls_namespace="ninja") 6 | router = Router() 7 | 8 | logger = structlog.get_logger(__name__) 9 | 10 | 11 | # OptionalSessionAuth is a custom authentication class that allows the user to be anonymous 12 | class OptionalSessionAuth(SessionAuth): 13 | def authenticate(self, request, key): 14 | return request.user 15 | 16 | 17 | @router.get("/ninja", url_name="ninja", auth=OptionalSessionAuth()) 18 | def ninja(request): 19 | logger.info("This is a ninja structured log") 20 | return {"result": "ok"} 21 | 22 | 23 | api.add_router("", router) 24 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/static/js/home.js: -------------------------------------------------------------------------------- 1 | const toastContainer = document.getElementById('toast-container') 2 | const toastTemplate = document.getElementById('toastTemplate'); 3 | let abortController = null; 4 | 5 | function log(title, url, body, isError, duration) { 6 | const newToast = toastTemplate.cloneNode(true) 7 | 8 | const text = body ? body.toString() : "" 9 | 10 | if (isError) { 11 | console.error(title, url, body, duration); 12 | } else { 13 | console.log(title, url, body, duration); 14 | } 15 | 16 | if (isError) { 17 | newToast.classList.add("border-danger") 18 | } else { 19 | newToast.classList.add("border-success") 20 | } 21 | newToast.removeAttribute('id'); 22 | const toastHeader = newToast.querySelector('.toast-header > .me-auto'); 23 | toastHeader.textContent = `${title} ${url}` 24 | if (duration) { 25 | const toastDuration = newToast.querySelector('.duration'); 26 | toastDuration.textContent = `${duration} ms` 27 | } 28 | const toastBody = newToast.querySelector('.toast-body'); 29 | if (body) { 30 | toastBody.textContent = text.slice(0, 400) 31 | } else { 32 | newToast.removeChild(toastBody); 33 | } 34 | toastContainer.appendChild(newToast); 35 | const toast = new bootstrap.Toast(newToast) 36 | 37 | toast.show() 38 | } 39 | 40 | 41 | async function fetchUrl(url) { 42 | abortController = new AbortController(); 43 | log("request_started", url); 44 | const start = new Date(); 45 | 46 | try { 47 | const response = await fetch(url, { 48 | method: 'get', 49 | headers: {"Content-Type": "application/json"}, 50 | signal: abortController.signal, 51 | }); 52 | const text = await response.text(); 53 | if (response.ok) { 54 | log("request_finished", url, text, false, new Date() - start); 55 | } else { 56 | log("request_failed", url, text, true, new Date() - start); 57 | } 58 | } catch (err) { 59 | log("request_failed", url, err, true, new Date() - start); 60 | } 61 | } 62 | 63 | 64 | async function fetchStreamingUrl(url) { 65 | const start = new Date(); 66 | try { 67 | abortController = new AbortController(); 68 | log("streaming_request_started", url); 69 | const response = await fetch(url, { 70 | method: 'get', 71 | signal: abortController.signal, 72 | }); 73 | log("streaming_request_finished", url, `Status code ${response.status}`, false, new Date() - start); 74 | 75 | const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 76 | 77 | log("streaming_response_started", url, undefined, false, new Date() - start); 78 | while (true) { 79 | const {value, done} = await reader.read(); 80 | if (done) break; 81 | log("received", url, value, false, new Date() - start); 82 | } 83 | 84 | log("streaming_response_finished", url, undefined, false, new Date() - start); 85 | } catch (err) { 86 | log("request_failed", url, err, true, new Date() - start); 87 | } 88 | } 89 | 90 | function cancelAsync() { 91 | if (abortController) 92 | abortController.abort(); 93 | } 94 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/home/tests/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/home/tests/test_api_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import api_views 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | class TestApiView: 9 | def test(self, caplog, request_factory): 10 | response = api_views.home_api_view(request_factory.get("/")) 11 | assert response.status_code == 200 12 | assert len(caplog.records) == 1 13 | record = caplog.records[0] 14 | assert record.msg["event"] == "This is a rest-framework structured log" 15 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/tests/test_ninja_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ninja.testing import TestClient 3 | 4 | from ..ninja_views import router 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestNinjaView: 10 | def test(self, caplog, request_factory): 11 | client = TestClient(router) 12 | response = client.get("/ninja") 13 | assert response.status_code == 200 14 | assert response.json() == {"result": "ok"} 15 | assert len(caplog.records) == 1 16 | record = caplog.records[0] 17 | assert record.msg["event"] == "This is a ninja structured log" 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import views 4 | 5 | pytestmark = pytest.mark.django_db 6 | pytest_plugins = ("pytest_asyncio",) 7 | 8 | 9 | class TestEnqueueSuccessfulTask: 10 | def test(self): 11 | response = views.enqueue_successful_task(None) 12 | assert response.status_code == 201 13 | 14 | 15 | class TestEnqueueFailingTask: 16 | def test(self): 17 | response = views.enqueue_failing_task(None) 18 | assert response.status_code == 201 19 | 20 | 21 | class TestEnqueueNestingTask: 22 | def test(self): 23 | response = views.enqueue_nesting_task(None) 24 | assert response.status_code == 201 25 | 26 | 27 | class TestRaiseException: 28 | def test(self): 29 | with pytest.raises(Exception) as e: 30 | views.raise_exception(None) 31 | assert str(e.value) == "This is a view raising an exception." 32 | 33 | 34 | class TestLogWithStandardLogger: 35 | def test(self): 36 | response = views.log_with_standard_logger(None) 37 | assert response.status_code == 200 38 | 39 | 40 | @pytest.mark.asyncio 41 | class TestAsyncView: 42 | async def test(self, mocker): 43 | mocker.patch("asyncio.sleep") 44 | response = await views.async_view(None) 45 | assert response.status_code == 200 46 | 47 | 48 | class TestRevokeTask: 49 | def test(self): 50 | response = views.revoke_task(None) 51 | assert response.status_code == 201 52 | 53 | 54 | class TestEnqueueUnknownTask: 55 | def test(self): 56 | response = views.enqueue_unknown_task(None) 57 | assert response.status_code == 201 58 | 59 | 60 | class TestEnqueueRejectedTask: 61 | def test(self): 62 | response = views.enqueue_rejected_task(None) 63 | assert response.status_code == 201 64 | 65 | 66 | @pytest.mark.asyncio 67 | class TestAsyncStreamingViewView: 68 | async def test(self, mocker): 69 | response = await views.async_streaming_view(None) 70 | assert response.status_code == 200 71 | 72 | mocker.patch("asyncio.sleep") 73 | assert b"0" == await anext(response.streaming_content) 74 | assert b"1" == await anext(response.streaming_content) 75 | assert b"2" == await anext(response.streaming_content) 76 | assert b"3" == await anext(response.streaming_content) 77 | assert b"4" == await anext(response.streaming_content) 78 | 79 | with pytest.raises(StopAsyncIteration): 80 | await anext(response.streaming_content) 81 | 82 | 83 | class TestSyncStreamingViewView: 84 | def test(self, mocker): 85 | response = views.sync_streaming_view(None) 86 | assert response.status_code == 200 87 | 88 | mocker.patch("time.sleep") 89 | assert b"0" == next(response.streaming_content) 90 | assert b"1" == next(response.streaming_content) 91 | assert b"2" == next(response.streaming_content) 92 | assert b"3" == next(response.streaming_content) 93 | assert b"4" == next(response.streaming_content) 94 | with pytest.raises(StopIteration): 95 | next(response.streaming_content) 96 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import time 4 | 5 | import structlog 6 | from django.http import HttpResponse, StreamingHttpResponse 7 | 8 | from django_structlog_demo_project.taskapp.celery import ( 9 | failing_task, 10 | nesting_task, 11 | rejected_task, 12 | successful_task, 13 | ) 14 | 15 | logger = structlog.get_logger(__name__) 16 | 17 | 18 | def enqueue_successful_task(request): 19 | logger.info("Enqueuing successful task") 20 | successful_task.apply_async(foo="bar", priority=5) 21 | return HttpResponse(status=201) 22 | 23 | 24 | def enqueue_failing_task(request): 25 | logger.info("Enqueuing failing task") 26 | failing_task.delay(foo="bar") 27 | return HttpResponse(status=201) 28 | 29 | 30 | def enqueue_nesting_task(request): 31 | logger.info("Enqueuing nesting task") 32 | nesting_task.delay() 33 | return HttpResponse(status=201) 34 | 35 | 36 | def log_with_standard_logger(request): 37 | logging.getLogger("foreign_logger").info("This is a standard logger") 38 | return HttpResponse(status=200) 39 | 40 | 41 | def revoke_task(request): 42 | async_result = successful_task.apply_async(countdown=1) 43 | async_result.revoke() 44 | return HttpResponse(status=201) 45 | 46 | 47 | def enqueue_unknown_task(request): 48 | from django_structlog_demo_project.taskapp.celery import unknown_task 49 | 50 | logger.info("Enqueuing unknown task") 51 | unknown_task.delay() 52 | return HttpResponse(status=201) 53 | 54 | 55 | def enqueue_rejected_task(request): 56 | rejected_task.delay() 57 | return HttpResponse(status=201) 58 | 59 | 60 | async def async_view(request): 61 | for num in range(1, 2): 62 | await asyncio.sleep(1) 63 | logger.info(f"This this is an async view {num}") 64 | return HttpResponse(status=200) 65 | 66 | 67 | async def async_streaming_response(): 68 | for chunk in range(0, 5): 69 | await asyncio.sleep(0.5) 70 | logger.info("streaming_chunk", chunk=chunk) 71 | yield chunk 72 | 73 | 74 | def sync_streaming_response(): 75 | for chunk in range(0, 5): 76 | time.sleep(0.5) 77 | logger.info("streaming_chunk", chunk=chunk) 78 | yield chunk 79 | 80 | 81 | def sync_streaming_view(request): 82 | logger.info("This this is a sync streaming view") 83 | return StreamingHttpResponse(sync_streaming_response()) 84 | 85 | 86 | async def async_streaming_view(request): 87 | logger.info("This this is an async streaming view") 88 | return StreamingHttpResponse(async_streaming_response()) 89 | 90 | 91 | def raise_exception(request): 92 | raise Exception("This is a view raising an exception.") 93 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/css/project.css: -------------------------------------------------------------------------------- 1 | /* These styles are generated from project.scss. */ 2 | 3 | .alert-debug { 4 | color: black; 5 | background-color: white; 6 | border-color: #d6e9c6; 7 | } 8 | 9 | .alert-error { 10 | color: #b94a48; 11 | background-color: #f2dede; 12 | border-color: #eed3d7; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/fonts/.gitkeep -------------------------------------------------------------------------------- /django_structlog_demo_project/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /django_structlog_demo_project/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss -------------------------------------------------------------------------------- /django_structlog_demo_project/static/sass/project.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | // project specific CSS goes here 6 | 7 | //////////////////////////////// 8 | //Variables// 9 | //////////////////////////////// 10 | 11 | // Alert colors 12 | 13 | $white: #fff; 14 | $mint-green: #d6e9c6; 15 | $black: #000; 16 | $pink: #f2dede; 17 | $dark-pink: #eed3d7; 18 | $red: #b94a48; 19 | 20 | //////////////////////////////// 21 | //Alerts// 22 | //////////////////////////////// 23 | 24 | // bootstrap alert CSS, translated to the django-standard levels of 25 | // debug, info, success, warning, error 26 | 27 | .alert-debug { 28 | background-color: $white; 29 | border-color: $mint-green; 30 | color: $black; 31 | } 32 | 33 | .alert-error { 34 | background-color: $pink; 35 | border-color: $dark-pink; 36 | color: $red; 37 | } 38 | -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/taskapp/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/celery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.config 3 | import os 4 | 5 | import structlog 6 | from celery import Celery, shared_task, signals 7 | from django.apps import AppConfig, apps 8 | from django.conf import settings 9 | 10 | from django_structlog.celery.steps import DjangoStructLogInitStep 11 | 12 | if not settings.configured: 13 | # set the default Django settings module for the 'celery' program. 14 | os.environ.setdefault( 15 | "DJANGO_SETTINGS_MODULE", "config.settings.local" 16 | ) # pragma: no cover 17 | 18 | 19 | app = Celery("django_structlog_demo_project", namespace="CELERY") 20 | 21 | app.config_from_object("django.conf:settings") 22 | 23 | # A step to initialize django-structlog 24 | app.steps["worker"].add(DjangoStructLogInitStep) 25 | 26 | 27 | @signals.setup_logging.connect 28 | def receiver_setup_logging( 29 | loglevel, logfile, format, colorize, **kwargs 30 | ): # pragma: no cover 31 | logging.config.dictConfig(settings.LOGGING) 32 | 33 | structlog.configure( 34 | processors=[ 35 | structlog.contextvars.merge_contextvars, 36 | structlog.stdlib.filter_by_level, 37 | structlog.processors.TimeStamper(fmt="iso"), 38 | structlog.stdlib.add_logger_name, 39 | structlog.stdlib.add_log_level, 40 | structlog.stdlib.PositionalArgumentsFormatter(), 41 | structlog.processors.StackInfoRenderer(), 42 | structlog.processors.format_exc_info, 43 | structlog.processors.UnicodeDecoder(), 44 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 45 | ], 46 | logger_factory=structlog.stdlib.LoggerFactory(), 47 | cache_logger_on_first_use=True, 48 | ) 49 | 50 | 51 | class CeleryAppConfig(AppConfig): 52 | name = "django_structlog_demo_project.taskapp" 53 | verbose_name = "Celery Config" 54 | 55 | def ready(self): 56 | installed_apps = [app_config.name for app_config in apps.get_app_configs()] 57 | app.autodiscover_tasks(lambda: installed_apps, force=True) 58 | 59 | 60 | @shared_task 61 | def successful_task(foo=None): 62 | import structlog 63 | 64 | logger = structlog.getLogger(__name__) 65 | logger.info("This is a successful task") 66 | 67 | 68 | @shared_task 69 | def failing_task(foo=None, **kwargs): 70 | raise Exception("This is a failed task") 71 | 72 | 73 | @shared_task 74 | def nesting_task(): 75 | logger = structlog.getLogger(__name__) 76 | structlog.contextvars.bind_contextvars(foo="Bar") 77 | logger.info("This is a nesting task") 78 | 79 | nested_task.delay() 80 | 81 | 82 | @shared_task 83 | def nested_task(): 84 | logger = structlog.getLogger(__name__) 85 | logger.info("This is a nested task") 86 | 87 | 88 | @shared_task 89 | def scheduled_task(): 90 | logger = structlog.getLogger(__name__) 91 | logger.info("This is a scheduled task") 92 | 93 | 94 | @shared_task 95 | def rejected_task(): 96 | pass 97 | 98 | 99 | if not settings.IS_WORKER: # pragma: no branch 100 | 101 | @shared_task 102 | def unknown_task(): 103 | """Simulate a task unavailable in the worker for demonstration purpose""" 104 | 105 | 106 | @signals.before_task_publish.connect 107 | def corrupt_rejected_task(sender=None, headers=None, body=None, **kwargs): 108 | """Simulate celery's task rejection mechanism by breaking up the message""" 109 | logger = structlog.getLogger(__name__) 110 | if headers.get("task") == f"{rejected_task.__module__}.{rejected_task.__name__}": 111 | logger.warn( 112 | f"corrupting {rejected_task.__name__}", 113 | task_id=headers.get("id"), 114 | ) 115 | del headers["task"] 116 | -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/taskapp/tests/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/tests/test_celery.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import celery 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | class TestSuccessfulTask: 9 | def test(self, caplog): 10 | celery.successful_task(foo="bar") 11 | assert len(caplog.records) == 1 12 | record = caplog.records[0] 13 | assert record.msg["event"] == "This is a successful task" 14 | 15 | 16 | class TestFailingTask: 17 | def test(self): 18 | with pytest.raises(Exception) as e: 19 | celery.failing_task(foo="bar") 20 | assert str(e.value) == "This is a failed task" 21 | 22 | 23 | class TestNestingTask: 24 | def test(self, caplog): 25 | celery.nesting_task() 26 | assert len(caplog.records) == 1 27 | record = caplog.records[0] 28 | assert record.msg["event"] == "This is a nesting task" 29 | 30 | 31 | class TestNestedTask: 32 | def test(self, caplog): 33 | celery.nested_task() 34 | assert len(caplog.records) == 1 35 | record = caplog.records[0] 36 | assert record.msg["event"] == "This is a nested task" 37 | 38 | 39 | class TestScheduledTask: 40 | def test(self, caplog): 41 | celery.scheduled_task() 42 | assert len(caplog.records) == 1 43 | record = caplog.records[0] 44 | assert record.msg["event"] == "This is a scheduled task" 45 | 46 | 47 | class TestRejectedTask: 48 | def test(self): 49 | assert celery.rejected_task() is None 50 | 51 | 52 | class TestCorruptRejectedTask: 53 | def test(self, caplog): 54 | task_id = "11111111-1111-1111-1111-111111111111" 55 | headers = dict( 56 | id=task_id, 57 | task="django_structlog_demo_project.taskapp.celery.rejected_task", 58 | ) 59 | celery.corrupt_rejected_task(sender=None, headers=headers) 60 | assert len(caplog.records) == 1 61 | record = caplog.records[0] 62 | assert record.msg["event"] == "corrupting rejected_task" 63 | assert record.msg["task_id"] == task_id 64 | assert "task" not in headers 65 | 66 | def test_other_tasks_not_corrupted(self, caplog): 67 | task_id = "11111111-1111-1111-1111-111111111111" 68 | headers = dict( 69 | id=task_id, 70 | task="django_structlog_demo_project.taskapp.celery.successful_task", 71 | ) 72 | celery.corrupt_rejected_task(sender=None, headers=headers) 73 | assert len(caplog.records) == 0 74 | assert ( 75 | headers["task"] 76 | == "django_structlog_demo_project.taskapp.celery.successful_task" 77 | ) 78 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/403_csrf.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Forbidden (403){% endblock %} 4 | 5 | {% block content %} 6 |

Forbidden (403)

7 | 8 |

CSRF verification failed. Request aborted.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found{% endblock %} 4 | 5 | {% block content %} 6 |

Page not found

7 | 8 |

This is not the page you were looking for.

9 | {% endblock content %} 10 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Server Error{% endblock %} 4 | 5 | {% block content %} 6 |

Ooops!!! 500

7 | 8 |

Looks like something went wrong!

9 | 10 |

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

11 | {% endblock content %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/account_inactive.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Account Inactive" %}

9 | 10 |

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

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %} 3 | 4 | {% block content %} 5 |
6 |
7 | {% block inner %}{% endblock %} 8 |
9 |
10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/email.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "account/base.html" %} 3 | 4 | {% load i18n %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Account" %}{% endblock %} 8 | 9 | {% block inner %} 10 |

{% trans "E-mail Addresses" %}

11 | 12 | {% if user.emailaddress_set.all %} 13 |

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

14 | 15 | 44 | 45 | {% else %} 46 |

{% 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." %}

47 | 48 | {% endif %} 49 | 50 | 51 |

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

52 | 53 |
54 | {% csrf_token %} 55 | {{ form|crispy }} 56 | 57 |
58 | 59 | {% endblock %} 60 | 61 | 62 | {% block javascript %} 63 | {{ block.super }} 64 | 79 | {% endblock %} 80 | 81 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/email_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %} 7 | 8 | 9 | {% block inner %} 10 |

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

11 | 12 | {% if confirmation %} 13 | 14 | {% user_display confirmation.email_address.user as user_display %} 15 | 16 |

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

17 | 18 |
19 | {% csrf_token %} 20 | 21 |
22 | 23 | {% else %} 24 | 25 | {% url 'account_email' as email_url %} 26 | 27 |

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

28 | 29 | {% endif %} 30 | 31 | {% endblock %} 32 | 33 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account socialaccount %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Sign In" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Sign In" %}

12 | 13 | {% get_providers as socialaccount_providers %} 14 | 15 | {% if socialaccount_providers %} 16 |

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

19 | 20 |
21 | 22 |
    23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %} 24 |
25 | 26 | 27 | 28 |
29 | 30 | {% include "socialaccount/snippets/login_extra.html" %} 31 | 32 | {% else %} 33 |

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

35 | {% endif %} 36 | 37 | 46 | 47 | {% endblock %} 48 | 49 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Out" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Out" %}

9 | 10 |

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

11 | 12 |
13 | {% csrf_token %} 14 | {% if redirect_field_value %} 15 | 16 | {% endif %} 17 | 18 |
19 | 20 | 21 | {% endblock %} 22 | 23 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_change.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Change Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | {% load crispy_forms_tags %} 6 | 7 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 8 | 9 | {% block inner %} 10 | 11 |

{% trans "Password Reset" %}

12 | {% if user.is_authenticated %} 13 | {% include "account/snippets/already_logged_in.html" %} 14 | {% endif %} 15 | 16 |

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

17 | 18 |
19 | {% csrf_token %} 20 | {{ form|crispy }} 21 | 22 |
23 | 24 |

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

25 | {% endblock %} 26 | 27 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load account %} 5 | 6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Password Reset" %}

10 | 11 | {% if user.is_authenticated %} 12 | {% include "account/snippets/already_logged_in.html" %} 13 | {% endif %} 14 | 15 |

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

16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_reset_from_key.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

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

9 | 10 | {% if token_fail %} 11 | {% url 'account_reset_password' as passwd_reset_url %} 12 |

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

13 | {% else %} 14 | {% if form %} 15 |
16 | {% csrf_token %} 17 | {{ form|crispy }} 18 | 19 |
20 | {% else %} 21 |

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

22 | {% endif %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_reset_from_key_done.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% block head_title %}{% trans "Change Password" %}{% endblock %} 5 | 6 | {% block inner %} 7 |

{% trans "Change Password" %}

8 |

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

9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/password_set.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Set Password" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Set Password" %}

10 | 11 |
12 | {% csrf_token %} 13 | {{ form|crispy }} 14 | 15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block head_title %}{% trans "Signup" %}{% endblock %} 7 | 8 | {% block inner %} 9 |

{% trans "Sign Up" %}

10 | 11 |

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

12 | 13 | 21 | 22 | {% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/signup_closed.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

{% trans "Sign Up Closed" %}

9 | 10 |

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

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/verification_sent.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

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

9 | 10 |

{% 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 %}

11 | 12 | {% endblock %} 13 | 14 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/account/verified_email_required.html: -------------------------------------------------------------------------------- 1 | {% extends "account/base.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %} 6 | 7 | {% block inner %} 8 |

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

9 | 10 | {% url 'account_email' as email_url %} 11 | 12 |

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

15 | 16 |

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

19 | 20 |

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

21 | 22 | 23 | {% endblock %} 24 | 25 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}django_structlog_demo_project{% endblock title %} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block css %} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endblock %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 43 |
44 | 81 | 82 |
83 | 84 |
85 | 86 | {% if messages %} 87 | {% for message in messages %} 88 |
{{ message }}
89 | {% endfor %} 90 | {% endif %} 91 | 92 | {% block content %} 93 |

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

94 | {% endblock content %} 95 | 96 |
97 | 98 | {% block modal %}{% endblock modal %} 99 | 100 | 102 | 103 | {% block javascript %} 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {% endblock javascript %} 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load static i18n %} 4 | {% block javascript %} 5 | {{ block.super }} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
Base
29 |
30 | 35 | 36 | 41 | 42 | 47 |
48 |
49 |
50 |
51 |
52 |
Async
53 |
54 | 55 | 60 | 61 | 66 | 67 | 72 |
73 |
74 |
75 |
76 |
77 |
rest-framework
78 |
79 | 84 |
85 |
86 |
87 |
88 |
89 |
ninja
90 |
91 | 96 |
97 |
98 |
99 |
100 |
101 |
Celery
102 |
103 | 108 | 109 | 114 | 115 | 120 | 121 | 126 | 127 | 132 | 133 | 138 |
139 |
140 |
141 |
142 |
143 | {% endblock %} 144 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/users/user_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}User: {{ object.username }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |
10 |
11 | 12 |

{{ object.username }}

13 | {% if object.name %} 14 |

{{ object.name }}

15 | {% endif %} 16 |
17 |
18 | 19 | {% if object == request.user %} 20 | 21 |
22 | 23 |
24 | My Info 25 | E-Mail 26 | 27 |
28 | 29 |
30 | 31 | {% endif %} 32 | 33 | 34 |
35 | {% endblock content %} 36 | 37 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/users/user_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block title %}{{ user.username }}{% endblock %} 5 | 6 | {% block content %} 7 |

{{ user.username }}

8 |
9 | {% csrf_token %} 10 | {{ form|crispy }} 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/users/user_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static i18n %} 3 | {% block title %}Members{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Users

8 | 9 |
10 | {% for user in user_list %} 11 | 12 |

{{ user.username }}

13 |
14 | {% endfor %} 15 |
16 |
17 | {% endblock content %} 18 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/users/adapters.py: -------------------------------------------------------------------------------- 1 | from allauth.account.adapter import DefaultAccountAdapter 2 | from django.conf import settings 3 | from django.http import HttpRequest 4 | 5 | 6 | class AccountAdapter(DefaultAccountAdapter): 7 | def is_open_for_signup(self, request: HttpRequest): 8 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 9 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth import admin as auth_admin 3 | from django.contrib.auth import get_user_model 4 | 5 | from django_structlog_demo_project.users.forms import UserChangeForm, UserCreationForm 6 | 7 | User = get_user_model() 8 | 9 | 10 | @admin.register(User) 11 | class UserAdmin(auth_admin.UserAdmin): 12 | form = UserChangeForm 13 | add_form = UserCreationForm 14 | fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets 15 | list_display = ["username", "name", "is_superuser"] 16 | search_fields = ["name"] 17 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersAppConfig(AppConfig): 5 | name = "django_structlog_demo_project.users" 6 | verbose_name = "Users" 7 | default_auto_field = "django.db.models.AutoField" 8 | 9 | def ready(self): 10 | # noinspection PyUnresolvedReferences 11 | from . import signals # noqa F401 12 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import forms, get_user_model 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | User = get_user_model() 6 | 7 | 8 | class UserChangeForm(forms.UserChangeForm): 9 | class Meta(forms.UserChangeForm.Meta): 10 | model = User 11 | 12 | 13 | class UserCreationForm(forms.UserCreationForm): 14 | error_message = forms.UserCreationForm.error_messages.update( 15 | {"duplicate_username": _("This username has already been taken.")} 16 | ) 17 | 18 | class Meta(forms.UserCreationForm.Meta): 19 | model = User 20 | 21 | def clean_username(self): 22 | username = self.cleaned_data["username"] 23 | 24 | try: 25 | User.objects.get(username=username) 26 | except User.DoesNotExist: 27 | return username 28 | 29 | raise ValidationError(self.error_messages["duplicate_username"]) 30 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.auth.models 2 | import django.contrib.auth.validators 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [("auth", "0008_alter_user_username_max_length")] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="User", 15 | fields=[ 16 | ( 17 | "id", 18 | models.AutoField( 19 | auto_created=True, 20 | primary_key=True, 21 | serialize=False, 22 | verbose_name="ID", 23 | ), 24 | ), 25 | ("password", models.CharField(max_length=128, verbose_name="password")), 26 | ( 27 | "last_login", 28 | models.DateTimeField( 29 | blank=True, null=True, verbose_name="last login" 30 | ), 31 | ), 32 | ( 33 | "is_superuser", 34 | models.BooleanField( 35 | default=False, 36 | help_text="Designates that this user has all permissions without explicitly assigning them.", 37 | verbose_name="superuser status", 38 | ), 39 | ), 40 | ( 41 | "username", 42 | models.CharField( 43 | error_messages={ 44 | "unique": "A user with that username already exists." 45 | }, 46 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 47 | max_length=150, 48 | unique=True, 49 | validators=[ 50 | django.contrib.auth.validators.UnicodeUsernameValidator() 51 | ], 52 | verbose_name="username", 53 | ), 54 | ), 55 | ( 56 | "first_name", 57 | models.CharField( 58 | blank=True, max_length=30, verbose_name="first name" 59 | ), 60 | ), 61 | ( 62 | "last_name", 63 | models.CharField( 64 | blank=True, max_length=150, verbose_name="last name" 65 | ), 66 | ), 67 | ( 68 | "email", 69 | models.EmailField( 70 | blank=True, max_length=254, verbose_name="email address" 71 | ), 72 | ), 73 | ( 74 | "is_staff", 75 | models.BooleanField( 76 | default=False, 77 | help_text="Designates whether the user can log into this admin site.", 78 | verbose_name="staff status", 79 | ), 80 | ), 81 | ( 82 | "is_active", 83 | models.BooleanField( 84 | default=True, 85 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 86 | verbose_name="active", 87 | ), 88 | ), 89 | ( 90 | "date_joined", 91 | models.DateTimeField( 92 | default=django.utils.timezone.now, verbose_name="date joined" 93 | ), 94 | ), 95 | ( 96 | "name", 97 | models.CharField( 98 | blank=True, max_length=255, verbose_name="Name of User" 99 | ), 100 | ), 101 | ( 102 | "groups", 103 | models.ManyToManyField( 104 | blank=True, 105 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 106 | related_name="user_set", 107 | related_query_name="user", 108 | to="auth.Group", 109 | verbose_name="groups", 110 | ), 111 | ), 112 | ( 113 | "user_permissions", 114 | models.ManyToManyField( 115 | blank=True, 116 | help_text="Specific permissions for this user.", 117 | related_name="user_set", 118 | related_query_name="user", 119 | to="auth.Permission", 120 | verbose_name="user permissions", 121 | ), 122 | ), 123 | ], 124 | options={ 125 | "verbose_name_plural": "users", 126 | "verbose_name": "user", 127 | "abstract": False, 128 | }, 129 | managers=[("objects", django.contrib.auth.models.UserManager())], 130 | ) 131 | ] 132 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/migrations/0002_alter_user_first_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.14 on 2022-08-02 17:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("users", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="user", 14 | name="first_name", 15 | field=models.CharField( 16 | blank=True, max_length=150, verbose_name="first name" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/migrations/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db.models import CharField 3 | from django.urls import reverse 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | # First Name and Last Name do not cover name patterns 9 | # around the globe. 10 | name = CharField(_("Name of User"), blank=True, max_length=255) 11 | 12 | def get_absolute_url(self): 13 | return reverse("users:detail", kwargs={"username": self.username}) 14 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/signals.py: -------------------------------------------------------------------------------- 1 | # Experiment with django worker signals here 2 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/tests/__init__.py -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/factories.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence 2 | 3 | from django.contrib.auth import get_user_model 4 | from factory import Faker, post_generation 5 | from factory.django import DjangoModelFactory 6 | 7 | 8 | class UserFactory(DjangoModelFactory): 9 | username = Faker("user_name") 10 | email = Faker("email") 11 | name = Faker("name") 12 | 13 | @post_generation 14 | def password(self, create: bool, extracted: Sequence[Any], **kwargs): 15 | password = Faker( 16 | "password", 17 | length=42, 18 | special_chars=True, 19 | digits=True, 20 | upper_case=True, 21 | lower_case=True, 22 | ).evaluate(None, None, extra={"locale": None}) 23 | self.set_password(password) 24 | 25 | class Meta: 26 | model = get_user_model() 27 | django_get_or_create = ["username"] 28 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_adapters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_structlog_demo_project.users.adapters import AccountAdapter 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | class TestUserCreationForm: 9 | def test_account_adapter(self): 10 | assert AccountAdapter().is_open_for_signup(None) 11 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_forms.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_structlog_demo_project.users.forms import UserCreationForm 4 | from django_structlog_demo_project.users.tests.factories import UserFactory 5 | 6 | pytestmark = pytest.mark.django_db 7 | 8 | 9 | class TestUserCreationForm: 10 | def test_clean_username(self): 11 | # A user with proto_user params does not exist yet. 12 | proto_user = UserFactory.build() 13 | 14 | form = UserCreationForm( 15 | { 16 | "username": proto_user.username, 17 | "password1": proto_user._password, 18 | "password2": proto_user._password, 19 | } 20 | ) 21 | 22 | assert form.is_valid() 23 | assert form.clean_username() == proto_user.username 24 | 25 | # Creating a user. 26 | form.save() 27 | 28 | # The user with proto_user params already exists, 29 | # hence cannot be created. 30 | form = UserCreationForm( 31 | { 32 | "username": proto_user.username, 33 | "password1": proto_user._password, 34 | "password2": proto_user._password, 35 | } 36 | ) 37 | 38 | assert not form.is_valid() 39 | assert len(form.errors) == 1 40 | assert "username" in form.errors 41 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | 4 | pytestmark = pytest.mark.django_db 5 | 6 | 7 | def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL): 8 | assert user.get_absolute_url() == "/users/{username}/".format( 9 | username=user.username 10 | ) 11 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.urls import resolve, reverse 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_detail(user: settings.AUTH_USER_MODEL): 9 | route = f"/users/{user.username}/" 10 | assert reverse("users:detail", kwargs={"username": user.username}) == route 11 | assert resolve(route).view_name == "users:detail" 12 | 13 | 14 | def test_detail_username_with_dot(): 15 | route = "/users/foo.bar/" 16 | assert reverse("users:detail", kwargs={"username": "foo.bar"}) == route 17 | assert resolve(route).view_name == "users:detail" 18 | 19 | 20 | def test_list(): 21 | assert reverse("users:list") == "/users/" 22 | assert resolve("/users/").view_name == "users:list" 23 | 24 | 25 | def test_update(): 26 | assert reverse("users:update") == "/users/~update/" 27 | assert resolve("/users/~update/").view_name == "users:update" 28 | 29 | 30 | def test_redirect(): 31 | assert reverse("users:redirect") == "/users/~redirect/" 32 | assert resolve("/users/~redirect/").view_name == "users:redirect" 33 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.test import RequestFactory 4 | 5 | from django_structlog_demo_project.users.views import UserRedirectView, UserUpdateView 6 | 7 | pytestmark = pytest.mark.django_db 8 | 9 | 10 | class TestUserUpdateView: 11 | """ 12 | TODO: 13 | extracting view initialization code as class-scoped fixture 14 | would be great if only pytest-django supported non-function-scoped 15 | fixture db access -- this is a work-in-progress for now: 16 | https://github.com/pytest-dev/pytest-django/pull/258 17 | """ 18 | 19 | def test_get_success_url( 20 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory 21 | ): 22 | view = UserUpdateView() 23 | request = request_factory.get("/fake-url/") 24 | request.user = user 25 | 26 | view.request = request 27 | 28 | assert view.get_success_url() == "/users/{username}/".format( 29 | username=user.username 30 | ) 31 | 32 | def test_get_object( 33 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory 34 | ): 35 | view = UserUpdateView() 36 | request = request_factory.get("/fake-url/") 37 | request.user = user 38 | 39 | view.request = request 40 | 41 | assert view.get_object() == user 42 | 43 | 44 | class TestUserRedirectView: 45 | def test_get_redirect_url( 46 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory 47 | ): 48 | view = UserRedirectView() 49 | request = request_factory.get("/fake-url") 50 | request.user = user 51 | 52 | view.request = request 53 | 54 | assert view.get_redirect_url() == "/users/{username}/".format( 55 | username=user.username 56 | ) 57 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from django_structlog_demo_project.users.views import ( 4 | user_detail_view, 5 | user_list_view, 6 | user_redirect_view, 7 | user_update_view, 8 | ) 9 | 10 | app_name = "users" 11 | urlpatterns = [ 12 | re_path(r"^$", view=user_list_view, name="list"), 13 | re_path(r"~redirect/", view=user_redirect_view, name="redirect"), 14 | re_path(r"~update/", view=user_update_view, name="update"), 15 | re_path(r"^(?P(\w|\.)+)/", view=user_detail_view, name="detail"), 16 | ] 17 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.urls import reverse 4 | from django.views.generic import DetailView, ListView, RedirectView, UpdateView 5 | 6 | User = get_user_model() 7 | 8 | 9 | class UserDetailView(LoginRequiredMixin, DetailView): 10 | model = User 11 | slug_field = "username" 12 | slug_url_kwarg = "username" 13 | 14 | 15 | user_detail_view = UserDetailView.as_view() 16 | 17 | 18 | class UserListView(LoginRequiredMixin, ListView): 19 | model = User 20 | slug_field = "username" 21 | slug_url_kwarg = "username" 22 | 23 | 24 | user_list_view = UserListView.as_view() 25 | 26 | 27 | class UserUpdateView(LoginRequiredMixin, UpdateView): 28 | model = User 29 | fields = ["name"] 30 | 31 | def get_success_url(self): 32 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 33 | 34 | def get_object(self, queryset=None): 35 | return User.objects.get(username=self.request.user.username) 36 | 37 | 38 | user_update_view = UserUpdateView.as_view() 39 | 40 | 41 | class UserRedirectView(LoginRequiredMixin, RedirectView): 42 | permanent = False 43 | 44 | def get_redirect_url(self, *args, **kwargs): 45 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 46 | 47 | 48 | user_redirect_view = UserRedirectView.as_view() 49 | -------------------------------------------------------------------------------- /docker-compose.amqp.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | local_postgres_data: {} 3 | local_postgres_data_backups: {} 4 | 5 | services: 6 | django: 7 | env_file: 8 | - ./.envs/.local/.django 9 | - ./.envs/.local/.amqp 10 | - ./.envs/.local/.postgres 11 | 12 | django_asgi: 13 | env_file: 14 | - ./.envs/.local/.django 15 | - ./.envs/.local/.amqp 16 | - ./.envs/.local/.postgres 17 | 18 | rabbitmq: 19 | hostname: rabbit 20 | image: rabbitmq:3.12-alpine 21 | environment: 22 | - RABBITMQ_DEFAULT_USER=admin 23 | - RABBITMQ_DEFAULT_PASS=unsecure-password 24 | ports: 25 | - "5672:5672" 26 | - "15672:15672" 27 | celeryworker: 28 | 29 | depends_on: 30 | - rabbitmq 31 | - postgres 32 | env_file: 33 | - ./.envs/.local/.django 34 | - ./.envs/.local/.amqp 35 | - ./.envs/.local/.postgres 36 | 37 | celerybeat: 38 | depends_on: 39 | - rabbitmq 40 | - postgres 41 | env_file: 42 | - ./.envs/.local/.django 43 | - ./.envs/.local/.amqp 44 | - ./.envs/.local/.postgres 45 | 46 | flower: 47 | depends_on: 48 | - rabbitmq 49 | - postgres 50 | env_file: 51 | - ./.envs/.local/.django 52 | - ./.envs/.local/.amqp 53 | - ./.envs/.local/.postgres 54 | -------------------------------------------------------------------------------- /docker-compose.docs.yml: -------------------------------------------------------------------------------- 1 | services: 2 | docs: 3 | build: 4 | context: . 5 | dockerfile: ./compose/local/docs/Dockerfile 6 | args: 7 | PYTHON_VERSION: 3.13 8 | image: django_structlog_demo_project_docs 9 | volumes: 10 | - .:/app:cached 11 | command: /start 12 | environment: 13 | - SPHINX_COMMAND=html 14 | ports: 15 | - "8080:8080" 16 | docs-test: 17 | image: django_structlog_demo_project_docs 18 | volumes: 19 | - .:/app:cached 20 | command: /start 21 | environment: 22 | - SPHINX_COMMAND=doctest -E 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | volumes: 2 | local_postgres_data: {} 3 | local_postgres_data_backups: {} 4 | 5 | services: 6 | django: &django 7 | build: 8 | context: . 9 | dockerfile: ./compose/local/django/Dockerfile 10 | args: 11 | PYTHON_VERSION: 3.13 12 | image: django_structlog_demo_project_local_django 13 | depends_on: 14 | - postgres 15 | volumes: 16 | - .:/app:cached 17 | env_file: 18 | - ./.envs/.local/.django 19 | - ./.envs/.local/.redis 20 | - ./.envs/.local/.postgres 21 | tty: true # needed for colors to show in console logs 22 | ports: 23 | - "8000:8000" 24 | command: /start 25 | 26 | django_wsgi: 27 | <<: *django 28 | environment: 29 | DJANGO_DEBUG: False 30 | ports: 31 | - "8001:8000" 32 | command: /start_wsgi 33 | 34 | django_asgi: 35 | <<: *django 36 | environment: 37 | DJANGO_DEBUG: False 38 | ports: 39 | - "8002:8000" 40 | command: /start_asgi 41 | 42 | postgres: 43 | build: 44 | context: . 45 | dockerfile: ./compose/local/postgres/Dockerfile 46 | image: django_structlog_demo_project_local_postgres 47 | volumes: 48 | - local_postgres_data:/var/lib/postgresql/data:cached 49 | - local_postgres_data_backups:/backups:cached 50 | env_file: 51 | - ./.envs/.local/.postgres 52 | 53 | redis: 54 | image: redis:7.4 55 | ports: 56 | - "6379:6379" 57 | 58 | celeryworker: 59 | image: django_structlog_demo_project_local_django 60 | depends_on: 61 | - redis 62 | - postgres 63 | volumes: 64 | - .:/app:cached 65 | env_file: 66 | - ./.envs/.local/.django 67 | - ./.envs/.local/.redis 68 | - ./.envs/.local/.postgres 69 | 70 | command: /start-celeryworker 71 | tty: true # needed for colors to show in console logs 72 | 73 | celerybeat: 74 | image: django_structlog_demo_project_local_django 75 | depends_on: 76 | - redis 77 | - postgres 78 | volumes: 79 | - .:/app:cached 80 | env_file: 81 | - ./.envs/.local/.django 82 | - ./.envs/.local/.redis 83 | - ./.envs/.local/.postgres 84 | 85 | command: /start-celerybeat 86 | tty: true # needed for colors to show in console logs 87 | 88 | flower: 89 | image: django_structlog_demo_project_local_django 90 | ports: 91 | - "5555:5555" 92 | volumes: 93 | - .:/app:cached 94 | env_file: 95 | - ./.envs/.local/.django 96 | - ./.envs/.local/.redis 97 | - ./.envs/.local/.postgres 98 | command: /start-flower 99 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/acknowledgements.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-acknowledgements-begin 3 | :end-before: inclusion-marker-acknowledgements-end 4 | -------------------------------------------------------------------------------- /docs/api_documentation.rst: -------------------------------------------------------------------------------- 1 | API documentation 2 | ================= 3 | 4 | django_structlog 5 | ^^^^^^^^^^^^^^^^ 6 | 7 | .. automodule:: django_structlog 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | .. automodule:: django_structlog.middlewares 13 | :members: RequestMiddleware 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | .. automodule:: django_structlog.signals 18 | :members: bind_extra_request_metadata, bind_extra_request_finished_metadata, bind_extra_request_failed_metadata, update_failure_response 19 | 20 | 21 | django_structlog.celery 22 | ^^^^^^^^^^^^^^^^^^^^^^^ 23 | 24 | .. automodule:: django_structlog.celery 25 | :members: 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | .. automodule:: django_structlog.celery.steps 30 | :members: DjangoStructLogInitStep 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | .. automodule:: django_structlog.celery.signals 35 | :members: bind_extra_task_metadata, modify_context_before_task_publish, pre_task_succeeded 36 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-authors-begin 3 | :end-before: inclusion-marker-authors-end 4 | -------------------------------------------------------------------------------- /docs/celery.rst: -------------------------------------------------------------------------------- 1 | .. _celery_integration: 2 | 3 | Celery Integration 4 | ================== 5 | 6 | Getting Started with Celery 7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 | 9 | In order to be able to support celery you need to configure both your webapp and your workers 10 | 11 | .. warning:: 12 | 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. 13 | 14 | Replace your requirements 15 | ------------------------- 16 | 17 | 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. 18 | 19 | Replace ``django-structlog`` with ``django-structlog[celery]`` in your ``requirements.txt``. 20 | 21 | .. code-block:: python 22 | 23 | django-structlog[celery]==X.Y.Z 24 | 25 | Enable celery integration in your web app 26 | ----------------------------------------- 27 | 28 | In your settings.py 29 | 30 | .. code-block:: python 31 | 32 | MIDDLEWARE = [ 33 | # ... 34 | 'django_structlog.middlewares.RequestMiddleware', 35 | ] 36 | 37 | DJANGO_STRUCTLOG_CELERY_ENABLED = True 38 | 39 | 40 | Initialize Celery Worker with DjangoStructLogInitStep 41 | ----------------------------------------------------- 42 | 43 | In your celery AppConfig's module. 44 | 45 | .. code-block:: python 46 | 47 | import logging 48 | 49 | import structlog 50 | from celery import Celery 51 | from celery.signals import setup_logging 52 | from django_structlog.celery.steps import DjangoStructLogInitStep 53 | 54 | app = Celery("your_celery_project") 55 | 56 | # A step to initialize django-structlog 57 | app.steps['worker'].add(DjangoStructLogInitStep) 58 | 59 | 60 | .. warning:: 61 | If you use ``celery``'s `task_protocol v1 `_, ``django-structlog`` will not be able to transfer metadata to child task. 62 | 63 | Ex: 64 | 65 | .. code-block:: python 66 | 67 | app = Celery("your_celery_project", task_protocol=1) 68 | 69 | Configure celery's logger 70 | ------------------------- 71 | 72 | In the same file as before 73 | 74 | .. code-block:: python 75 | 76 | @setup_logging.connect 77 | def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover 78 | logging.config.dictConfig( 79 | { 80 | "version": 1, 81 | "disable_existing_loggers": False, 82 | "formatters": { 83 | "json_formatter": { 84 | "()": structlog.stdlib.ProcessorFormatter, 85 | "processor": structlog.processors.JSONRenderer(), 86 | }, 87 | "plain_console": { 88 | "()": structlog.stdlib.ProcessorFormatter, 89 | "processor": structlog.dev.ConsoleRenderer(), 90 | }, 91 | "key_value": { 92 | "()": structlog.stdlib.ProcessorFormatter, 93 | "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']), 94 | }, 95 | }, 96 | "handlers": { 97 | "console": { 98 | "class": "logging.StreamHandler", 99 | "formatter": "plain_console", 100 | }, 101 | "json_file": { 102 | "class": "logging.handlers.WatchedFileHandler", 103 | "filename": "logs/json.log", 104 | "formatter": "json_formatter", 105 | }, 106 | "flat_line_file": { 107 | "class": "logging.handlers.WatchedFileHandler", 108 | "filename": "logs/flat_line.log", 109 | "formatter": "key_value", 110 | }, 111 | }, 112 | "loggers": { 113 | "django_structlog": { 114 | "handlers": ["console", "flat_line_file", "json_file"], 115 | "level": "INFO", 116 | }, 117 | "django_structlog_demo_project": { 118 | "handlers": ["console", "flat_line_file", "json_file"], 119 | "level": "INFO", 120 | }, 121 | } 122 | } 123 | ) 124 | 125 | structlog.configure( 126 | processors=[ 127 | structlog.contextvars.merge_contextvars, 128 | structlog.stdlib.filter_by_level, 129 | structlog.processors.TimeStamper(fmt="iso"), 130 | structlog.stdlib.add_logger_name, 131 | structlog.stdlib.add_log_level, 132 | structlog.stdlib.PositionalArgumentsFormatter(), 133 | structlog.processors.StackInfoRenderer(), 134 | structlog.processors.format_exc_info, 135 | structlog.processors.UnicodeDecoder(), 136 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 137 | ], 138 | logger_factory=structlog.stdlib.LoggerFactory(), 139 | cache_logger_on_first_use=True, 140 | ) 141 | 142 | 143 | .. _celery_signals: 144 | 145 | Signals 146 | ^^^^^^^ 147 | 148 | .. _modify_context_before_task_publish: 149 | 150 | modify_context_before_task_publish 151 | ---------------------------------- 152 | 153 | 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. 154 | 155 | By example you can strip down the ``context`` to keep only some of the keys: 156 | 157 | .. code-block:: python 158 | 159 | @receiver(signals.modify_context_before_task_publish) 160 | def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs): 161 | keys_to_keep = {"request_id", "parent_task_id"} 162 | new_dict = {key_to_keep: context[key_to_keep] for key_to_keep in keys_to_keep if key_to_keep in context} 163 | context.clear() 164 | context.update(new_dict) 165 | 166 | 167 | bind_extra_task_metadata 168 | ------------------------ 169 | 170 | 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 171 | in celery's ``receiver_task_pre_run``. 172 | 173 | .. code-block:: python 174 | 175 | from django_structlog.celery import signals 176 | import structlog 177 | 178 | @receiver(signals.bind_extra_task_metadata) 179 | def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None, **kwargs): 180 | structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id) 181 | 182 | -------------------------------------------------------------------------------- /docs/commands.rst: -------------------------------------------------------------------------------- 1 | .. _commands: 2 | 3 | Commands 4 | ======== 5 | 6 | Prerequisites 7 | ^^^^^^^^^^^^^ 8 | 9 | Install ``django-structlog`` with command support (it will install `django-extensions `_). 10 | 11 | .. code-block:: bash 12 | 13 | pip install django-structlog[commands] 14 | 15 | Alternatively install `django-extensions `_ directly: 16 | 17 | .. code-block:: bash 18 | 19 | pip install django-extensions 20 | 21 | Configuration 22 | ^^^^^^^^^^^^^ 23 | 24 | Enable ``django-structlog``'s command logging: 25 | 26 | .. code-block:: python 27 | 28 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True 29 | 30 | Add ``django-extensions``'s `@signalcommand `_ to your commands 31 | 32 | .. code-block:: python 33 | 34 | import structlog 35 | from django.core.management import BaseCommand 36 | from django_extensions.management.utils import signalcommand # <- add this 37 | 38 | logger = structlog.getLogger(__name__) 39 | 40 | 41 | class Command(BaseCommand): 42 | def add_arguments(self, parser): 43 | parser.add_argument("foo", type=str) 44 | 45 | @signalcommand # <- add this 46 | def handle(self, foo, *args, **options): 47 | logger.info("my log", foo=foo) 48 | return 0 49 | 50 | Results 51 | ^^^^^^^ 52 | 53 | Log will add ``command_name`` and ``command_id`` to the logs: 54 | 55 | .. code-block:: bash 56 | 57 | $ python manage.py example_command bar 58 | 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 59 | 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 60 | 2023-09-13T21:10:50.085877Z [info ] command_finished [django_structlog.commands] command_id=be723d34-59f5-468e-9258-24232aa4cedd 61 | 62 | 63 | It also supports nested commands which will keep track of parent commands through ``parent_id``: 64 | 65 | .. code-block:: bash 66 | 67 | $ python manage.py example_command bar 68 | 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 69 | 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 70 | 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 71 | 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 72 | 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 73 | 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 74 | 2023-09-15T00:10:10.470585Z [info ] command_finished [django_structlog.commands] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed 75 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.join(os.path.dirname(__file__), "..")) 5 | 6 | 7 | # Configuration file for the Sphinx documentation builder. 8 | # 9 | # This file only contains a selection of the most common options. For a full 10 | # list see the documentation: 11 | # http://www.sphinx-doc.org/en/master/config 12 | 13 | # -- Path setup -------------------------------------------------------------- 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # The master toctree document. 24 | master_doc = "index" 25 | 26 | # -- Project information ----------------------------------------------------- 27 | 28 | project = "django-structlog" 29 | copyright = "2019, Jules Robichaud-Gagnon" 30 | author = "Jules Robichaud-Gagnon" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ["_templates"] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 47 | 48 | 49 | def get_version(precision): 50 | import django_structlog 51 | 52 | return ".".join(str(v) for v in django_structlog.VERSION[:precision]) 53 | 54 | 55 | # Full version 56 | release = get_version(3) 57 | 58 | # Minor version 59 | version = get_version(2) 60 | 61 | # -- Options for HTML output ------------------------------------------------- 62 | 63 | # The theme to use for HTML and HTML Help pages. See the documentation for 64 | # a list of builtin themes. 65 | # 66 | html_theme = "sphinx_rtd_theme" 67 | 68 | # Add any paths that contain custom static files (such as style sheets) here, 69 | # relative to this directory. They are copied after the builtin static files, 70 | # so a file named "default.css" will overwrite the builtin "default.css". 71 | html_static_path = ["_static"] 72 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | Configuration 4 | ============= 5 | 6 | In your ``settings.py`` you can customize ``django-structlog``. 7 | 8 | Example: 9 | 10 | .. code-block:: python 11 | 12 | import logging 13 | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL = logging.INFO 14 | 15 | 16 | .. _settings: 17 | 18 | Settings 19 | -------- 20 | 21 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 22 | | Key | Type | Default | Description | 23 | +==========================================+=========+=================+===============================================================================+ 24 | | DJANGO_STRUCTLOG_CELERY_ENABLED | boolean | False | See :ref:`celery_integration` | 25 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 26 | | DJANGO_STRUCTLOG_IP_LOGGING_ENABLED | boolean | True | automatically bind user ip using `django-ipware` | 27 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 28 | | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL | int | logging.WARNING | Log level of 4XX status codes | 29 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 30 | | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False | See :ref:`commands` | 31 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 32 | | DJANGO_STRUCTLOG_USER_ID_FIELD | string | ``"pk"`` | Change field used to identify user in logs, ``None`` to disable user binding | 33 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+ 34 | -------------------------------------------------------------------------------- /docs/demo.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-demo-begin 3 | :end-before: inclusion-marker-demo-end 4 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | .. _development: 2 | 3 | Development 4 | =========== 5 | 6 | Prerequisites 7 | ------------- 8 | 9 | - `docker `_ 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | .. code-block:: bash 16 | 17 | $ git clone https://github.com/jrobichaud/django-structlog.git 18 | $ cd django-structlog 19 | $ pip install -r requirements.txt 20 | $ pre-commit install 21 | 22 | 23 | Start Demo App 24 | -------------- 25 | 26 | .. code-block:: bash 27 | 28 | $ docker compose up --build 29 | 30 | - ``runserver_plus`` server: http://127.0.0.1:8000/ 31 | - ``WSGI`` server: http://127.0.0.1:8001/ 32 | - ``ASGI`` server: http://127.0.0.1:8002/ 33 | 34 | Use ``RabbitMQ`` broker instead of ``redis`` 35 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 36 | 37 | .. code-block:: bash 38 | 39 | $ docker compose -f ./docker-compose.yml -f ./docker-compose.amqp.yml up --build 40 | 41 | 42 | Building, Serving and Testing the Documentation Locally 43 | ------------------------------------------------------- 44 | 45 | .. code-block:: bash 46 | 47 | $ docker compose -p django-structlog-docs -f docker-compose.docs.yml up --build 48 | Serving on http://127.0.0.1:8080 49 | -------------------------------------------------------------------------------- /docs/example_outputs.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-example-outputs-begin 3 | :end-before: inclusion-marker-example-outputs-end 4 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-getting-started-begin 3 | :end-before: inclusion-marker-getting-started-end 4 | -------------------------------------------------------------------------------- /docs/how_tos.rst: -------------------------------------------------------------------------------- 1 | .. _how_tos: 2 | 3 | How Tos 4 | ======= 5 | 6 | These are code snippets on how to achieve some specific use cases. 7 | 8 | .. warning:: 9 | 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. 10 | 11 | 12 | Bind ``request_id`` to response's header 13 | ---------------------------------------- 14 | 15 | You can add the ``request_id`` to a custom response header ``X-Request-ID`` in order to trace the request by the caller. 16 | 17 | Origin: `#231 `_ 18 | 19 | .. code-block:: python 20 | 21 | from django.dispatch import receiver 22 | from django_structlog import signals 23 | import structlog 24 | 25 | 26 | @receiver(signals.update_failure_response) 27 | @receiver(signals.bind_extra_request_finished_metadata) 28 | def add_request_id_to_error_response(response, logger, **kwargs): 29 | context = structlog.contextvars.get_merged_contextvars(logger) 30 | response['X-Request-ID'] = context["request_id"] 31 | 32 | Bind ``rest_framework_simplejwt`` token's user id 33 | ------------------------------------------------- 34 | 35 | Bind token's user_id from `rest_framework_simplejwt `_ to the request. 36 | 37 | It is a workaround for ``restframework``'s non-standard authentication system. 38 | It prevents access of the user in middlewares, therefore ``django-structlog`` cannot bind the ``user_id`` by default. 39 | 40 | .. code-block:: python 41 | 42 | import structlog 43 | from django.dispatch import receiver 44 | from django_structlog.signals import bind_extra_request_metadata 45 | from rest_framework_simplejwt.tokens import UntypedToken 46 | 47 | @receiver(bind_extra_request_metadata) 48 | def bind_token_user_id(request, logger, **kwargs): 49 | try: 50 | header = request.META.get("HTTP_AUTHORIZATION") 51 | if header: 52 | raw_token = header.split()[1] 53 | token = UntypedToken(raw_token) 54 | user_id = token["user_id"] 55 | structlog.contextvars.bind_contextvars(user_id=user_id) 56 | except Exception: 57 | pass 58 | 59 | Bind AWS's ``X-Amzn-Trace-Id`` 60 | ------------------------------ 61 | 62 | See `Request tracing for your Application Load Balancer `_ 63 | 64 | Origin: `#324 `_ 65 | 66 | .. code-block:: python 67 | 68 | from django.dispatch import receiver 69 | from django_structlog import signals 70 | from django_structlog.middlewares.request import get_request_header 71 | import structlog 72 | 73 | @receiver(signals.bind_extra_request_metadata) 74 | def bind_trace_id(request, logger, **kwargs): 75 | trace_id = get_request_header( 76 | request, "x-amzn-trace-id", "HTTP_X_AMZN_TRACE_ID" 77 | ) 78 | if trace_id: 79 | structlog.contextvars.bind_contextvars(trace_id=trace_id) 80 | 81 | Filter logs from being recorded 82 | ------------------------------- 83 | 84 | You can add a custom filter to prevent some specific logs from being recorded, based on your criteria 85 | 86 | See `Django logging documentation `_ 87 | 88 | 89 | Origin: `#412 `_ 90 | 91 | .. code-block:: python 92 | 93 | # your_project/logging/filters.py 94 | 95 | import logging 96 | 97 | class ExcludeEventsFilter(logging.Filter): 98 | def __init__(self, excluded_event_type=None): 99 | super().__init__() 100 | self.excluded_event_type = excluded_event_type 101 | 102 | def filter(self, record): 103 | if not isinstance(record.msg, dict) or self.excluded_event_type is None: 104 | return True # Include the log message if msg is not a dictionary or excluded_event_type is not provided 105 | 106 | if record.msg.get('event') in self.excluded_event_type: 107 | return False # Exclude the log message 108 | return True # Include the log message 109 | 110 | 111 | # in your settings.py 112 | 113 | LOGGING = { 114 | 'version': 1, 115 | 'disable_existing_loggers': False, 116 | 'handlers': { 117 | 'console': { 118 | 'class': 'logging.StreamHandler', 119 | 'filters': ['exclude_request_started'] 120 | }, 121 | }, 122 | 'filters': { 123 | 'exclude_request_started': { 124 | '()': 'your_project.logging.filters.ExcludeEventsFilter', 125 | 'excluded_event_type': ['request_started'] # Example excluding request_started event 126 | }, 127 | }, 128 | 'loggers': { 129 | 'django': { 130 | 'handlers': ['console'], 131 | 'level': 'DEBUG', 132 | }, 133 | }, 134 | } 135 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: introduction-begin 3 | :end-before: introduction-end 4 | 5 | 6 | Contents, indices and tables 7 | ============================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | getting_started 13 | configuration 14 | celery 15 | commands 16 | api_documentation 17 | events 18 | example_outputs 19 | how_tos 20 | running_tests 21 | development 22 | demo 23 | changelog 24 | upgrade_guide 25 | authors 26 | acknowledgements 27 | licence 28 | 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/licence.rst: -------------------------------------------------------------------------------- 1 | Licence 2 | ======= 3 | 4 | .. include:: ../LICENSE.rst 5 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==8.2.3 2 | sphinx_rtd_theme==3.0.2 3 | celery==5.5.3 4 | django>=4.2,<6 5 | structlog 6 | sphinx-autobuild==2024.10.3 7 | Jinja2==3.1.6 8 | importlib-metadata>=8.0.0,<9 9 | -------------------------------------------------------------------------------- /docs/running_tests.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-running-tests-begin 3 | :end-before: inclusion-marker-running-tests-end 4 | -------------------------------------------------------------------------------- /docs/upgrade_guide.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-upgrade-guide-begin 3 | :end-before: inclusion-marker-upgrade-guide-end 4 | -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/logs/.gitkeep -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 7 | 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError: 11 | # The above import may fail for some other reason. Ensure that the 12 | # issue is really that Django is missing to avoid masking other 13 | # exceptions on Python 2. 14 | try: 15 | import django # noqa 16 | except ImportError: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) 22 | 23 | raise 24 | 25 | # This allows easy placement of apps within the interior 26 | # django_structlog_demo_project directory. 27 | current_path = os.path.dirname(os.path.abspath(__file__)) 28 | sys.path.append(os.path.join(current_path, "django_structlog_demo_project")) 29 | 30 | execute_from_command_line(sys.argv) 31 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "django-structlog" 7 | description = "Structured Logging for Django" 8 | authors = [ 9 | { name = "Jules Robichaud-Gagnon", email = "j.robichaudg+pypi@gmail.com" }, 10 | ] 11 | readme = "README.rst" 12 | dynamic = ["version"] 13 | requires-python = ">=3.9" 14 | license = { text = "MIT" } 15 | dependencies = [ 16 | "django>=4.2", 17 | "structlog>=21.4.0", 18 | "asgiref>=3.6.0", 19 | "django-ipware>=6.0.2", 20 | ] 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Framework :: Django", 24 | "Framework :: Django :: 4.2", 25 | "Framework :: Django :: 5.0", 26 | "Framework :: Django :: 5.1", 27 | "Framework :: Django :: 5.2", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3 :: Only", 30 | "Programming Language :: Python :: 3.9", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Programming Language :: Python :: 3.12", 34 | "Programming Language :: Python :: 3.13", 35 | "Topic :: System :: Logging", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | "Typing :: Typed", 39 | ] 40 | 41 | [project.urls] 42 | homepage = "https://github.com/jrobichaud/django-structlog" 43 | repository = "https://github.com/jrobichaud/django-structlog" 44 | documentation = "https://django-structlog.readthedocs.io" 45 | tracker = "https://github.com/jrobichaud/django-structlog/issues" 46 | changelog = "https://django-structlog.readthedocs.io/en/latest/changelog.html" 47 | 48 | [project.optional-dependencies] 49 | celery = [ 50 | "celery>=5.1" 51 | ] 52 | commands = [ 53 | "django-extensions>=1.4.9" 54 | ] 55 | 56 | [tool.setuptools.dynamic] 57 | version = { attr = "django_structlog.__version__" } 58 | 59 | [tool.setuptools.packages.find] 60 | include = [ 61 | "django_structlog", 62 | "django_structlog.*", 63 | ] 64 | 65 | [tool.black] 66 | line-length = 88 67 | target-version = [ 68 | 'py39', 69 | 'py310', 70 | 'py311', 71 | 'py312', 72 | 'py313', 73 | ] 74 | include = '\.pyi?$' 75 | exclude = ''' 76 | /( 77 | \.git 78 | | \.hg 79 | | \.tox 80 | | \.venv 81 | | _build 82 | | buck-out 83 | | build 84 | | dist 85 | )/ 86 | ''' 87 | 88 | [tool.ruff] 89 | line-length = 88 90 | target-version = "py313" 91 | lint.ignore = [ 92 | 'E501', 93 | ] 94 | 95 | [tool.pytest.ini_options] 96 | DJANGO_SETTINGS_MODULE = "config.settings.test_demo_app" 97 | 98 | [tool.tox] 99 | legacy_tox_ini = """ 100 | [tox] 101 | # Test against latest supported version of each of python for each Django version. 102 | # 103 | # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml 104 | envlist = 105 | py{39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5, 106 | py31{0,1}-django5{0,1,2}-celery5{3,4}-redis4-kombu5, 107 | py312-django{42,50,51,52}-celery5{3,4}-redis4-kombu5, 108 | py313-django5{1,2}-celery5{3,4}-redis4-kombu5, 109 | 110 | [gh-actions] 111 | python = 112 | 3.9: py39 113 | 3.10: py310 114 | 3.11: py311 115 | 3.12: py312 116 | 3.13: py313 117 | 118 | [testenv] 119 | setenv = 120 | PYTHONPATH={toxinidir} 121 | CELERY_BROKER_URL=redis://0.0.0.0:6379 122 | CELERY_RESULT_BACKEND=redis://0.0.0.0:6379 123 | DJANGO_SETTINGS_MODULE=config.settings.test 124 | pip_pre = True 125 | deps = 126 | redis3: redis>=3, <4 127 | redis4: redis>=4, <5 128 | kombu5: kombu<6 129 | celery51: Celery >=5.1, <5.2 130 | celery52: Celery >=5.2, <5.3 131 | celery53: Celery >=5.3, <5.4 132 | celery54: Celery >=5.4, <5.5 133 | django42: Django >=4.2, <5.0 134 | django50: Django >=5.0, <5.1 135 | django51: Django >=5.1, <5.2 136 | django52: Django >=5.2, <6.0 137 | -r{toxinidir}/requirements/ci.txt 138 | 139 | commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app 140 | """ 141 | 142 | [tool.coverage.run] 143 | branch = true 144 | 145 | [tool.coverage.report] 146 | precision = 2 147 | skip_covered = true 148 | show_missing = true 149 | exclude_lines = [ 150 | "pragma: no cover", 151 | "raise NotImplementedError" 152 | ] 153 | include = [ 154 | "./django_structlog/*", 155 | "./django_structlog_demo_project/*", 156 | "./test_app/*", 157 | ] 158 | 159 | [tool.mypy] 160 | python_version=3.9 161 | strict=true 162 | packages=[ 163 | "django_structlog", 164 | "test_app", 165 | ] 166 | 167 | [tool.isort] 168 | profile = "black" 169 | filter_files = true 170 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/local.txt 2 | -------------------------------------------------------------------------------- /requirements/black.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 # https://github.com/ambv/black 2 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | # Django 2 | # ------------------------------------------------------------------------------ 3 | django-environ==0.12.0 # https://github.com/joke2k/django-environ 4 | django-redis==5.4.0 # https://github.com/niwinz/django-redis 5 | django-extensions==4.1 6 | 7 | structlog>=21.4.0 8 | colorama>=0.4.3 9 | 10 | psycopg[binary]==3.2.9 # https://github.com/psycopg/psycopg 11 | 12 | # Testing 13 | # ------------------------------------------------------------------------------ 14 | pytest==8.3.5 # https://github.com/pytest-dev/pytest 15 | pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar 16 | pytest-cov==6.1.1 17 | 18 | # Code quality 19 | # ------------------------------------------------------------------------------ 20 | -r coverage.txt 21 | pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django 22 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 23 | 24 | # Django 25 | # ------------------------------------------------------------------------------ 26 | factory-boy==3.3.3 # https://github.com/FactoryBoy/factory_boy 27 | 28 | django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin 29 | pytest-django==4.11.1 # https://github.com/pytest-dev/pytest-django 30 | 31 | # Setup tools 32 | # ------------------------------------------------------------------------------ 33 | setuptools>=41.0.1 34 | -------------------------------------------------------------------------------- /requirements/coverage.txt: -------------------------------------------------------------------------------- 1 | coverage==7.8.2 # https://github.com/nedbat/coveragepy 2 | -------------------------------------------------------------------------------- /requirements/deployment.txt: -------------------------------------------------------------------------------- 1 | importlib-metadata>=8.0.0,<9 2 | -------------------------------------------------------------------------------- /requirements/isort.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/requirements/isort.txt -------------------------------------------------------------------------------- /requirements/local-base.txt: -------------------------------------------------------------------------------- 1 | pytz==2025.2 # https://github.com/stub42/pytz 2 | python-slugify==8.0.4 # https://github.com/un33k/python-slugify 3 | 4 | # Django 5 | # ------------------------------------------------------------------------------ 6 | django==5.2.0 # https://www.djangoproject.com/ 7 | django-environ==0.12.0 # https://github.com/joke2k/django-environ 8 | django-model-utils==5.0.0 # https://github.com/jazzband/django-model-utils 9 | django-allauth==65.9.0 # https://github.com/pennersr/django-allauth 10 | django-crispy-forms==2.4 # https://github.com/django-crispy-forms/django-crispy-forms 11 | crispy-bootstrap5==2025.4 # https://github.com/django-crispy-forms/crispy-bootstrap5 12 | django-redis==5.4.0 # https://github.com/niwinz/django-redis 13 | asgiref==3.8.1 # https://github.com/django/asgiref 14 | 15 | # Django REST Framework 16 | djangorestframework==3.16.0 # https://github.com/encode/django-rest-framework 17 | coreapi==2.3.3 # https://github.com/core-api/python-client 18 | 19 | # django-ninja 20 | django-ninja==1.4.1 # https://github.com/vitalik/django-ninja 21 | 22 | structlog==25.3.0 23 | colorama==0.4.6 24 | django-ipware==7.0.1 25 | 26 | Werkzeug==3.1.3 # https://github.com/pallets/werkzeug 27 | ipdb==0.13.13 # https://github.com/gotcha/ipdb 28 | psycopg[binary]==3.2.9 # https://github.com/psycopg/psycopg 29 | 30 | # Testing 31 | # ------------------------------------------------------------------------------ 32 | pytest==8.3.5 # https://github.com/pytest-dev/pytest 33 | pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar 34 | pytest-cov==6.1.1 35 | pytest-asyncio==0.26.0 # https://github.com/pytest-dev/pytest-asyncio 36 | pytest-mock==3.14.1 # https://github.com/pytest-dev/pytest-mock 37 | 38 | # Code quality 39 | # ------------------------------------------------------------------------------ 40 | -r ruff.txt 41 | -r coverage.txt 42 | -r black.txt 43 | pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django 44 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 45 | 46 | # Django 47 | # ------------------------------------------------------------------------------ 48 | factory-boy==3.3.3 # https://github.com/FactoryBoy/factory_boy 49 | 50 | django-extensions==4.1 # https://github.com/django-extensions/django-extensions 51 | django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin 52 | pytest-django==4.11.1 # https://github.com/pytest-dev/pytest-django 53 | 54 | # pre-commit 55 | # ------------------------------------------------------------------------------ 56 | pre-commit==4.2.0 # https://github.com/pre-commit/pre-commit 57 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r local-base.txt 2 | 3 | redis==6.1.0 # https://github.com/antirez/redis 4 | celery==5.5.2 # pyup: < 5.0 # https://github.com/celery/celery 5 | kombu==5.5.3 6 | flower==2.0.1 # https://github.com/mher/flower 7 | uvicorn==0.34.2 # https://github.com/encode/uvicorn 8 | gunicorn==23.0.0 # https://github.com/benoitc/gunicorn 9 | amqp==5.3.1 # https://github.com/celery/py-amqp 10 | -------------------------------------------------------------------------------- /requirements/mypy.txt: -------------------------------------------------------------------------------- 1 | mypy==1.15.0 2 | celery-types==0.23.0 3 | django-stubs[compatible-mypy]==5.1.3 4 | -------------------------------------------------------------------------------- /requirements/ruff.txt: -------------------------------------------------------------------------------- 1 | ruff==0.11.12 # https://github.com/astral-sh/ruff 2 | -------------------------------------------------------------------------------- /test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/__init__.py -------------------------------------------------------------------------------- /test_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | name = "test_app" 6 | -------------------------------------------------------------------------------- /test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /test_app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/__init__.py -------------------------------------------------------------------------------- /test_app/tests/celery/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/celery/__init__.py -------------------------------------------------------------------------------- /test_app/tests/celery/test_steps.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.test import TestCase 4 | 5 | from django_structlog.celery import steps 6 | 7 | 8 | class TestDjangoStructLogInitStep(TestCase): 9 | def test_call(self) -> None: 10 | with patch( 11 | "django_structlog.celery.receivers.CeleryReceiver.connect_worker_signals", 12 | autospec=True, 13 | ) as mock_connect: 14 | step = steps.DjangoStructLogInitStep(None) 15 | 16 | mock_connect.assert_called_once() 17 | 18 | self.assertIsNotNone(step.receiver) 19 | -------------------------------------------------------------------------------- /test_app/tests/middlewares/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/middlewares/__init__.py -------------------------------------------------------------------------------- /test_app/tests/test_app_settings.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from django_structlog import app_settings 4 | 5 | 6 | class TestAppSettings(TestCase): 7 | def test_celery_enabled(self) -> None: 8 | settings = app_settings.AppSettings() 9 | 10 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True): 11 | self.assertTrue(settings.CELERY_ENABLED) 12 | 13 | def test_celery_disabled(self) -> None: 14 | settings = app_settings.AppSettings() 15 | 16 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False): 17 | self.assertFalse(settings.CELERY_ENABLED) 18 | -------------------------------------------------------------------------------- /test_app/tests/test_apps.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import create_autospec, patch 2 | 3 | from django.test import TestCase 4 | 5 | from django_structlog import apps, commands 6 | from django_structlog.celery import receivers 7 | 8 | 9 | class TestAppConfig(TestCase): 10 | def test_celery_enabled(self) -> None: 11 | app = apps.DjangoStructLogConfig( 12 | "django_structlog", __import__("django_structlog") 13 | ) 14 | mock_receiver = create_autospec(spec=receivers.CeleryReceiver) 15 | with patch( 16 | "django_structlog.celery.receivers.CeleryReceiver", 17 | return_value=mock_receiver, 18 | ): 19 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True): 20 | app.ready() 21 | mock_receiver.connect_signals.assert_called_once() 22 | 23 | self.assertTrue(hasattr(app, "_celery_receiver")) 24 | self.assertIsNotNone(app._celery_receiver) 25 | 26 | def test_celery_disabled(self) -> None: 27 | app = apps.DjangoStructLogConfig( 28 | "django_structlog", __import__("django_structlog") 29 | ) 30 | 31 | mock_receiver = create_autospec(spec=receivers.CeleryReceiver) 32 | with patch( 33 | "django_structlog.celery.receivers.CeleryReceiver", 34 | return_value=mock_receiver, 35 | ): 36 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False): 37 | app.ready() 38 | mock_receiver.connect_signals.assert_not_called() 39 | 40 | self.assertFalse(hasattr(app, "_celery_receiver")) 41 | 42 | def test_command_enabled(self) -> None: 43 | app = apps.DjangoStructLogConfig( 44 | "django_structlog", __import__("django_structlog") 45 | ) 46 | mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver) 47 | with patch( 48 | "django_structlog.commands.DjangoCommandReceiver", 49 | return_value=mock_receiver, 50 | ): 51 | with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=True): 52 | app.ready() 53 | mock_receiver.connect_signals.assert_called_once() 54 | 55 | self.assertTrue(hasattr(app, "_django_command_receiver")) 56 | self.assertIsNotNone(app._django_command_receiver) 57 | 58 | def test_command_disabled(self) -> None: 59 | app = apps.DjangoStructLogConfig( 60 | "django_structlog", __import__("django_structlog") 61 | ) 62 | 63 | mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver) 64 | with patch( 65 | "django_structlog.commands.DjangoCommandReceiver", 66 | return_value=mock_receiver, 67 | ): 68 | with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=False): 69 | app.ready() 70 | mock_receiver.connect_signals.assert_not_called() 71 | 72 | self.assertFalse(hasattr(app, "_django_command_receiver")) 73 | -------------------------------------------------------------------------------- /test_app/tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | import structlog 5 | from django.core.management import BaseCommand, call_command 6 | from django.test import TestCase 7 | from django_extensions.management.utils import ( 8 | signalcommand, 9 | ) 10 | 11 | 12 | class TestCommands(TestCase): 13 | def test_command(self) -> None: 14 | class Command(BaseCommand): 15 | 16 | @signalcommand # type: ignore[misc] 17 | def handle(self, *args: Any, **options: Any) -> Any: 18 | structlog.getLogger("command").info("command_event") 19 | 20 | with ( 21 | self.assertLogs("command", logging.INFO) as command_log_results, 22 | self.assertLogs( 23 | "django_structlog.commands", logging.INFO 24 | ) as django_structlog_commands_log_results, 25 | ): 26 | call_command(Command()) 27 | 28 | self.assertEqual(1, len(command_log_results.records)) 29 | record: Any 30 | record = command_log_results.records[0] 31 | self.assertEqual("command_event", record.msg["event"]) 32 | self.assertIn("command_id", record.msg) 33 | 34 | self.assertEqual(2, len(django_structlog_commands_log_results.records)) 35 | record = django_structlog_commands_log_results.records[0] 36 | self.assertEqual("command_started", record.msg["event"]) 37 | self.assertIn("command_id", record.msg) 38 | record = django_structlog_commands_log_results.records[1] 39 | self.assertEqual("command_finished", record.msg["event"]) 40 | self.assertIn("command_id", record.msg) 41 | 42 | def test_nested_command(self) -> None: 43 | class Command(BaseCommand): 44 | @signalcommand # type: ignore[misc] 45 | def handle(self, *args: Any, **options: Any) -> None: 46 | logger = structlog.getLogger("command") 47 | logger.info("command_event_1") 48 | call_command(NestedCommand()) 49 | logger.info("command_event_2") 50 | 51 | class NestedCommand(BaseCommand): 52 | @signalcommand # type: ignore[misc] 53 | def handle(self, *args: Any, **options: Any) -> None: 54 | structlog.getLogger("nested_command").info("nested_command_event") 55 | 56 | with ( 57 | self.assertLogs("command", logging.INFO) as command_log_results, 58 | self.assertLogs("nested_command", logging.INFO), 59 | self.assertLogs( 60 | "django_structlog.commands", logging.INFO 61 | ) as django_structlog_commands_log_results, 62 | ): 63 | call_command(Command()) 64 | 65 | self.assertEqual(2, len(command_log_results.records)) 66 | command_event_1: Any = command_log_results.records[0] 67 | self.assertEqual("command_event_1", command_event_1.msg["event"]) 68 | self.assertIn("command_id", command_event_1.msg) 69 | command_event_2: Any = command_log_results.records[1] 70 | self.assertEqual("command_event_2", command_event_2.msg["event"]) 71 | self.assertIn("command_id", command_event_2.msg) 72 | self.assertEqual( 73 | command_event_1.msg["command_id"], command_event_2.msg["command_id"] 74 | ) 75 | 76 | self.assertEqual(4, len(django_structlog_commands_log_results.records)) 77 | command_started_1: Any = django_structlog_commands_log_results.records[0] 78 | self.assertEqual("command_started", command_started_1.msg["event"]) 79 | self.assertIn("command_id", command_started_1.msg) 80 | 81 | command_started_2: Any = django_structlog_commands_log_results.records[1] 82 | self.assertEqual("command_started", command_started_2.msg["event"]) 83 | self.assertIn("command_id", command_started_2.msg) 84 | self.assertIn("parent_command_id", command_started_2.msg) 85 | self.assertEqual( 86 | command_started_1.msg["command_id"], 87 | command_started_2.msg["parent_command_id"], 88 | ) 89 | 90 | command_finished_1: Any = django_structlog_commands_log_results.records[2] 91 | self.assertEqual("command_finished", command_finished_1.msg["event"]) 92 | self.assertIn("command_id", command_finished_1.msg) 93 | self.assertIn("parent_command_id", command_finished_1.msg) 94 | self.assertEqual( 95 | command_started_1.msg["command_id"], 96 | command_finished_1.msg["parent_command_id"], 97 | ) 98 | 99 | command_finished_2: Any = django_structlog_commands_log_results.records[3] 100 | self.assertEqual("command_finished", command_finished_2.msg["event"]) 101 | self.assertIn("command_id", command_finished_2.msg) 102 | self.assertNotIn("parent_command_id", command_finished_2.msg) 103 | self.assertEqual( 104 | command_event_1.msg["command_id"], command_finished_2.msg["command_id"] 105 | ) 106 | --------------------------------------------------------------------------------