├── logs └── .gitkeep ├── config ├── __init__.py ├── settings │ ├── __init__.py │ ├── test.py │ ├── test_demo_app.py │ ├── local.py │ └── base.py ├── wsgi.py └── urls.py ├── docs ├── _static │ └── .gitkeep ├── licence.rst ├── demo.rst ├── authors.rst ├── running_tests.rst ├── acknowledgements.rst ├── example_outputs.rst ├── getting_started.rst ├── index.rst ├── Makefile ├── development.rst ├── make.bat ├── api_documentation.rst ├── conf.py ├── changelog.rst ├── celery.rst └── events.rst ├── test_app ├── tests │ ├── __init__.py │ ├── celery │ │ ├── __init__.py │ │ ├── test_steps.py │ │ └── test_receivers.py │ └── middlewares │ │ ├── __init__.py │ │ ├── test_celery.py │ │ └── test_request.py ├── migrations │ └── __init__.py ├── __init__.py └── apps.py ├── .gitattributes ├── MANIFEST.in ├── django_structlog_demo_project ├── home │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_api_views.py │ │ └── test_views.py │ ├── api_views.py │ └── views.py ├── users │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_models.py │ │ ├── test_adapters.py │ │ ├── factories.py │ │ ├── test_urls.py │ │ ├── test_forms.py │ │ └── test_views.py │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ ├── apps.py │ ├── models.py │ ├── urls.py │ ├── admin.py │ ├── adapters.py │ ├── forms.py │ └── views.py ├── static │ ├── fonts │ │ └── .gitkeep │ ├── sass │ │ ├── custom_bootstrap_vars.scss │ │ └── project.scss │ ├── js │ │ └── project.js │ ├── images │ │ └── favicons │ │ │ └── favicon.ico │ └── css │ │ └── project.css ├── taskapp │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_celery.py │ └── celery.py ├── templates │ ├── pages │ │ ├── about.html │ │ └── home.html │ ├── 404.html │ ├── 403_csrf.html │ ├── account │ │ ├── base.html │ │ ├── account_inactive.html │ │ ├── password_reset_from_key_done.html │ │ ├── signup_closed.html │ │ ├── verification_sent.html │ │ ├── password_set.html │ │ ├── password_reset_done.html │ │ ├── password_change.html │ │ ├── logout.html │ │ ├── signup.html │ │ ├── verified_email_required.html │ │ ├── password_reset.html │ │ ├── password_reset_from_key.html │ │ ├── email_confirm.html │ │ ├── login.html │ │ └── email.html │ ├── 500.html │ ├── users │ │ ├── user_list.html │ │ ├── user_form.html │ │ └── user_detail.html │ └── base.html ├── __init__.py ├── contrib │ ├── __init__.py │ └── sites │ │ ├── __init__.py │ │ └── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_domain_unique.py │ │ ├── 0003_set_site_domain_and_name.py │ │ └── 0001_initial.py └── conftest.py ├── requirements.txt ├── .dockerignore ├── pytest.ini ├── django_structlog ├── celery │ ├── __init__.py │ ├── signals.py │ ├── middlewares.py │ ├── steps.py │ └── receivers.py ├── apps.py ├── middlewares │ ├── __init__.py │ └── request.py ├── __init__.py └── signals.py ├── setup.cfg ├── requirements ├── doc.txt ├── ci.txt └── local.txt ├── compose └── local │ ├── postgres │ ├── maintenance │ │ ├── _sourced │ │ │ ├── constants.sh │ │ │ ├── yes_no.sh │ │ │ ├── countdown.sh │ │ │ └── messages.sh │ │ ├── backups │ │ ├── backup │ │ └── restore │ └── Dockerfile │ ├── django │ ├── celery │ │ ├── worker │ │ │ └── start │ │ ├── beat │ │ │ └── start │ │ └── flower │ │ │ └── start │ ├── start │ ├── entrypoint │ └── Dockerfile │ └── docs │ ├── Dockerfile │ └── start ├── .idea ├── encodings.xml ├── vcs.xml ├── modules.xml ├── misc.xml ├── django-structlog.iml └── inspectionProfiles │ └── Project_Default.xml ├── .envs └── .local │ ├── .postgres │ └── .django ├── .coveragerc ├── .pre-commit-config.yaml ├── pyproject.toml ├── docker-compose.docs.yml ├── tox.ini ├── LICENSE.rst ├── manage.py ├── setup.py ├── docker-compose.yml ├── .travis.yml ├── .gitignore └── README.rst /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_app/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /test_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_app/tests/celery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.rst 2 | -------------------------------------------------------------------------------- /test_app/tests/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements/local.txt 2 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.coveragerc 3 | !.env 4 | !.pylintrc 5 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE=config.settings.test 3 | -------------------------------------------------------------------------------- /docs/licence.rst: -------------------------------------------------------------------------------- 1 | Licence 2 | ======= 3 | 4 | .. include:: ../LICENSE.rst 5 | -------------------------------------------------------------------------------- /test_app/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "test_app.apps.TestAppConfig" 2 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/pages/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} -------------------------------------------------------------------------------- /django_structlog/celery/__init__.py: -------------------------------------------------------------------------------- 1 | """ ``celery`` integration for ``django_structlog``. 2 | """ 3 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/js/project.js: -------------------------------------------------------------------------------- 1 | /* Project specific Javascript goes here. */ 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .tox,.git,docs,venv,build,.pytest_cache,.idea,logs 3 | ignore = E501 4 | -------------------------------------------------------------------------------- /test_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TestAppConfig(AppConfig): 5 | name = "test_app" 6 | -------------------------------------------------------------------------------- /requirements/doc.txt: -------------------------------------------------------------------------------- 1 | sphinx<2 2 | sphinx_rtd_theme 3 | celery==4.3.0 4 | django>=2.2,<3 5 | structlog 6 | sphinx-autobuild>=0.7.1 7 | -------------------------------------------------------------------------------- /docs/demo.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-demo-begin 3 | :end-before: inclusion-marker-demo-end 4 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-authors-begin 3 | :end-before: inclusion-marker-authors-end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /django_structlog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoStructLogConfig(AppConfig): 5 | name = "django_structlog" 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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/acknowledgements.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :start-after: inclusion-marker-acknowledgements-begin 3 | :end-before: inclusion-marker-acknowledgements-end 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /compose/local/django/celery/worker/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery -A django_structlog_demo_project.taskapp worker -l INFO 8 | -------------------------------------------------------------------------------- /django_structlog_demo_project/static/images/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellohaptik/django-structlog/master/django_structlog_demo_project/static/images/favicons/favicon.ico -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /compose/local/django/celery/beat/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | rm -f './celerybeat.pid' 8 | celery -A django_structlog_demo_project.taskapp beat -l INFO 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /django_structlog/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from .request import RequestMiddleware # noqa F401 2 | 3 | # noinspection PyUnresolvedReferences 4 | from ..celery.middlewares import CeleryMiddleware # noqa F401 5 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /compose/local/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:9.6 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | include = 3 | ./django_structlog/* 4 | ./django_structlog_demo_project/* 5 | ./test_app/* 6 | exclude_lines = 7 | pragma: no cover 8 | raise NotImplementedError 9 | 10 | fail_under = 100 11 | skip_covered = True 12 | show_missing = True -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | - repo: https://gitlab.com/pycqa/flake8 8 | rev: 3.7.8 9 | hooks: 10 | - id: flake8 11 | -------------------------------------------------------------------------------- /compose/local/django/celery/flower/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o nounset 5 | 6 | 7 | celery flower \ 8 | --app=django_structlog_demo_project.taskapp \ 9 | --broker="${CELERY_BROKER_URL}" \ 10 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" 11 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | target-version = ['py35', 'py36', 'py37', 'py38'] 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | | \.hg 9 | | \.mypy_cache 10 | | \.tox 11 | | \.venv 12 | | _build 13 | | buck-out 14 | | build 15 | | dist 16 | )/ 17 | ''' 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /django_structlog/__init__.py: -------------------------------------------------------------------------------- 1 | """ ``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``. 2 | """ 3 | 4 | 5 | default_app_config = "django_structlog.apps.DjangoStructLogConfig" 6 | name = "django_structlog" 7 | 8 | VERSION = (1, 4, 1) 9 | 10 | __version__ = ".".join(str(v) for v in VERSION) 11 | -------------------------------------------------------------------------------- /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/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/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/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/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersAppConfig(AppConfig): 5 | 6 | name = "django_structlog_demo_project.users" 7 | verbose_name = "Users" 8 | 9 | def ready(self): 10 | try: 11 | import users.signals # noqa F401 12 | except ImportError: 13 | pass 14 | -------------------------------------------------------------------------------- /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/home/api_views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.decorators import api_view 2 | from rest_framework.response import Response 3 | import structlog 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN apk update \ 6 | && apk add --virtual build-dependencies \ 7 | build-base 8 | 9 | WORKDIR /app 10 | 11 | # Requirements are installed here to ensure they will be cached. 12 | COPY ./requirements /requirements 13 | RUN pip install -r /requirements/doc.txt 14 | 15 | COPY ./compose/local/docs/start /start 16 | RUN sed -i 's/\r//' /start 17 | RUN chmod +x /start 18 | -------------------------------------------------------------------------------- /.envs/.local/.django: -------------------------------------------------------------------------------- 1 | # General 2 | # ------------------------------------------------------------------------------ 3 | USE_DOCKER=yes 4 | IPYTHONDIR=/app/.ipython 5 | # Redis 6 | # ------------------------------------------------------------------------------ 7 | REDIS_URL=redis://redis:6379/0 8 | 9 | # Celery 10 | # ------------------------------------------------------------------------------ 11 | 12 | # Flower 13 | CELERY_FLOWER_USER=debug 14 | CELERY_FLOWER_PASSWORD=debug 15 | 16 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_adapters.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from django_structlog_demo_project.users.adapters import ( 4 | SocialAccountAdapter, 5 | AccountAdapter, 6 | ) 7 | 8 | pytestmark = pytest.mark.django_db 9 | 10 | 11 | class TestUserCreationForm: 12 | def test_account_adapter(self): 13 | assert AccountAdapter().is_open_for_signup(None) 14 | 15 | def test_social_account_adapter(self): 16 | assert SocialAccountAdapter().is_open_for_signup(None, None) 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | celery 14 | api_documentation 15 | events 16 | example_outputs 17 | running_tests 18 | development 19 | demo 20 | changelog 21 | authors 22 | acknowledgements 23 | licence 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /django_structlog/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | 4 | bind_extra_request_metadata = django.dispatch.Signal( 5 | providing_args=["request", "logger"] 6 | ) 7 | """ Signal to add extra ``structlog`` bindings from ``django``'s request. 8 | 9 | >>> from django.dispatch import receiver 10 | >>> from django_structlog import signals 11 | >>> 12 | >>> @receiver(signals.bind_extra_request_metadata) 13 | ... def bind_user_email(request, logger, **kwargs): 14 | ... logger.bind(user_email=getattr(request.user, 'email', '')) 15 | 16 | """ 17 | -------------------------------------------------------------------------------- /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/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/celery/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | 4 | bind_extra_task_metadata = django.dispatch.Signal(providing_args=["task", "logger"]) 5 | """ Signal to add extra ``structlog`` bindings from ``celery``'s task. 6 | 7 | >>> from django.dispatch import receiver 8 | >>> from django_structlog.celery import signals 9 | >>> 10 | >>> @receiver(signals.bind_extra_task_metadata) 11 | ... def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None): 12 | ... logger.bind(correlation_id=task.request.correlation_id) 13 | 14 | """ 15 | -------------------------------------------------------------------------------- /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 ugettext_lazy as _ 5 | 6 | 7 | class User(AbstractUser): 8 | 9 | # First Name and Last Name do not cover name patterns 10 | # around the globe. 11 | name = CharField(_("Name of User"), blank=True, max_length=255) 12 | 13 | def get_absolute_url(self): 14 | return reverse("users:detail", kwargs={"username": self.username}) 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docker-compose.docs.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | docs: 5 | build: 6 | context: . 7 | dockerfile: ./compose/local/docs/Dockerfile 8 | image: django_structlog_demo_project_docs 9 | volumes: 10 | - .:/app:cached 11 | command: /start 12 | environment: 13 | - SPHINX_COMMAND=html 14 | ports: 15 | - "5000:5000" 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 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from django_structlog_demo_project.users.views import ( 4 | user_list_view, 5 | user_redirect_view, 6 | user_update_view, 7 | user_detail_view, 8 | ) 9 | 10 | app_name = "users" 11 | urlpatterns = [ 12 | url(r"^$", view=user_list_view, name="list"), 13 | url(r"~redirect/", view=user_redirect_view, name="redirect"), 14 | url(r"~update/", view=user_update_view, name="update"), 15 | url(r"^(?P\w+)/", view=user_detail_view, name="detail"), 16 | ] 17 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | -------------------------------------------------------------------------------- /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_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/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 | 13 | form = UserChangeForm 14 | add_form = UserCreationForm 15 | fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets 16 | list_display = ["username", "name", "is_superuser"] 17 | search_fields = ["name"] 18 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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/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 | 7 | dependencies = [("sites", "0001_initial")] 8 | 9 | operations = [ 10 | migrations.AlterField( 11 | model_name="site", 12 | name="domain", 13 | field=models.CharField( 14 | max_length=100, 15 | unique=True, 16 | validators=[django.contrib.sites.models._simple_domain_name_validator], 17 | verbose_name="domain name", 18 | ), 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from allauth.account.adapter import DefaultAccountAdapter 4 | from allauth.socialaccount.adapter import DefaultSocialAccountAdapter 5 | from django.conf import settings 6 | from django.http import HttpRequest 7 | 8 | 9 | class AccountAdapter(DefaultAccountAdapter): 10 | def is_open_for_signup(self, request: HttpRequest): 11 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 12 | 13 | 14 | class SocialAccountAdapter(DefaultSocialAccountAdapter): 15 | def is_open_for_signup(self, request: HttpRequest, sociallogin: Any): 16 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True) 17 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | Prerequisites 5 | ------------- 6 | 7 | - `docker `_ 8 | 9 | 10 | Installation 11 | ------------ 12 | 13 | .. code-block:: bash 14 | 15 | $ git clone https://github.com/jrobichaud/django-structlog.git 16 | $ cd django-structlog 17 | $ pip install -r requirements.txt 18 | $ pre-commit install 19 | 20 | 21 | Start Demo App 22 | -------------- 23 | 24 | .. code-block:: bash 25 | 26 | $ docker-compose up --build 27 | 28 | 29 | Building, Serving and Testing the Documentation Locally 30 | ------------------------------------------------------- 31 | 32 | .. code-block:: bash 33 | 34 | $ docker-compose -f docker-compose.docs.yml up --build 35 | Serving on http://0.0.0.0:5000 36 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -p 5000 \ 11 | -H 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 "setup.py" \ 30 | --ignore "*.log" \ 31 | --ignore "*.ini" 32 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | # Test against latest supported version of each of python 2 and 3 for 3 | # each Django version. 4 | # 5 | # Also, make sure that all python versions used here are included in .travis.yml 6 | envlist = 7 | py{35,36,37}-django111-celery43, 8 | py{35,36,37}-django2{0,1,2}-celery43, 9 | py{36,37,38}-django3{0}-celery44, 10 | 11 | [testenv] 12 | setenv = 13 | PYTHONPATH={toxinidir} 14 | CELERY_BROKER_URL=redis://0.0.0.0:6379 15 | DJANGO_SETTINGS_MODULE=config.settings.test 16 | deps = 17 | celery43: Celery >=4.3, <4.4 18 | celery44: Celery >=4.4, <4.5 19 | django111: Django >=1.11, <2.0 20 | django20: Django >=2.0, <2.1 21 | django21: Django >=2.1, <2.2 22 | django22: Django >=2.2, <2.3 23 | -r{toxinidir}/requirements/ci.txt 24 | 25 | commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app 26 | -------------------------------------------------------------------------------- /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 DjangoModelFactory, Faker, post_generation 5 | 6 | 7 | class UserFactory(DjangoModelFactory): 8 | 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 | ).generate(extra_kwargs={}) 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/home/tests/test_views.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .. import views 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | class TestEnqueueSuccessfulTask: 9 | def test(self): 10 | response = views.enqueue_successful_task(None) 11 | assert response.status_code == 201 12 | 13 | 14 | class TestEnqueueFailingTask: 15 | def test(self): 16 | response = views.enqueue_failing_task(None) 17 | assert response.status_code == 201 18 | 19 | 20 | class TestEnqueueNestingTask: 21 | def test(self): 22 | response = views.enqueue_nesting_task(None) 23 | assert response.status_code == 201 24 | 25 | 26 | class TestRaiseException: 27 | def test(self): 28 | with pytest.raises(Exception) as e: 29 | views.raise_exception(None) 30 | assert str(e.value) == "This is a view raising an exception." 31 | -------------------------------------------------------------------------------- /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 | from django.conf import settings 7 | from django.db import migrations 8 | 9 | 10 | def update_site_forward(apps, schema_editor): 11 | """Set site domain and name.""" 12 | site_model = apps.get_model("sites", "Site") 13 | site_model.objects.update_or_create( 14 | id=settings.SITE_ID, 15 | defaults={"domain": "example.com", "name": "django_structlog_demo_project"}, 16 | ) 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [("sites", "0002_alter_domain_unique")] 22 | 23 | operations = [migrations.RunPython(update_site_forward, migrations.RunPython.noop)] 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /django_structlog_demo_project/home/views.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | from django.http import HttpResponse 3 | from django_structlog_demo_project.taskapp.celery import ( 4 | successful_task, 5 | failing_task, 6 | nesting_task, 7 | ) 8 | 9 | logger = structlog.get_logger(__name__) 10 | 11 | 12 | def enqueue_successful_task(request): 13 | logger.info("Enqueuing successful task") 14 | successful_task.delay(foo="bar") 15 | return HttpResponse(status=201) 16 | 17 | 18 | def enqueue_failing_task(request): 19 | logger.info("Enqueuing failing task") 20 | failing_task.delay(foo="bar") 21 | return HttpResponse(status=201) 22 | 23 | 24 | def enqueue_nesting_task(request): 25 | logger.info("Enqueuing nesting task") 26 | nesting_task.delay() 27 | return HttpResponse(status=201) 28 | 29 | 30 | def raise_exception(request): 31 | raise Exception("This is a view raising an exception.") 32 | -------------------------------------------------------------------------------- /django_structlog/celery/middlewares.py: -------------------------------------------------------------------------------- 1 | from ..celery.receivers import receiver_before_task_publish, receiver_after_task_publish 2 | 3 | 4 | class CeleryMiddleware: 5 | """ 6 | ``CeleryMiddleware`` initializes ``celery`` signals to pass ``django``'s request information to ``celery`` worker's logger. 7 | 8 | >>> MIDDLEWARE = [ 9 | ... # ... 10 | ... 'django_structlog.middlewares.RequestMiddleware', 11 | ... 'django_structlog.middlewares.CeleryMiddleware', 12 | ... ] 13 | 14 | """ 15 | 16 | def __init__(self, get_response=None): 17 | self.get_response = get_response 18 | from celery.signals import before_task_publish, after_task_publish 19 | 20 | before_task_publish.connect(receiver_before_task_publish) 21 | after_task_publish.connect(receiver_after_task_publish) 22 | 23 | def __call__(self, request): 24 | return self.get_response(request) 25 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.urls import reverse, resolve 4 | 5 | pytestmark = pytest.mark.django_db 6 | 7 | 8 | def test_detail(user: settings.AUTH_USER_MODEL): 9 | route = "/users/{username}/".format(username=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_list(): 15 | assert reverse("users:list") == "/users/" 16 | assert resolve("/users/").view_name == "users:list" 17 | 18 | 19 | def test_update(): 20 | assert reverse("users:update") == "/users/~update/" 21 | assert resolve("/users/~update/").view_name == "users:update" 22 | 23 | 24 | def test_redirect(): 25 | assert reverse("users:redirect") == "/users/~redirect/" 26 | assert resolve("/users/~redirect/").view_name == "users:redirect" 27 | -------------------------------------------------------------------------------- /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/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/users/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import get_user_model, forms 2 | from django.core.exceptions import ValidationError 3 | from django.utils.translation import ugettext_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 | 15 | error_message = forms.UserCreationForm.error_messages.update( 16 | {"duplicate_username": _("This username has already been taken.")} 17 | ) 18 | 19 | class Meta(forms.UserCreationForm.Meta): 20 | model = User 21 | 22 | def clean_username(self): 23 | username = self.cleaned_data["username"] 24 | 25 | try: 26 | User.objects.get(username=username) 27 | except User.DoesNotExist: 28 | return username 29 | 30 | raise ValidationError(self.error_messages["duplicate_username"]) 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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.middlewares 30 | :members: CeleryMiddleware 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | .. automodule:: django_structlog.celery.steps 35 | :members: DjangoStructLogInitStep 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | .. automodule:: django_structlog.celery.signals 40 | :members: bind_extra_task_metadata 41 | -------------------------------------------------------------------------------- /.idea/django-structlog.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /compose/local/django/entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -o nounset 6 | 7 | 8 | 9 | # N.B. If only .env files supported variable expansion... 10 | export CELERY_BROKER_URL="${REDIS_URL}" 11 | 12 | 13 | if [ -z "${POSTGRES_USER}" ]; then 14 | base_postgres_image_default_user='postgres' 15 | export POSTGRES_USER="${base_postgres_image_default_user}" 16 | fi 17 | export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" 18 | 19 | postgres_ready() { 20 | python << END 21 | import sys 22 | 23 | import psycopg2 24 | 25 | try: 26 | psycopg2.connect( 27 | dbname="${POSTGRES_DB}", 28 | user="${POSTGRES_USER}", 29 | password="${POSTGRES_PASSWORD}", 30 | host="${POSTGRES_HOST}", 31 | port="${POSTGRES_PORT}", 32 | ) 33 | except psycopg2.OperationalError: 34 | sys.exit(-1) 35 | sys.exit(0) 36 | 37 | END 38 | } 39 | until postgres_ready; do 40 | >&2 echo 'Waiting for PostgreSQL to become available...' 41 | sleep 1 42 | done 43 | >&2 echo 'PostgreSQL is available' 44 | 45 | exec "$@" 46 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test_app/tests/middlewares/test_celery.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch, call 2 | from django.test import TestCase 3 | 4 | from django_structlog import middlewares 5 | 6 | 7 | class TestCeleryMiddleware(TestCase): 8 | def test_call(self): 9 | from celery.signals import before_task_publish, after_task_publish 10 | from django_structlog.celery.receivers import ( 11 | receiver_before_task_publish, 12 | receiver_after_task_publish, 13 | ) 14 | 15 | mock_get_response = Mock() 16 | mock_request = Mock() 17 | with patch( 18 | "celery.utils.dispatch.signal.Signal.connect", autospec=True 19 | ) as mock_connect: 20 | middleware = middlewares.CeleryMiddleware(mock_get_response) 21 | 22 | middleware(mock_request) 23 | mock_get_response.assert_called_once_with(mock_request) 24 | 25 | mock_connect.assert_has_calls( 26 | [ 27 | call(before_task_publish, receiver_before_task_publish), 28 | call(after_task_publish, receiver_after_task_publish), 29 | ] 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /compose/local/django/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN apk update \ 6 | # psycopg2 dependencies 7 | && apk add --virtual build-deps gcc python3-dev musl-dev \ 8 | && apk add postgresql-dev \ 9 | # CFFI dependencies 10 | && apk add libffi-dev py-cffi \ 11 | # Translations dependencies 12 | && apk add gettext \ 13 | # https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell 14 | && apk add postgresql-client 15 | 16 | # Requirements are installed here to ensure they will be cached. 17 | COPY ./requirements /requirements 18 | RUN pip install -r /requirements/local.txt 19 | 20 | COPY ./compose/local/django/entrypoint /entrypoint 21 | RUN sed -i 's/\r//' /entrypoint 22 | RUN chmod +x /entrypoint 23 | 24 | COPY ./compose/local/django/start /start 25 | RUN sed -i 's/\r//' /start 26 | RUN chmod +x /start 27 | 28 | COPY ./compose/local/django/celery/worker/start /start-celeryworker 29 | RUN sed -i 's/\r//' /start-celeryworker 30 | RUN chmod +x /start-celeryworker 31 | 32 | COPY ./compose/local/django/celery/beat/start /start-celerybeat 33 | RUN sed -i 's/\r//' /start-celerybeat 34 | RUN chmod +x /start-celerybeat 35 | 36 | COPY ./compose/local/django/celery/flower/start /start-flower 37 | RUN sed -i 's/\r//' /start-flower 38 | RUN chmod +x /start-flower 39 | 40 | WORKDIR /app 41 | 42 | ENTRYPOINT ["/entrypoint"] 43 | -------------------------------------------------------------------------------- /django_structlog_demo_project/templates/pages/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block content %} 5 |
6 |
7 | {% csrf_token %} 8 |
9 | 10 |
11 |
12 |
13 | {% csrf_token %} 14 |
15 | 16 |
17 |
18 |
19 | {% csrf_token %} 20 |
21 | 22 |
23 |
24 |
25 | {% csrf_token %} 26 |
27 | 28 |
29 | 30 |
31 | rest-framework API View 32 |
33 | 34 | 35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /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 | 11 | model = User 12 | slug_field = "username" 13 | slug_url_kwarg = "username" 14 | 15 | 16 | user_detail_view = UserDetailView.as_view() 17 | 18 | 19 | class UserListView(LoginRequiredMixin, ListView): 20 | 21 | model = User 22 | slug_field = "username" 23 | slug_url_kwarg = "username" 24 | 25 | 26 | user_list_view = UserListView.as_view() 27 | 28 | 29 | class UserUpdateView(LoginRequiredMixin, UpdateView): 30 | 31 | model = User 32 | fields = ["name"] 33 | 34 | def get_success_url(self): 35 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 36 | 37 | def get_object(self, queryset=None): 38 | return User.objects.get(username=self.request.user.username) 39 | 40 | 41 | user_update_view = UserUpdateView.as_view() 42 | 43 | 44 | class UserRedirectView(LoginRequiredMixin, RedirectView): 45 | 46 | permanent = False 47 | 48 | def get_redirect_url(self, *args, **kwargs): 49 | return reverse("users:detail", kwargs={"username": self.request.user.username}) 50 | 51 | 52 | user_redirect_view = UserRedirectView.as_view() 53 | -------------------------------------------------------------------------------- /requirements/ci.txt: -------------------------------------------------------------------------------- 1 | redis==3.3.11 # https://github.com/antirez/redis 2 | flower==0.9.3 # https://github.com/mher/flower 3 | 4 | # Django 5 | # ------------------------------------------------------------------------------ 6 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 7 | django-redis==4.11.0 # https://github.com/niwinz/django-redis 8 | 9 | structlog>=19.2.0,<20 10 | colorama>=0.4.3 11 | 12 | psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2 13 | 14 | # Testing 15 | # ------------------------------------------------------------------------------ 16 | pytest==4.4.0 # https://github.com/pytest-dev/pytest 17 | pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar 18 | pytest-cov==2.6.1 19 | 20 | # Code quality 21 | # ------------------------------------------------------------------------------ 22 | flake8==3.7.5 # https://github.com/PyCQA/flake8 23 | coverage==5.0.1 # https://github.com/nedbat/coveragepy 24 | pylint-django==2.0.6 # https://github.com/PyCQA/pylint-django 25 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 26 | 27 | # Django 28 | # ------------------------------------------------------------------------------ 29 | factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy 30 | 31 | django-coverage-plugin==1.6.0 # https://github.com/nedbat/django_coverage_plugin 32 | pytest-django==3.4.8 # https://github.com/pytest-dev/pytest-django 33 | 34 | # Setup tools 35 | # ------------------------------------------------------------------------------ 36 | setuptools>=41.0.1 37 | -------------------------------------------------------------------------------- /django_structlog/celery/steps.py: -------------------------------------------------------------------------------- 1 | from celery import bootsteps 2 | 3 | from . import receivers 4 | 5 | 6 | class DjangoStructLogInitStep(bootsteps.Step): 7 | """ ``celery`` worker boot step to initialize ``django_structlog``. 8 | 9 | >>> from celery import Celery 10 | >>> from django_structlog.celery.steps import DjangoStructLogInitStep 11 | >>> 12 | >>> app = Celery("django_structlog_demo_project") 13 | >>> app.steps['worker'].add(DjangoStructLogInitStep) 14 | 15 | """ 16 | 17 | def __init__(self, parent, **kwargs): 18 | super().__init__(parent, **kwargs) 19 | from celery.signals import ( 20 | before_task_publish, 21 | after_task_publish, 22 | task_prerun, 23 | task_retry, 24 | task_success, 25 | task_failure, 26 | task_revoked, 27 | task_unknown, 28 | task_rejected, 29 | ) 30 | 31 | before_task_publish.connect(receivers.receiver_before_task_publish) 32 | after_task_publish.connect(receivers.receiver_after_task_publish) 33 | task_prerun.connect(receivers.receiver_task_pre_run) 34 | task_retry.connect(receivers.receiver_task_retry) 35 | task_success.connect(receivers.receiver_task_success) 36 | task_failure.connect(receivers.receiver_task_failure) 37 | task_revoked.connect(receivers.receiver_task_revoked) 38 | task_unknown.connect(receivers.receiver_task_unknown) 39 | task_rejected.connect(receivers.receiver_task_rejected) 40 | -------------------------------------------------------------------------------- /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 | 8 | dependencies = [] 9 | 10 | operations = [ 11 | migrations.CreateModel( 12 | name="Site", 13 | fields=[ 14 | ( 15 | "id", 16 | models.AutoField( 17 | verbose_name="ID", 18 | serialize=False, 19 | auto_created=True, 20 | primary_key=True, 21 | ), 22 | ), 23 | ( 24 | "domain", 25 | models.CharField( 26 | max_length=100, 27 | verbose_name="domain name", 28 | validators=[_simple_domain_name_validator], 29 | ), 30 | ), 31 | ("name", models.CharField(max_length=50, verbose_name="display name")), 32 | ], 33 | options={ 34 | "ordering": ("domain",), 35 | "db_table": "django_site", 36 | "verbose_name": "site", 37 | "verbose_name_plural": "sites", 38 | }, 39 | bases=(models.Model,), 40 | managers=[("objects", django.contrib.sites.models.SiteManager())], 41 | ) 42 | ] 43 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_namespace_packages 2 | 3 | import django_structlog 4 | 5 | with open("README.rst", "r") as fh: 6 | long_description = fh.read() 7 | 8 | 9 | setup( 10 | name="django-structlog", 11 | version=django_structlog.__version__, 12 | author="Jules Robichaud-Gagnon", 13 | author_email="j.robichaudg+pypi@gmail.com", 14 | description="Structured Logging for Django", 15 | long_description=long_description, 16 | long_description_content_type="text/x-rst", 17 | url="https://github.com/jrobichaud/django-structlog", 18 | packages=find_namespace_packages( 19 | include=["django_structlog", "django_structlog.*"] 20 | ), 21 | install_requires=["django>=1.11", "structlog", "django-ipware"], 22 | include_package_data=True, 23 | license="MIT", 24 | classifiers=[ 25 | "Framework :: Django", 26 | "Framework :: Django :: 1.11", 27 | "Framework :: Django :: 2.0", 28 | "Framework :: Django :: 2.1", 29 | "Framework :: Django :: 2.2", 30 | "Framework :: Django :: 3.0", 31 | "Programming Language :: Python :: 3", 32 | "Programming Language :: Python :: 3.5", 33 | "Programming Language :: Python :: 3.6", 34 | "Programming Language :: Python :: 3.7", 35 | "Programming Language :: Python :: 3.8", 36 | "Programming Language :: Python :: Implementation :: PyPy", 37 | "Topic :: System :: Logging", 38 | "License :: OSI Approved :: MIT License", 39 | "Operating System :: OS Independent", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | import os 17 | import sys 18 | 19 | from django.core.wsgi import get_wsgi_application 20 | 21 | # This allows easy placement of apps within the interior 22 | # django_structlog_demo_project directory. 23 | app_path = os.path.abspath( 24 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) 25 | ) 26 | sys.path.append(os.path.join(app_path, "django_structlog_demo_project")) 27 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 28 | # if running multiple sites in the same mod_wsgi process. To fix this, use 29 | # mod_wsgi daemon mode with each site in its own daemon process, or use 30 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local" 31 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") 32 | 33 | # This application object is used by any WSGI server configured to use this 34 | # file. This includes Django's development server, if the WSGI_APPLICATION 35 | # setting points here. 36 | application = get_wsgi_application() 37 | # Apply WSGI middleware here. 38 | # from helloworld.wsgi import HelloWorldApplication 39 | # application = HelloWorldApplication(application) 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test_app/tests/celery/test_steps.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, call 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): 10 | from celery.signals import ( 11 | before_task_publish, 12 | after_task_publish, 13 | task_prerun, 14 | task_retry, 15 | task_success, 16 | task_failure, 17 | task_revoked, 18 | task_unknown, 19 | task_rejected, 20 | ) 21 | from django_structlog.celery.receivers import ( 22 | receiver_before_task_publish, 23 | receiver_after_task_publish, 24 | receiver_task_pre_run, 25 | receiver_task_retry, 26 | receiver_task_success, 27 | receiver_task_failure, 28 | receiver_task_revoked, 29 | receiver_task_unknown, 30 | receiver_task_rejected, 31 | ) 32 | 33 | with patch( 34 | "celery.utils.dispatch.signal.Signal.connect", autospec=True 35 | ) as mock_connect: 36 | steps.DjangoStructLogInitStep(None) 37 | 38 | mock_connect.assert_has_calls( 39 | [ 40 | call(before_task_publish, receiver_before_task_publish), 41 | call(after_task_publish, receiver_after_task_publish), 42 | call(task_prerun, receiver_task_pre_run), 43 | call(task_retry, receiver_task_retry), 44 | call(task_success, receiver_task_success), 45 | call(task_failure, receiver_task_failure), 46 | call(task_revoked, receiver_task_revoked), 47 | call(task_unknown, receiver_task_unknown), 48 | call(task_rejected, receiver_task_rejected), 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | local_postgres_data: {} 5 | local_postgres_data_backups: {} 6 | 7 | services: 8 | django: 9 | build: 10 | context: . 11 | dockerfile: ./compose/local/django/Dockerfile 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/.postgres 20 | restart: on-failure 21 | ports: 22 | - "8000:8000" 23 | command: /start 24 | 25 | postgres: 26 | build: 27 | context: . 28 | dockerfile: ./compose/local/postgres/Dockerfile 29 | image: django_structlog_demo_project_local_postgres 30 | volumes: 31 | - local_postgres_data:/var/lib/postgresql/data:cached 32 | - local_postgres_data_backups:/backups:cached 33 | env_file: 34 | - ./.envs/.local/.postgres 35 | 36 | redis: 37 | image: redis:3.2 38 | ports: 39 | - "6379:6379" 40 | 41 | celeryworker: 42 | image: django_structlog_demo_project_local_django 43 | depends_on: 44 | - redis 45 | - postgres 46 | volumes: 47 | - .:/app:cached 48 | env_file: 49 | - ./.envs/.local/.django 50 | - ./.envs/.local/.postgres 51 | 52 | command: /start-celeryworker 53 | 54 | celerybeat: 55 | image: django_structlog_demo_project_local_django 56 | depends_on: 57 | - redis 58 | - postgres 59 | volumes: 60 | - .:/app:cached 61 | env_file: 62 | - ./.envs/.local/.django 63 | - ./.envs/.local/.postgres 64 | 65 | command: /start-celerybeat 66 | 67 | flower: 68 | image: django_structlog_demo_project_local_django 69 | ports: 70 | - "5555:5555" 71 | volumes: 72 | - .:/app:cached 73 | env_file: 74 | - ./.envs/.local/.django 75 | - ./.envs/.local/.postgres 76 | command: /start-flower 77 | -------------------------------------------------------------------------------- /django_structlog/celery/receivers.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | 3 | from . import signals 4 | 5 | 6 | logger = structlog.getLogger(__name__) 7 | 8 | 9 | def receiver_before_task_publish(sender=None, headers=None, body=None, **kwargs): 10 | immutable_logger = structlog.threadlocal.as_immutable(logger) 11 | # noinspection PyProtectedMember 12 | context = dict(immutable_logger._context) 13 | if "task_id" in context: 14 | context["parent_task_id"] = context.pop("task_id") 15 | headers["__django_structlog__"] = context 16 | 17 | 18 | def receiver_after_task_publish(sender=None, headers=None, body=None, **kwargs): 19 | logger.info( 20 | "task_enqueued", child_task_id=headers["id"], child_task_name=headers["task"] 21 | ) 22 | 23 | 24 | def receiver_task_pre_run(task_id, task, *args, **kwargs): 25 | logger.new() 26 | logger.bind(task_id=task_id) 27 | signals.bind_extra_task_metadata.send( 28 | sender=receiver_task_pre_run, task=task, logger=logger 29 | ) 30 | metadata = getattr(task.request, "__django_structlog__", {}) 31 | logger.bind(**metadata) 32 | 33 | 34 | def receiver_task_retry(request=None, reason=None, einfo=None, **kwargs): 35 | logger.warning("task_retrying", reason=reason) 36 | 37 | 38 | def receiver_task_success(result=None, **kwargs): 39 | logger.info("task_succeed", result=str(result)) 40 | 41 | 42 | def receiver_task_failure( 43 | task_id=None, exception=None, traceback=None, einfo=None, *args, **kwargs 44 | ): 45 | 46 | logger.exception("task_failed", error=str(exception), exception=exception) 47 | 48 | 49 | def receiver_task_revoked( 50 | request=None, terminated=None, signum=None, expired=None, **kwargs 51 | ): 52 | logger.warning( 53 | "task_revoked", terminated=terminated, signum=signum, expired=expired 54 | ) 55 | 56 | 57 | def receiver_task_unknown(message=None, exc=None, name=None, id=None, **kwargs): 58 | logger.error("task_not_found", message=message) 59 | 60 | 61 | def receiver_task_rejected(message=None, exc=None, **kwargs): 62 | logger.error("task_rejected", message=message) 63 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | sudo: false 4 | script: tox 5 | cache: pip 6 | python: 7 | - '3.5' 8 | - '3.6' 9 | - '3.7' 10 | - '3.8' 11 | install: pip install tox-travis tox flake8 coverage codecov 12 | after_success: codecov 13 | services: redis-server 14 | jobs: 15 | include: 16 | - name: 'Test demo app' 17 | python: '3.7' 18 | install: pip install codecov -r requirements.txt 19 | env: 20 | CELERY_BROKER_URL=redis://0.0.0.0:6379 21 | DJANGO_SETTINGS_MODULE=config.settings.test_demo_app 22 | script: 23 | - pytest --cov=./django_structlog_demo_project --cov-append django_structlog_demo_project 24 | - name: 'Test build doc' 25 | python: '3.7' 26 | install: pip install -r requirements/doc.txt 27 | script: 28 | - cd docs 29 | - make html 30 | - make doctest 31 | services: [] 32 | after_success: skip 33 | - stage: linter 34 | name: 'black' 35 | python: '3.7' 36 | install: pip install black 37 | script: black --check --verbose . 38 | services: [] 39 | after_success: skip 40 | - stage: linter 41 | name: 'flake8' 42 | python: '3.7' 43 | install: pip install flake8 44 | script: flake8 45 | services: [] 46 | after_success: skip 47 | - stage: deploy 48 | name: 'PyPI deployment' 49 | python: '3.7' 50 | script: skip 51 | services: [] 52 | install: skip 53 | deploy: 54 | provider: pypi 55 | user: jrobichaud 56 | password: 57 | secure: UYfQKAmVJvwaZc0jX7ej9DYnNgNK8aQb6rXtWm9WUm6BfvxKdGjWvCJHl/CU2NaAqZV+UAsypQPswoPZX0CiGbKUT/zf8irfs9Ay6/UZjnWGd1PPGi3lPHOJ0zLwKzrsKC1K308BZ1pSdQsbKuOHGATFXKKwDLHfuDA89rQaNOD6cJcAZct1PAQROH6R9OIhHzCXBbZkP8Ak1mic7XQnjiJBWUw/YxRez9gYkNOJjN7QEcJC3HickCfWa5SaHqF3Yss+z9+tXbVHZ9305pVyFbh+QqUtdi9GfDnZEoMac9XyovSxRQwgJAxZH+FBud8tROQFGRR2Qd8AHlkR2f0s6DoF8EBoPRP5GUa89Svlw23o1SVx0jqXQAmhctwL+a5SHed0fhiLC/eYnKT9Qq43VofeD8rLYRvWA8F2Bx3nGeV7YmWzlrqWLNK/p88BfEIQ02dq6ZIbJA3P9lOqWi+E73Tm/f5F+TZcxufFmwPWts2RGDfRng5qEBe8J9eQAR6CmeFG/wRsbchC5QHA9UH6LZVlQmX9Z9sW0HDsbet1ovVDo+j8cMqVbwd3A4XEBHLsO+cJxdWjLuDrrsQAB4k4dVHuosO5SRMN5bxc/Rz/2c3TKdClyfzjW2e38RdABRCmkxU3cS6JOps1qxW7Ky+jth7euE6NTf9nhTuRXV1gKVE= 58 | on: 59 | tags: true 60 | distributions: "sdist bdist_wheel" 61 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | sys.path.append(os.path.join(os.path.dirname(__file__), "..")) 4 | 5 | 6 | # Configuration file for the Sphinx documentation builder. 7 | # 8 | # This file only contains a selection of the most common options. For a full 9 | # list see the documentation: 10 | # http://www.sphinx-doc.org/en/master/config 11 | 12 | # -- Path setup -------------------------------------------------------------- 13 | 14 | # If extensions (or modules to document with autodoc) are in another directory, 15 | # add these directories to sys.path here. If the directory is relative to the 16 | # documentation root, use os.path.abspath to make it absolute, like shown here. 17 | # 18 | # import os 19 | # import sys 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # The master toctree document. 23 | master_doc = "index" 24 | 25 | # -- Project information ----------------------------------------------------- 26 | 27 | project = "django-structlog" 28 | copyright = "2019, Jules Robichaud-Gagnon" 29 | author = "Jules Robichaud-Gagnon" 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 46 | 47 | 48 | def get_version(precision): 49 | import django_structlog 50 | 51 | return ".".join(str(v) for v in django_structlog.VERSION[:precision]) 52 | 53 | 54 | # Full version 55 | release = get_version(3) 56 | 57 | # Minor version 58 | version = get_version(2) 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | 62 | # The theme to use for HTML and HTML Help pages. See the documentation for 63 | # a list of builtin themes. 64 | # 65 | html_theme = "sphinx_rtd_theme" 66 | 67 | # Add any paths that contain custom static files (such as style sheets) here, 68 | # relative to this directory. They are copied after the builtin static files, 69 | # so a file named "default.css" will overwrite the builtin "default.css". 70 | html_static_path = ["_static"] 71 | -------------------------------------------------------------------------------- /config/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import url, include 3 | from django.conf.urls.static import static 4 | from django.contrib import admin 5 | from django.views.generic import TemplateView 6 | from django.views import defaults as default_views 7 | 8 | from django_structlog_demo_project.home import views, api_views 9 | 10 | 11 | def uncaught_exception_view(request): 12 | raise Exception("Uncaught Exception") 13 | 14 | 15 | urlpatterns = [ 16 | url(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"), 17 | url( 18 | r"^success_task$", views.enqueue_successful_task, name="enqueue_successful_task" 19 | ), 20 | url(r"^failing_task$", views.enqueue_failing_task, name="enqueue_failing_task"), 21 | url(r"^nesting_task$", views.enqueue_nesting_task, name="enqueue_nesting_task"), 22 | url(r"^raise_exception", views.raise_exception, name="raise_exception"), 23 | url(r"^api_view$", api_views.home_api_view, name="api_view"), 24 | url( 25 | r"^about/", TemplateView.as_view(template_name="pages/about.html"), name="about" 26 | ), 27 | # Django Admin, use {% url 'admin:index' %} 28 | url(settings.ADMIN_URL, admin.site.urls), 29 | # User management 30 | url( 31 | r"^users/", 32 | include("django_structlog_demo_project.users.urls", namespace="users"), 33 | ), 34 | url(r"^accounts/", include("allauth.urls")), 35 | # Your stuff: custom urls includes go here 36 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 37 | 38 | if settings.DEBUG: 39 | # This allows the error pages to be debugged during development, just visit 40 | # these url in browser to see how these error pages look like. 41 | urlpatterns += [ 42 | url( 43 | r"^400/", 44 | default_views.bad_request, 45 | kwargs={"exception": Exception("Bad Request!")}, 46 | ), 47 | url( 48 | r"^403/", 49 | default_views.permission_denied, 50 | kwargs={"exception": Exception("Permission Denied")}, 51 | ), 52 | url( 53 | r"^404/", 54 | default_views.page_not_found, 55 | kwargs={"exception": Exception("Page not Found")}, 56 | ), 57 | url(r"^500/", default_views.server_error), 58 | url(r"^uncaught_exception/", uncaught_exception_view), 59 | ] 60 | if "debug_toolbar" in settings.INSTALLED_APPS: 61 | import debug_toolbar 62 | 63 | urlpatterns = [url(r"^__debug__/", include(debug_toolbar.urls))] + urlpatterns 64 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | pytz==2018.9 # https://github.com/stub42/pytz 2 | python-slugify==3.0.2 # https://github.com/un33k/python-slugify 3 | argon2-cffi==19.1.0 # https://github.com/hynek/argon2_cffi 4 | whitenoise==4.1.2 # https://github.com/evansd/whitenoise 5 | redis==3.2.1 # https://github.com/antirez/redis 6 | celery==4.3.0 # pyup: < 5.0 # https://github.com/celery/celery 7 | flower==0.9.3 # https://github.com/mher/flower 8 | 9 | # Django 10 | # ------------------------------------------------------------------------------ 11 | django>=2,<3 # pyup: < 2.1 # https://www.djangoproject.com/ 12 | django-environ==0.4.5 # https://github.com/joke2k/django-environ 13 | django-model-utils==3.1.2 # https://github.com/jazzband/django-model-utils 14 | django-allauth==0.39.1 # https://github.com/pennersr/django-allauth 15 | django-crispy-forms==1.7.2 # https://github.com/django-crispy-forms/django-crispy-forms 16 | django-redis==4.10.0 # https://github.com/niwinz/django-redis 17 | 18 | # Django REST Framework 19 | djangorestframework==3.9.2 # https://github.com/encode/django-rest-framework 20 | coreapi==2.3.3 # https://github.com/core-api/python-client 21 | 22 | structlog>=19.2.0,<20 23 | colorama>=0.4.1 24 | django-ipware==2.1.0 25 | 26 | Werkzeug==0.15.2 # https://github.com/pallets/werkzeug 27 | ipdb==0.12 # https://github.com/gotcha/ipdb 28 | psycopg2-binary==2.8 # https://github.com/psycopg/psycopg2 29 | 30 | # Testing 31 | # ------------------------------------------------------------------------------ 32 | mypy==0.761 # https://github.com/python/mypy 33 | pytest==4.4.0 # https://github.com/pytest-dev/pytest 34 | pytest-sugar==0.9.2 # https://github.com/Frozenball/pytest-sugar 35 | pytest-cov==2.6.1 36 | 37 | # Code quality 38 | # ------------------------------------------------------------------------------ 39 | flake8==3.7.8 # https://github.com/PyCQA/flake8 40 | coverage==5.0.1 # https://github.com/nedbat/coveragepy 41 | black==19.10b0 # https://github.com/ambv/black 42 | pylint-django==2.0.10 # https://github.com/PyCQA/pylint-django 43 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery 44 | 45 | # Django 46 | # ------------------------------------------------------------------------------ 47 | factory-boy==2.11.1 # https://github.com/FactoryBoy/factory_boy 48 | 49 | django-debug-toolbar==1.11 # https://github.com/jazzband/django-debug-toolbar 50 | django-extensions==2.1.6 # https://github.com/django-extensions/django-extensions 51 | django-coverage-plugin==1.6.0 # https://github.com/nedbat/django_coverage_plugin 52 | pytest-django==3.4.8 # https://github.com/pytest-dev/pytest-django 53 | 54 | # pre-commit 55 | # ------------------------------------------------------------------------------ 56 | pre-commit==1.16.1 # https://github.com/pre-commit/pre-commit 57 | -------------------------------------------------------------------------------- /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/middlewares/request.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import structlog 4 | import traceback 5 | from django.http import Http404 6 | 7 | from .. import signals 8 | 9 | logger = structlog.getLogger(__name__) 10 | 11 | 12 | def get_request_header(request, header_key, meta_key): 13 | if hasattr(request, "headers"): 14 | return request.headers.get(header_key) 15 | 16 | return request.META.get(meta_key) 17 | 18 | 19 | class RequestMiddleware: 20 | """ ``RequestMiddleware`` adds request metadata to ``structlog``'s logger context automatically. 21 | 22 | >>> MIDDLEWARE = [ 23 | ... # ... 24 | ... 'django_structlog.middlewares.RequestMiddleware', 25 | ... ] 26 | 27 | """ 28 | 29 | def __init__(self, get_response): 30 | self.get_response = get_response 31 | self._raised_exception = False 32 | 33 | def __call__(self, request): 34 | from ipware import get_client_ip 35 | 36 | request_id = get_request_header( 37 | request, "x-request-id", "HTTP_X_REQUEST_ID" 38 | ) or str(uuid.uuid4()) 39 | 40 | correlation_id = get_request_header( 41 | request, "x-correlation-id", "HTTP_X_CORRELATION_ID" 42 | ) 43 | 44 | with structlog.threadlocal.tmp_bind(logger): 45 | logger.bind(request_id=request_id) 46 | 47 | if hasattr(request, "user"): 48 | logger.bind(user_id=request.user.pk) 49 | 50 | if correlation_id: 51 | logger.bind(correlation_id=correlation_id) 52 | 53 | ip, _ = get_client_ip(request) 54 | logger.bind(ip=ip) 55 | signals.bind_extra_request_metadata.send( 56 | sender=self.__class__, request=request, logger=logger 57 | ) 58 | 59 | logger.info( 60 | "request_started", 61 | request=request, 62 | user_agent=request.META.get("HTTP_USER_AGENT"), 63 | ) 64 | self._raised_exception = False 65 | response = self.get_response(request) 66 | if not self._raised_exception: 67 | logger.info( 68 | "request_finished", code=response.status_code, request=request 69 | ) 70 | 71 | return response 72 | 73 | def process_exception(self, request, exception): 74 | if isinstance(exception, Http404): 75 | # We don't log an exception here, and we don't set that we handled 76 | # an error as we want the standard `request_finished` log message 77 | # to be emitted. 78 | return 79 | 80 | self._raised_exception = True 81 | 82 | traceback_object = exception.__traceback__ 83 | formatted_traceback = traceback.format_tb(traceback_object) 84 | logger.exception( 85 | "request_failed", 86 | code=500, 87 | request=request, 88 | error=exception, 89 | error_traceback=formatted_traceback, 90 | ) 91 | -------------------------------------------------------------------------------- /django_structlog_demo_project/taskapp/celery.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | import structlog 5 | from celery import Celery, shared_task, signals 6 | from django.apps import apps, AppConfig 7 | from django.conf import settings 8 | 9 | from django_structlog.celery.steps import DjangoStructLogInitStep 10 | 11 | if not settings.configured: 12 | # set the default Django settings module for the 'celery' program. 13 | os.environ.setdefault( 14 | "DJANGO_SETTINGS_MODULE", "config.settings.local" 15 | ) # pragma: no cover 16 | 17 | 18 | app = Celery("django_structlog_demo_project") 19 | # Using a string here means the worker will not have to 20 | # pickle the object when using Windows. 21 | # - namespace='CELERY' means all celery-related configuration keys 22 | # should have a `CELERY_` prefix. 23 | app.config_from_object("django.conf:settings", namespace="CELERY") 24 | 25 | # A step to initialize django-structlog 26 | app.steps["worker"].add(DjangoStructLogInitStep) 27 | 28 | 29 | @signals.setup_logging.connect 30 | def receiver_setup_logging( 31 | loglevel, logfile, format, colorize, **kwargs 32 | ): # pragma: no cover 33 | logging.basicConfig(**settings.LOGGING) 34 | 35 | structlog.configure( 36 | processors=[ 37 | structlog.stdlib.filter_by_level, 38 | structlog.processors.TimeStamper(fmt="iso"), 39 | structlog.stdlib.add_logger_name, 40 | structlog.stdlib.add_log_level, 41 | structlog.stdlib.PositionalArgumentsFormatter(), 42 | structlog.processors.StackInfoRenderer(), 43 | structlog.processors.format_exc_info, 44 | structlog.processors.UnicodeDecoder(), 45 | structlog.processors.ExceptionPrettyPrinter(), 46 | # structlog.processors.KeyValueRenderer(), 47 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 48 | ], 49 | context_class=structlog.threadlocal.wrap_dict(dict), 50 | logger_factory=structlog.stdlib.LoggerFactory(), 51 | wrapper_class=structlog.stdlib.BoundLogger, 52 | cache_logger_on_first_use=True, 53 | ) 54 | 55 | 56 | class CeleryAppConfig(AppConfig): 57 | name = "django_structlog_demo_project.taskapp" 58 | verbose_name = "Celery Config" 59 | 60 | def ready(self): 61 | installed_apps = [app_config.name for app_config in apps.get_app_configs()] 62 | app.autodiscover_tasks(lambda: installed_apps, force=True) 63 | 64 | 65 | @app.task(bind=True) 66 | def debug_task(self): 67 | print("Request: {request}".format(request=self.request)) # pragma: no cover 68 | 69 | 70 | @shared_task 71 | def successful_task(foo=None): 72 | import structlog 73 | 74 | logger = structlog.getLogger(__name__) 75 | logger.info("This is a successful task") 76 | 77 | 78 | @shared_task 79 | def failing_task(foo=None, **kwargs): 80 | raise Exception("This is a failed task") 81 | 82 | 83 | @shared_task 84 | def nesting_task(): 85 | logger = structlog.getLogger(__name__) 86 | logger.bind(foo="Bar") 87 | logger.info("This is a nesting task") 88 | 89 | nested_task.delay() 90 | 91 | 92 | @shared_task 93 | def nested_task(): 94 | logger = structlog.getLogger(__name__) 95 | logger.info("This is a nested task") 96 | -------------------------------------------------------------------------------- /config/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | import os 5 | 6 | import environ 7 | 8 | import structlog 9 | 10 | 11 | env = environ.Env() 12 | 13 | ROOT_DIR = ( 14 | environ.Path(__file__) - 3 15 | ) # (test_app/config/settings/base.py - 3 = test_app/) 16 | APPS_DIR = ROOT_DIR.path("test_app") 17 | 18 | # APPS 19 | # ------------------------------------------------------------------------------ 20 | INSTALLED_APPS = [ 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sessions", 24 | "django.contrib.sites", 25 | "django.contrib.messages", 26 | "django.contrib.staticfiles", 27 | ] 28 | 29 | 30 | # GENERAL 31 | # ------------------------------------------------------------------------------ 32 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 33 | DEBUG = False 34 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 35 | SECRET_KEY = env( 36 | "DJANGO_SECRET_KEY", 37 | default="SqlHVcvZwwazrUrjtUiMJerENM8bU3k2p7WZu1WgA4yc8R1DcDc2Rh54m8dRvWcs", 38 | ) 39 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 40 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 41 | 42 | # CACHES 43 | # ------------------------------------------------------------------------------ 44 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 45 | CACHES = { 46 | "default": { 47 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 48 | "LOCATION": "", 49 | } 50 | } 51 | 52 | # EMAIL 53 | # ------------------------------------------------------------------------------ 54 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 55 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 56 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 57 | EMAIL_HOST = "localhost" 58 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 59 | EMAIL_PORT = 1025 60 | 61 | # Your stuff... 62 | # ------------------------------------------------------------------------------ 63 | 64 | LOGGING = { 65 | "version": 1, 66 | "disable_existing_loggers": True, 67 | "formatters": { 68 | "plain": { 69 | "()": structlog.stdlib.ProcessorFormatter, 70 | "processor": structlog.dev.ConsoleRenderer(colors=False), 71 | }, 72 | "colored": { 73 | "()": structlog.stdlib.ProcessorFormatter, 74 | "processor": structlog.dev.ConsoleRenderer(colors=True), 75 | }, 76 | }, 77 | "filters": {}, 78 | "handlers": { 79 | "structured_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, 80 | "structured_file": { 81 | "class": "logging.handlers.WatchedFileHandler", 82 | "filename": "test.log", 83 | "formatter": "plain", 84 | }, 85 | }, 86 | "loggers": {"": {"handlers": ["structured_stream"], "level": "INFO"}}, 87 | } 88 | 89 | structlog.configure( 90 | processors=[ 91 | structlog.stdlib.filter_by_level, 92 | structlog.processors.TimeStamper(fmt="iso"), 93 | structlog.stdlib.add_logger_name, 94 | structlog.stdlib.add_log_level, 95 | structlog.stdlib.PositionalArgumentsFormatter(), 96 | structlog.processors.StackInfoRenderer(), 97 | structlog.processors.format_exc_info, 98 | structlog.processors.UnicodeDecoder(), 99 | structlog.processors.ExceptionPrettyPrinter(), 100 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 101 | ], 102 | context_class=structlog.threadlocal.wrap_dict(dict), 103 | logger_factory=structlog.stdlib.LoggerFactory(), 104 | wrapper_class=structlog.stdlib.BoundLogger, 105 | cache_logger_on_first_use=True, 106 | ) 107 | 108 | DATABASES = { 109 | "default": { 110 | "ENGINE": "django.db.backends.sqlite3", 111 | "NAME": os.path.join(str(ROOT_DIR), "db.sqlite3"), 112 | } 113 | } 114 | 115 | INSTALLED_APPS += ["django_structlog", "test_app"] 116 | -------------------------------------------------------------------------------- /config/settings/test_demo_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | With these settings, tests run faster. 3 | """ 4 | import os 5 | 6 | import structlog 7 | 8 | from .base import * # noqa: F403 9 | from .base import env, INSTALLED_APPS, ROOT_DIR 10 | 11 | # GENERAL 12 | # ------------------------------------------------------------------------------ 13 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 14 | DEBUG = False 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 16 | SECRET_KEY = env( 17 | "DJANGO_SECRET_KEY", 18 | default="SqlHVcvZwwazrUrjtUiMJerENM8bU3k2p7WZu1WgA4yc8R1DcDc2Rh54m8dRvWcs", 19 | ) 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner 21 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 22 | 23 | # CACHES 24 | # ------------------------------------------------------------------------------ 25 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 26 | CACHES = { 27 | "default": { 28 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 29 | "LOCATION": "", 30 | } 31 | } 32 | 33 | # PASSWORDS 34 | # ------------------------------------------------------------------------------ 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 36 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 37 | 38 | # TEMPLATES 39 | # ------------------------------------------------------------------------------ 40 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 41 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405 42 | TEMPLATES[0]["OPTIONS"]["loaders"] = [ # noqa F405 43 | ( 44 | "django.template.loaders.cached.Loader", 45 | [ 46 | "django.template.loaders.filesystem.Loader", 47 | "django.template.loaders.app_directories.Loader", 48 | ], 49 | ) 50 | ] 51 | 52 | # EMAIL 53 | # ------------------------------------------------------------------------------ 54 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 55 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 56 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 57 | EMAIL_HOST = "localhost" 58 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 59 | EMAIL_PORT = 1025 60 | 61 | # Your stuff... 62 | # ------------------------------------------------------------------------------ 63 | 64 | LOGGING = { 65 | "version": 1, 66 | "disable_existing_loggers": True, 67 | "formatters": { 68 | "plain": { 69 | "()": structlog.stdlib.ProcessorFormatter, 70 | "processor": structlog.dev.ConsoleRenderer(colors=False), 71 | }, 72 | "colored": { 73 | "()": structlog.stdlib.ProcessorFormatter, 74 | "processor": structlog.dev.ConsoleRenderer(colors=True), 75 | }, 76 | }, 77 | "filters": {}, 78 | "handlers": { 79 | "structured_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, 80 | "structured_file": { 81 | "class": "logging.handlers.WatchedFileHandler", 82 | "filename": "test.log", 83 | "formatter": "plain", 84 | }, 85 | }, 86 | "loggers": {"": {"handlers": ["structured_stream"], "level": "INFO"}}, 87 | } 88 | 89 | structlog.configure( 90 | processors=[ 91 | structlog.stdlib.filter_by_level, 92 | structlog.processors.TimeStamper(fmt="iso"), 93 | structlog.stdlib.add_logger_name, 94 | structlog.stdlib.add_log_level, 95 | structlog.stdlib.PositionalArgumentsFormatter(), 96 | structlog.processors.StackInfoRenderer(), 97 | structlog.processors.format_exc_info, 98 | structlog.processors.UnicodeDecoder(), 99 | structlog.processors.ExceptionPrettyPrinter(), 100 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 101 | ], 102 | context_class=structlog.threadlocal.wrap_dict(dict), 103 | logger_factory=structlog.stdlib.LoggerFactory(), 104 | wrapper_class=structlog.stdlib.BoundLogger, 105 | cache_logger_on_first_use=True, 106 | ) 107 | 108 | DATABASES = { 109 | "default": { 110 | "ENGINE": "django.db.backends.sqlite3", 111 | "NAME": os.path.join(str(ROOT_DIR), "db.sqlite3"), 112 | } 113 | } 114 | 115 | INSTALLED_APPS += ["django_structlog_demo_project"] 116 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | 5 | 1.4.1 (February 8, 2020) 6 | ------------------------ 7 | 8 | *New:* 9 | - Bind ``X-Correlation-ID`` HTTP header's value as ``correlation_id`` when provided in request. 10 | 11 | 12 | 1.4.0 (February 7, 2020) 13 | ------------------------ 14 | 15 | *New:* 16 | - Use ``X-Request-ID`` HTTP header's value as ``request_id`` when provided in request. See `#22 `_. Special thanks to `@jairhenrique `_ 17 | 18 | 19 | 1.3.5 (December 23, 2019) 20 | ------------------------- 21 | 22 | *New:* 23 | - Add python 3.8, celery 4.4 and django 3.0 to the test matrix. 24 | 25 | *Improvements:* 26 | - Extract ``test_app`` from ``django_structlog_demo_app`` in order to test ``django_structlog`` all by itself 27 | - Improve CI execution speed by merging stages 28 | - Upgrade a few development depencencies 29 | 30 | 31 | 1.3.4 (November 27, 2019) 32 | ------------------------- 33 | 34 | *Bugfix:* 35 | - Exception logging not working properly with ``DEBUG = False``. See `#19 `_. Special thanks to `@danpalmer `_ 36 | 37 | 38 | 1.3.3 (October 6, 2019) 39 | ----------------------- 40 | 41 | *Bugfix:* 42 | - Fix support of different primary key for ``User`` model. See `#13 `_. Special thanks to `@dhararon `_ 43 | 44 | 45 | 1.3.2 (September 21, 2019) 46 | -------------------------- 47 | 48 | *Improvements:* 49 | - Add support of projects without ``AuthenticationMiddleware``. See `#9 `_. Special thanks to `@dhararon `_ 50 | 51 | 52 | 1.3.1 (September 4, 2019) 53 | ------------------------- 54 | 55 | *Bugfixes:* 56 | - Remove extraneous ``rest-framework`` dependency introduced by `#7 `_. See `#8 `_ . Special thanks to `@ghickman `_ 57 | 58 | 59 | 1.3.0 (September 3, 2019) 60 | ------------------------- 61 | 62 | *Improvements:* 63 | - Improve django uncaught exception formatting. See `#7 `_. Special thanks to `@paulstuartparker `_ 64 | 65 | 66 | 1.2.3 (May 18, 2019) 67 | -------------------- 68 | 69 | *Bugfixes:* 70 | - Fix ``structlog`` dependency not being installed 71 | 72 | *Improvements:* 73 | - Use `black `_ code formatter 74 | 75 | 76 | 1.2.2 (May 13, 2019) 77 | -------------------- 78 | 79 | *Improvements:* 80 | - Use appropriate packaging 81 | 82 | 83 | 1.2.1 (May 8, 2019) 84 | ------------------- 85 | 86 | *Bugfixes:* 87 | - Fix missing license file to be included in distribution 88 | 89 | 90 | 1.2.0 (May 8, 2019) 91 | ------------------- 92 | 93 | *Changes:* 94 | - In the event ``task_enqueued``, ``task_id`` and ``task_name`` are renamed ``child_task_id`` and ``child_task_name`` respectively to avoid override of ``task_id`` in nested tasks. 95 | 96 | 97 | 1.1.6 (May 8, 2019) 98 | ------------------- 99 | 100 | *New:* 101 | - Add ``task_name`` when a task is enqueued 102 | 103 | 104 | 1.1.5 (May 8, 2019) 105 | ------------------- 106 | 107 | *New:* 108 | - Add support of tasks calling other tasks (introducing ``parent_task_id``) 109 | 110 | *Bugfixes:* 111 | - Fix missing packages 112 | 113 | 114 | 1.1.4 (April 22, 2019) 115 | ---------------------- 116 | 117 | *Improvements:* 118 | - Wheel distribution 119 | 120 | 121 | 1.1.3 (April 22, 2019) 122 | ---------------------- 123 | 124 | *Improvements:* 125 | - api documentation 126 | - code documentation 127 | 128 | 1.1.2 (April 19, 2019) 129 | ---------------------- 130 | 131 | *Changes:* 132 | - Rewrite the log texts as events 133 | 134 | 1.1.1 (April 18, 2019) 135 | ---------------------- 136 | 137 | *New:* 138 | - Add ``celery`` signal ``signals.bind_extra_task_metadata`` 139 | 140 | 141 | 1.1 (April 16, 2019) 142 | -------------------- 143 | 144 | *New:* 145 | - Add ``celery`` tasks support 146 | 147 | 148 | 1.0.4 to 1.0.7 (April 14, 2019) 149 | ------------------------------- 150 | 151 | *New:* 152 | - Automated releases with tags on ``travis`` 153 | 154 | 1.0.3 (April 14, 2019) 155 | ---------------------- 156 | 157 | *Bugfixes:* 158 | - Add ``bind_extra_request_metadata`` documentation 159 | 160 | 1.0.2 (April 13, 2019) 161 | ---------------------- 162 | 163 | *Bugfixes:* 164 | - Tweaked documentation. 165 | 166 | 1.0.0 (April 13, 2019) 167 | ---------------------- 168 | 169 | *New*: 170 | - Fist public release. -------------------------------------------------------------------------------- /docs/celery.rst: -------------------------------------------------------------------------------- 1 | Celery Integration 2 | ================== 3 | 4 | Getting Started with Celery 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | In order to be able to support celery you need to configure both your webapp and your workers 8 | 9 | Add CeleryMiddleWare in your web app 10 | ------------------------------------ 11 | 12 | In your settings.py 13 | 14 | .. code-block:: python 15 | 16 | MIDDLEWARE = [ 17 | # ... 18 | 'django_structlog.middlewares.RequestMiddleware', 19 | 'django_structlog.middlewares.CeleryMiddleware', 20 | ] 21 | 22 | 23 | Initialize Celery Worker with DjangoStructLogInitStep 24 | ----------------------------------------------------- 25 | 26 | In your celery AppConfig's module. 27 | 28 | .. code-block:: python 29 | 30 | import logging 31 | 32 | import structlog 33 | from celery import Celery 34 | from celery.signals import setup_logging 35 | from django_structlog.celery.steps import DjangoStructLogInitStep 36 | 37 | app = Celery("your_celery_project") 38 | 39 | # A step to initialize django-structlog 40 | app.steps['worker'].add(DjangoStructLogInitStep) 41 | 42 | 43 | Configure celery's logger 44 | ------------------------- 45 | 46 | In the same file as before 47 | 48 | .. code-block:: python 49 | 50 | @setup_logging.connect 51 | def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover 52 | logging.basicConfig( 53 | { 54 | "version": 1, 55 | "disable_existing_loggers": False, 56 | "formatters": { 57 | "json_formatter": { 58 | "()": structlog.stdlib.ProcessorFormatter, 59 | "processor": structlog.processors.JSONRenderer(), 60 | }, 61 | "plain_console": { 62 | "()": structlog.stdlib.ProcessorFormatter, 63 | "processor": structlog.dev.ConsoleRenderer(), 64 | }, 65 | "key_value": { 66 | "()": structlog.stdlib.ProcessorFormatter, 67 | "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']), 68 | }, 69 | }, 70 | "handlers": { 71 | "console": { 72 | "class": "logging.StreamHandler", 73 | "formatter": "plain_console", 74 | }, 75 | "json_file": { 76 | "class": "logging.handlers.WatchedFileHandler", 77 | "filename": "logs/json.log", 78 | "formatter": "json_formatter", 79 | }, 80 | "flat_line_file": { 81 | "class": "logging.handlers.WatchedFileHandler", 82 | "filename": "logs/flat_line.log", 83 | "formatter": "key_value", 84 | }, 85 | }, 86 | "loggers": { 87 | "django_structlog": { 88 | "handlers": ["console", "flat_line_file", "json_file"], 89 | "level": "INFO", 90 | }, 91 | "django_structlog_demo_project": { 92 | "handlers": ["console", "flat_line_file", "json_file"], 93 | "level": "INFO", 94 | }, 95 | } 96 | } 97 | ) 98 | 99 | structlog.configure( 100 | processors=[ 101 | structlog.stdlib.filter_by_level, 102 | structlog.processors.TimeStamper(fmt="iso"), 103 | structlog.stdlib.add_logger_name, 104 | structlog.stdlib.add_log_level, 105 | structlog.stdlib.PositionalArgumentsFormatter(), 106 | structlog.processors.StackInfoRenderer(), 107 | structlog.processors.format_exc_info, 108 | structlog.processors.UnicodeDecoder(), 109 | structlog.processors.ExceptionPrettyPrinter(), 110 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 111 | ], 112 | context_class=structlog.threadlocal.wrap_dict(dict), 113 | logger_factory=structlog.stdlib.LoggerFactory(), 114 | wrapper_class=structlog.stdlib.BoundLogger, 115 | cache_logger_on_first_use=True, 116 | ) 117 | 118 | 119 | .. _celery_signals: 120 | 121 | Signals 122 | ^^^^^^^ 123 | 124 | 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 125 | in celery's ``receiver_task_pre_run``. 126 | 127 | .. code-block:: python 128 | 129 | from django_structlog.celery import signals 130 | 131 | @receiver(signals.bind_extra_task_metadata) 132 | def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None): 133 | logger.bind(correlation_id=task.request.correlation_id) 134 | -------------------------------------------------------------------------------- /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 | 15 | 16 | 17 | 18 | {% block css %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% endblock %} 32 | 33 | 34 | 35 | 36 | 37 |
38 | 74 | 75 |
76 | 77 |
78 | 79 | {% if messages %} 80 | {% for message in messages %} 81 |
{{ message }}
82 | {% endfor %} 83 | {% endif %} 84 | 85 | {% block content %} 86 |

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

87 | {% endblock content %} 88 | 89 |
90 | 91 | {% block modal %}{% endblock modal %} 92 | 93 | 95 | 96 | {% block javascript %} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {% endblock javascript %} 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /django_structlog_demo_project/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | import django.contrib.auth.models 2 | import django.contrib.auth.validators 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [("auth", "0008_alter_user_username_max_length")] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="User", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("password", models.CharField(max_length=128, verbose_name="password")), 27 | ( 28 | "last_login", 29 | models.DateTimeField( 30 | blank=True, null=True, verbose_name="last login" 31 | ), 32 | ), 33 | ( 34 | "is_superuser", 35 | models.BooleanField( 36 | default=False, 37 | help_text="Designates that this user has all permissions without explicitly assigning them.", 38 | verbose_name="superuser status", 39 | ), 40 | ), 41 | ( 42 | "username", 43 | models.CharField( 44 | error_messages={ 45 | "unique": "A user with that username already exists." 46 | }, 47 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 48 | max_length=150, 49 | unique=True, 50 | validators=[ 51 | django.contrib.auth.validators.UnicodeUsernameValidator() 52 | ], 53 | verbose_name="username", 54 | ), 55 | ), 56 | ( 57 | "first_name", 58 | models.CharField( 59 | blank=True, max_length=30, verbose_name="first name" 60 | ), 61 | ), 62 | ( 63 | "last_name", 64 | models.CharField( 65 | blank=True, max_length=150, verbose_name="last name" 66 | ), 67 | ), 68 | ( 69 | "email", 70 | models.EmailField( 71 | blank=True, max_length=254, verbose_name="email address" 72 | ), 73 | ), 74 | ( 75 | "is_staff", 76 | models.BooleanField( 77 | default=False, 78 | help_text="Designates whether the user can log into this admin site.", 79 | verbose_name="staff status", 80 | ), 81 | ), 82 | ( 83 | "is_active", 84 | models.BooleanField( 85 | default=True, 86 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 87 | verbose_name="active", 88 | ), 89 | ), 90 | ( 91 | "date_joined", 92 | models.DateTimeField( 93 | default=django.utils.timezone.now, verbose_name="date joined" 94 | ), 95 | ), 96 | ( 97 | "name", 98 | models.CharField( 99 | blank=True, max_length=255, verbose_name="Name of User" 100 | ), 101 | ), 102 | ( 103 | "groups", 104 | models.ManyToManyField( 105 | blank=True, 106 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 107 | related_name="user_set", 108 | related_query_name="user", 109 | to="auth.Group", 110 | verbose_name="groups", 111 | ), 112 | ), 113 | ( 114 | "user_permissions", 115 | models.ManyToManyField( 116 | blank=True, 117 | help_text="Specific permissions for this user.", 118 | related_name="user_set", 119 | related_query_name="user", 120 | to="auth.Permission", 121 | verbose_name="user permissions", 122 | ), 123 | ), 124 | ], 125 | options={ 126 | "verbose_name_plural": "users", 127 | "verbose_name": "user", 128 | "abstract": False, 129 | }, 130 | managers=[("objects", django.contrib.auth.models.UserManager())], 131 | ) 132 | ] 133 | -------------------------------------------------------------------------------- /config/settings/local.py: -------------------------------------------------------------------------------- 1 | import structlog 2 | 3 | from .base import * # noqa: F403 4 | from .base import env 5 | 6 | # GENERAL 7 | # ------------------------------------------------------------------------------ 8 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 9 | DEBUG = True 10 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key 11 | SECRET_KEY = env( 12 | "DJANGO_SECRET_KEY", 13 | default="DXatocQyyroxzcpo0tDxK3v5Rm4fatD9U7UeuLWwnZMOIaCQdPWovuqp4rxOct1T", 14 | ) 15 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts 16 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"] 17 | 18 | # CACHES 19 | # ------------------------------------------------------------------------------ 20 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches 21 | CACHES = { 22 | "default": { 23 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 24 | "LOCATION": "", 25 | } 26 | } 27 | 28 | # TEMPLATES 29 | # ------------------------------------------------------------------------------ 30 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 31 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405 32 | 33 | # EMAIL 34 | # ------------------------------------------------------------------------------ 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 36 | EMAIL_BACKEND = env( 37 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 38 | ) 39 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host 40 | EMAIL_HOST = "localhost" 41 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port 42 | EMAIL_PORT = 1025 43 | 44 | # django-debug-toolbar 45 | # ------------------------------------------------------------------------------ 46 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites 47 | INSTALLED_APPS += ["debug_toolbar"] # noqa F405 48 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware 49 | MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 50 | # https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config 51 | DEBUG_TOOLBAR_CONFIG = { 52 | "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], 53 | "SHOW_TEMPLATE_CONTEXT": True, 54 | } 55 | # https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips 56 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] 57 | if env("USE_DOCKER") == "yes": 58 | import socket 59 | 60 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) 61 | INTERNAL_IPS += [ip[:-1] + "1" for ip in ips] 62 | 63 | # django-extensions 64 | # ------------------------------------------------------------------------------ 65 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration 66 | INSTALLED_APPS += ["django_extensions"] # noqa F405 67 | # Celery 68 | # ------------------------------------------------------------------------------ 69 | 70 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates 71 | CELERY_TASK_EAGER_PROPAGATES = True 72 | 73 | # DATABASES 74 | # ------------------------------------------------------------------------------ 75 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 76 | DATABASES = {"default": env.db("DATABASE_URL")} 77 | DATABASES["default"]["ATOMIC_REQUESTS"] = True 78 | 79 | # Your stuff... 80 | # ------------------------------------------------------------------------------ 81 | 82 | LOGGING = { 83 | "version": 1, 84 | "disable_existing_loggers": False, 85 | "formatters": { 86 | "json_formatter": { 87 | "()": structlog.stdlib.ProcessorFormatter, 88 | "processor": structlog.processors.JSONRenderer(), 89 | }, 90 | "colored": { 91 | "()": structlog.stdlib.ProcessorFormatter, 92 | "processor": structlog.dev.ConsoleRenderer(colors=True), 93 | }, 94 | "key_value": { 95 | "()": structlog.stdlib.ProcessorFormatter, 96 | "processor": structlog.processors.KeyValueRenderer( 97 | key_order=["timestamp", "level", "event", "logger"] 98 | ), 99 | }, 100 | }, 101 | "handlers": { 102 | "colored_stream": {"class": "logging.StreamHandler", "formatter": "colored"}, 103 | "json_file": { 104 | "class": "logging.handlers.WatchedFileHandler", 105 | "filename": "logs/json.log", 106 | "formatter": "json_formatter", 107 | }, 108 | "flat_line_file": { 109 | "class": "logging.handlers.WatchedFileHandler", 110 | "filename": "logs/flat_line.log", 111 | "formatter": "key_value", 112 | }, 113 | }, 114 | "loggers": { 115 | "django_structlog": { 116 | "handlers": ["colored_stream", "flat_line_file", "json_file"], 117 | "level": "INFO", 118 | }, 119 | "django_structlog_demo_project": { 120 | "handlers": ["colored_stream", "flat_line_file", "json_file"], 121 | "level": "INFO", 122 | }, 123 | }, 124 | } 125 | 126 | structlog.configure( 127 | processors=[ 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.processors.ExceptionPrettyPrinter(), 137 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 138 | ], 139 | context_class=structlog.threadlocal.wrap_dict(dict), 140 | logger_factory=structlog.stdlib.LoggerFactory(), 141 | wrapper_class=structlog.stdlib.BoundLogger, 142 | cache_logger_on_first_use=True, 143 | ) 144 | 145 | MIDDLEWARE += [ 146 | "django_structlog.middlewares.RequestMiddleware", 147 | "django_structlog.middlewares.CeleryMiddleware", 148 | ] 149 | -------------------------------------------------------------------------------- /.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 | # mypy 80 | .mypy_cache/ 81 | 82 | 83 | ### Node template 84 | # Logs 85 | logs 86 | *.log 87 | npm-debug.log* 88 | yarn-debug.log* 89 | yarn-error.log* 90 | 91 | # Runtime data 92 | pids 93 | *.pid 94 | *.seed 95 | *.pid.lock 96 | 97 | # Directory for instrumented libs generated by jscoverage/JSCover 98 | lib-cov 99 | 100 | # Coverage directory used by tools like istanbul 101 | coverage 102 | 103 | # nyc test coverage 104 | .nyc_output 105 | 106 | # Bower dependency directory (https://bower.io/) 107 | bower_components 108 | 109 | # node-waf configuration 110 | .lock-wscript 111 | 112 | # Compiled binary addons (http://nodejs.org/api/addons.html) 113 | build/Release 114 | 115 | # Dependency directories 116 | node_modules/ 117 | jspm_packages/ 118 | 119 | # Typescript v1 declaration files 120 | typings/ 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Optional REPL history 129 | .node_repl_history 130 | 131 | # Output of 'npm pack' 132 | *.tgz 133 | 134 | # Yarn Integrity file 135 | .yarn-integrity 136 | 137 | 138 | ### Linux template 139 | *~ 140 | 141 | # temporary files which can be created if a process still has a handle open of a deleted file 142 | .fuse_hidden* 143 | 144 | # KDE directory preferences 145 | .directory 146 | 147 | # Linux trash folder which might appear on any partition or disk 148 | .Trash-* 149 | 150 | # .nfs files are created when an open file is removed but is still being accessed 151 | .nfs* 152 | 153 | 154 | ### VisualStudioCode template 155 | .vscode/* 156 | !.vscode/settings.json 157 | !.vscode/tasks.json 158 | !.vscode/launch.json 159 | !.vscode/extensions.json 160 | 161 | 162 | # Provided default Pycharm Run/Debug Configurations should be tracked by git 163 | # In case of local modifications made by Pycharm, use update-index command 164 | # for each changed file, like this: 165 | # git update-index --assume-unchanged .idea/django_structlog_demo_project.iml 166 | ### JetBrains template 167 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 168 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 169 | 170 | # User-specific stuff: 171 | .idea/**/workspace.xml 172 | .idea/**/tasks.xml 173 | .idea/dictionaries 174 | 175 | # Sensitive or high-churn files: 176 | .idea/**/dataSources/ 177 | .idea/**/dataSources.ids 178 | .idea/**/dataSources.xml 179 | .idea/**/dataSources.local.xml 180 | .idea/**/sqlDataSources.xml 181 | .idea/**/dynamic.xml 182 | .idea/**/uiDesigner.xml 183 | 184 | # Gradle: 185 | .idea/**/gradle.xml 186 | .idea/**/libraries 187 | 188 | # CMake 189 | cmake-build-debug/ 190 | 191 | # Mongo Explorer plugin: 192 | .idea/**/mongoSettings.xml 193 | 194 | ## File-based project format: 195 | *.iws 196 | 197 | ## Plugin-specific files: 198 | 199 | # IntelliJ 200 | out/ 201 | 202 | # mpeltonen/sbt-idea plugin 203 | .idea_modules/ 204 | 205 | # JIRA plugin 206 | atlassian-ide-plugin.xml 207 | 208 | # Cursive Clojure plugin 209 | .idea/replstate.xml 210 | 211 | # Crashlytics plugin (for Android Studio and IntelliJ) 212 | com_crashlytics_export_strings.xml 213 | crashlytics.properties 214 | crashlytics-build.properties 215 | fabric.properties 216 | 217 | 218 | 219 | ### Windows template 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Dump file 226 | *.stackdump 227 | 228 | # Folder config file 229 | Desktop.ini 230 | 231 | # Recycle Bin used on file shares 232 | $RECYCLE.BIN/ 233 | 234 | # Windows Installer files 235 | *.cab 236 | *.msi 237 | *.msm 238 | *.msp 239 | 240 | # Windows shortcuts 241 | *.lnk 242 | 243 | 244 | ### macOS template 245 | # General 246 | *.DS_Store 247 | .AppleDouble 248 | .LSOverride 249 | 250 | # Icon must end with two \r 251 | Icon 252 | 253 | # Thumbnails 254 | ._* 255 | 256 | # Files that might appear in the root of a volume 257 | .DocumentRevisions-V100 258 | .fseventsd 259 | .Spotlight-V100 260 | .TemporaryItems 261 | .Trashes 262 | .VolumeIcon.icns 263 | .com.apple.timemachine.donotpresent 264 | 265 | # Directories potentially created on remote AFP share 266 | .AppleDB 267 | .AppleDesktop 268 | Network Trash Folder 269 | Temporary Items 270 | .apdisk 271 | 272 | 273 | ### SublimeText template 274 | # Cache files for Sublime Text 275 | *.tmlanguage.cache 276 | *.tmPreferences.cache 277 | *.stTheme.cache 278 | 279 | # Workspace files are user-specific 280 | *.sublime-workspace 281 | 282 | # Project files should be checked into the repository, unless a significant 283 | # proportion of contributors will probably not be using Sublime Text 284 | # *.sublime-project 285 | 286 | # SFTP configuration file 287 | sftp-config.json 288 | 289 | # Package control specific files 290 | Package Control.last-run 291 | Package Control.ca-list 292 | Package Control.ca-bundle 293 | Package Control.system-ca-bundle 294 | Package Control.cache/ 295 | Package Control.ca-certs/ 296 | Package Control.merged-ca-bundle 297 | Package Control.user-ca-bundle 298 | oscrypto-ca-bundle.crt 299 | bh_unicode_properties.cache 300 | 301 | # Sublime-github package stores a github token in this file 302 | # https://packagecontrol.io/packages/sublime-github 303 | GitHub.sublime-settings 304 | 305 | 306 | ### Vim template 307 | # Swap 308 | [._]*.s[a-v][a-z] 309 | [._]*.sw[a-p] 310 | [._]s[a-v][a-z] 311 | [._]sw[a-p] 312 | 313 | # Session 314 | Session.vim 315 | 316 | # Temporary 317 | .netrwhist 318 | 319 | # Auto-generated tag files 320 | tags 321 | 322 | 323 | ### Project template 324 | 325 | django_structlog_demo_project/media/ 326 | 327 | .pytest_cache/ 328 | 329 | 330 | .ipython/ 331 | .env 332 | .envs/* 333 | !.envs/.local/ 334 | .vscode/ 335 | -------------------------------------------------------------------------------- /docs/events.rst: -------------------------------------------------------------------------------- 1 | Events and Metadata 2 | =================== 3 | 4 | Django's RequestMiddleware 5 | -------------------------- 6 | 7 | Request Events 8 | ^^^^^^^^^^^^^^ 9 | 10 | +------------------+---------+------------------------------+ 11 | | Event | Type | Description | 12 | +==================+=========+==============================+ 13 | | request_started | INFO | Django received a request | 14 | +------------------+---------+------------------------------+ 15 | | request_finished | INFO | request completed normally | 16 | +------------------+---------+------------------------------+ 17 | | request_failed | ERROR | unhandled exception occurred | 18 | +------------------+---------+------------------------------+ 19 | 20 | Request Bound Metadata 21 | ^^^^^^^^^^^^^^^^^^^^^^ 22 | 23 | These metadata are repeated on each log of the current request and will be also be repeated in all children Celery tasks. 24 | 25 | +------------------+------------------------------------------------------------------------------------------+ 26 | | Key | Value | 27 | +==================+==========================================================================================+ 28 | | request_id | UUID for the request or value of ``X-Request-ID`` HTTP header when provided | 29 | +------------------+------------------------------------------------------------------------------------------+ 30 | | correlation_id | value of ``X-Correlation-ID`` HTTP header when provided | 31 | +------------------+------------------------------------------------------------------------------------------+ 32 | | user_id | user's id or None (requires `django.contrib.auth.middleware.AuthenticationMiddleware`_) | 33 | +------------------+------------------------------------------------------------------------------------------+ 34 | | ip | request's ip | 35 | +------------------+------------------------------------------------------------------------------------------+ 36 | 37 | To bind more metadata or override existing metadata from request see :ref:`django_signals` 38 | 39 | .. _`django.contrib.auth.middleware.AuthenticationMiddleware`: https://docs.djangoproject.com/en/dev/ref/middleware/#module-django.contrib.auth.middleware 40 | 41 | 42 | Request Events Metadata 43 | ^^^^^^^^^^^^^^^^^^^^^^^ 44 | 45 | These metadata appear once along with their associated event 46 | 47 | +------------------+------------------+------------------------+ 48 | | Event | Key | Value | 49 | +==================+==================+========================+ 50 | | request_started | request | request as string | 51 | +------------------+------------------+------------------------+ 52 | | request_started | user_agent | request's user agent | 53 | +------------------+------------------+------------------------+ 54 | | request_finished | code | request's status code | 55 | +------------------+------------------+------------------------+ 56 | | request_failed | error | exception object | 57 | +------------------+------------------+------------------------+ 58 | | request_failed | error_traceback | exception's traceback | 59 | +------------------+------------------+------------------------+ 60 | 61 | 62 | CeleryMiddleware 63 | ---------------- 64 | 65 | Task Events 66 | ^^^^^^^^^^^ 67 | 68 | +--------------------+---------+------------------------------------------------+ 69 | | Event | Type | Description | 70 | +====================+=========+================================================+ 71 | | task_enqueued | INFO | A task was enqueued by request or another task | 72 | +--------------------+---------+------------------------------------------------+ 73 | | task_retrying | WARNING | Worker retry task | 74 | +--------------------+---------+------------------------------------------------+ 75 | | task_succeed | INFO | Task completed successfully | 76 | +--------------------+---------+------------------------------------------------+ 77 | | task_failed | ERROR | Task failed | 78 | +--------------------+---------+------------------------------------------------+ 79 | | task_revoked | WARNING | Task was canceled | 80 | +--------------------+---------+------------------------------------------------+ 81 | | task_not_found | ERROR | Celery app did not discover the requested task | 82 | +--------------------+---------+------------------------------------------------+ 83 | | task_task_rejected | ERROR | Task could not be enqueued | 84 | +--------------------+---------+------------------------------------------------+ 85 | 86 | Task Bound Metadata 87 | ^^^^^^^^^^^^^^^^^^^ 88 | 89 | These metadata are repeated on each log of the current task and will be also be repeated in all children Celery tasks. 90 | Take note that all the caller's logger bound metadata are also bound to the task's logger. 91 | 92 | +------------------+------------------------------------+ 93 | | Key | Value | 94 | +==================+====================================+ 95 | | task_id | UUID of the current task | 96 | +------------------+------------------------------------+ 97 | | parent_task_id | UUID of the parent's task (if any) | 98 | +------------------+------------------------------------+ 99 | 100 | To bind more metadata or override existing metadata from task see :ref:`celery_signals` 101 | 102 | 103 | Task Event Metadata 104 | ^^^^^^^^^^^^^^^^^^^ 105 | 106 | These metadata appear once along with their associated event 107 | 108 | +------------------+------------------+----------------------------------------+ 109 | | Event | Key | Value | 110 | +==================+==================+========================================+ 111 | | task_enqueued | child_task_id | id of the task being enqueued | 112 | +------------------+------------------+----------------------------------------+ 113 | | task_enqueued | child_task_name | name of the task being enqueued | 114 | +------------------+------------------+----------------------------------------+ 115 | | task_retrying | reason | reason for retry | 116 | +------------------+------------------+----------------------------------------+ 117 | | task_succeed | result | result of the task | 118 | +------------------+------------------+----------------------------------------+ 119 | | task_failed | error | exception as string | 120 | +------------------+------------------+----------------------------------------+ 121 | | task_revoked | terminated | Set to True if the task was terminated | 122 | +------------------+------------------+----------------------------------------+ 123 | | task_revoked | signum | see Celery's documentation | 124 | +------------------+------------------+----------------------------------------+ 125 | | task_revoked | expired | see Celery's documentation | 126 | +------------------+------------------+----------------------------------------+ 127 | 128 | 129 | -------------------------------------------------------------------------------- /test_app/tests/celery/test_receivers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest.mock import Mock 3 | 4 | import structlog 5 | from celery import shared_task 6 | from django.contrib.auth.models import AnonymousUser 7 | from django.dispatch import receiver 8 | from django.test import TestCase, RequestFactory 9 | 10 | from django_structlog.celery import receivers, signals 11 | 12 | 13 | class TestReceivers(TestCase): 14 | def setUp(self): 15 | self.factory = RequestFactory() 16 | self.logger = structlog.getLogger(__name__) 17 | 18 | def test_defer_task(self): 19 | expected_uuid = "00000000-0000-0000-0000-000000000000" 20 | 21 | request = self.factory.get("/foo") 22 | request.user = AnonymousUser() 23 | 24 | @shared_task 25 | def test_task(value): # pragma: no cover 26 | pass 27 | 28 | from celery.signals import before_task_publish, after_task_publish 29 | 30 | before_task_publish.connect(receivers.receiver_before_task_publish) 31 | after_task_publish.connect(receivers.receiver_after_task_publish) 32 | try: 33 | with structlog.threadlocal.tmp_bind(self.logger): 34 | self.logger.bind(request_id=expected_uuid) 35 | with self.assertLogs( 36 | logging.getLogger("django_structlog.celery.receivers"), logging.INFO 37 | ) as log_results: 38 | test_task.delay("foo") 39 | finally: 40 | before_task_publish.disconnect(receivers.receiver_before_task_publish) 41 | after_task_publish.disconnect(receivers.receiver_after_task_publish) 42 | 43 | self.assertEqual(1, len(log_results.records)) 44 | record = log_results.records[0] 45 | self.assertEqual("task_enqueued", record.msg["event"]) 46 | self.assertEqual("INFO", record.levelname) 47 | self.assertIn("child_task_id", record.msg) 48 | self.assertEqual(expected_uuid, record.msg["request_id"]) 49 | 50 | def test_receiver_before_task_publish(self): 51 | expected_uuid = "00000000-0000-0000-0000-000000000000" 52 | expected_user_id = "1234" 53 | expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" 54 | 55 | headers = {} 56 | with structlog.threadlocal.tmp_bind(self.logger): 57 | self.logger.bind( 58 | request_id=expected_uuid, 59 | user_id=expected_user_id, 60 | task_id=expected_parent_task_uuid, 61 | ) 62 | receivers.receiver_before_task_publish(headers=headers) 63 | 64 | self.assertDictEqual( 65 | { 66 | "__django_structlog__": { 67 | "request_id": expected_uuid, 68 | "user_id": expected_user_id, 69 | "parent_task_id": expected_parent_task_uuid, 70 | } 71 | }, 72 | headers, 73 | ) 74 | 75 | def test_receiver_after_task_publish(self): 76 | expected_task_id = "00000000-0000-0000-0000-000000000000" 77 | expected_task_name = "Foo" 78 | headers = {"id": expected_task_id, "task": expected_task_name} 79 | 80 | with self.assertLogs( 81 | logging.getLogger("django_structlog.celery.receivers"), logging.INFO 82 | ) as log_results: 83 | receivers.receiver_after_task_publish(headers=headers) 84 | 85 | self.assertEqual(1, len(log_results.records)) 86 | record = log_results.records[0] 87 | self.assertEqual("task_enqueued", record.msg["event"]) 88 | self.assertEqual("INFO", record.levelname) 89 | self.assertIn("child_task_id", record.msg) 90 | self.assertEqual(expected_task_id, record.msg["child_task_id"]) 91 | self.assertIn("child_task_name", record.msg) 92 | self.assertEqual(expected_task_name, record.msg["child_task_name"]) 93 | 94 | def test_receiver_task_pre_run(self): 95 | expected_request_uuid = "00000000-0000-0000-0000-000000000000" 96 | task_id = "11111111-1111-1111-1111-111111111111" 97 | expected_user_id = "1234" 98 | task = Mock() 99 | task.request = Mock() 100 | task.request.__django_structlog__ = { 101 | "request_id": expected_request_uuid, 102 | "user_id": expected_user_id, 103 | } 104 | with structlog.threadlocal.tmp_bind(self.logger): 105 | self.logger.bind(foo="bar") 106 | 107 | structlog.threadlocal.as_immutable(self.logger) 108 | immutable_logger = structlog.threadlocal.as_immutable(self.logger) 109 | context = immutable_logger._context 110 | self.assertDictEqual({"foo": "bar"}, context) 111 | 112 | receivers.receiver_task_pre_run(task_id, task) 113 | immutable_logger = structlog.threadlocal.as_immutable(self.logger) 114 | context = immutable_logger._context 115 | 116 | self.assertDictEqual( 117 | { 118 | "task_id": task_id, 119 | "request_id": expected_request_uuid, 120 | "user_id": expected_user_id, 121 | }, 122 | context, 123 | ) 124 | 125 | def test_signal(self): 126 | @receiver(signals.bind_extra_task_metadata) 127 | def receiver_bind_extra_request_metadata( 128 | sender, signal, task=None, logger=None 129 | ): 130 | logger.bind(correlation_id=task.request.correlation_id) 131 | 132 | expected_correlation_uuid = "00000000-0000-0000-0000-000000000000" 133 | task_id = "11111111-1111-1111-1111-111111111111" 134 | task = Mock() 135 | task.request = Mock() 136 | task.request.correlation_id = expected_correlation_uuid 137 | with structlog.threadlocal.tmp_bind(self.logger): 138 | self.logger.bind(foo="bar") 139 | 140 | structlog.threadlocal.as_immutable(self.logger) 141 | immutable_logger = structlog.threadlocal.as_immutable(self.logger) 142 | context = immutable_logger._context 143 | self.assertDictEqual({"foo": "bar"}, context) 144 | 145 | receivers.receiver_task_pre_run(task_id, task) 146 | immutable_logger = structlog.threadlocal.as_immutable(self.logger) 147 | context = immutable_logger._context 148 | 149 | self.assertEqual(context["correlation_id"], expected_correlation_uuid) 150 | self.assertEqual(context["task_id"], task_id) 151 | 152 | def test_receiver_task_retry(self): 153 | expected_reason = "foo" 154 | 155 | with self.assertLogs( 156 | logging.getLogger("django_structlog.celery.receivers"), logging.WARNING 157 | ) as log_results: 158 | receivers.receiver_task_retry(reason=expected_reason) 159 | 160 | self.assertEqual(1, len(log_results.records)) 161 | record = log_results.records[0] 162 | self.assertEqual("task_retrying", record.msg["event"]) 163 | self.assertEqual("WARNING", record.levelname) 164 | self.assertIn("reason", record.msg) 165 | self.assertEqual(expected_reason, record.msg["reason"]) 166 | 167 | def test_receiver_task_success(self): 168 | expected_result = "foo" 169 | 170 | with self.assertLogs( 171 | logging.getLogger("django_structlog.celery.receivers"), logging.INFO 172 | ) as log_results: 173 | receivers.receiver_task_success(result=expected_result) 174 | 175 | self.assertEqual(1, len(log_results.records)) 176 | record = log_results.records[0] 177 | self.assertEqual("task_succeed", record.msg["event"]) 178 | self.assertEqual("INFO", record.levelname) 179 | self.assertIn("result", record.msg) 180 | self.assertEqual(expected_result, record.msg["result"]) 181 | 182 | def test_receiver_task_failure(self): 183 | expected_exception = "foo" 184 | 185 | with self.assertLogs( 186 | logging.getLogger("django_structlog.celery.receivers"), logging.ERROR 187 | ) as log_results: 188 | receivers.receiver_task_failure(exception=Exception("foo")) 189 | 190 | self.assertEqual(1, len(log_results.records)) 191 | record = log_results.records[0] 192 | self.assertEqual("task_failed", record.msg["event"]) 193 | self.assertEqual("ERROR", record.levelname) 194 | self.assertIn("error", record.msg) 195 | self.assertEqual(expected_exception, record.msg["error"]) 196 | 197 | def test_receiver_task_revoked(self): 198 | with self.assertLogs( 199 | logging.getLogger("django_structlog.celery.receivers"), logging.WARNING 200 | ) as log_results: 201 | receivers.receiver_task_revoked(terminated=True, signum=1, expired=False) 202 | 203 | self.assertEqual(1, len(log_results.records)) 204 | record = log_results.records[0] 205 | self.assertEqual("task_revoked", record.msg["event"]) 206 | self.assertEqual("WARNING", record.levelname) 207 | self.assertIn("terminated", record.msg) 208 | self.assertTrue(record.msg["terminated"]) 209 | self.assertIn("signum", record.msg) 210 | self.assertEqual(1, record.msg["signum"]) 211 | self.assertIn("expired", record.msg) 212 | self.assertFalse(record.msg["expired"]) 213 | 214 | def test_receiver_task_unknown(self): 215 | expected_message = "foo" 216 | 217 | with self.assertLogs( 218 | logging.getLogger("django_structlog.celery.receivers"), logging.ERROR 219 | ) as log_results: 220 | receivers.receiver_task_unknown(message=expected_message) 221 | 222 | self.assertEqual(1, len(log_results.records)) 223 | record = log_results.records[0] 224 | self.assertEqual("task_not_found", record.msg["event"]) 225 | self.assertEqual("ERROR", record.levelname) 226 | self.assertIn("message", record.msg) 227 | self.assertEqual(expected_message, record.msg["message"]) 228 | 229 | def test_receiver_task_rejected(self): 230 | expected_message = "foo" 231 | 232 | with self.assertLogs( 233 | logging.getLogger("django_structlog.celery.receivers"), logging.ERROR 234 | ) as log_results: 235 | receivers.receiver_task_rejected(message=expected_message) 236 | 237 | self.assertEqual(1, len(log_results.records)) 238 | record = log_results.records[0] 239 | self.assertEqual("task_rejected", record.msg["event"]) 240 | self.assertEqual("ERROR", record.levelname) 241 | self.assertIn("message", record.msg) 242 | self.assertEqual(expected_message, record.msg["message"]) 243 | 244 | def tearDown(self): 245 | self.logger.new() 246 | -------------------------------------------------------------------------------- /config/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base settings to build other settings files upon. 3 | """ 4 | 5 | import environ 6 | from django.core.exceptions import ImproperlyConfigured 7 | 8 | ROOT_DIR = ( 9 | environ.Path(__file__) - 3 10 | ) # (django_structlog_demo_project/config/settings/base.py - 3 = django_structlog_demo_project/) 11 | APPS_DIR = ROOT_DIR.path("django_structlog_demo_project") 12 | 13 | env = environ.Env() 14 | 15 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 16 | if READ_DOT_ENV_FILE: 17 | # OS environment variables take precedence over variables from .env 18 | env.read_env(str(ROOT_DIR.path(".env"))) 19 | 20 | # GENERAL 21 | # ------------------------------------------------------------------------------ 22 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug 23 | DEBUG = env.bool("DJANGO_DEBUG", False) 24 | # Local time zone. Choices are 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # though not all of them may be available with every OS. 27 | # In Windows, this must be set to your system time zone. 28 | TIME_ZONE = "UTC" 29 | # https://docs.djangoproject.com/en/dev/ref/settings/#language-code 30 | LANGUAGE_CODE = "en-us" 31 | # https://docs.djangoproject.com/en/dev/ref/settings/#site-id 32 | SITE_ID = 1 33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-i18n 34 | USE_I18N = True 35 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-l10n 36 | USE_L10N = True 37 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz 38 | USE_TZ = True 39 | 40 | # URLS 41 | # ------------------------------------------------------------------------------ 42 | # https://docs.djangoproject.com/en/dev/ref/settings/#root-urlconf 43 | ROOT_URLCONF = "config.urls" 44 | # https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application 45 | WSGI_APPLICATION = "config.wsgi.application" 46 | 47 | # APPS 48 | # ------------------------------------------------------------------------------ 49 | DJANGO_APPS = [ 50 | "django.contrib.auth", 51 | "django.contrib.contenttypes", 52 | "django.contrib.sessions", 53 | "django.contrib.sites", 54 | "django.contrib.messages", 55 | "django.contrib.staticfiles", 56 | # "django.contrib.humanize", # Handy template tags 57 | "django.contrib.admin", 58 | ] 59 | THIRD_PARTY_APPS = [ 60 | "crispy_forms", 61 | "allauth", 62 | "allauth.account", 63 | "allauth.socialaccount", 64 | "rest_framework", 65 | ] 66 | LOCAL_APPS = [ 67 | "django_structlog_demo_project.users.apps.UsersAppConfig", 68 | # Your stuff: custom apps go here 69 | ] 70 | # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps 71 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 72 | 73 | # MIGRATIONS 74 | # ------------------------------------------------------------------------------ 75 | # https://docs.djangoproject.com/en/dev/ref/settings/#migration-modules 76 | MIGRATION_MODULES = {"sites": "django_structlog_demo_project.contrib.sites.migrations"} 77 | 78 | # AUTHENTICATION 79 | # ------------------------------------------------------------------------------ 80 | # https://docs.djangoproject.com/en/dev/ref/settings/#authentication-backends 81 | AUTHENTICATION_BACKENDS = [ 82 | "django.contrib.auth.backends.ModelBackend", 83 | "allauth.account.auth_backends.AuthenticationBackend", 84 | ] 85 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model 86 | AUTH_USER_MODEL = "users.User" 87 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-redirect-url 88 | LOGIN_REDIRECT_URL = "users:redirect" 89 | # https://docs.djangoproject.com/en/dev/ref/settings/#login-url 90 | LOGIN_URL = "account_login" 91 | 92 | # PASSWORDS 93 | # ------------------------------------------------------------------------------ 94 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers 95 | PASSWORD_HASHERS = [ 96 | # https://docs.djangoproject.com/en/dev/topics/auth/passwords/#using-argon2-with-django 97 | "django.contrib.auth.hashers.Argon2PasswordHasher", 98 | "django.contrib.auth.hashers.PBKDF2PasswordHasher", 99 | "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 100 | "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 101 | "django.contrib.auth.hashers.BCryptPasswordHasher", 102 | ] 103 | # https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators 104 | AUTH_PASSWORD_VALIDATORS = [ 105 | { 106 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 107 | }, 108 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 109 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 110 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 111 | ] 112 | 113 | # MIDDLEWARE 114 | # ------------------------------------------------------------------------------ 115 | # https://docs.djangoproject.com/en/dev/ref/settings/#middleware 116 | MIDDLEWARE = [ 117 | "django.middleware.security.SecurityMiddleware", 118 | "django.contrib.sessions.middleware.SessionMiddleware", 119 | "django.middleware.common.CommonMiddleware", 120 | "django.middleware.csrf.CsrfViewMiddleware", 121 | "django.contrib.auth.middleware.AuthenticationMiddleware", 122 | "django.contrib.messages.middleware.MessageMiddleware", 123 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 124 | ] 125 | 126 | # STATIC 127 | # ------------------------------------------------------------------------------ 128 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-root 129 | STATIC_ROOT = str(ROOT_DIR("staticfiles")) 130 | # https://docs.djangoproject.com/en/dev/ref/settings/#static-url 131 | STATIC_URL = "/static/" 132 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS 133 | STATICFILES_DIRS = [str(APPS_DIR.path("static"))] 134 | # https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders 135 | STATICFILES_FINDERS = [ 136 | "django.contrib.staticfiles.finders.FileSystemFinder", 137 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 138 | ] 139 | 140 | # MEDIA 141 | # ------------------------------------------------------------------------------ 142 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-root 143 | MEDIA_ROOT = str(APPS_DIR("media")) 144 | # https://docs.djangoproject.com/en/dev/ref/settings/#media-url 145 | MEDIA_URL = "/media/" 146 | 147 | # TEMPLATES 148 | # ------------------------------------------------------------------------------ 149 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates 150 | TEMPLATES = [ 151 | { 152 | # https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND 153 | "BACKEND": "django.template.backends.django.DjangoTemplates", 154 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs 155 | "DIRS": [str(APPS_DIR.path("templates"))], 156 | "OPTIONS": { 157 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-debug 158 | "debug": DEBUG, 159 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders 160 | # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types 161 | "loaders": [ 162 | "django.template.loaders.filesystem.Loader", 163 | "django.template.loaders.app_directories.Loader", 164 | ], 165 | # https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors 166 | "context_processors": [ 167 | "django.template.context_processors.debug", 168 | "django.template.context_processors.request", 169 | "django.contrib.auth.context_processors.auth", 170 | "django.template.context_processors.i18n", 171 | "django.template.context_processors.media", 172 | "django.template.context_processors.static", 173 | "django.template.context_processors.tz", 174 | "django.contrib.messages.context_processors.messages", 175 | ], 176 | }, 177 | } 178 | ] 179 | # http://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs 180 | CRISPY_TEMPLATE_PACK = "bootstrap4" 181 | 182 | # FIXTURES 183 | # ------------------------------------------------------------------------------ 184 | # https://docs.djangoproject.com/en/dev/ref/settings/#fixture-dirs 185 | FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) 186 | 187 | # SECURITY 188 | # ------------------------------------------------------------------------------ 189 | # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly 190 | SESSION_COOKIE_HTTPONLY = True 191 | # https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-httponly 192 | CSRF_COOKIE_HTTPONLY = True 193 | # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter 194 | SECURE_BROWSER_XSS_FILTER = True 195 | # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options 196 | X_FRAME_OPTIONS = "DENY" 197 | 198 | # EMAIL 199 | # ------------------------------------------------------------------------------ 200 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend 201 | EMAIL_BACKEND = env( 202 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend" 203 | ) 204 | 205 | # ADMIN 206 | # ------------------------------------------------------------------------------ 207 | # Django Admin URL. 208 | ADMIN_URL = "admin/" 209 | # https://docs.djangoproject.com/en/dev/ref/settings/#admins 210 | ADMINS = [] 211 | # https://docs.djangoproject.com/en/dev/ref/settings/#managers 212 | MANAGERS = ADMINS 213 | 214 | # Celery 215 | # ------------------------------------------------------------------------------ 216 | INSTALLED_APPS += ["django_structlog_demo_project.taskapp.celery.CeleryAppConfig"] 217 | if USE_TZ: 218 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone 219 | CELERY_TIMEZONE = TIME_ZONE 220 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url 221 | 222 | try: 223 | CELERY_BROKER_URL = env("CELERY_BROKER_URL") 224 | # This breaks when running tests locally. 225 | except ImproperlyConfigured: 226 | CELERY_BROKER_URL = "redis://0.0.0.0:6379/0" 227 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend 228 | CELERY_RESULT_BACKEND = CELERY_BROKER_URL 229 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content 230 | CELERY_ACCEPT_CONTENT = ["json"] 231 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer 232 | CELERY_TASK_SERIALIZER = "json" 233 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer 234 | CELERY_RESULT_SERIALIZER = "json" 235 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit 236 | # TODO: set to whatever value is adequate in your circumstances 237 | CELERYD_TASK_TIME_LIMIT = 5 * 60 238 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit 239 | # TODO: set to whatever value is adequate in your circumstances 240 | CELERYD_TASK_SOFT_TIME_LIMIT = 60 241 | # django-allauth 242 | # ------------------------------------------------------------------------------ 243 | ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", True) 244 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 245 | ACCOUNT_AUTHENTICATION_METHOD = "username" 246 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 247 | ACCOUNT_EMAIL_REQUIRED = True 248 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 249 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 250 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 251 | ACCOUNT_ADAPTER = "django_structlog_demo_project.users.adapters.AccountAdapter" 252 | # https://django-allauth.readthedocs.io/en/latest/configuration.html 253 | SOCIALACCOUNT_ADAPTER = ( 254 | "django_structlog_demo_project.users.adapters.SocialAccountAdapter" 255 | ) 256 | 257 | 258 | # Your stuff... 259 | # ------------------------------------------------------------------------------ 260 | 261 | INSTALLED_APPS += ["django_structlog"] 262 | -------------------------------------------------------------------------------- /test_app/tests/middlewares/test_request.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from unittest import mock 3 | from unittest.mock import patch, Mock 4 | 5 | from django.contrib.auth.models import AnonymousUser, User 6 | from django.dispatch import receiver 7 | from django.http import Http404, HttpResponseNotFound 8 | from django.test import TestCase, RequestFactory 9 | import structlog 10 | 11 | from django_structlog import middlewares 12 | from django_structlog.middlewares.request import get_request_header 13 | from django_structlog.signals import bind_extra_request_metadata 14 | 15 | 16 | class TestRequestMiddleware(TestCase): 17 | def setUp(self): 18 | self.factory = RequestFactory() 19 | self.logger = structlog.getLogger(__name__) 20 | self.log_results = None 21 | 22 | def test_process_request_without_user(self): 23 | mock_response = Mock() 24 | mock_response.status_code.return_value = 200 25 | expected_uuid = "00000000-0000-0000-0000-000000000000" 26 | 27 | def get_response(_response): 28 | with self.assertLogs(__name__, logging.INFO) as log_results: 29 | self.logger.info("hello") 30 | self.log_results = log_results 31 | return mock_response 32 | 33 | request = self.factory.get("/foo") 34 | 35 | middleware = middlewares.RequestMiddleware(get_response) 36 | with patch("uuid.UUID.__str__", return_value=expected_uuid): 37 | middleware(request) 38 | 39 | self.assertEqual(1, len(self.log_results.records)) 40 | record = self.log_results.records[0] 41 | 42 | self.assertEqual("INFO", record.levelname) 43 | self.assertIn("request_id", record.msg) 44 | self.assertNotIn("user_id", record.msg) 45 | self.assertEqual(expected_uuid, record.msg["request_id"]) 46 | with self.assertLogs(__name__, logging.INFO) as log_results: 47 | self.logger.info("hello") 48 | self.assertEqual(1, len(log_results.records)) 49 | record = log_results.records[0] 50 | self.assertNotIn("request_id", record.msg) 51 | 52 | def test_process_request_anonymous(self): 53 | mock_response = Mock() 54 | mock_response.status_code.return_value = 200 55 | expected_uuid = "00000000-0000-0000-0000-000000000000" 56 | 57 | def get_response(_response): 58 | with self.assertLogs(__name__, logging.INFO) as log_results: 59 | self.logger.info("hello") 60 | self.log_results = log_results 61 | return mock_response 62 | 63 | request = self.factory.get("/foo") 64 | request.user = AnonymousUser() 65 | 66 | middleware = middlewares.RequestMiddleware(get_response) 67 | with patch("uuid.UUID.__str__", return_value=expected_uuid): 68 | middleware(request) 69 | 70 | self.assertEqual(1, len(self.log_results.records)) 71 | record = self.log_results.records[0] 72 | 73 | self.assertEqual("INFO", record.levelname) 74 | self.assertIn("request_id", record.msg) 75 | self.assertEqual(expected_uuid, record.msg["request_id"]) 76 | self.assertIn("user_id", record.msg) 77 | self.assertIsNone(record.msg["user_id"]) 78 | with self.assertLogs(__name__, logging.INFO) as log_results: 79 | self.logger.info("hello") 80 | self.assertEqual(1, len(log_results.records)) 81 | record = log_results.records[0] 82 | self.assertNotIn("request_id", record.msg) 83 | self.assertNotIn("user_id", record.msg) 84 | 85 | def test_process_request_user(self): 86 | mock_response = Mock() 87 | mock_response.status_code.return_value = 200 88 | expected_uuid = "00000000-0000-0000-0000-000000000000" 89 | 90 | def get_response(_response): 91 | with self.assertLogs(__name__, logging.INFO) as log_results: 92 | self.logger.info("hello") 93 | self.log_results = log_results 94 | return mock_response 95 | 96 | request = self.factory.get("/foo") 97 | 98 | mock_user = User.objects.create() 99 | request.user = mock_user 100 | 101 | middleware = middlewares.RequestMiddleware(get_response) 102 | with patch("uuid.UUID.__str__", return_value=expected_uuid): 103 | middleware(request) 104 | 105 | self.assertEqual(1, len(self.log_results.records)) 106 | record = self.log_results.records[0] 107 | 108 | self.assertEqual("INFO", record.levelname) 109 | self.assertIn("request_id", record.msg) 110 | self.assertEqual(expected_uuid, record.msg["request_id"]) 111 | self.assertIn("user_id", record.msg) 112 | self.assertEqual(mock_user.id, record.msg["user_id"]) 113 | with self.assertLogs(__name__, logging.INFO) as log_results: 114 | self.logger.info("hello") 115 | self.assertEqual(1, len(log_results.records)) 116 | record = log_results.records[0] 117 | self.assertNotIn("request_id", record.msg) 118 | self.assertNotIn("user_id", record.msg) 119 | 120 | def test_signal(self): 121 | @receiver(bind_extra_request_metadata) 122 | def receiver_bind_extra_request_metadata( 123 | sender, signal, request=None, logger=None 124 | ): 125 | logger.bind(user_email=getattr(request.user, "email", "")) 126 | 127 | mock_response = Mock() 128 | mock_response.status_code.return_value = 200 129 | 130 | def get_response(_response): 131 | with self.assertLogs(__name__, logging.INFO) as log_results: 132 | self.logger.info("hello") 133 | self.log_results = log_results 134 | return mock_response 135 | 136 | request = self.factory.get("/foo") 137 | 138 | mock_user = User.objects.create(email="foo@example.com") 139 | request.user = mock_user 140 | 141 | middleware = middlewares.RequestMiddleware(get_response) 142 | middleware(request) 143 | 144 | self.assertEqual(1, len(self.log_results.records)) 145 | record = self.log_results.records[0] 146 | 147 | self.assertEqual("INFO", record.levelname) 148 | self.assertIn("request_id", record.msg) 149 | self.assertEqual(mock_user.email, record.msg["user_email"]) 150 | self.assertIn("user_id", record.msg) 151 | self.assertEqual(mock_user.id, record.msg["user_id"]) 152 | 153 | def test_process_request_error(self): 154 | expected_uuid = "00000000-0000-0000-0000-000000000000" 155 | 156 | request = self.factory.get("/foo") 157 | request.user = AnonymousUser() 158 | 159 | middleware = middlewares.RequestMiddleware(None) 160 | 161 | exception = Exception("This is an exception") 162 | 163 | def get_response(_response): 164 | """ Simulate an exception """ 165 | middleware.process_exception(request, exception) 166 | 167 | middleware.get_response = get_response 168 | 169 | with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( 170 | logging.getLogger("django_structlog"), logging.INFO 171 | ) as log_results: 172 | middleware(request) 173 | 174 | self.assertEqual(2, len(log_results.records)) 175 | record = log_results.records[0] 176 | self.assertEqual("INFO", record.levelname) 177 | self.assertIn("request_id", record.msg) 178 | self.assertEqual(expected_uuid, record.msg["request_id"]) 179 | self.assertIn("user_id", record.msg) 180 | self.assertIsNone(record.msg["user_id"]) 181 | 182 | record = log_results.records[1] 183 | self.assertEqual("ERROR", record.levelname) 184 | self.assertIn("request_id", record.msg) 185 | self.assertEqual(expected_uuid, record.msg["request_id"]) 186 | self.assertIn("user_id", record.msg) 187 | self.assertIsNone(record.msg["user_id"]) 188 | 189 | self.assertIn("code", record.msg) 190 | self.assertEqual(record.msg["code"], 500) 191 | self.assertIn("error", record.msg) 192 | self.assertEqual(record.msg["error"], exception) 193 | self.assertIn("error_traceback", record.msg) 194 | self.assertEqual(type(record.msg["error_traceback"]), list) 195 | self.assertIn("request", record.msg) 196 | 197 | with self.assertLogs(__name__, logging.INFO) as log_results: 198 | self.logger.info("hello") 199 | self.assertEqual(1, len(log_results.records)) 200 | record = log_results.records[0] 201 | self.assertNotIn("request_id", record.msg) 202 | self.assertNotIn("user_id", record.msg) 203 | 204 | def test_process_request_404_are_processed_as_regular_requests(self): 205 | expected_uuid = "00000000-0000-0000-0000-000000000000" 206 | 207 | request = self.factory.get("/foo") 208 | request.user = AnonymousUser() 209 | 210 | middleware = middlewares.RequestMiddleware(None) 211 | 212 | exception = Http404() 213 | 214 | def get_response(_response): 215 | """ Simulate an exception """ 216 | middleware.process_exception(request, exception) 217 | return HttpResponseNotFound() 218 | 219 | middleware.get_response = get_response 220 | 221 | with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( 222 | logging.getLogger("django_structlog"), logging.INFO 223 | ) as log_results: 224 | middleware(request) 225 | 226 | self.assertEqual(2, len(log_results.records)) 227 | record = log_results.records[0] 228 | self.assertEqual("INFO", record.levelname) 229 | self.assertIn("request_id", record.msg) 230 | self.assertEqual(expected_uuid, record.msg["request_id"]) 231 | self.assertIn("user_id", record.msg) 232 | self.assertIsNone(record.msg["user_id"]) 233 | 234 | record = log_results.records[1] 235 | self.assertEqual("INFO", record.levelname) 236 | self.assertIn("request_id", record.msg) 237 | self.assertEqual(expected_uuid, record.msg["request_id"]) 238 | self.assertIn("user_id", record.msg) 239 | self.assertIsNone(record.msg["user_id"]) 240 | 241 | self.assertIn("code", record.msg) 242 | self.assertEqual(record.msg["code"], 404) 243 | self.assertNotIn("error", record.msg) 244 | self.assertNotIn("error_traceback", record.msg) 245 | self.assertIn("request", record.msg) 246 | 247 | with self.assertLogs(__name__, logging.INFO) as log_results: 248 | self.logger.info("hello") 249 | self.assertEqual(1, len(log_results.records)) 250 | record = log_results.records[0] 251 | self.assertNotIn("request_id", record.msg) 252 | self.assertNotIn("user_id", record.msg) 253 | 254 | def test_should_log_request_id_from_request_x_request_id_header(self): 255 | mock_response = Mock() 256 | mock_response.status_code.return_value = 200 257 | x_request_id = "my-fake-request-id" 258 | 259 | def get_response(_response): 260 | with self.assertLogs(__name__, logging.INFO) as log_results: 261 | self.logger.info("hello") 262 | self.log_results = log_results 263 | return mock_response 264 | 265 | request = RequestFactory(HTTP_X_REQUEST_ID=x_request_id).get("/foo") 266 | 267 | middleware = middlewares.RequestMiddleware(get_response) 268 | middleware(request) 269 | 270 | self.assertEqual(1, len(self.log_results.records)) 271 | record = self.log_results.records[0] 272 | 273 | self.assertEqual("INFO", record.levelname) 274 | self.assertIn("request_id", record.msg) 275 | self.assertNotIn("user_id", record.msg) 276 | self.assertEqual(x_request_id, record.msg["request_id"]) 277 | 278 | def test_should_log_correlation_id_from_request_x_correlation_id_header(self): 279 | mock_response = Mock() 280 | mock_response.status_code.return_value = 200 281 | x_correlation_id = "my-fake-correlation-id" 282 | 283 | def get_response(_response): 284 | with self.assertLogs(__name__, logging.INFO) as log_results: 285 | self.logger.info("hello") 286 | self.log_results = log_results 287 | return mock_response 288 | 289 | request = RequestFactory(HTTP_X_CORRELATION_ID=x_correlation_id).get("/foo") 290 | 291 | middleware = middlewares.RequestMiddleware(get_response) 292 | middleware(request) 293 | 294 | self.assertEqual(1, len(self.log_results.records)) 295 | record = self.log_results.records[0] 296 | 297 | self.assertEqual("INFO", record.levelname) 298 | self.assertIn("request_id", record.msg) 299 | self.assertNotIn("user_id", record.msg) 300 | self.assertEqual(x_correlation_id, record.msg["correlation_id"]) 301 | 302 | def tearDown(self): 303 | self.logger.new() 304 | 305 | 306 | class TestGetRequestHeader(TestCase): 307 | def test_django_22_or_higher(self): 308 | mock_request = mock.MagicMock(spec=["headers"]) 309 | get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") 310 | mock_request.headers.get.assert_called_once_with("x-foo-bar") 311 | 312 | def test_django_prior_to_22(self): 313 | mock_request = mock.MagicMock(spec=["META"]) 314 | get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") 315 | mock_request.META.get.assert_called_once_with("HTTP_X_FOO_BAR") 316 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. inclusion-marker-introduction-begin 2 | 3 | django-structlog 4 | ================ 5 | 6 | | |pypi| |wheels| |build-status| |docs| |coverage| |open_issues| |pull_requests| 7 | | |django| |python| |license| |black| 8 | | |watchers| |stars| |forks| 9 | 10 | .. |build-status| image:: https://secure.travis-ci.org/jrobichaud/django-structlog.svg?branch=master 11 | :target: https://travis-ci.org/jrobichaud/django-structlog 12 | :alt: Build Status 13 | 14 | .. |pypi| image:: https://img.shields.io/pypi/v/django-structlog.svg 15 | :target: https://pypi.org/project/django-structlog/ 16 | :alt: PyPI version 17 | 18 | .. |docs| image:: https://readthedocs.org/projects/django-structlog/badge/?version=latest 19 | :target: https://django-structlog.readthedocs.io/en/latest/?badge=latest 20 | :alt: Documentation Status 21 | 22 | .. |coverage| image:: https://img.shields.io/codecov/c/github/jrobichaud/django-structlog.svg 23 | :target: https://codecov.io/gh/jrobichaud/django-structlog 24 | :alt: codecov 25 | 26 | .. |python| image:: https://img.shields.io/pypi/pyversions/django-structlog.svg 27 | :target: https://pypi.org/project/django-structlog/ 28 | :alt: Supported Python versions 29 | 30 | .. |license| image:: https://img.shields.io/pypi/l/django-structlog.svg 31 | :target: https://github.com/jrobichaud/django-structlog/blob/master/LICENSE.rst 32 | :alt: License 33 | 34 | .. |open_issues| image:: https://img.shields.io/github/issues/jrobichaud/django-structlog.svg 35 | :target: https://github.com/jrobichaud/django-structlog/issues 36 | :alt: GitHub issues 37 | 38 | .. |django| image:: https://img.shields.io/pypi/djversions/django-structlog.svg 39 | :target: https://pypi.org/project/django-structlog/ 40 | :alt: PyPI - Django Version 41 | 42 | .. |pull_requests| image:: https://img.shields.io/github/issues-pr/jrobichaud/django-structlog.svg 43 | :target: https://github.com/jrobichaud/django-structlog/pulls 44 | :alt: GitHub pull requests 45 | 46 | .. |forks| image:: https://img.shields.io/github/forks/jrobichaud/django-structlog.svg?style=social 47 | :target: https://github.com/jrobichaud/django-structlog/ 48 | :alt: GitHub forks 49 | 50 | .. |stars| image:: https://img.shields.io/github/stars/jrobichaud/django-structlog.svg?style=social 51 | :target: https://github.com/jrobichaud/django-structlog/ 52 | :alt: GitHub stars 53 | 54 | .. |watchers| image:: https://img.shields.io/github/watchers/jrobichaud/django-structlog.svg?style=social 55 | :target: https://github.com/jrobichaud/django-structlog/ 56 | :alt: GitHub watchers 57 | 58 | .. |wheels| image:: https://img.shields.io/pypi/wheel/django-structlog.svg 59 | :target: https://pypi.org/project/django-structlog/ 60 | :alt: PyPI - Wheel 61 | 62 | .. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg 63 | :target: https://github.com/python/black 64 | :alt: Black 65 | 66 | 67 | django-structlog is a structured logging integration for `Django `_ project using `structlog `_ 68 | 69 | Logging will then produce additional cohesive metadata on each logs that makes it easier to track events or incidents. 70 | 71 | 72 | Additional Popular Integrations 73 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 74 | 75 | - `Django REST framework `_ is supported by default; 76 | - `Celery `_'s task logging requires additional configurations, see `documentation `_ for details. 77 | 78 | 79 | Logging comparison 80 | ^^^^^^^^^^^^^^^^^^ 81 | 82 | Standard logging: 83 | ~~~~~~~~~~~~~~~~~ 84 | 85 | .. code-block:: python 86 | 87 | >>> import logging 88 | >>> logger = logging.get_logger(__name__) 89 | >>> logger.info("An error occurred") 90 | 91 | .. code-block:: bash 92 | 93 | An error occurred 94 | 95 | Well... ok 96 | 97 | With django-structlog and flat_line: 98 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 99 | 100 | .. code-block:: python 101 | 102 | >>> import structlog 103 | >>> logger = structlog.get_logger(__name__) 104 | >>> logger.info("an_error_occurred", bar="Buz") 105 | 106 | .. code-block:: bash 107 | 108 | timestamp='2019-04-13T19:39:31.089925Z' level='info' event='an_error_occurred' logger='my_awesome_project.my_awesome_module' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' bar='Buz' 109 | 110 | Then you can search with commands like: 111 | 112 | .. code-block:: bash 113 | 114 | $ cat logs/flat_line.log | grep request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' 115 | 116 | With django-structlog and json 117 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 118 | 119 | .. code-block:: python 120 | 121 | >>> import structlog 122 | >>> logger = structlog.get_logger(__name__) 123 | >>> logger.info("an_error_occurred", bar="Buz") 124 | 125 | .. code-block:: json 126 | 127 | {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "an_error_occurred", "timestamp": "2019-04-13T19:39:31.089925Z", "logger": "my_awesome_project.my_awesome_module", "level": "info", "bar": "Buz"} 128 | 129 | Then you can search with commands like: 130 | 131 | .. code-block:: bash 132 | 133 | $ cat logs/json.log | jq '.[] | select(.request_id="3a8f801c-072b-4805-8f38-e1337f363ed4")' -s 134 | 135 | .. inclusion-marker-introduction-end 136 | 137 | .. inclusion-marker-getting-started-begin 138 | 139 | Getting Started 140 | =============== 141 | 142 | These steps will show how to integrate the middleware to your awesome application. 143 | 144 | Installation 145 | ^^^^^^^^^^^^ 146 | 147 | Install the library 148 | 149 | .. code-block:: bash 150 | 151 | pip install django-structlog 152 | 153 | Add middleware 154 | 155 | .. code-block:: python 156 | 157 | MIDDLEWARE = [ 158 | # ... 159 | 'django_structlog.middlewares.RequestMiddleware', 160 | ] 161 | 162 | Add appropriate structlog configuration to your ``settings.py`` 163 | 164 | .. code-block:: python 165 | 166 | import structlog 167 | 168 | LOGGING = { 169 | "version": 1, 170 | "disable_existing_loggers": False, 171 | "formatters": { 172 | "json_formatter": { 173 | "()": structlog.stdlib.ProcessorFormatter, 174 | "processor": structlog.processors.JSONRenderer(), 175 | }, 176 | "plain_console": { 177 | "()": structlog.stdlib.ProcessorFormatter, 178 | "processor": structlog.dev.ConsoleRenderer(), 179 | }, 180 | "key_value": { 181 | "()": structlog.stdlib.ProcessorFormatter, 182 | "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']), 183 | }, 184 | }, 185 | "handlers": { 186 | "console": { 187 | "class": "logging.StreamHandler", 188 | "formatter": "plain_console", 189 | }, 190 | "json_file": { 191 | "class": "logging.handlers.WatchedFileHandler", 192 | "filename": "logs/json.log", 193 | "formatter": "json_formatter", 194 | }, 195 | "flat_line_file": { 196 | "class": "logging.handlers.WatchedFileHandler", 197 | "filename": "logs/flat_line.log", 198 | "formatter": "key_value", 199 | }, 200 | }, 201 | "loggers": { 202 | "django_structlog": { 203 | "handlers": ["console", "flat_line_file", "json_file"], 204 | "level": "INFO", 205 | }, 206 | "django_structlog_demo_project": { 207 | "handlers": ["console", "flat_line_file", "json_file"], 208 | "level": "INFO", 209 | }, 210 | } 211 | } 212 | 213 | structlog.configure( 214 | processors=[ 215 | structlog.stdlib.filter_by_level, 216 | structlog.processors.TimeStamper(fmt="iso"), 217 | structlog.stdlib.add_logger_name, 218 | structlog.stdlib.add_log_level, 219 | structlog.stdlib.PositionalArgumentsFormatter(), 220 | structlog.processors.StackInfoRenderer(), 221 | structlog.processors.format_exc_info, 222 | structlog.processors.UnicodeDecoder(), 223 | structlog.processors.ExceptionPrettyPrinter(), 224 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter, 225 | ], 226 | context_class=structlog.threadlocal.wrap_dict(dict), 227 | logger_factory=structlog.stdlib.LoggerFactory(), 228 | wrapper_class=structlog.stdlib.BoundLogger, 229 | cache_logger_on_first_use=True, 230 | ) 231 | 232 | Start logging with ``structlog`` instead of ``logging``. 233 | 234 | .. code-block:: python 235 | 236 | import structlog 237 | logger = structlog.get_logger(__name__) 238 | 239 | .. _django_signals: 240 | 241 | Extending Request Log Metadata 242 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 243 | 244 | By default only a ``request_id`` and the ``user_id`` are bound from the request but pertinent log metadata may vary from a project to another. 245 | 246 | If you need to add more metadata from the request you can implement a convenient signal receiver to bind them. You can also override existing bound metadata the same way. 247 | 248 | .. code-block:: python 249 | 250 | from django.dispatch import receiver 251 | 252 | from django_structlog.signals import bind_extra_request_metadata 253 | 254 | 255 | @receiver(bind_extra_request_metadata) 256 | def bind_user_email(request, logger, **kwargs): 257 | logger.bind(user_email=getattr(request.user, 'email', '')) 258 | 259 | 260 | .. inclusion-marker-getting-started-end 261 | 262 | .. inclusion-marker-example-outputs-begin 263 | 264 | Example outputs 265 | =============== 266 | 267 | Flat lines file (\ ``logs/flat_lines.log``\ ) 268 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 269 | 270 | .. code-block:: bash 271 | 272 | timestamp='2019-04-13T19:39:29.321453Z' level='info' event='request_started' logger='django_structlog.middlewares.request' request_id='c53dff1d-3fc5-4257-a78a-9a567c937561' user_id=1 ip='0.0.0.0' request= user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' 273 | timestamp='2019-04-13T19:39:29.345207Z' level='info' event='request_finished' logger='django_structlog.middlewares.request' request_id='c53dff1d-3fc5-4257-a78a-9a567c937561' user_id=1 ip='0.0.0.0' code=200 274 | timestamp='2019-04-13T19:39:31.086155Z' level='info' event='request_started' logger='django_structlog.middlewares.request' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' request= user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36' 275 | timestamp='2019-04-13T19:39:31.089925Z' level='info' event='Enqueuing successful task' logger='django_structlog_demo_project.home.views' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' 276 | timestamp='2019-04-13T19:39:31.147590Z' level='info' event='task_enqueued' logger='django_structlog.middlewares.celery' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' child_task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' 277 | timestamp='2019-04-13T19:39:31.153081Z' level='info' event='This is a successful task' logger='django_structlog_demo_project.taskapp.celery' task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' 278 | timestamp='2019-04-13T19:39:31.160043Z' level='info' event='request_finished' logger='django_structlog.middlewares.request' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' code=201 279 | timestamp='2019-04-13T19:39:31.162372Z' level='info' event='task_succeed' logger='django_structlog.middlewares.celery' task_id='6b11fd80-3cdf-4de5-acc2-3fd4633aa654' request_id='3a8f801c-072b-4805-8f38-e1337f363ed4' user_id=1 ip='0.0.0.0' result='None' 280 | 281 | Json file (\ ``logs/json.log``\ ) 282 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 283 | 284 | .. code-block:: json 285 | 286 | {"request_id": "c53dff1d-3fc5-4257-a78a-9a567c937561", "user_id": 1, "ip": "0.0.0.0", "request": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", "event": "request_started", "timestamp": "2019-04-13T19:39:29.321453Z", "logger": "django_structlog.middlewares.request", "level": "info"} 287 | {"request_id": "c53dff1d-3fc5-4257-a78a-9a567c937561", "user_id": 1, "ip": "0.0.0.0", "code": 200, "event": "request_finished", "timestamp": "2019-04-13T19:39:29.345207Z", "logger": "django_structlog.middlewares.request", "level": "info"} 288 | {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "request": "", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36", "event": "request_started", "timestamp": "2019-04-13T19:39:31.086155Z", "logger": "django_structlog.middlewares.request", "level": "info"} 289 | {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "Enqueuing successful task", "timestamp": "2019-04-13T19:39:31.089925Z", "logger": "django_structlog_demo_project.home.views", "level": "info"} 290 | {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "child_task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "event": "task_enqueued", "timestamp": "2019-04-13T19:39:31.147590Z", "logger": "django_structlog.middlewares.celery", "level": "info"} 291 | {"task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "event": "This is a successful task", "timestamp": "2019-04-13T19:39:31.153081Z", "logger": "django_structlog_demo_project.taskapp.celery", "level": "info"} 292 | {"request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "code": 201, "event": "request_finished", "timestamp": "2019-04-13T19:39:31.160043Z", "logger": "django_structlog.middlewares.request", "level": "info"} 293 | {"task_id": "6b11fd80-3cdf-4de5-acc2-3fd4633aa654", "request_id": "3a8f801c-072b-4805-8f38-e1337f363ed4", "user_id": 1, "ip": "0.0.0.0", "result": "None", "event": "task_succeed", "timestamp": "2019-04-13T19:39:31.162372Z", "logger": "django_structlog.middlewares.celery", "level": "info"} 294 | 295 | .. inclusion-marker-example-outputs-end 296 | 297 | .. inclusion-marker-running-tests-begin 298 | 299 | Running the tests 300 | ================= 301 | 302 | Note: For the moment redis is needed to run the tests. The easiest way start docker's demo. 303 | 304 | .. code-block:: bash 305 | 306 | docker-compose up --build 307 | 308 | In another shell 309 | 310 | .. code-block:: bash 311 | 312 | pip install -r requirements.txt 313 | env CELERY_BROKER_URL=redis://0.0.0.0:6379 pytest test_app 314 | env CELERY_BROKER_URL=redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE=config.settings.test_demo_app pytest django_structlog_demo_project 315 | 316 | .. inclusion-marker-running-tests-end 317 | 318 | 319 | .. inclusion-marker-demo-begin 320 | 321 | Demo app 322 | ======== 323 | 324 | .. code-block:: bash 325 | 326 | docker-compose up --build 327 | 328 | Open ``http://0.0.0.0:8000/`` in your browser. 329 | 330 | Navigate while looking into the log files and shell's output. 331 | 332 | .. inclusion-marker-demo-end 333 | 334 | 335 | .. inclusion-marker-authors-begin 336 | 337 | Authors 338 | ======= 339 | 340 | 341 | * **Jules Robichaud-Gagnon** - *Initial work* - `jrobichaud `_ 342 | 343 | See also the list of `contributors `_ who participated in this project. 344 | 345 | .. inclusion-marker-authors-end 346 | 347 | 348 | .. inclusion-marker-acknowledgements-begin 349 | 350 | Acknowledgments 351 | =============== 352 | 353 | * Big thanks to `@ferd `_ for his `bad opinions `_ that inspired the author enough to spend time on this library. 354 | * `This issue `_ helped the author to figure out how to integrate ``structlog`` in Django. 355 | * `This stack overflow question `_ was also helpful. 356 | 357 | .. inclusion-marker-acknowledgements-end 358 | 359 | License 360 | ======= 361 | 362 | This project is licensed under the MIT License - see the `LICENSE `_ file for details 363 | --------------------------------------------------------------------------------