.yml (exec |run --rm) postgres restore <1>
11 |
12 |
13 | set -o errexit
14 | set -o pipefail
15 | set -o nounset
16 |
17 |
18 | working_dir="$(dirname ${0})"
19 | source "${working_dir}/_sourced/constants.sh"
20 | source "${working_dir}/_sourced/messages.sh"
21 |
22 |
23 | if [[ -z ${1+x} ]]; then
24 | message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
25 | exit 1
26 | fi
27 | backup_filename="${BACKUP_DIR_PATH}/${1}"
28 | if [[ ! -f "${backup_filename}" ]]; then
29 | message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
30 | exit 1
31 | fi
32 |
33 | message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
34 |
35 | if [[ "${POSTGRES_USER}" == "postgres" ]]; then
36 | message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
37 | exit 1
38 | fi
39 |
40 | export PGHOST="${POSTGRES_HOST}"
41 | export PGPORT="${POSTGRES_PORT}"
42 | export PGUSER="${POSTGRES_USER}"
43 | export PGPASSWORD="${POSTGRES_PASSWORD}"
44 | export PGDATABASE="${POSTGRES_DB}"
45 |
46 | message_info "Dropping the database..."
47 | dropdb "${PGDATABASE}"
48 |
49 | message_info "Creating a new database..."
50 | createdb --owner="${POSTGRES_USER}"
51 |
52 | message_info "Applying the backup to the new database..."
53 | gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
54 |
55 | message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."
56 |
--------------------------------------------------------------------------------
/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/config/__init__.py
--------------------------------------------------------------------------------
/config/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for django_structlog_demo_project project.
3 | """
4 |
5 | import os
6 | import sys
7 |
8 | from django.core.asgi import get_asgi_application
9 |
10 | # This allows easy placement of apps within the interior
11 | # django_structlog_demo_project directory.
12 | app_path = os.path.abspath(
13 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
14 | )
15 | sys.path.append(os.path.join(app_path, "django_structlog_demo_project"))
16 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
17 | # if running multiple sites in the same mod_wsgi process. To fix this, use
18 | # mod_wsgi daemon mode with each site in its own daemon process, or use
19 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local"
20 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
21 |
22 | # This application object is used by any WSGI server configured to use this
23 | # file. This includes Django's development server, if the WSGI_APPLICATION
24 | # setting points here.
25 | application = get_asgi_application()
26 | # Apply WSGI middleware here.
27 | # from helloworld.wsgi import HelloWorldApplication
28 | # application = HelloWorldApplication(application)
29 |
--------------------------------------------------------------------------------
/config/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/config/settings/__init__.py
--------------------------------------------------------------------------------
/config/settings/local.py:
--------------------------------------------------------------------------------
1 | import structlog
2 |
3 | from .base import * # noqa: F403
4 | from .base import MIDDLEWARE, env
5 |
6 | # GENERAL
7 | # ------------------------------------------------------------------------------
8 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
9 | DEBUG = env.bool("DJANGO_DEBUG", default=True)
10 |
11 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
12 | SECRET_KEY = env(
13 | "DJANGO_SECRET_KEY",
14 | default="DXatocQyyroxzcpo0tDxK3v5Rm4fatD9U7UeuLWwnZMOIaCQdPWovuqp4rxOct1T",
15 | )
16 | # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
17 | ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1"]
18 |
19 | IS_WORKER = env.bool("IS_WORKER", default=False)
20 |
21 | # CACHES
22 | # ------------------------------------------------------------------------------
23 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
24 | CACHES = {
25 | "default": {
26 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
27 | "LOCATION": "",
28 | }
29 | }
30 |
31 | # TEMPLATES
32 | # ------------------------------------------------------------------------------
33 | # https://docs.djangoproject.com/en/dev/ref/settings/#templates
34 | TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # noqa F405
35 |
36 | # EMAIL
37 | # ------------------------------------------------------------------------------
38 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
39 | EMAIL_BACKEND = env(
40 | "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
41 | )
42 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
43 | EMAIL_HOST = "localhost"
44 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
45 | EMAIL_PORT = 1025
46 |
47 | INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"]
48 | if env("USE_DOCKER") == "yes":
49 | import socket
50 |
51 | hostname, _, ips = socket.gethostbyname_ex(socket.gethostname())
52 | INTERNAL_IPS += [ip[:-1] + "1" for ip in ips]
53 |
54 | # django-extensions
55 | # ------------------------------------------------------------------------------
56 | # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
57 | INSTALLED_APPS += ["django_extensions"] # noqa F405
58 | # Celery
59 | # ------------------------------------------------------------------------------
60 |
61 | # http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates
62 | CELERY_TASK_EAGER_PROPAGATES = True
63 |
64 | CELERY_BEAT_SCHEDULE = {
65 | "example-scheduled-task": {
66 | "task": "django_structlog_demo_project.taskapp.celery.scheduled_task",
67 | "schedule": 30.0,
68 | },
69 | }
70 |
71 | # DATABASES
72 | # ------------------------------------------------------------------------------
73 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases
74 | DATABASES = {"default": env.db("DATABASE_URL")}
75 |
76 | # Your stuff...
77 | # ------------------------------------------------------------------------------
78 |
79 | LOGGING = {
80 | "version": 1,
81 | "disable_existing_loggers": False,
82 | "formatters": {
83 | "json_formatter": {
84 | "()": structlog.stdlib.ProcessorFormatter,
85 | "processor": structlog.processors.JSONRenderer(),
86 | "foreign_pre_chain": [
87 | structlog.contextvars.merge_contextvars,
88 | structlog.processors.TimeStamper(fmt="iso"),
89 | structlog.stdlib.add_logger_name,
90 | structlog.stdlib.add_log_level,
91 | structlog.stdlib.PositionalArgumentsFormatter(),
92 | ],
93 | },
94 | "colored": {
95 | "()": structlog.stdlib.ProcessorFormatter,
96 | "processor": structlog.dev.ConsoleRenderer(colors=True),
97 | "foreign_pre_chain": [
98 | structlog.contextvars.merge_contextvars,
99 | structlog.processors.TimeStamper(fmt="iso"),
100 | structlog.stdlib.add_logger_name,
101 | structlog.stdlib.add_log_level,
102 | structlog.stdlib.PositionalArgumentsFormatter(),
103 | ],
104 | },
105 | "key_value": {
106 | "()": structlog.stdlib.ProcessorFormatter,
107 | "processor": structlog.processors.KeyValueRenderer(
108 | key_order=["timestamp", "level", "event", "logger"]
109 | ),
110 | "foreign_pre_chain": [
111 | structlog.contextvars.merge_contextvars,
112 | structlog.processors.TimeStamper(fmt="iso"),
113 | structlog.stdlib.add_logger_name,
114 | structlog.stdlib.add_log_level,
115 | structlog.stdlib.PositionalArgumentsFormatter(),
116 | ],
117 | },
118 | },
119 | "handlers": {
120 | "colored_stream": {"class": "logging.StreamHandler", "formatter": "colored"},
121 | "json_file": {
122 | "class": "logging.handlers.WatchedFileHandler",
123 | "filename": "logs/json.log",
124 | "formatter": "json_formatter",
125 | },
126 | "flat_line_file": {
127 | "class": "logging.handlers.WatchedFileHandler",
128 | "filename": "logs/flat_line.log",
129 | "formatter": "key_value",
130 | },
131 | },
132 | "loggers": {
133 | "django_structlog": {
134 | "handlers": ["colored_stream", "flat_line_file", "json_file"],
135 | "level": "INFO",
136 | },
137 | "django_structlog_demo_project": {
138 | "handlers": ["colored_stream", "flat_line_file", "json_file"],
139 | "level": "INFO",
140 | },
141 | "foreign_logger": {
142 | "handlers": ["colored_stream", "flat_line_file", "json_file"],
143 | "level": "INFO",
144 | },
145 | },
146 | }
147 |
148 | structlog.configure(
149 | processors=[
150 | structlog.contextvars.merge_contextvars,
151 | structlog.stdlib.filter_by_level,
152 | structlog.processors.TimeStamper(fmt="iso"),
153 | structlog.stdlib.add_logger_name,
154 | structlog.stdlib.add_log_level,
155 | structlog.stdlib.PositionalArgumentsFormatter(),
156 | structlog.processors.StackInfoRenderer(),
157 | structlog.processors.format_exc_info,
158 | structlog.processors.UnicodeDecoder(),
159 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
160 | ],
161 | logger_factory=structlog.stdlib.LoggerFactory(),
162 | cache_logger_on_first_use=True,
163 | )
164 |
165 | MIDDLEWARE += [
166 | "django_structlog.middlewares.RequestMiddleware",
167 | ]
168 |
169 | DJANGO_STRUCTLOG_CELERY_ENABLED = True
170 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
171 |
--------------------------------------------------------------------------------
/config/settings/test.py:
--------------------------------------------------------------------------------
1 | """
2 | With these settings, tests run faster.
3 | """
4 |
5 | import os
6 |
7 | import environ
8 | import structlog
9 |
10 | env = environ.Env()
11 |
12 | ROOT_DIR = (
13 | environ.Path(__file__) - 3
14 | ) # (test_app/config/settings/base.py - 3 = test_app/)
15 | APPS_DIR = ROOT_DIR.path("test_app")
16 |
17 | # APPS
18 | # ------------------------------------------------------------------------------
19 | INSTALLED_APPS = [
20 | "django.contrib.auth",
21 | "django.contrib.contenttypes",
22 | "django.contrib.sessions",
23 | "django.contrib.sites",
24 | "django.contrib.messages",
25 | "django.contrib.staticfiles",
26 | ]
27 |
28 |
29 | # GENERAL
30 | # ------------------------------------------------------------------------------
31 | # https://docs.djangoproject.com/en/dev/ref/settings/#debug
32 | DEBUG = False
33 | # https://docs.djangoproject.com/en/dev/ref/settings/#use-tz
34 | USE_TZ = True
35 | # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
36 | SECRET_KEY = env(
37 | "DJANGO_SECRET_KEY",
38 | default="SqlHVcvZwwazrUrjtUiMJerENM8bU3k2p7WZu1WgA4yc8R1DcDc2Rh54m8dRvWcs",
39 | )
40 | # https://docs.djangoproject.com/en/dev/ref/settings/#test-runner
41 | TEST_RUNNER = "django.test.runner.DiscoverRunner"
42 |
43 | # CACHES
44 | # ------------------------------------------------------------------------------
45 | # https://docs.djangoproject.com/en/dev/ref/settings/#caches
46 | CACHES = {
47 | "default": {
48 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
49 | "LOCATION": "",
50 | }
51 | }
52 |
53 | # EMAIL
54 | # ------------------------------------------------------------------------------
55 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
56 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
57 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-host
58 | EMAIL_HOST = "localhost"
59 | # https://docs.djangoproject.com/en/dev/ref/settings/#email-port
60 | EMAIL_PORT = 1025
61 |
62 | # Your stuff...
63 | # ------------------------------------------------------------------------------
64 |
65 | LOGGING = {
66 | "version": 1,
67 | "disable_existing_loggers": True,
68 | "formatters": {
69 | "plain": {
70 | "()": structlog.stdlib.ProcessorFormatter,
71 | "processor": structlog.dev.ConsoleRenderer(colors=False),
72 | },
73 | "colored": {
74 | "()": structlog.stdlib.ProcessorFormatter,
75 | "processor": structlog.dev.ConsoleRenderer(colors=True),
76 | },
77 | },
78 | "filters": {},
79 | "handlers": {
80 | "structured_stream": {"class": "logging.StreamHandler", "formatter": "colored"},
81 | "structured_file": {
82 | "class": "logging.handlers.WatchedFileHandler",
83 | "filename": "test.log",
84 | "formatter": "plain",
85 | },
86 | },
87 | "loggers": {"": {"handlers": ["structured_stream"], "level": "INFO"}},
88 | }
89 |
90 | structlog.configure(
91 | processors=[
92 | structlog.contextvars.merge_contextvars,
93 | structlog.stdlib.filter_by_level,
94 | structlog.processors.TimeStamper(fmt="iso"),
95 | structlog.stdlib.add_logger_name,
96 | structlog.stdlib.add_log_level,
97 | structlog.stdlib.PositionalArgumentsFormatter(),
98 | structlog.processors.StackInfoRenderer(),
99 | structlog.processors.format_exc_info,
100 | structlog.processors.UnicodeDecoder(),
101 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
102 | ],
103 | logger_factory=structlog.stdlib.LoggerFactory(),
104 | cache_logger_on_first_use=True,
105 | )
106 |
107 | DATABASES = {
108 | "default": {
109 | "ENGINE": "django.db.backends.sqlite3",
110 | "NAME": os.path.join(str(ROOT_DIR), "db.sqlite3"),
111 | }
112 | }
113 |
114 | INSTALLED_APPS += ["django_structlog", "test_app"]
115 |
116 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
117 |
118 | IS_WORKER = False
119 |
--------------------------------------------------------------------------------
/config/settings/test_demo_app.py:
--------------------------------------------------------------------------------
1 | """
2 | With these settings, tests run faster.
3 | """
4 |
5 | # noinspection PyUnresolvedReferences
6 | from .base import * # noqa: F401,F403
7 |
8 | # noinspection PyUnresolvedReferences
9 | from .test import DATABASES, LOGGING # noqa: F401
10 |
11 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
12 |
13 | IS_WORKER = False
14 |
--------------------------------------------------------------------------------
/config/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.conf.urls import include
3 | from django.conf.urls.static import static
4 | from django.contrib import admin
5 | from django.urls import re_path
6 | from django.views import defaults as default_views
7 | from django.views.generic import TemplateView
8 |
9 | from django_structlog_demo_project.home import api_views, ninja_views, views
10 |
11 |
12 | def uncaught_exception_view(request):
13 | raise Exception("Uncaught Exception")
14 |
15 |
16 | urlpatterns = [
17 | re_path(r"^$", TemplateView.as_view(template_name="pages/home.html"), name="home"),
18 | re_path(
19 | r"^success_task$", views.enqueue_successful_task, name="enqueue_successful_task"
20 | ),
21 | re_path(r"^failing_task$", views.enqueue_failing_task, name="enqueue_failing_task"),
22 | re_path(r"^nesting_task$", views.enqueue_nesting_task, name="enqueue_nesting_task"),
23 | re_path(r"^unknown_task$", views.enqueue_unknown_task, name="enqueue_unknown_task"),
24 | re_path(
25 | r"^rejected_task$", views.enqueue_rejected_task, name="enqueue_rejected_task"
26 | ),
27 | re_path(r"^raise_exception", views.raise_exception, name="raise_exception"),
28 | re_path(
29 | r"^standard_logger", views.log_with_standard_logger, name="standard_logger"
30 | ),
31 | re_path(r"^async_view", views.async_view, name="async_view"),
32 | re_path(r"^api_view$", api_views.home_api_view, name="api_view"),
33 | re_path(
34 | r"^about/", TemplateView.as_view(template_name="pages/about.html"), name="about"
35 | ),
36 | re_path(r"^revoke_task", views.revoke_task, name="revoke_task"),
37 | re_path(
38 | r"^async_streaming_view",
39 | views.async_streaming_view,
40 | name="async_streaming_view",
41 | ),
42 | re_path(
43 | r"^sync_streaming_view", views.sync_streaming_view, name="sync_streaming_view"
44 | ),
45 | # Django Admin, use {% url 'admin:index' %}
46 | re_path(settings.ADMIN_URL, admin.site.urls),
47 | # User management
48 | re_path(
49 | r"^users/",
50 | include("django_structlog_demo_project.users.urls", namespace="users"),
51 | ),
52 | re_path(r"^accounts/", include("allauth.urls")),
53 | re_path("^ninja/", ninja_views.api.urls),
54 | # Your stuff: custom urls includes go here
55 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
56 |
57 | if settings.DEBUG:
58 | # This allows the error pages to be debugged during development, just visit
59 | # these url in browser to see how these error pages look like.
60 | urlpatterns += [
61 | re_path(
62 | r"^400/",
63 | default_views.bad_request,
64 | kwargs={"exception": Exception("Bad Request!")},
65 | ),
66 | re_path(
67 | r"^403/",
68 | default_views.permission_denied,
69 | kwargs={"exception": Exception("Permission Denied")},
70 | ),
71 | re_path(
72 | r"^404/",
73 | default_views.page_not_found,
74 | kwargs={"exception": Exception("Page not Found")},
75 | ),
76 | re_path(r"^500/", default_views.server_error),
77 | re_path(r"^uncaught_exception/", uncaught_exception_view),
78 | ]
79 |
--------------------------------------------------------------------------------
/config/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for django_structlog_demo_project project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 |
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 |
15 | """
16 |
17 | import os
18 | import sys
19 |
20 | from django.core.wsgi import get_wsgi_application
21 |
22 | # This allows easy placement of apps within the interior
23 | # django_structlog_demo_project directory.
24 | app_path = os.path.abspath(
25 | os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
26 | )
27 | sys.path.append(os.path.join(app_path, "django_structlog_demo_project"))
28 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
29 | # if running multiple sites in the same mod_wsgi process. To fix this, use
30 | # mod_wsgi daemon mode with each site in its own daemon process, or use
31 | # os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.local"
32 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
33 |
34 | # This application object is used by any WSGI server configured to use this
35 | # file. This includes Django's development server, if the WSGI_APPLICATION
36 | # setting points here.
37 | application = get_wsgi_application()
38 | # Apply WSGI middleware here.
39 | # from helloworld.wsgi import HelloWorldApplication
40 | # application = HelloWorldApplication(application)
41 |
--------------------------------------------------------------------------------
/django_structlog/__init__.py:
--------------------------------------------------------------------------------
1 | """``django-structlog`` is a structured logging integration for ``Django`` project using ``structlog``."""
2 |
3 | name = "django_structlog"
4 |
5 | VERSION = (9, 1, 1)
6 |
7 | __version__ = ".".join(str(v) for v in VERSION)
8 |
--------------------------------------------------------------------------------
/django_structlog/app_settings.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 |
5 |
6 | # noinspection PyPep8Naming
7 | class AppSettings:
8 | PREFIX = "DJANGO_STRUCTLOG_"
9 |
10 | @property
11 | def CELERY_ENABLED(self) -> bool:
12 | return getattr(settings, self.PREFIX + "CELERY_ENABLED", False)
13 |
14 | @property
15 | def IP_LOGGING_ENABLED(self) -> bool:
16 | return getattr(settings, self.PREFIX + "IP_LOGGING_ENABLED", True)
17 |
18 | @property
19 | def STATUS_4XX_LOG_LEVEL(self) -> int:
20 | return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING)
21 |
22 | @property
23 | def COMMAND_LOGGING_ENABLED(self) -> bool:
24 | return getattr(settings, self.PREFIX + "COMMAND_LOGGING_ENABLED", False)
25 |
26 | @property
27 | def USER_ID_FIELD(self) -> str:
28 | return getattr(settings, self.PREFIX + "USER_ID_FIELD", "pk")
29 |
30 |
31 | app_settings = AppSettings()
32 |
--------------------------------------------------------------------------------
/django_structlog/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 | from .app_settings import app_settings
4 |
5 |
6 | class DjangoStructLogConfig(AppConfig):
7 | name = "django_structlog"
8 |
9 | def ready(self) -> None:
10 | if app_settings.CELERY_ENABLED:
11 | from .celery.receivers import CeleryReceiver
12 |
13 | self._celery_receiver = CeleryReceiver()
14 | self._celery_receiver.connect_signals()
15 |
16 | if app_settings.COMMAND_LOGGING_ENABLED:
17 | from .commands import DjangoCommandReceiver
18 |
19 | self._django_command_receiver = DjangoCommandReceiver()
20 | self._django_command_receiver.connect_signals()
21 |
--------------------------------------------------------------------------------
/django_structlog/celery/__init__.py:
--------------------------------------------------------------------------------
1 | """``celery`` integration for ``django_structlog``."""
2 |
--------------------------------------------------------------------------------
/django_structlog/celery/receivers.py:
--------------------------------------------------------------------------------
1 | import time
2 | from typing import TYPE_CHECKING, Any, Optional, Type, cast
3 |
4 | import structlog
5 | from celery import current_app
6 | from celery.signals import (
7 | after_task_publish,
8 | before_task_publish,
9 | task_failure,
10 | task_prerun,
11 | task_rejected,
12 | task_retry,
13 | task_revoked,
14 | task_success,
15 | task_unknown,
16 | )
17 |
18 | from . import signals
19 |
20 | if TYPE_CHECKING: # pragma: no cover
21 | from types import TracebackType
22 |
23 | logger = structlog.getLogger(__name__)
24 |
25 |
26 | class CeleryReceiver:
27 | _priority: Optional[str]
28 |
29 | def __init__(self) -> None:
30 | self._priority = None
31 |
32 | def receiver_before_task_publish(
33 | self,
34 | sender: Optional[Type[Any]] = None,
35 | headers: Optional[dict[str, Any]] = None,
36 | body: Optional[dict[str, str]] = None,
37 | properties: Optional[dict[str, Any]] = None,
38 | routing_key: Optional[str] = None,
39 | **kwargs: dict[str, str],
40 | ) -> None:
41 | if current_app.conf.task_protocol < 2:
42 | return
43 |
44 | context = structlog.contextvars.get_merged_contextvars(logger)
45 | if "task_id" in context:
46 | context["parent_task_id"] = context.pop("task_id")
47 |
48 | signals.modify_context_before_task_publish.send(
49 | sender=self.receiver_before_task_publish,
50 | context=context,
51 | task_routing_key=routing_key,
52 | task_properties=properties,
53 | )
54 | if properties:
55 | self._priority = properties.get("priority", None)
56 | cast(dict[str, Any], headers)["__django_structlog__"] = context
57 |
58 | def receiver_after_task_publish(
59 | self,
60 | sender: Optional[Type[Any]] = None,
61 | headers: Optional[dict[str, Optional[str]]] = None,
62 | body: Optional[dict[str, Optional[str]]] = None,
63 | routing_key: Optional[str] = None,
64 | **kwargs: Any,
65 | ) -> None:
66 | properties = {}
67 | if self._priority is not None:
68 | properties["priority"] = self._priority
69 | self._priority = None
70 |
71 | logger.info(
72 | "task_enqueued",
73 | child_task_id=(
74 | headers.get("id")
75 | if headers
76 | else cast(dict[str, Optional[str]], body).get("id")
77 | ),
78 | child_task_name=(
79 | headers.get("task")
80 | if headers
81 | else cast(dict[str, Optional[str]], body).get("task")
82 | ),
83 | routing_key=routing_key,
84 | **properties,
85 | )
86 |
87 | def receiver_task_prerun(
88 | self, task_id: str, task: Any, *args: Any, **kwargs: Any
89 | ) -> None:
90 | structlog.contextvars.clear_contextvars()
91 | structlog.contextvars.bind_contextvars(task_id=task_id)
92 | metadata = getattr(task.request, "__django_structlog__", {})
93 | structlog.contextvars.bind_contextvars(**metadata)
94 | signals.bind_extra_task_metadata.send(
95 | sender=self.receiver_task_prerun, task=task, logger=logger
96 | )
97 | # Record the start time so we can log the task duration later.
98 | task.request._django_structlog_started_at = time.monotonic_ns()
99 | logger.info("task_started", task=task.name)
100 |
101 | def receiver_task_retry(
102 | self,
103 | request: Optional[Any] = None,
104 | reason: Optional[str] = None,
105 | einfo: Optional[Any] = None,
106 | **kwargs: Any,
107 | ) -> None:
108 | logger.warning("task_retrying", reason=reason)
109 |
110 | def receiver_task_success(
111 | self, result: Optional[str] = None, sender: Optional[Any] = None, **kwargs: Any
112 | ) -> None:
113 | signals.pre_task_succeeded.send(
114 | sender=self.receiver_task_success, logger=logger, result=result
115 | )
116 |
117 | log_vars: dict[str, Any] = {}
118 | self.add_duration_ms(sender, log_vars)
119 | logger.info("task_succeeded", **log_vars)
120 |
121 | def receiver_task_failure(
122 | self,
123 | task_id: Optional[str] = None,
124 | exception: Optional[Exception] = None,
125 | traceback: Optional["TracebackType"] = None,
126 | einfo: Optional[Any] = None,
127 | sender: Optional[Type[Any]] = None,
128 | *args: Any,
129 | **kwargs: Any,
130 | ) -> None:
131 | log_vars: dict[str, Any] = {}
132 | self.add_duration_ms(sender, log_vars)
133 | throws = getattr(sender, "throws", ())
134 | if isinstance(exception, throws):
135 | logger.info(
136 | "task_failed",
137 | error=str(exception),
138 | **log_vars,
139 | )
140 | else:
141 | logger.exception(
142 | "task_failed",
143 | error=str(exception),
144 | exception=exception,
145 | **log_vars,
146 | )
147 |
148 | @classmethod
149 | def add_duration_ms(
150 | cls, task: Optional[Type[Any]], log_vars: dict[str, Any]
151 | ) -> None:
152 | if task and hasattr(task.request, "_django_structlog_started_at"):
153 | started_at: int = task.request._django_structlog_started_at
154 | log_vars["duration_ms"] = round(
155 | (time.monotonic_ns() - started_at) / 1_000_000
156 | )
157 |
158 | def receiver_task_revoked(
159 | self,
160 | request: Any,
161 | terminated: Optional[bool] = None,
162 | signum: Optional[Any] = None,
163 | expired: Optional[Any] = None,
164 | **kwargs: Any,
165 | ) -> None:
166 | metadata = getattr(request, "__django_structlog__", {}).copy()
167 | metadata["task_id"] = request.id
168 | metadata["task"] = request.task
169 |
170 | logger.warning(
171 | "task_revoked",
172 | terminated=terminated,
173 | signum=signum.value if signum is not None else None,
174 | signame=signum.name if signum is not None else None,
175 | expired=expired,
176 | **metadata,
177 | )
178 |
179 | def receiver_task_unknown(
180 | self,
181 | message: Optional[str] = None,
182 | exc: Optional[Exception] = None,
183 | name: Optional[str] = None,
184 | id: Optional[str] = None,
185 | **kwargs: Any,
186 | ) -> None:
187 | logger.error(
188 | "task_not_found",
189 | task=name,
190 | task_id=id,
191 | )
192 |
193 | def receiver_task_rejected(
194 | self, message: Any, exc: Optional[Exception] = None, **kwargs: Any
195 | ) -> None:
196 | logger.exception(
197 | "task_rejected", task_id=message.properties.get("correlation_id")
198 | )
199 |
200 | def connect_signals(self) -> None:
201 | before_task_publish.connect(self.receiver_before_task_publish)
202 | after_task_publish.connect(self.receiver_after_task_publish)
203 |
204 | def connect_worker_signals(self) -> None:
205 | before_task_publish.connect(self.receiver_before_task_publish)
206 | after_task_publish.connect(self.receiver_after_task_publish)
207 | task_prerun.connect(self.receiver_task_prerun)
208 | task_retry.connect(self.receiver_task_retry)
209 | task_success.connect(self.receiver_task_success)
210 | task_failure.connect(self.receiver_task_failure)
211 | task_revoked.connect(self.receiver_task_revoked)
212 | task_unknown.connect(self.receiver_task_unknown)
213 | task_rejected.connect(self.receiver_task_rejected)
214 |
--------------------------------------------------------------------------------
/django_structlog/celery/signals.py:
--------------------------------------------------------------------------------
1 | import django.dispatch
2 |
3 | bind_extra_task_metadata = django.dispatch.Signal()
4 | """ Signal to add extra ``structlog`` bindings from ``celery``'s task.
5 |
6 | :param task: the celery task being run
7 | :param logger: the logger to bind more metadata or override existing bound metadata
8 |
9 | >>> from django.dispatch import receiver
10 | >>> from django_structlog.celery import signals
11 | >>> import structlog
12 | >>>
13 | >>> @receiver(signals.bind_extra_task_metadata)
14 | ... def receiver_bind_extra_task_metadata(sender, signal, task=None, logger=None, **kwargs):
15 | ... structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id)
16 |
17 | """
18 |
19 |
20 | modify_context_before_task_publish = django.dispatch.Signal()
21 | """ Signal to modify context passed over to ``celery`` task's context. You must modify the ``context`` dict.
22 |
23 | :param context: the context dict that will be passed over to the task runner's logger
24 | :param task_routing_key: routing key of the task
25 | :param task_properties: task's message properties
26 |
27 | >>> from django.dispatch import receiver
28 | >>> from django_structlog.celery import signals
29 | >>>
30 | >>> @receiver(signals.modify_context_before_task_publish)
31 | ... def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs):
32 | ... keys_to_keep = {"request_id", "parent_task_id"}
33 | ... new_dict = {
34 | ... key_to_keep: context[key_to_keep]
35 | ... for key_to_keep in keys_to_keep
36 | ... if key_to_keep in context
37 | ... }
38 | ... context.clear()
39 | ... context.update(new_dict)
40 |
41 | """
42 |
43 | pre_task_succeeded = django.dispatch.Signal()
44 | """ Signal to add ``structlog`` bindings from ``celery``'s successful task.
45 |
46 | :param logger: the logger to bind more metadata or override existing bound metadata
47 | :param result: result of the succeeding task
48 |
49 | >>> from django.dispatch import receiver
50 | >>> from django_structlog.celery import signals
51 | >>> import structlog
52 | >>>
53 | >>> @receiver(signals.pre_task_succeeded)
54 | ... def receiver_pre_task_succeeded(sender, signal, logger=None, result=None, **kwargs):
55 | ... structlog.contextvars.bind_contextvars(result=str(result))
56 |
57 | """
58 |
--------------------------------------------------------------------------------
/django_structlog/celery/steps.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from celery import bootsteps
4 |
5 | from .receivers import CeleryReceiver
6 |
7 |
8 | class DjangoStructLogInitStep(bootsteps.Step):
9 | """``celery`` worker boot step to initialize ``django_structlog``.
10 |
11 | >>> from celery import Celery
12 | >>> from django_structlog.celery.steps import DjangoStructLogInitStep
13 | >>>
14 | >>> app = Celery("django_structlog_demo_project")
15 | >>> app.steps['worker'].add(DjangoStructLogInitStep)
16 |
17 | """
18 |
19 | def __init__(self, parent: Any, **kwargs: Any) -> None:
20 | super().__init__(parent, **kwargs)
21 | self.receiver = CeleryReceiver()
22 | self.receiver.connect_worker_signals()
23 |
--------------------------------------------------------------------------------
/django_structlog/commands.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from typing import TYPE_CHECKING, Any, List, Mapping, Tuple, Type
3 |
4 | import structlog
5 | from django_extensions.management.signals import (
6 | post_command,
7 | pre_command,
8 | )
9 |
10 | if TYPE_CHECKING: # pragma: no cover
11 | import contextvars
12 |
13 | logger = structlog.getLogger(__name__)
14 |
15 |
16 | class DjangoCommandReceiver:
17 | stack: List[Tuple[str, Mapping[str, "contextvars.Token[Any]"]]]
18 |
19 | def __init__(self) -> None:
20 | self.stack = []
21 |
22 | def pre_receiver(self, sender: Type[Any], *args: Any, **kwargs: Any) -> None:
23 | command_id = str(uuid.uuid4())
24 | if len(self.stack):
25 | parent_command_id, _ = self.stack[-1]
26 | tokens = structlog.contextvars.bind_contextvars(
27 | parent_command_id=parent_command_id, command_id=command_id
28 | )
29 | else:
30 | tokens = structlog.contextvars.bind_contextvars(command_id=command_id)
31 | self.stack.append((command_id, tokens))
32 |
33 | logger.info(
34 | "command_started",
35 | command_name=sender.__module__.replace(".management.commands", ""),
36 | )
37 |
38 | def post_receiver(
39 | self, sender: Type[Any], outcome: str, *args: Any, **kwargs: Any
40 | ) -> None:
41 | logger.info("command_finished")
42 |
43 | if len(self.stack): # pragma: no branch
44 | command_id, tokens = self.stack.pop()
45 | structlog.contextvars.reset_contextvars(**tokens)
46 |
47 | def connect_signals(self) -> None:
48 | pre_command.connect(self.pre_receiver)
49 | post_command.connect(self.post_receiver)
50 |
--------------------------------------------------------------------------------
/django_structlog/middlewares/__init__.py:
--------------------------------------------------------------------------------
1 | from .request import RequestMiddleware # noqa F401
2 |
3 | __all__ = [
4 | "RequestMiddleware",
5 | ]
6 |
--------------------------------------------------------------------------------
/django_structlog/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog/py.typed
--------------------------------------------------------------------------------
/django_structlog/signals.py:
--------------------------------------------------------------------------------
1 | import django.dispatch
2 |
3 | bind_extra_request_metadata = django.dispatch.Signal()
4 | """ Signal to add extra ``structlog`` bindings from ``django``'s request.
5 |
6 | :param request: the request returned by the view
7 | :param logger: the logger
8 | :param log_kwargs: dictionary of log metadata for the ``request_started`` event. It contains ``request`` and ``user_agent`` keys. You may modify it to add extra information.
9 |
10 | >>> from django.contrib.sites.shortcuts import get_current_site
11 | >>> from django.dispatch import receiver
12 | >>> from django_structlog import signals
13 | >>> import structlog
14 | >>>
15 | >>> @receiver(signals.bind_extra_request_metadata)
16 | ... def bind_domain(request, logger, log_kwargs, **kwargs):
17 | ... current_site = get_current_site(request)
18 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain)
19 |
20 | """
21 |
22 | bind_extra_request_finished_metadata = django.dispatch.Signal()
23 | """ Signal to add extra ``structlog`` bindings from ``django``'s finished request and response.
24 |
25 | :param logger: the logger
26 | :param response: the response resulting of the request
27 | :param log_kwargs: dictionary of log metadata for the ``request_finished`` event. It contains ``request`` and ``code`` keys. You may modify it to add extra information.
28 |
29 | >>> from django.contrib.sites.shortcuts import get_current_site
30 | >>> from django.dispatch import receiver
31 | >>> from django_structlog import signals
32 | >>> import structlog
33 | >>>
34 | >>> @receiver(signals.bind_extra_request_finished_metadata)
35 | ... def bind_domain(request, logger, response, log_kwargs, **kwargs):
36 | ... current_site = get_current_site(request)
37 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain)
38 |
39 | """
40 |
41 | bind_extra_request_failed_metadata = django.dispatch.Signal()
42 | """ Signal to add extra ``structlog`` bindings from ``django``'s failed request and exception.
43 |
44 | :param logger: the logger
45 | :param exception: the exception resulting of the request
46 | :param log_kwargs: dictionary of log metadata for the ``request_failed`` event. It contains ``request`` and ``code`` keys. You may modify it to add extra information.
47 |
48 | >>> from django.contrib.sites.shortcuts import get_current_site
49 | >>> from django.dispatch import receiver
50 | >>> from django_structlog import signals
51 | >>> import structlog
52 | >>>
53 | >>> @receiver(signals.bind_extra_request_failed_metadata)
54 | ... def bind_domain(request, logger, exception, log_kwargs, **kwargs):
55 | ... current_site = get_current_site(request)
56 | ... structlog.contextvars.bind_contextvars(domain=current_site.domain)
57 |
58 | """
59 |
60 | update_failure_response = django.dispatch.Signal()
61 | """ Signal to update response failure response before it is returned.
62 |
63 | :param request: the request returned by the view
64 | :param response: the response resulting of the request
65 | :param logger: the logger
66 | :param exception: the exception
67 |
68 | >>> from django.dispatch import receiver
69 | >>> from django_structlog import signals
70 | >>> import structlog
71 | >>>
72 | >>> @receiver(signals.update_failure_response)
73 | ... def add_request_id_to_error_response(request, response, logger, exception, **kwargs):
74 | ... context = structlog.contextvars.get_merged_contextvars(logger)
75 | ... response['X-Request-ID'] = context["request_id"]
76 |
77 | """
78 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.0"
2 | __version_info__ = tuple(
3 | [
4 | int(num) if num.isdigit() else num
5 | for num in __version__.replace("-", ".", 1).split(".")
6 | ]
7 | )
8 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CommandExamplesAppConfig(AppConfig):
5 | name = "django_structlog_demo_project.command_examples"
6 | default_auto_field = "django.db.models.AutoField"
7 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/management/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/management/commands/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/management/commands/example_command.py:
--------------------------------------------------------------------------------
1 | import structlog
2 | from django.core import management
3 | from django.core.management import BaseCommand
4 | from django_extensions.management.utils import signalcommand
5 |
6 | logger = structlog.getLogger(__name__)
7 |
8 |
9 | class Command(BaseCommand):
10 | def add_arguments(self, parser):
11 | parser.add_argument("foo", type=str)
12 |
13 | @signalcommand
14 | def handle(self, foo, *args, **options):
15 | logger.info("my log", foo=foo)
16 | management.call_command("example_nested_command", "buz", verbosity=0)
17 | logger.info("my log 2")
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/management/commands/example_nested_command.py:
--------------------------------------------------------------------------------
1 | import structlog
2 | from django.core.management import BaseCommand
3 | from django_extensions.management.utils import signalcommand
4 |
5 | logger = structlog.getLogger(__name__)
6 |
7 |
8 | class Command(BaseCommand):
9 | def add_arguments(self, parser):
10 | parser.add_argument("baz", type=str)
11 |
12 | @signalcommand
13 | def handle(self, baz, *args, **options):
14 | logger.info("my nested log", baz=baz)
15 | return 0
16 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/command_examples/tests/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/command_examples/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.core import management
3 |
4 | from django_structlog_demo_project.command_examples.management.commands import (
5 | example_command,
6 | )
7 |
8 | pytestmark = pytest.mark.django_db
9 |
10 |
11 | class TestCommand:
12 | def test_command(self):
13 | assert (
14 | management.call_command(example_command.Command(), "bar", verbosity=0)
15 | is None
16 | )
17 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.conf import settings
3 | from django.test import RequestFactory
4 |
5 | from django_structlog_demo_project.users.tests.factories import UserFactory
6 |
7 |
8 | @pytest.fixture(autouse=True)
9 | def media_storage(settings, tmpdir):
10 | settings.MEDIA_ROOT = tmpdir.strpath
11 |
12 |
13 | @pytest.fixture
14 | def user() -> settings.AUTH_USER_MODEL:
15 | return UserFactory()
16 |
17 |
18 | @pytest.fixture
19 | def request_factory() -> RequestFactory:
20 | return RequestFactory()
21 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | import django.contrib.sites.models
2 | from django.contrib.sites.models import _simple_domain_name_validator
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = []
8 |
9 | operations = [
10 | migrations.CreateModel(
11 | name="Site",
12 | fields=[
13 | (
14 | "id",
15 | models.AutoField(
16 | verbose_name="ID",
17 | serialize=False,
18 | auto_created=True,
19 | primary_key=True,
20 | ),
21 | ),
22 | (
23 | "domain",
24 | models.CharField(
25 | max_length=100,
26 | verbose_name="domain name",
27 | validators=[_simple_domain_name_validator],
28 | ),
29 | ),
30 | ("name", models.CharField(max_length=50, verbose_name="display name")),
31 | ],
32 | options={
33 | "ordering": ("domain",),
34 | "db_table": "django_site",
35 | "verbose_name": "site",
36 | "verbose_name_plural": "sites",
37 | },
38 | bases=(models.Model,),
39 | managers=[("objects", django.contrib.sites.models.SiteManager())],
40 | )
41 | ]
42 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/migrations/0002_alter_domain_unique.py:
--------------------------------------------------------------------------------
1 | import django.contrib.sites.models
2 | from django.db import migrations, models
3 |
4 |
5 | class Migration(migrations.Migration):
6 | dependencies = [("sites", "0001_initial")]
7 |
8 | operations = [
9 | migrations.AlterField(
10 | model_name="site",
11 | name="domain",
12 | field=models.CharField(
13 | max_length=100,
14 | unique=True,
15 | validators=[django.contrib.sites.models._simple_domain_name_validator],
16 | verbose_name="domain name",
17 | ),
18 | )
19 | ]
20 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/migrations/0003_set_site_domain_and_name.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
7 | from django.conf import settings
8 | from django.db import migrations
9 |
10 |
11 | def update_site_forward(apps, schema_editor):
12 | """Set site domain and name."""
13 | site_model = apps.get_model("sites", "Site")
14 | site_model.objects.update_or_create(
15 | id=settings.SITE_ID,
16 | defaults={"domain": "example.com", "name": "django_structlog_demo_project"},
17 | )
18 |
19 |
20 | class Migration(migrations.Migration):
21 | dependencies = [("sites", "0002_alter_domain_unique")]
22 |
23 | operations = [migrations.RunPython(update_site_forward, migrations.RunPython.noop)]
24 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/migrations/0004_alter_site_options.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.14 on 2022-08-02 17:18
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("sites", "0003_set_site_domain_and_name"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterModelOptions(
13 | name="site",
14 | options={
15 | "ordering": ["domain"],
16 | "verbose_name": "site",
17 | "verbose_name_plural": "sites",
18 | },
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/contrib/sites/migrations/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/home/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/api_views.py:
--------------------------------------------------------------------------------
1 | import structlog
2 | from rest_framework.decorators import api_view
3 | from rest_framework.response import Response
4 |
5 | logger = structlog.get_logger(__name__)
6 |
7 |
8 | @api_view()
9 | def home_api_view(request):
10 | logger.info("This is a rest-framework structured log")
11 | return Response({"message": "Hello, world!"})
12 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class HomeAppConfig(AppConfig):
5 | name = "django_structlog_demo_project.home"
6 | default_auto_field = "django.db.models.AutoField"
7 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/ninja_views.py:
--------------------------------------------------------------------------------
1 | import structlog
2 | from ninja import NinjaAPI, Router
3 | from ninja.security import SessionAuth
4 |
5 | api = NinjaAPI(urls_namespace="ninja")
6 | router = Router()
7 |
8 | logger = structlog.get_logger(__name__)
9 |
10 |
11 | # OptionalSessionAuth is a custom authentication class that allows the user to be anonymous
12 | class OptionalSessionAuth(SessionAuth):
13 | def authenticate(self, request, key):
14 | return request.user
15 |
16 |
17 | @router.get("/ninja", url_name="ninja", auth=OptionalSessionAuth())
18 | def ninja(request):
19 | logger.info("This is a ninja structured log")
20 | return {"result": "ok"}
21 |
22 |
23 | api.add_router("", router)
24 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/static/js/home.js:
--------------------------------------------------------------------------------
1 | const toastContainer = document.getElementById('toast-container')
2 | const toastTemplate = document.getElementById('toastTemplate');
3 | let abortController = null;
4 |
5 | function log(title, url, body, isError, duration) {
6 | const newToast = toastTemplate.cloneNode(true)
7 |
8 | const text = body ? body.toString() : ""
9 |
10 | if (isError) {
11 | console.error(title, url, body, duration);
12 | } else {
13 | console.log(title, url, body, duration);
14 | }
15 |
16 | if (isError) {
17 | newToast.classList.add("border-danger")
18 | } else {
19 | newToast.classList.add("border-success")
20 | }
21 | newToast.removeAttribute('id');
22 | const toastHeader = newToast.querySelector('.toast-header > .me-auto');
23 | toastHeader.textContent = `${title} ${url}`
24 | if (duration) {
25 | const toastDuration = newToast.querySelector('.duration');
26 | toastDuration.textContent = `${duration} ms`
27 | }
28 | const toastBody = newToast.querySelector('.toast-body');
29 | if (body) {
30 | toastBody.textContent = text.slice(0, 400)
31 | } else {
32 | newToast.removeChild(toastBody);
33 | }
34 | toastContainer.appendChild(newToast);
35 | const toast = new bootstrap.Toast(newToast)
36 |
37 | toast.show()
38 | }
39 |
40 |
41 | async function fetchUrl(url) {
42 | abortController = new AbortController();
43 | log("request_started", url);
44 | const start = new Date();
45 |
46 | try {
47 | const response = await fetch(url, {
48 | method: 'get',
49 | headers: {"Content-Type": "application/json"},
50 | signal: abortController.signal,
51 | });
52 | const text = await response.text();
53 | if (response.ok) {
54 | log("request_finished", url, text, false, new Date() - start);
55 | } else {
56 | log("request_failed", url, text, true, new Date() - start);
57 | }
58 | } catch (err) {
59 | log("request_failed", url, err, true, new Date() - start);
60 | }
61 | }
62 |
63 |
64 | async function fetchStreamingUrl(url) {
65 | const start = new Date();
66 | try {
67 | abortController = new AbortController();
68 | log("streaming_request_started", url);
69 | const response = await fetch(url, {
70 | method: 'get',
71 | signal: abortController.signal,
72 | });
73 | log("streaming_request_finished", url, `Status code ${response.status}`, false, new Date() - start);
74 |
75 | const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
76 |
77 | log("streaming_response_started", url, undefined, false, new Date() - start);
78 | while (true) {
79 | const {value, done} = await reader.read();
80 | if (done) break;
81 | log("received", url, value, false, new Date() - start);
82 | }
83 |
84 | log("streaming_response_finished", url, undefined, false, new Date() - start);
85 | } catch (err) {
86 | log("request_failed", url, err, true, new Date() - start);
87 | }
88 | }
89 |
90 | function cancelAsync() {
91 | if (abortController)
92 | abortController.abort();
93 | }
94 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/home/tests/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/tests/test_api_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from .. import api_views
4 |
5 | pytestmark = pytest.mark.django_db
6 |
7 |
8 | class TestApiView:
9 | def test(self, caplog, request_factory):
10 | response = api_views.home_api_view(request_factory.get("/"))
11 | assert response.status_code == 200
12 | assert len(caplog.records) == 1
13 | record = caplog.records[0]
14 | assert record.msg["event"] == "This is a rest-framework structured log"
15 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/tests/test_ninja_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from ninja.testing import TestClient
3 |
4 | from ..ninja_views import router
5 |
6 | pytestmark = pytest.mark.django_db
7 |
8 |
9 | class TestNinjaView:
10 | def test(self, caplog, request_factory):
11 | client = TestClient(router)
12 | response = client.get("/ninja")
13 | assert response.status_code == 200
14 | assert response.json() == {"result": "ok"}
15 | assert len(caplog.records) == 1
16 | record = caplog.records[0]
17 | assert record.msg["event"] == "This is a ninja structured log"
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from .. import views
4 |
5 | pytestmark = pytest.mark.django_db
6 | pytest_plugins = ("pytest_asyncio",)
7 |
8 |
9 | class TestEnqueueSuccessfulTask:
10 | def test(self):
11 | response = views.enqueue_successful_task(None)
12 | assert response.status_code == 201
13 |
14 |
15 | class TestEnqueueFailingTask:
16 | def test(self):
17 | response = views.enqueue_failing_task(None)
18 | assert response.status_code == 201
19 |
20 |
21 | class TestEnqueueNestingTask:
22 | def test(self):
23 | response = views.enqueue_nesting_task(None)
24 | assert response.status_code == 201
25 |
26 |
27 | class TestRaiseException:
28 | def test(self):
29 | with pytest.raises(Exception) as e:
30 | views.raise_exception(None)
31 | assert str(e.value) == "This is a view raising an exception."
32 |
33 |
34 | class TestLogWithStandardLogger:
35 | def test(self):
36 | response = views.log_with_standard_logger(None)
37 | assert response.status_code == 200
38 |
39 |
40 | @pytest.mark.asyncio
41 | class TestAsyncView:
42 | async def test(self, mocker):
43 | mocker.patch("asyncio.sleep")
44 | response = await views.async_view(None)
45 | assert response.status_code == 200
46 |
47 |
48 | class TestRevokeTask:
49 | def test(self):
50 | response = views.revoke_task(None)
51 | assert response.status_code == 201
52 |
53 |
54 | class TestEnqueueUnknownTask:
55 | def test(self):
56 | response = views.enqueue_unknown_task(None)
57 | assert response.status_code == 201
58 |
59 |
60 | class TestEnqueueRejectedTask:
61 | def test(self):
62 | response = views.enqueue_rejected_task(None)
63 | assert response.status_code == 201
64 |
65 |
66 | @pytest.mark.asyncio
67 | class TestAsyncStreamingViewView:
68 | async def test(self, mocker):
69 | response = await views.async_streaming_view(None)
70 | assert response.status_code == 200
71 |
72 | mocker.patch("asyncio.sleep")
73 | assert b"0" == await anext(response.streaming_content)
74 | assert b"1" == await anext(response.streaming_content)
75 | assert b"2" == await anext(response.streaming_content)
76 | assert b"3" == await anext(response.streaming_content)
77 | assert b"4" == await anext(response.streaming_content)
78 |
79 | with pytest.raises(StopAsyncIteration):
80 | await anext(response.streaming_content)
81 |
82 |
83 | class TestSyncStreamingViewView:
84 | def test(self, mocker):
85 | response = views.sync_streaming_view(None)
86 | assert response.status_code == 200
87 |
88 | mocker.patch("time.sleep")
89 | assert b"0" == next(response.streaming_content)
90 | assert b"1" == next(response.streaming_content)
91 | assert b"2" == next(response.streaming_content)
92 | assert b"3" == next(response.streaming_content)
93 | assert b"4" == next(response.streaming_content)
94 | with pytest.raises(StopIteration):
95 | next(response.streaming_content)
96 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/home/views.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import time
4 |
5 | import structlog
6 | from django.http import HttpResponse, StreamingHttpResponse
7 |
8 | from django_structlog_demo_project.taskapp.celery import (
9 | failing_task,
10 | nesting_task,
11 | rejected_task,
12 | successful_task,
13 | )
14 |
15 | logger = structlog.get_logger(__name__)
16 |
17 |
18 | def enqueue_successful_task(request):
19 | logger.info("Enqueuing successful task")
20 | successful_task.apply_async(foo="bar", priority=5)
21 | return HttpResponse(status=201)
22 |
23 |
24 | def enqueue_failing_task(request):
25 | logger.info("Enqueuing failing task")
26 | failing_task.delay(foo="bar")
27 | return HttpResponse(status=201)
28 |
29 |
30 | def enqueue_nesting_task(request):
31 | logger.info("Enqueuing nesting task")
32 | nesting_task.delay()
33 | return HttpResponse(status=201)
34 |
35 |
36 | def log_with_standard_logger(request):
37 | logging.getLogger("foreign_logger").info("This is a standard logger")
38 | return HttpResponse(status=200)
39 |
40 |
41 | def revoke_task(request):
42 | async_result = successful_task.apply_async(countdown=1)
43 | async_result.revoke()
44 | return HttpResponse(status=201)
45 |
46 |
47 | def enqueue_unknown_task(request):
48 | from django_structlog_demo_project.taskapp.celery import unknown_task
49 |
50 | logger.info("Enqueuing unknown task")
51 | unknown_task.delay()
52 | return HttpResponse(status=201)
53 |
54 |
55 | def enqueue_rejected_task(request):
56 | rejected_task.delay()
57 | return HttpResponse(status=201)
58 |
59 |
60 | async def async_view(request):
61 | for num in range(1, 2):
62 | await asyncio.sleep(1)
63 | logger.info(f"This this is an async view {num}")
64 | return HttpResponse(status=200)
65 |
66 |
67 | async def async_streaming_response():
68 | for chunk in range(0, 5):
69 | await asyncio.sleep(0.5)
70 | logger.info("streaming_chunk", chunk=chunk)
71 | yield chunk
72 |
73 |
74 | def sync_streaming_response():
75 | for chunk in range(0, 5):
76 | time.sleep(0.5)
77 | logger.info("streaming_chunk", chunk=chunk)
78 | yield chunk
79 |
80 |
81 | def sync_streaming_view(request):
82 | logger.info("This this is a sync streaming view")
83 | return StreamingHttpResponse(sync_streaming_response())
84 |
85 |
86 | async def async_streaming_view(request):
87 | logger.info("This this is an async streaming view")
88 | return StreamingHttpResponse(async_streaming_response())
89 |
90 |
91 | def raise_exception(request):
92 | raise Exception("This is a view raising an exception.")
93 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/css/project.css:
--------------------------------------------------------------------------------
1 | /* These styles are generated from project.scss. */
2 |
3 | .alert-debug {
4 | color: black;
5 | background-color: white;
6 | border-color: #d6e9c6;
7 | }
8 |
9 | .alert-error {
10 | color: #b94a48;
11 | background-color: #f2dede;
12 | border-color: #eed3d7;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/fonts/.gitkeep
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/images/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/images/favicons/favicon.ico
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/js/project.js:
--------------------------------------------------------------------------------
1 | /* Project specific Javascript goes here. */
2 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/static/sass/custom_bootstrap_vars.scss
--------------------------------------------------------------------------------
/django_structlog_demo_project/static/sass/project.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | // project specific CSS goes here
6 |
7 | ////////////////////////////////
8 | //Variables//
9 | ////////////////////////////////
10 |
11 | // Alert colors
12 |
13 | $white: #fff;
14 | $mint-green: #d6e9c6;
15 | $black: #000;
16 | $pink: #f2dede;
17 | $dark-pink: #eed3d7;
18 | $red: #b94a48;
19 |
20 | ////////////////////////////////
21 | //Alerts//
22 | ////////////////////////////////
23 |
24 | // bootstrap alert CSS, translated to the django-standard levels of
25 | // debug, info, success, warning, error
26 |
27 | .alert-debug {
28 | background-color: $white;
29 | border-color: $mint-green;
30 | color: $black;
31 | }
32 |
33 | .alert-error {
34 | background-color: $pink;
35 | border-color: $dark-pink;
36 | color: $red;
37 | }
38 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/taskapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/taskapp/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/taskapp/celery.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import logging.config
3 | import os
4 |
5 | import structlog
6 | from celery import Celery, shared_task, signals
7 | from django.apps import AppConfig, apps
8 | from django.conf import settings
9 |
10 | from django_structlog.celery.steps import DjangoStructLogInitStep
11 |
12 | if not settings.configured:
13 | # set the default Django settings module for the 'celery' program.
14 | os.environ.setdefault(
15 | "DJANGO_SETTINGS_MODULE", "config.settings.local"
16 | ) # pragma: no cover
17 |
18 |
19 | app = Celery("django_structlog_demo_project", namespace="CELERY")
20 |
21 | app.config_from_object("django.conf:settings")
22 |
23 | # A step to initialize django-structlog
24 | app.steps["worker"].add(DjangoStructLogInitStep)
25 |
26 |
27 | @signals.setup_logging.connect
28 | def receiver_setup_logging(
29 | loglevel, logfile, format, colorize, **kwargs
30 | ): # pragma: no cover
31 | logging.config.dictConfig(settings.LOGGING)
32 |
33 | structlog.configure(
34 | processors=[
35 | structlog.contextvars.merge_contextvars,
36 | structlog.stdlib.filter_by_level,
37 | structlog.processors.TimeStamper(fmt="iso"),
38 | structlog.stdlib.add_logger_name,
39 | structlog.stdlib.add_log_level,
40 | structlog.stdlib.PositionalArgumentsFormatter(),
41 | structlog.processors.StackInfoRenderer(),
42 | structlog.processors.format_exc_info,
43 | structlog.processors.UnicodeDecoder(),
44 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
45 | ],
46 | logger_factory=structlog.stdlib.LoggerFactory(),
47 | cache_logger_on_first_use=True,
48 | )
49 |
50 |
51 | class CeleryAppConfig(AppConfig):
52 | name = "django_structlog_demo_project.taskapp"
53 | verbose_name = "Celery Config"
54 |
55 | def ready(self):
56 | installed_apps = [app_config.name for app_config in apps.get_app_configs()]
57 | app.autodiscover_tasks(lambda: installed_apps, force=True)
58 |
59 |
60 | @shared_task
61 | def successful_task(foo=None):
62 | import structlog
63 |
64 | logger = structlog.getLogger(__name__)
65 | logger.info("This is a successful task")
66 |
67 |
68 | @shared_task
69 | def failing_task(foo=None, **kwargs):
70 | raise Exception("This is a failed task")
71 |
72 |
73 | @shared_task
74 | def nesting_task():
75 | logger = structlog.getLogger(__name__)
76 | structlog.contextvars.bind_contextvars(foo="Bar")
77 | logger.info("This is a nesting task")
78 |
79 | nested_task.delay()
80 |
81 |
82 | @shared_task
83 | def nested_task():
84 | logger = structlog.getLogger(__name__)
85 | logger.info("This is a nested task")
86 |
87 |
88 | @shared_task
89 | def scheduled_task():
90 | logger = structlog.getLogger(__name__)
91 | logger.info("This is a scheduled task")
92 |
93 |
94 | @shared_task
95 | def rejected_task():
96 | pass
97 |
98 |
99 | if not settings.IS_WORKER: # pragma: no branch
100 |
101 | @shared_task
102 | def unknown_task():
103 | """Simulate a task unavailable in the worker for demonstration purpose"""
104 |
105 |
106 | @signals.before_task_publish.connect
107 | def corrupt_rejected_task(sender=None, headers=None, body=None, **kwargs):
108 | """Simulate celery's task rejection mechanism by breaking up the message"""
109 | logger = structlog.getLogger(__name__)
110 | if headers.get("task") == f"{rejected_task.__module__}.{rejected_task.__name__}":
111 | logger.warn(
112 | f"corrupting {rejected_task.__name__}",
113 | task_id=headers.get("id"),
114 | )
115 | del headers["task"]
116 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/taskapp/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/taskapp/tests/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/taskapp/tests/test_celery.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from .. import celery
4 |
5 | pytestmark = pytest.mark.django_db
6 |
7 |
8 | class TestSuccessfulTask:
9 | def test(self, caplog):
10 | celery.successful_task(foo="bar")
11 | assert len(caplog.records) == 1
12 | record = caplog.records[0]
13 | assert record.msg["event"] == "This is a successful task"
14 |
15 |
16 | class TestFailingTask:
17 | def test(self):
18 | with pytest.raises(Exception) as e:
19 | celery.failing_task(foo="bar")
20 | assert str(e.value) == "This is a failed task"
21 |
22 |
23 | class TestNestingTask:
24 | def test(self, caplog):
25 | celery.nesting_task()
26 | assert len(caplog.records) == 1
27 | record = caplog.records[0]
28 | assert record.msg["event"] == "This is a nesting task"
29 |
30 |
31 | class TestNestedTask:
32 | def test(self, caplog):
33 | celery.nested_task()
34 | assert len(caplog.records) == 1
35 | record = caplog.records[0]
36 | assert record.msg["event"] == "This is a nested task"
37 |
38 |
39 | class TestScheduledTask:
40 | def test(self, caplog):
41 | celery.scheduled_task()
42 | assert len(caplog.records) == 1
43 | record = caplog.records[0]
44 | assert record.msg["event"] == "This is a scheduled task"
45 |
46 |
47 | class TestRejectedTask:
48 | def test(self):
49 | assert celery.rejected_task() is None
50 |
51 |
52 | class TestCorruptRejectedTask:
53 | def test(self, caplog):
54 | task_id = "11111111-1111-1111-1111-111111111111"
55 | headers = dict(
56 | id=task_id,
57 | task="django_structlog_demo_project.taskapp.celery.rejected_task",
58 | )
59 | celery.corrupt_rejected_task(sender=None, headers=headers)
60 | assert len(caplog.records) == 1
61 | record = caplog.records[0]
62 | assert record.msg["event"] == "corrupting rejected_task"
63 | assert record.msg["task_id"] == task_id
64 | assert "task" not in headers
65 |
66 | def test_other_tasks_not_corrupted(self, caplog):
67 | task_id = "11111111-1111-1111-1111-111111111111"
68 | headers = dict(
69 | id=task_id,
70 | task="django_structlog_demo_project.taskapp.celery.successful_task",
71 | )
72 | celery.corrupt_rejected_task(sender=None, headers=headers)
73 | assert len(caplog.records) == 0
74 | assert (
75 | headers["task"]
76 | == "django_structlog_demo_project.taskapp.celery.successful_task"
77 | )
78 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/403_csrf.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Forbidden (403){% endblock %}
4 |
5 | {% block content %}
6 | Forbidden (403)
7 |
8 | CSRF verification failed. Request aborted.
9 | {% endblock content %}
10 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Page not found{% endblock %}
4 |
5 | {% block content %}
6 | Page not found
7 |
8 | This is not the page you were looking for.
9 | {% endblock content %}
10 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Server Error{% endblock %}
4 |
5 | {% block content %}
6 | Ooops!!! 500
7 |
8 | Looks like something went wrong!
9 |
10 | We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
11 | {% endblock content %}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/account_inactive.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Account Inactive" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% trans "Account Inactive" %}
9 |
10 | {% trans "This account is inactive." %}
11 | {% endblock %}
12 |
13 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}{% block head_title %}{% endblock head_title %}{% endblock title %}
3 |
4 | {% block content %}
5 |
6 |
7 | {% block inner %}{% endblock %}
8 |
9 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/email.html:
--------------------------------------------------------------------------------
1 |
2 | {% extends "account/base.html" %}
3 |
4 | {% load i18n %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Account" %}{% endblock %}
8 |
9 | {% block inner %}
10 | {% trans "E-mail Addresses" %}
11 |
12 | {% if user.emailaddress_set.all %}
13 | {% trans 'The following e-mail addresses are associated with your account:' %}
14 |
15 |
44 |
45 | {% else %}
46 | {% trans 'Warning:'%} {% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
47 |
48 | {% endif %}
49 |
50 |
51 | {% trans "Add E-mail Address" %}
52 |
53 |
58 |
59 | {% endblock %}
60 |
61 |
62 | {% block javascript %}
63 | {{ block.super }}
64 |
79 | {% endblock %}
80 |
81 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/email_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
7 |
8 |
9 | {% block inner %}
10 | {% trans "Confirm E-mail Address" %}
11 |
12 | {% if confirmation %}
13 |
14 | {% user_display confirmation.email_address.user as user_display %}
15 |
16 | {% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}
17 |
18 |
22 |
23 | {% else %}
24 |
25 | {% url 'account_email' as email_url %}
26 |
27 | {% blocktrans %}This e-mail confirmation link expired or is invalid. Please issue a new e-mail confirmation request .{% endblocktrans %}
28 |
29 | {% endif %}
30 |
31 | {% endblock %}
32 |
33 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account socialaccount %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Sign In" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 | {% trans "Sign In" %}
12 |
13 | {% get_providers as socialaccount_providers %}
14 |
15 | {% if socialaccount_providers %}
16 | {% blocktrans with site.name as site_name %}Please sign in with one
17 | of your existing third party accounts. Or, sign up
18 | for a {{ site_name }} account and sign in below:{% endblocktrans %}
19 |
20 |
21 |
22 |
23 | {% include "socialaccount/snippets/provider_list.html" with process="login" %}
24 |
25 |
26 |
{% trans 'or' %}
27 |
28 |
29 |
30 | {% include "socialaccount/snippets/login_extra.html" %}
31 |
32 | {% else %}
33 | {% blocktrans %}If you have not created an account yet, then please
34 | sign up first.{% endblocktrans %}
35 | {% endif %}
36 |
37 |
46 |
47 | {% endblock %}
48 |
49 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Sign Out" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% trans "Sign Out" %}
9 |
10 | {% trans 'Are you sure you want to sign out?' %}
11 |
12 |
19 |
20 |
21 | {% endblock %}
22 |
23 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_change.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% trans "Change Password" %}
10 |
11 |
16 | {% endblock %}
17 |
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 | {% load crispy_forms_tags %}
6 |
7 | {% block head_title %}{% trans "Password Reset" %}{% endblock %}
8 |
9 | {% block inner %}
10 |
11 | {% trans "Password Reset" %}
12 | {% if user.is_authenticated %}
13 | {% include "account/snippets/already_logged_in.html" %}
14 | {% endif %}
15 |
16 | {% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}
17 |
18 |
23 |
24 | {% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}
25 | {% endblock %}
26 |
27 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_reset_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}{% trans "Password Reset" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% trans "Password Reset" %}
10 |
11 | {% if user.is_authenticated %}
12 | {% include "account/snippets/already_logged_in.html" %}
13 | {% endif %}
14 |
15 | {% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}
16 | {% endblock %}
17 |
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_reset_from_key.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}
9 |
10 | {% if token_fail %}
11 | {% url 'account_reset_password' as passwd_reset_url %}
12 | {% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset .{% endblocktrans %}
13 | {% else %}
14 | {% if form %}
15 |
20 | {% else %}
21 | {% trans 'Your password is now changed.' %}
22 | {% endif %}
23 | {% endif %}
24 | {% endblock %}
25 |
26 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_reset_from_key_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% block head_title %}{% trans "Change Password" %}{% endblock %}
5 |
6 | {% block inner %}
7 | {% trans "Change Password" %}
8 | {% trans 'Your password is now changed.' %}
9 | {% endblock %}
10 |
11 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/password_set.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% trans "Set Password" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% trans "Set Password" %}
10 |
11 |
16 | {% endblock %}
17 |
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/signup.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}{% trans "Signup" %}{% endblock %}
7 |
8 | {% block inner %}
9 | {% trans "Sign Up" %}
10 |
11 | {% blocktrans %}Already have an account? Then please sign in .{% endblocktrans %}
12 |
13 |
21 |
22 | {% endblock %}
23 |
24 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/signup_closed.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% trans "Sign Up Closed" %}
9 |
10 | {% trans "We are sorry, but the sign up is currently closed." %}
11 | {% endblock %}
12 |
13 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/verification_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% trans "Verify Your E-mail Address" %}
9 |
10 | {% blocktrans %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}
11 |
12 | {% endblock %}
13 |
14 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/account/verified_email_required.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
6 |
7 | {% block inner %}
8 | {% trans "Verify Your E-mail Address" %}
9 |
10 | {% url 'account_email' as email_url %}
11 |
12 | {% blocktrans %}This part of the site requires us to verify that
13 | you are who you claim to be. For this purpose, we require that you
14 | verify ownership of your e-mail address. {% endblocktrans %}
15 |
16 | {% blocktrans %}We have sent an e-mail to you for
17 | verification. Please click on the link inside this e-mail. Please
18 | contact us if you do not receive it within a few minutes.{% endblocktrans %}
19 |
20 | {% blocktrans %}Note: you can still change your e-mail address .{% endblocktrans %}
21 |
22 |
23 | {% endblock %}
24 |
25 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static i18n %}
2 |
3 |
4 |
5 |
6 | {% block title %}django_structlog_demo_project{% endblock title %}
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% block css %}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
43 |
44 |
45 |
46 |
47 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {% if messages %}
87 | {% for message in messages %}
88 |
{{ message }}
89 | {% endfor %}
90 | {% endif %}
91 |
92 | {% block content %}
93 |
Use this document as a way to quick start any new project.
94 | {% endblock content %}
95 |
96 |
97 |
98 | {% block modal %}{% endblock modal %}
99 |
100 |
102 |
103 | {% block javascript %}
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | {% endblock javascript %}
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/pages/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/pages/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load static i18n %}
4 | {% block javascript %}
5 | {{ block.super }}
6 |
7 |
8 | {% endblock %}
9 |
10 | {% block content %}
11 |
12 |
24 |
25 |
26 |
27 |
28 |
Base
29 |
30 |
33 | Raise exception
34 |
35 |
36 |
39 | Streaming view
40 |
41 |
42 |
45 | Standard logger
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Async
53 |
54 |
55 |
58 | Async streaming view
59 |
60 |
61 |
64 | Async View
65 |
66 |
67 |
70 | Cancel
71 |
72 |
73 |
74 |
75 |
76 |
77 |
rest-framework
78 |
79 |
82 | API View
83 |
84 |
85 |
86 |
87 |
88 |
89 |
ninja
90 |
91 |
94 | API View
95 |
96 |
97 |
98 |
99 |
100 |
101 |
Celery
102 |
103 |
106 | Successful task
107 |
108 |
109 |
112 | Failing task
113 |
114 |
115 |
118 | Nesting task
119 |
120 |
121 |
124 | Unknown task
125 |
126 |
127 |
130 | Rejected task
131 |
132 |
133 |
136 | Revoke task
137 |
138 |
139 |
140 |
141 |
142 |
143 | {% endblock %}
144 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/users/user_detail.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static %}
3 |
4 | {% block title %}User: {{ object.username }}{% endblock %}
5 |
6 | {% block content %}
7 |
8 |
9 |
10 |
11 |
12 |
{{ object.username }}
13 | {% if object.name %}
14 |
{{ object.name }}
15 | {% endif %}
16 |
17 |
18 |
19 | {% if object == request.user %}
20 |
21 |
30 |
31 | {% endif %}
32 |
33 |
34 |
35 | {% endblock content %}
36 |
37 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/users/user_form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load crispy_forms_tags %}
3 |
4 | {% block title %}{{ user.username }}{% endblock %}
5 |
6 | {% block content %}
7 | {{ user.username }}
8 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/templates/users/user_list.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load static i18n %}
3 | {% block title %}Members{% endblock %}
4 |
5 | {% block content %}
6 |
17 | {% endblock content %}
18 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/adapters.py:
--------------------------------------------------------------------------------
1 | from allauth.account.adapter import DefaultAccountAdapter
2 | from django.conf import settings
3 | from django.http import HttpRequest
4 |
5 |
6 | class AccountAdapter(DefaultAccountAdapter):
7 | def is_open_for_signup(self, request: HttpRequest):
8 | return getattr(settings, "ACCOUNT_ALLOW_REGISTRATION", True)
9 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth import admin as auth_admin
3 | from django.contrib.auth import get_user_model
4 |
5 | from django_structlog_demo_project.users.forms import UserChangeForm, UserCreationForm
6 |
7 | User = get_user_model()
8 |
9 |
10 | @admin.register(User)
11 | class UserAdmin(auth_admin.UserAdmin):
12 | form = UserChangeForm
13 | add_form = UserCreationForm
14 | fieldsets = (("User", {"fields": ("name",)}),) + auth_admin.UserAdmin.fieldsets
15 | list_display = ["username", "name", "is_superuser"]
16 | search_fields = ["name"]
17 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class UsersAppConfig(AppConfig):
5 | name = "django_structlog_demo_project.users"
6 | verbose_name = "Users"
7 | default_auto_field = "django.db.models.AutoField"
8 |
9 | def ready(self):
10 | # noinspection PyUnresolvedReferences
11 | from . import signals # noqa F401
12 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/forms.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import forms, get_user_model
2 | from django.core.exceptions import ValidationError
3 | from django.utils.translation import gettext_lazy as _
4 |
5 | User = get_user_model()
6 |
7 |
8 | class UserChangeForm(forms.UserChangeForm):
9 | class Meta(forms.UserChangeForm.Meta):
10 | model = User
11 |
12 |
13 | class UserCreationForm(forms.UserCreationForm):
14 | error_message = forms.UserCreationForm.error_messages.update(
15 | {"duplicate_username": _("This username has already been taken.")}
16 | )
17 |
18 | class Meta(forms.UserCreationForm.Meta):
19 | model = User
20 |
21 | def clean_username(self):
22 | username = self.cleaned_data["username"]
23 |
24 | try:
25 | User.objects.get(username=username)
26 | except User.DoesNotExist:
27 | return username
28 |
29 | raise ValidationError(self.error_messages["duplicate_username"])
30 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | import django.contrib.auth.models
2 | import django.contrib.auth.validators
3 | import django.utils.timezone
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 | initial = True
9 |
10 | dependencies = [("auth", "0008_alter_user_username_max_length")]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name="User",
15 | fields=[
16 | (
17 | "id",
18 | models.AutoField(
19 | auto_created=True,
20 | primary_key=True,
21 | serialize=False,
22 | verbose_name="ID",
23 | ),
24 | ),
25 | ("password", models.CharField(max_length=128, verbose_name="password")),
26 | (
27 | "last_login",
28 | models.DateTimeField(
29 | blank=True, null=True, verbose_name="last login"
30 | ),
31 | ),
32 | (
33 | "is_superuser",
34 | models.BooleanField(
35 | default=False,
36 | help_text="Designates that this user has all permissions without explicitly assigning them.",
37 | verbose_name="superuser status",
38 | ),
39 | ),
40 | (
41 | "username",
42 | models.CharField(
43 | error_messages={
44 | "unique": "A user with that username already exists."
45 | },
46 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
47 | max_length=150,
48 | unique=True,
49 | validators=[
50 | django.contrib.auth.validators.UnicodeUsernameValidator()
51 | ],
52 | verbose_name="username",
53 | ),
54 | ),
55 | (
56 | "first_name",
57 | models.CharField(
58 | blank=True, max_length=30, verbose_name="first name"
59 | ),
60 | ),
61 | (
62 | "last_name",
63 | models.CharField(
64 | blank=True, max_length=150, verbose_name="last name"
65 | ),
66 | ),
67 | (
68 | "email",
69 | models.EmailField(
70 | blank=True, max_length=254, verbose_name="email address"
71 | ),
72 | ),
73 | (
74 | "is_staff",
75 | models.BooleanField(
76 | default=False,
77 | help_text="Designates whether the user can log into this admin site.",
78 | verbose_name="staff status",
79 | ),
80 | ),
81 | (
82 | "is_active",
83 | models.BooleanField(
84 | default=True,
85 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
86 | verbose_name="active",
87 | ),
88 | ),
89 | (
90 | "date_joined",
91 | models.DateTimeField(
92 | default=django.utils.timezone.now, verbose_name="date joined"
93 | ),
94 | ),
95 | (
96 | "name",
97 | models.CharField(
98 | blank=True, max_length=255, verbose_name="Name of User"
99 | ),
100 | ),
101 | (
102 | "groups",
103 | models.ManyToManyField(
104 | blank=True,
105 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
106 | related_name="user_set",
107 | related_query_name="user",
108 | to="auth.Group",
109 | verbose_name="groups",
110 | ),
111 | ),
112 | (
113 | "user_permissions",
114 | models.ManyToManyField(
115 | blank=True,
116 | help_text="Specific permissions for this user.",
117 | related_name="user_set",
118 | related_query_name="user",
119 | to="auth.Permission",
120 | verbose_name="user permissions",
121 | ),
122 | ),
123 | ],
124 | options={
125 | "verbose_name_plural": "users",
126 | "verbose_name": "user",
127 | "abstract": False,
128 | },
129 | managers=[("objects", django.contrib.auth.models.UserManager())],
130 | )
131 | ]
132 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/migrations/0002_alter_user_first_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.14 on 2022-08-02 17:18
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="user",
14 | name="first_name",
15 | field=models.CharField(
16 | blank=True, max_length=150, verbose_name="first name"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/migrations/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractUser
2 | from django.db.models import CharField
3 | from django.urls import reverse
4 | from django.utils.translation import gettext_lazy as _
5 |
6 |
7 | class User(AbstractUser):
8 | # First Name and Last Name do not cover name patterns
9 | # around the globe.
10 | name = CharField(_("Name of User"), blank=True, max_length=255)
11 |
12 | def get_absolute_url(self):
13 | return reverse("users:detail", kwargs={"username": self.username})
14 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/signals.py:
--------------------------------------------------------------------------------
1 | # Experiment with django worker signals here
2 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/django_structlog_demo_project/users/tests/__init__.py
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/factories.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Sequence
2 |
3 | from django.contrib.auth import get_user_model
4 | from factory import Faker, post_generation
5 | from factory.django import DjangoModelFactory
6 |
7 |
8 | class UserFactory(DjangoModelFactory):
9 | username = Faker("user_name")
10 | email = Faker("email")
11 | name = Faker("name")
12 |
13 | @post_generation
14 | def password(self, create: bool, extracted: Sequence[Any], **kwargs):
15 | password = Faker(
16 | "password",
17 | length=42,
18 | special_chars=True,
19 | digits=True,
20 | upper_case=True,
21 | lower_case=True,
22 | ).evaluate(None, None, extra={"locale": None})
23 | self.set_password(password)
24 |
25 | class Meta:
26 | model = get_user_model()
27 | django_get_or_create = ["username"]
28 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/test_adapters.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django_structlog_demo_project.users.adapters import AccountAdapter
4 |
5 | pytestmark = pytest.mark.django_db
6 |
7 |
8 | class TestUserCreationForm:
9 | def test_account_adapter(self):
10 | assert AccountAdapter().is_open_for_signup(None)
11 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/test_forms.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from django_structlog_demo_project.users.forms import UserCreationForm
4 | from django_structlog_demo_project.users.tests.factories import UserFactory
5 |
6 | pytestmark = pytest.mark.django_db
7 |
8 |
9 | class TestUserCreationForm:
10 | def test_clean_username(self):
11 | # A user with proto_user params does not exist yet.
12 | proto_user = UserFactory.build()
13 |
14 | form = UserCreationForm(
15 | {
16 | "username": proto_user.username,
17 | "password1": proto_user._password,
18 | "password2": proto_user._password,
19 | }
20 | )
21 |
22 | assert form.is_valid()
23 | assert form.clean_username() == proto_user.username
24 |
25 | # Creating a user.
26 | form.save()
27 |
28 | # The user with proto_user params already exists,
29 | # hence cannot be created.
30 | form = UserCreationForm(
31 | {
32 | "username": proto_user.username,
33 | "password1": proto_user._password,
34 | "password2": proto_user._password,
35 | }
36 | )
37 |
38 | assert not form.is_valid()
39 | assert len(form.errors) == 1
40 | assert "username" in form.errors
41 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/test_models.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.conf import settings
3 |
4 | pytestmark = pytest.mark.django_db
5 |
6 |
7 | def test_user_get_absolute_url(user: settings.AUTH_USER_MODEL):
8 | assert user.get_absolute_url() == "/users/{username}/".format(
9 | username=user.username
10 | )
11 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/test_urls.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.conf import settings
3 | from django.urls import resolve, reverse
4 |
5 | pytestmark = pytest.mark.django_db
6 |
7 |
8 | def test_detail(user: settings.AUTH_USER_MODEL):
9 | route = f"/users/{user.username}/"
10 | assert reverse("users:detail", kwargs={"username": user.username}) == route
11 | assert resolve(route).view_name == "users:detail"
12 |
13 |
14 | def test_detail_username_with_dot():
15 | route = "/users/foo.bar/"
16 | assert reverse("users:detail", kwargs={"username": "foo.bar"}) == route
17 | assert resolve(route).view_name == "users:detail"
18 |
19 |
20 | def test_list():
21 | assert reverse("users:list") == "/users/"
22 | assert resolve("/users/").view_name == "users:list"
23 |
24 |
25 | def test_update():
26 | assert reverse("users:update") == "/users/~update/"
27 | assert resolve("/users/~update/").view_name == "users:update"
28 |
29 |
30 | def test_redirect():
31 | assert reverse("users:redirect") == "/users/~redirect/"
32 | assert resolve("/users/~redirect/").view_name == "users:redirect"
33 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/tests/test_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from django.conf import settings
3 | from django.test import RequestFactory
4 |
5 | from django_structlog_demo_project.users.views import UserRedirectView, UserUpdateView
6 |
7 | pytestmark = pytest.mark.django_db
8 |
9 |
10 | class TestUserUpdateView:
11 | """
12 | TODO:
13 | extracting view initialization code as class-scoped fixture
14 | would be great if only pytest-django supported non-function-scoped
15 | fixture db access -- this is a work-in-progress for now:
16 | https://github.com/pytest-dev/pytest-django/pull/258
17 | """
18 |
19 | def test_get_success_url(
20 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
21 | ):
22 | view = UserUpdateView()
23 | request = request_factory.get("/fake-url/")
24 | request.user = user
25 |
26 | view.request = request
27 |
28 | assert view.get_success_url() == "/users/{username}/".format(
29 | username=user.username
30 | )
31 |
32 | def test_get_object(
33 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
34 | ):
35 | view = UserUpdateView()
36 | request = request_factory.get("/fake-url/")
37 | request.user = user
38 |
39 | view.request = request
40 |
41 | assert view.get_object() == user
42 |
43 |
44 | class TestUserRedirectView:
45 | def test_get_redirect_url(
46 | self, user: settings.AUTH_USER_MODEL, request_factory: RequestFactory
47 | ):
48 | view = UserRedirectView()
49 | request = request_factory.get("/fake-url")
50 | request.user = user
51 |
52 | view.request = request
53 |
54 | assert view.get_redirect_url() == "/users/{username}/".format(
55 | username=user.username
56 | )
57 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import re_path
2 |
3 | from django_structlog_demo_project.users.views import (
4 | user_detail_view,
5 | user_list_view,
6 | user_redirect_view,
7 | user_update_view,
8 | )
9 |
10 | app_name = "users"
11 | urlpatterns = [
12 | re_path(r"^$", view=user_list_view, name="list"),
13 | re_path(r"~redirect/", view=user_redirect_view, name="redirect"),
14 | re_path(r"~update/", view=user_update_view, name="update"),
15 | re_path(r"^(?P(\w|\.)+)/", view=user_detail_view, name="detail"),
16 | ]
17 |
--------------------------------------------------------------------------------
/django_structlog_demo_project/users/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 | from django.contrib.auth.mixins import LoginRequiredMixin
3 | from django.urls import reverse
4 | from django.views.generic import DetailView, ListView, RedirectView, UpdateView
5 |
6 | User = get_user_model()
7 |
8 |
9 | class UserDetailView(LoginRequiredMixin, DetailView):
10 | model = User
11 | slug_field = "username"
12 | slug_url_kwarg = "username"
13 |
14 |
15 | user_detail_view = UserDetailView.as_view()
16 |
17 |
18 | class UserListView(LoginRequiredMixin, ListView):
19 | model = User
20 | slug_field = "username"
21 | slug_url_kwarg = "username"
22 |
23 |
24 | user_list_view = UserListView.as_view()
25 |
26 |
27 | class UserUpdateView(LoginRequiredMixin, UpdateView):
28 | model = User
29 | fields = ["name"]
30 |
31 | def get_success_url(self):
32 | return reverse("users:detail", kwargs={"username": self.request.user.username})
33 |
34 | def get_object(self, queryset=None):
35 | return User.objects.get(username=self.request.user.username)
36 |
37 |
38 | user_update_view = UserUpdateView.as_view()
39 |
40 |
41 | class UserRedirectView(LoginRequiredMixin, RedirectView):
42 | permanent = False
43 |
44 | def get_redirect_url(self, *args, **kwargs):
45 | return reverse("users:detail", kwargs={"username": self.request.user.username})
46 |
47 |
48 | user_redirect_view = UserRedirectView.as_view()
49 |
--------------------------------------------------------------------------------
/docker-compose.amqp.yml:
--------------------------------------------------------------------------------
1 | volumes:
2 | local_postgres_data: {}
3 | local_postgres_data_backups: {}
4 |
5 | services:
6 | django:
7 | env_file:
8 | - ./.envs/.local/.django
9 | - ./.envs/.local/.amqp
10 | - ./.envs/.local/.postgres
11 |
12 | django_asgi:
13 | env_file:
14 | - ./.envs/.local/.django
15 | - ./.envs/.local/.amqp
16 | - ./.envs/.local/.postgres
17 |
18 | rabbitmq:
19 | hostname: rabbit
20 | image: rabbitmq:3.12-alpine
21 | environment:
22 | - RABBITMQ_DEFAULT_USER=admin
23 | - RABBITMQ_DEFAULT_PASS=unsecure-password
24 | ports:
25 | - "5672:5672"
26 | - "15672:15672"
27 | celeryworker:
28 |
29 | depends_on:
30 | - rabbitmq
31 | - postgres
32 | env_file:
33 | - ./.envs/.local/.django
34 | - ./.envs/.local/.amqp
35 | - ./.envs/.local/.postgres
36 |
37 | celerybeat:
38 | depends_on:
39 | - rabbitmq
40 | - postgres
41 | env_file:
42 | - ./.envs/.local/.django
43 | - ./.envs/.local/.amqp
44 | - ./.envs/.local/.postgres
45 |
46 | flower:
47 | depends_on:
48 | - rabbitmq
49 | - postgres
50 | env_file:
51 | - ./.envs/.local/.django
52 | - ./.envs/.local/.amqp
53 | - ./.envs/.local/.postgres
54 |
--------------------------------------------------------------------------------
/docker-compose.docs.yml:
--------------------------------------------------------------------------------
1 | services:
2 | docs:
3 | build:
4 | context: .
5 | dockerfile: ./compose/local/docs/Dockerfile
6 | args:
7 | PYTHON_VERSION: 3.13
8 | image: django_structlog_demo_project_docs
9 | volumes:
10 | - .:/app:cached
11 | command: /start
12 | environment:
13 | - SPHINX_COMMAND=html
14 | ports:
15 | - "8080:8080"
16 | docs-test:
17 | image: django_structlog_demo_project_docs
18 | volumes:
19 | - .:/app:cached
20 | command: /start
21 | environment:
22 | - SPHINX_COMMAND=doctest -E
23 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | volumes:
2 | local_postgres_data: {}
3 | local_postgres_data_backups: {}
4 |
5 | services:
6 | django: &django
7 | build:
8 | context: .
9 | dockerfile: ./compose/local/django/Dockerfile
10 | args:
11 | PYTHON_VERSION: 3.13
12 | image: django_structlog_demo_project_local_django
13 | depends_on:
14 | - postgres
15 | volumes:
16 | - .:/app:cached
17 | env_file:
18 | - ./.envs/.local/.django
19 | - ./.envs/.local/.redis
20 | - ./.envs/.local/.postgres
21 | tty: true # needed for colors to show in console logs
22 | ports:
23 | - "8000:8000"
24 | command: /start
25 |
26 | django_wsgi:
27 | <<: *django
28 | environment:
29 | DJANGO_DEBUG: False
30 | ports:
31 | - "8001:8000"
32 | command: /start_wsgi
33 |
34 | django_asgi:
35 | <<: *django
36 | environment:
37 | DJANGO_DEBUG: False
38 | ports:
39 | - "8002:8000"
40 | command: /start_asgi
41 |
42 | postgres:
43 | build:
44 | context: .
45 | dockerfile: ./compose/local/postgres/Dockerfile
46 | image: django_structlog_demo_project_local_postgres
47 | volumes:
48 | - local_postgres_data:/var/lib/postgresql/data:cached
49 | - local_postgres_data_backups:/backups:cached
50 | env_file:
51 | - ./.envs/.local/.postgres
52 |
53 | redis:
54 | image: redis:7.4
55 | ports:
56 | - "6379:6379"
57 |
58 | celeryworker:
59 | image: django_structlog_demo_project_local_django
60 | depends_on:
61 | - redis
62 | - postgres
63 | volumes:
64 | - .:/app:cached
65 | env_file:
66 | - ./.envs/.local/.django
67 | - ./.envs/.local/.redis
68 | - ./.envs/.local/.postgres
69 |
70 | command: /start-celeryworker
71 | tty: true # needed for colors to show in console logs
72 |
73 | celerybeat:
74 | image: django_structlog_demo_project_local_django
75 | depends_on:
76 | - redis
77 | - postgres
78 | volumes:
79 | - .:/app:cached
80 | env_file:
81 | - ./.envs/.local/.django
82 | - ./.envs/.local/.redis
83 | - ./.envs/.local/.postgres
84 |
85 | command: /start-celerybeat
86 | tty: true # needed for colors to show in console logs
87 |
88 | flower:
89 | image: django_structlog_demo_project_local_django
90 | ports:
91 | - "5555:5555"
92 | volumes:
93 | - .:/app:cached
94 | env_file:
95 | - ./.envs/.local/.django
96 | - ./.envs/.local/.redis
97 | - ./.envs/.local/.postgres
98 | command: /start-flower
99 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SOURCEDIR = .
8 | BUILDDIR = _build
9 |
10 | # Put it first so that "make" without argument is like "make help".
11 | help:
12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
13 |
14 | .PHONY: help Makefile
15 |
16 | # Catch-all target: route all unknown targets to Sphinx using the new
17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
18 | %: Makefile
19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/_static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/docs/_static/.gitkeep
--------------------------------------------------------------------------------
/docs/acknowledgements.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-acknowledgements-begin
3 | :end-before: inclusion-marker-acknowledgements-end
4 |
--------------------------------------------------------------------------------
/docs/api_documentation.rst:
--------------------------------------------------------------------------------
1 | API documentation
2 | =================
3 |
4 | django_structlog
5 | ^^^^^^^^^^^^^^^^
6 |
7 | .. automodule:: django_structlog
8 | :members:
9 | :undoc-members:
10 | :show-inheritance:
11 |
12 | .. automodule:: django_structlog.middlewares
13 | :members: RequestMiddleware
14 | :undoc-members:
15 | :show-inheritance:
16 |
17 | .. automodule:: django_structlog.signals
18 | :members: bind_extra_request_metadata, bind_extra_request_finished_metadata, bind_extra_request_failed_metadata, update_failure_response
19 |
20 |
21 | django_structlog.celery
22 | ^^^^^^^^^^^^^^^^^^^^^^^
23 |
24 | .. automodule:: django_structlog.celery
25 | :members:
26 | :undoc-members:
27 | :show-inheritance:
28 |
29 | .. automodule:: django_structlog.celery.steps
30 | :members: DjangoStructLogInitStep
31 | :undoc-members:
32 | :show-inheritance:
33 |
34 | .. automodule:: django_structlog.celery.signals
35 | :members: bind_extra_task_metadata, modify_context_before_task_publish, pre_task_succeeded
36 |
--------------------------------------------------------------------------------
/docs/authors.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-authors-begin
3 | :end-before: inclusion-marker-authors-end
4 |
--------------------------------------------------------------------------------
/docs/celery.rst:
--------------------------------------------------------------------------------
1 | .. _celery_integration:
2 |
3 | Celery Integration
4 | ==================
5 |
6 | Getting Started with Celery
7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 |
9 | In order to be able to support celery you need to configure both your webapp and your workers
10 |
11 | .. warning::
12 | If json is used to serialize your celery tasks, the log context in use when executing a task (through ``apply_async`` or ``delay``) should only contain JSON-serializable data. You can use modify_context_before_task_publish_ to ensure this is the case.
13 |
14 | Replace your requirements
15 | -------------------------
16 |
17 | First of all, make sure your ``django-structlog`` installation knows you use ``celery`` in order to validate compatibility with your installed version. See `Installing “Extras” `_ for more information.
18 |
19 | Replace ``django-structlog`` with ``django-structlog[celery]`` in your ``requirements.txt``.
20 |
21 | .. code-block:: python
22 |
23 | django-structlog[celery]==X.Y.Z
24 |
25 | Enable celery integration in your web app
26 | -----------------------------------------
27 |
28 | In your settings.py
29 |
30 | .. code-block:: python
31 |
32 | MIDDLEWARE = [
33 | # ...
34 | 'django_structlog.middlewares.RequestMiddleware',
35 | ]
36 |
37 | DJANGO_STRUCTLOG_CELERY_ENABLED = True
38 |
39 |
40 | Initialize Celery Worker with DjangoStructLogInitStep
41 | -----------------------------------------------------
42 |
43 | In your celery AppConfig's module.
44 |
45 | .. code-block:: python
46 |
47 | import logging
48 |
49 | import structlog
50 | from celery import Celery
51 | from celery.signals import setup_logging
52 | from django_structlog.celery.steps import DjangoStructLogInitStep
53 |
54 | app = Celery("your_celery_project")
55 |
56 | # A step to initialize django-structlog
57 | app.steps['worker'].add(DjangoStructLogInitStep)
58 |
59 |
60 | .. warning::
61 | If you use ``celery``'s `task_protocol v1 `_, ``django-structlog`` will not be able to transfer metadata to child task.
62 |
63 | Ex:
64 |
65 | .. code-block:: python
66 |
67 | app = Celery("your_celery_project", task_protocol=1)
68 |
69 | Configure celery's logger
70 | -------------------------
71 |
72 | In the same file as before
73 |
74 | .. code-block:: python
75 |
76 | @setup_logging.connect
77 | def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover
78 | logging.config.dictConfig(
79 | {
80 | "version": 1,
81 | "disable_existing_loggers": False,
82 | "formatters": {
83 | "json_formatter": {
84 | "()": structlog.stdlib.ProcessorFormatter,
85 | "processor": structlog.processors.JSONRenderer(),
86 | },
87 | "plain_console": {
88 | "()": structlog.stdlib.ProcessorFormatter,
89 | "processor": structlog.dev.ConsoleRenderer(),
90 | },
91 | "key_value": {
92 | "()": structlog.stdlib.ProcessorFormatter,
93 | "processor": structlog.processors.KeyValueRenderer(key_order=['timestamp', 'level', 'event', 'logger']),
94 | },
95 | },
96 | "handlers": {
97 | "console": {
98 | "class": "logging.StreamHandler",
99 | "formatter": "plain_console",
100 | },
101 | "json_file": {
102 | "class": "logging.handlers.WatchedFileHandler",
103 | "filename": "logs/json.log",
104 | "formatter": "json_formatter",
105 | },
106 | "flat_line_file": {
107 | "class": "logging.handlers.WatchedFileHandler",
108 | "filename": "logs/flat_line.log",
109 | "formatter": "key_value",
110 | },
111 | },
112 | "loggers": {
113 | "django_structlog": {
114 | "handlers": ["console", "flat_line_file", "json_file"],
115 | "level": "INFO",
116 | },
117 | "django_structlog_demo_project": {
118 | "handlers": ["console", "flat_line_file", "json_file"],
119 | "level": "INFO",
120 | },
121 | }
122 | }
123 | )
124 |
125 | structlog.configure(
126 | processors=[
127 | structlog.contextvars.merge_contextvars,
128 | structlog.stdlib.filter_by_level,
129 | structlog.processors.TimeStamper(fmt="iso"),
130 | structlog.stdlib.add_logger_name,
131 | structlog.stdlib.add_log_level,
132 | structlog.stdlib.PositionalArgumentsFormatter(),
133 | structlog.processors.StackInfoRenderer(),
134 | structlog.processors.format_exc_info,
135 | structlog.processors.UnicodeDecoder(),
136 | structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
137 | ],
138 | logger_factory=structlog.stdlib.LoggerFactory(),
139 | cache_logger_on_first_use=True,
140 | )
141 |
142 |
143 | .. _celery_signals:
144 |
145 | Signals
146 | ^^^^^^^
147 |
148 | .. _modify_context_before_task_publish:
149 |
150 | modify_context_before_task_publish
151 | ----------------------------------
152 |
153 | You can connect to ``modify_context_before_task_publish`` signal in order to modify the metadata before it is stored in the task's message.
154 |
155 | By example you can strip down the ``context`` to keep only some of the keys:
156 |
157 | .. code-block:: python
158 |
159 | @receiver(signals.modify_context_before_task_publish)
160 | def receiver_modify_context_before_task_publish(sender, signal, context, task_routing_key=None, task_properties=None, **kwargs):
161 | keys_to_keep = {"request_id", "parent_task_id"}
162 | new_dict = {key_to_keep: context[key_to_keep] for key_to_keep in keys_to_keep if key_to_keep in context}
163 | context.clear()
164 | context.update(new_dict)
165 |
166 |
167 | bind_extra_task_metadata
168 | ------------------------
169 |
170 | You can optionally connect to ``bind_extra_task_metadata`` signal in order to bind more metadata to the logger or override existing bound metadata. This is called
171 | in celery's ``receiver_task_pre_run``.
172 |
173 | .. code-block:: python
174 |
175 | from django_structlog.celery import signals
176 | import structlog
177 |
178 | @receiver(signals.bind_extra_task_metadata)
179 | def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None, **kwargs):
180 | structlog.contextvars.bind_contextvars(correlation_id=task.request.correlation_id)
181 |
182 |
--------------------------------------------------------------------------------
/docs/commands.rst:
--------------------------------------------------------------------------------
1 | .. _commands:
2 |
3 | Commands
4 | ========
5 |
6 | Prerequisites
7 | ^^^^^^^^^^^^^
8 |
9 | Install ``django-structlog`` with command support (it will install `django-extensions `_).
10 |
11 | .. code-block:: bash
12 |
13 | pip install django-structlog[commands]
14 |
15 | Alternatively install `django-extensions `_ directly:
16 |
17 | .. code-block:: bash
18 |
19 | pip install django-extensions
20 |
21 | Configuration
22 | ^^^^^^^^^^^^^
23 |
24 | Enable ``django-structlog``'s command logging:
25 |
26 | .. code-block:: python
27 |
28 | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED = True
29 |
30 | Add ``django-extensions``'s `@signalcommand `_ to your commands
31 |
32 | .. code-block:: python
33 |
34 | import structlog
35 | from django.core.management import BaseCommand
36 | from django_extensions.management.utils import signalcommand # <- add this
37 |
38 | logger = structlog.getLogger(__name__)
39 |
40 |
41 | class Command(BaseCommand):
42 | def add_arguments(self, parser):
43 | parser.add_argument("foo", type=str)
44 |
45 | @signalcommand # <- add this
46 | def handle(self, foo, *args, **options):
47 | logger.info("my log", foo=foo)
48 | return 0
49 |
50 | Results
51 | ^^^^^^^
52 |
53 | Log will add ``command_name`` and ``command_id`` to the logs:
54 |
55 | .. code-block:: bash
56 |
57 | $ python manage.py example_command bar
58 | 2023-09-13T21:10:50.084368Z [info ] command_started [django_structlog.commands] command_name=django_structlog_demo_project.users.example_command command_id=be723d34-59f5-468e-9258-24232aa4cedd
59 | 2023-09-13T21:10:50.085325Z [info ] my log [django_structlog_demo_project.users.management.commands.example_command] command_id=be723d34-59f5-468e-9258-24232aa4cedd foo=bar
60 | 2023-09-13T21:10:50.085877Z [info ] command_finished [django_structlog.commands] command_id=be723d34-59f5-468e-9258-24232aa4cedd
61 |
62 |
63 | It also supports nested commands which will keep track of parent commands through ``parent_id``:
64 |
65 | .. code-block:: bash
66 |
67 | $ python manage.py example_command bar
68 | 2023-09-15T00:10:10.466616Z [info ] command_started [django_structlog.commands] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed command_name=django_structlog_demo_project.users.example_command
69 | 2023-09-15T00:10:10.467250Z [info ] my log [django_structlog_demo_project.users.management.commands.example_command] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed foo=bar
70 | 2023-09-15T00:10:10.468176Z [info ] command_started [django_structlog.commands] baz=2 command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 command_name=django_structlog_demo_project.users.example_nested_command parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed
71 | 2023-09-15T00:10:10.468871Z [info ] my nested log [django_structlog_demo_project.users.management.commands.example_nested_command] command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed
72 | 2023-09-15T00:10:10.469418Z [info ] command_finished [django_structlog.commands] command_id=57524ccb-a8eb-4d30-a989-4e83ffdca9c0 parent_command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed
73 | 2023-09-15T00:10:10.469964Z [info ] my log 2 [django_structlog_demo_project.users.management.commands.example_command] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed
74 | 2023-09-15T00:10:10.470585Z [info ] command_finished [django_structlog.commands] command_id=f2a8c9a8-5aa3-4e22-b11c-f387449a34ed
75 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
5 |
6 |
7 | # Configuration file for the Sphinx documentation builder.
8 | #
9 | # This file only contains a selection of the most common options. For a full
10 | # list see the documentation:
11 | # http://www.sphinx-doc.org/en/master/config
12 |
13 | # -- Path setup --------------------------------------------------------------
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | # import os
20 | # import sys
21 | # sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # The master toctree document.
24 | master_doc = "index"
25 |
26 | # -- Project information -----------------------------------------------------
27 |
28 | project = "django-structlog"
29 | copyright = "2019, Jules Robichaud-Gagnon"
30 | author = "Jules Robichaud-Gagnon"
31 |
32 |
33 | # -- General configuration ---------------------------------------------------
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37 | # ones.
38 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.doctest"]
39 |
40 | # Add any paths that contain templates here, relative to this directory.
41 | templates_path = ["_templates"]
42 |
43 | # List of patterns, relative to source directory, that match files and
44 | # directories to ignore when looking for source files.
45 | # This pattern also affects html_static_path and html_extra_path.
46 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
47 |
48 |
49 | def get_version(precision):
50 | import django_structlog
51 |
52 | return ".".join(str(v) for v in django_structlog.VERSION[:precision])
53 |
54 |
55 | # Full version
56 | release = get_version(3)
57 |
58 | # Minor version
59 | version = get_version(2)
60 |
61 | # -- Options for HTML output -------------------------------------------------
62 |
63 | # The theme to use for HTML and HTML Help pages. See the documentation for
64 | # a list of builtin themes.
65 | #
66 | html_theme = "sphinx_rtd_theme"
67 |
68 | # Add any paths that contain custom static files (such as style sheets) here,
69 | # relative to this directory. They are copied after the builtin static files,
70 | # so a file named "default.css" will overwrite the builtin "default.css".
71 | html_static_path = ["_static"]
72 |
--------------------------------------------------------------------------------
/docs/configuration.rst:
--------------------------------------------------------------------------------
1 | .. _configuration:
2 |
3 | Configuration
4 | =============
5 |
6 | In your ``settings.py`` you can customize ``django-structlog``.
7 |
8 | Example:
9 |
10 | .. code-block:: python
11 |
12 | import logging
13 | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL = logging.INFO
14 |
15 |
16 | .. _settings:
17 |
18 | Settings
19 | --------
20 |
21 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
22 | | Key | Type | Default | Description |
23 | +==========================================+=========+=================+===============================================================================+
24 | | DJANGO_STRUCTLOG_CELERY_ENABLED | boolean | False | See :ref:`celery_integration` |
25 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
26 | | DJANGO_STRUCTLOG_IP_LOGGING_ENABLED | boolean | True | automatically bind user ip using `django-ipware` |
27 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
28 | | DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL | int | logging.WARNING | Log level of 4XX status codes |
29 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
30 | | DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED | boolean | False | See :ref:`commands` |
31 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
32 | | DJANGO_STRUCTLOG_USER_ID_FIELD | string | ``"pk"`` | Change field used to identify user in logs, ``None`` to disable user binding |
33 | +------------------------------------------+---------+-----------------+-------------------------------------------------------------------------------+
34 |
--------------------------------------------------------------------------------
/docs/demo.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-demo-begin
3 | :end-before: inclusion-marker-demo-end
4 |
--------------------------------------------------------------------------------
/docs/development.rst:
--------------------------------------------------------------------------------
1 | .. _development:
2 |
3 | Development
4 | ===========
5 |
6 | Prerequisites
7 | -------------
8 |
9 | - `docker `_
10 |
11 |
12 | Installation
13 | ------------
14 |
15 | .. code-block:: bash
16 |
17 | $ git clone https://github.com/jrobichaud/django-structlog.git
18 | $ cd django-structlog
19 | $ pip install -r requirements.txt
20 | $ pre-commit install
21 |
22 |
23 | Start Demo App
24 | --------------
25 |
26 | .. code-block:: bash
27 |
28 | $ docker compose up --build
29 |
30 | - ``runserver_plus`` server: http://127.0.0.1:8000/
31 | - ``WSGI`` server: http://127.0.0.1:8001/
32 | - ``ASGI`` server: http://127.0.0.1:8002/
33 |
34 | Use ``RabbitMQ`` broker instead of ``redis``
35 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
36 |
37 | .. code-block:: bash
38 |
39 | $ docker compose -f ./docker-compose.yml -f ./docker-compose.amqp.yml up --build
40 |
41 |
42 | Building, Serving and Testing the Documentation Locally
43 | -------------------------------------------------------
44 |
45 | .. code-block:: bash
46 |
47 | $ docker compose -p django-structlog-docs -f docker-compose.docs.yml up --build
48 | Serving on http://127.0.0.1:8080
49 |
--------------------------------------------------------------------------------
/docs/example_outputs.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-example-outputs-begin
3 | :end-before: inclusion-marker-example-outputs-end
4 |
--------------------------------------------------------------------------------
/docs/getting_started.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-getting-started-begin
3 | :end-before: inclusion-marker-getting-started-end
4 |
--------------------------------------------------------------------------------
/docs/how_tos.rst:
--------------------------------------------------------------------------------
1 | .. _how_tos:
2 |
3 | How Tos
4 | =======
5 |
6 | These are code snippets on how to achieve some specific use cases.
7 |
8 | .. warning::
9 | Be aware they are untested. Please `open an issue `_ if there are bugs in these examples or if you want to share some great examples that should be there.
10 |
11 |
12 | Bind ``request_id`` to response's header
13 | ----------------------------------------
14 |
15 | You can add the ``request_id`` to a custom response header ``X-Request-ID`` in order to trace the request by the caller.
16 |
17 | Origin: `#231 `_
18 |
19 | .. code-block:: python
20 |
21 | from django.dispatch import receiver
22 | from django_structlog import signals
23 | import structlog
24 |
25 |
26 | @receiver(signals.update_failure_response)
27 | @receiver(signals.bind_extra_request_finished_metadata)
28 | def add_request_id_to_error_response(response, logger, **kwargs):
29 | context = structlog.contextvars.get_merged_contextvars(logger)
30 | response['X-Request-ID'] = context["request_id"]
31 |
32 | Bind ``rest_framework_simplejwt`` token's user id
33 | -------------------------------------------------
34 |
35 | Bind token's user_id from `rest_framework_simplejwt `_ to the request.
36 |
37 | It is a workaround for ``restframework``'s non-standard authentication system.
38 | It prevents access of the user in middlewares, therefore ``django-structlog`` cannot bind the ``user_id`` by default.
39 |
40 | .. code-block:: python
41 |
42 | import structlog
43 | from django.dispatch import receiver
44 | from django_structlog.signals import bind_extra_request_metadata
45 | from rest_framework_simplejwt.tokens import UntypedToken
46 |
47 | @receiver(bind_extra_request_metadata)
48 | def bind_token_user_id(request, logger, **kwargs):
49 | try:
50 | header = request.META.get("HTTP_AUTHORIZATION")
51 | if header:
52 | raw_token = header.split()[1]
53 | token = UntypedToken(raw_token)
54 | user_id = token["user_id"]
55 | structlog.contextvars.bind_contextvars(user_id=user_id)
56 | except Exception:
57 | pass
58 |
59 | Bind AWS's ``X-Amzn-Trace-Id``
60 | ------------------------------
61 |
62 | See `Request tracing for your Application Load Balancer `_
63 |
64 | Origin: `#324 `_
65 |
66 | .. code-block:: python
67 |
68 | from django.dispatch import receiver
69 | from django_structlog import signals
70 | from django_structlog.middlewares.request import get_request_header
71 | import structlog
72 |
73 | @receiver(signals.bind_extra_request_metadata)
74 | def bind_trace_id(request, logger, **kwargs):
75 | trace_id = get_request_header(
76 | request, "x-amzn-trace-id", "HTTP_X_AMZN_TRACE_ID"
77 | )
78 | if trace_id:
79 | structlog.contextvars.bind_contextvars(trace_id=trace_id)
80 |
81 | Filter logs from being recorded
82 | -------------------------------
83 |
84 | You can add a custom filter to prevent some specific logs from being recorded, based on your criteria
85 |
86 | See `Django logging documentation `_
87 |
88 |
89 | Origin: `#412 `_
90 |
91 | .. code-block:: python
92 |
93 | # your_project/logging/filters.py
94 |
95 | import logging
96 |
97 | class ExcludeEventsFilter(logging.Filter):
98 | def __init__(self, excluded_event_type=None):
99 | super().__init__()
100 | self.excluded_event_type = excluded_event_type
101 |
102 | def filter(self, record):
103 | if not isinstance(record.msg, dict) or self.excluded_event_type is None:
104 | return True # Include the log message if msg is not a dictionary or excluded_event_type is not provided
105 |
106 | if record.msg.get('event') in self.excluded_event_type:
107 | return False # Exclude the log message
108 | return True # Include the log message
109 |
110 |
111 | # in your settings.py
112 |
113 | LOGGING = {
114 | 'version': 1,
115 | 'disable_existing_loggers': False,
116 | 'handlers': {
117 | 'console': {
118 | 'class': 'logging.StreamHandler',
119 | 'filters': ['exclude_request_started']
120 | },
121 | },
122 | 'filters': {
123 | 'exclude_request_started': {
124 | '()': 'your_project.logging.filters.ExcludeEventsFilter',
125 | 'excluded_event_type': ['request_started'] # Example excluding request_started event
126 | },
127 | },
128 | 'loggers': {
129 | 'django': {
130 | 'handlers': ['console'],
131 | 'level': 'DEBUG',
132 | },
133 | },
134 | }
135 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: introduction-begin
3 | :end-before: introduction-end
4 |
5 |
6 | Contents, indices and tables
7 | ============================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 |
12 | getting_started
13 | configuration
14 | celery
15 | commands
16 | api_documentation
17 | events
18 | example_outputs
19 | how_tos
20 | running_tests
21 | development
22 | demo
23 | changelog
24 | upgrade_guide
25 | authors
26 | acknowledgements
27 | licence
28 |
29 | * :ref:`genindex`
30 | * :ref:`modindex`
31 | * :ref:`search`
32 |
--------------------------------------------------------------------------------
/docs/licence.rst:
--------------------------------------------------------------------------------
1 | Licence
2 | =======
3 |
4 | .. include:: ../LICENSE.rst
5 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==8.2.3
2 | sphinx_rtd_theme==3.0.2
3 | celery==5.5.3
4 | django>=4.2,<6
5 | structlog
6 | sphinx-autobuild==2024.10.3
7 | Jinja2==3.1.6
8 | importlib-metadata>=8.0.0,<9
9 |
--------------------------------------------------------------------------------
/docs/running_tests.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-running-tests-begin
3 | :end-before: inclusion-marker-running-tests-end
4 |
--------------------------------------------------------------------------------
/docs/upgrade_guide.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: inclusion-marker-upgrade-guide-begin
3 | :end-before: inclusion-marker-upgrade-guide-end
4 |
--------------------------------------------------------------------------------
/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/logs/.gitkeep
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
7 |
8 | try:
9 | from django.core.management import execute_from_command_line
10 | except ImportError:
11 | # The above import may fail for some other reason. Ensure that the
12 | # issue is really that Django is missing to avoid masking other
13 | # exceptions on Python 2.
14 | try:
15 | import django # noqa
16 | except ImportError:
17 | raise ImportError(
18 | "Couldn't import Django. Are you sure it's installed and "
19 | "available on your PYTHONPATH environment variable? Did you "
20 | "forget to activate a virtual environment?"
21 | )
22 |
23 | raise
24 |
25 | # This allows easy placement of apps within the interior
26 | # django_structlog_demo_project directory.
27 | current_path = os.path.dirname(os.path.abspath(__file__))
28 | sys.path.append(os.path.join(current_path, "django_structlog_demo_project"))
29 |
30 | execute_from_command_line(sys.argv)
31 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "django-structlog"
7 | description = "Structured Logging for Django"
8 | authors = [
9 | { name = "Jules Robichaud-Gagnon", email = "j.robichaudg+pypi@gmail.com" },
10 | ]
11 | readme = "README.rst"
12 | dynamic = ["version"]
13 | requires-python = ">=3.9"
14 | license = { text = "MIT" }
15 | dependencies = [
16 | "django>=4.2",
17 | "structlog>=21.4.0",
18 | "asgiref>=3.6.0",
19 | "django-ipware>=6.0.2",
20 | ]
21 | classifiers = [
22 | "Development Status :: 5 - Production/Stable",
23 | "Framework :: Django",
24 | "Framework :: Django :: 4.2",
25 | "Framework :: Django :: 5.0",
26 | "Framework :: Django :: 5.1",
27 | "Framework :: Django :: 5.2",
28 | "Programming Language :: Python :: 3",
29 | "Programming Language :: Python :: 3 :: Only",
30 | "Programming Language :: Python :: 3.9",
31 | "Programming Language :: Python :: 3.10",
32 | "Programming Language :: Python :: 3.11",
33 | "Programming Language :: Python :: 3.12",
34 | "Programming Language :: Python :: 3.13",
35 | "Topic :: System :: Logging",
36 | "License :: OSI Approved :: MIT License",
37 | "Operating System :: OS Independent",
38 | "Typing :: Typed",
39 | ]
40 |
41 | [project.urls]
42 | homepage = "https://github.com/jrobichaud/django-structlog"
43 | repository = "https://github.com/jrobichaud/django-structlog"
44 | documentation = "https://django-structlog.readthedocs.io"
45 | tracker = "https://github.com/jrobichaud/django-structlog/issues"
46 | changelog = "https://django-structlog.readthedocs.io/en/latest/changelog.html"
47 |
48 | [project.optional-dependencies]
49 | celery = [
50 | "celery>=5.1"
51 | ]
52 | commands = [
53 | "django-extensions>=1.4.9"
54 | ]
55 |
56 | [tool.setuptools.dynamic]
57 | version = { attr = "django_structlog.__version__" }
58 |
59 | [tool.setuptools.packages.find]
60 | include = [
61 | "django_structlog",
62 | "django_structlog.*",
63 | ]
64 |
65 | [tool.black]
66 | line-length = 88
67 | target-version = [
68 | 'py39',
69 | 'py310',
70 | 'py311',
71 | 'py312',
72 | 'py313',
73 | ]
74 | include = '\.pyi?$'
75 | exclude = '''
76 | /(
77 | \.git
78 | | \.hg
79 | | \.tox
80 | | \.venv
81 | | _build
82 | | buck-out
83 | | build
84 | | dist
85 | )/
86 | '''
87 |
88 | [tool.ruff]
89 | line-length = 88
90 | target-version = "py313"
91 | lint.ignore = [
92 | 'E501',
93 | ]
94 |
95 | [tool.pytest.ini_options]
96 | DJANGO_SETTINGS_MODULE = "config.settings.test_demo_app"
97 |
98 | [tool.tox]
99 | legacy_tox_ini = """
100 | [tox]
101 | # Test against latest supported version of each of python for each Django version.
102 | #
103 | # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml
104 | envlist =
105 | py{39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5,
106 | py31{0,1}-django5{0,1,2}-celery5{3,4}-redis4-kombu5,
107 | py312-django{42,50,51,52}-celery5{3,4}-redis4-kombu5,
108 | py313-django5{1,2}-celery5{3,4}-redis4-kombu5,
109 |
110 | [gh-actions]
111 | python =
112 | 3.9: py39
113 | 3.10: py310
114 | 3.11: py311
115 | 3.12: py312
116 | 3.13: py313
117 |
118 | [testenv]
119 | setenv =
120 | PYTHONPATH={toxinidir}
121 | CELERY_BROKER_URL=redis://0.0.0.0:6379
122 | CELERY_RESULT_BACKEND=redis://0.0.0.0:6379
123 | DJANGO_SETTINGS_MODULE=config.settings.test
124 | pip_pre = True
125 | deps =
126 | redis3: redis>=3, <4
127 | redis4: redis>=4, <5
128 | kombu5: kombu<6
129 | celery51: Celery >=5.1, <5.2
130 | celery52: Celery >=5.2, <5.3
131 | celery53: Celery >=5.3, <5.4
132 | celery54: Celery >=5.4, <5.5
133 | django42: Django >=4.2, <5.0
134 | django50: Django >=5.0, <5.1
135 | django51: Django >=5.1, <5.2
136 | django52: Django >=5.2, <6.0
137 | -r{toxinidir}/requirements/ci.txt
138 |
139 | commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app
140 | """
141 |
142 | [tool.coverage.run]
143 | branch = true
144 |
145 | [tool.coverage.report]
146 | precision = 2
147 | skip_covered = true
148 | show_missing = true
149 | exclude_lines = [
150 | "pragma: no cover",
151 | "raise NotImplementedError"
152 | ]
153 | include = [
154 | "./django_structlog/*",
155 | "./django_structlog_demo_project/*",
156 | "./test_app/*",
157 | ]
158 |
159 | [tool.mypy]
160 | python_version=3.9
161 | strict=true
162 | packages=[
163 | "django_structlog",
164 | "test_app",
165 | ]
166 |
167 | [tool.isort]
168 | profile = "black"
169 | filter_files = true
170 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | -r requirements/local.txt
2 |
--------------------------------------------------------------------------------
/requirements/black.txt:
--------------------------------------------------------------------------------
1 | black==25.1.0 # https://github.com/ambv/black
2 |
--------------------------------------------------------------------------------
/requirements/ci.txt:
--------------------------------------------------------------------------------
1 | # Django
2 | # ------------------------------------------------------------------------------
3 | django-environ==0.12.0 # https://github.com/joke2k/django-environ
4 | django-redis==5.4.0 # https://github.com/niwinz/django-redis
5 | django-extensions==4.1
6 |
7 | structlog>=21.4.0
8 | colorama>=0.4.3
9 |
10 | psycopg[binary]==3.2.9 # https://github.com/psycopg/psycopg
11 |
12 | # Testing
13 | # ------------------------------------------------------------------------------
14 | pytest==8.3.5 # https://github.com/pytest-dev/pytest
15 | pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar
16 | pytest-cov==6.1.1
17 |
18 | # Code quality
19 | # ------------------------------------------------------------------------------
20 | -r coverage.txt
21 | pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django
22 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
23 |
24 | # Django
25 | # ------------------------------------------------------------------------------
26 | factory-boy==3.3.3 # https://github.com/FactoryBoy/factory_boy
27 |
28 | django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin
29 | pytest-django==4.11.1 # https://github.com/pytest-dev/pytest-django
30 |
31 | # Setup tools
32 | # ------------------------------------------------------------------------------
33 | setuptools>=41.0.1
34 |
--------------------------------------------------------------------------------
/requirements/coverage.txt:
--------------------------------------------------------------------------------
1 | coverage==7.8.2 # https://github.com/nedbat/coveragepy
2 |
--------------------------------------------------------------------------------
/requirements/deployment.txt:
--------------------------------------------------------------------------------
1 | importlib-metadata>=8.0.0,<9
2 |
--------------------------------------------------------------------------------
/requirements/isort.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/requirements/isort.txt
--------------------------------------------------------------------------------
/requirements/local-base.txt:
--------------------------------------------------------------------------------
1 | pytz==2025.2 # https://github.com/stub42/pytz
2 | python-slugify==8.0.4 # https://github.com/un33k/python-slugify
3 |
4 | # Django
5 | # ------------------------------------------------------------------------------
6 | django==5.2.0 # https://www.djangoproject.com/
7 | django-environ==0.12.0 # https://github.com/joke2k/django-environ
8 | django-model-utils==5.0.0 # https://github.com/jazzband/django-model-utils
9 | django-allauth==65.9.0 # https://github.com/pennersr/django-allauth
10 | django-crispy-forms==2.4 # https://github.com/django-crispy-forms/django-crispy-forms
11 | crispy-bootstrap5==2025.4 # https://github.com/django-crispy-forms/crispy-bootstrap5
12 | django-redis==5.4.0 # https://github.com/niwinz/django-redis
13 | asgiref==3.8.1 # https://github.com/django/asgiref
14 |
15 | # Django REST Framework
16 | djangorestframework==3.16.0 # https://github.com/encode/django-rest-framework
17 | coreapi==2.3.3 # https://github.com/core-api/python-client
18 |
19 | # django-ninja
20 | django-ninja==1.4.1 # https://github.com/vitalik/django-ninja
21 |
22 | structlog==25.3.0
23 | colorama==0.4.6
24 | django-ipware==7.0.1
25 |
26 | Werkzeug==3.1.3 # https://github.com/pallets/werkzeug
27 | ipdb==0.13.13 # https://github.com/gotcha/ipdb
28 | psycopg[binary]==3.2.9 # https://github.com/psycopg/psycopg
29 |
30 | # Testing
31 | # ------------------------------------------------------------------------------
32 | pytest==8.3.5 # https://github.com/pytest-dev/pytest
33 | pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar
34 | pytest-cov==6.1.1
35 | pytest-asyncio==0.26.0 # https://github.com/pytest-dev/pytest-asyncio
36 | pytest-mock==3.14.1 # https://github.com/pytest-dev/pytest-mock
37 |
38 | # Code quality
39 | # ------------------------------------------------------------------------------
40 | -r ruff.txt
41 | -r coverage.txt
42 | -r black.txt
43 | pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django
44 | pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery
45 |
46 | # Django
47 | # ------------------------------------------------------------------------------
48 | factory-boy==3.3.3 # https://github.com/FactoryBoy/factory_boy
49 |
50 | django-extensions==4.1 # https://github.com/django-extensions/django-extensions
51 | django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin
52 | pytest-django==4.11.1 # https://github.com/pytest-dev/pytest-django
53 |
54 | # pre-commit
55 | # ------------------------------------------------------------------------------
56 | pre-commit==4.2.0 # https://github.com/pre-commit/pre-commit
57 |
--------------------------------------------------------------------------------
/requirements/local.txt:
--------------------------------------------------------------------------------
1 | -r local-base.txt
2 |
3 | redis==6.1.0 # https://github.com/antirez/redis
4 | celery==5.5.2 # pyup: < 5.0 # https://github.com/celery/celery
5 | kombu==5.5.3
6 | flower==2.0.1 # https://github.com/mher/flower
7 | uvicorn==0.34.2 # https://github.com/encode/uvicorn
8 | gunicorn==23.0.0 # https://github.com/benoitc/gunicorn
9 | amqp==5.3.1 # https://github.com/celery/py-amqp
10 |
--------------------------------------------------------------------------------
/requirements/mypy.txt:
--------------------------------------------------------------------------------
1 | mypy==1.15.0
2 | celery-types==0.23.0
3 | django-stubs[compatible-mypy]==5.1.3
4 |
--------------------------------------------------------------------------------
/requirements/ruff.txt:
--------------------------------------------------------------------------------
1 | ruff==0.11.12 # https://github.com/astral-sh/ruff
2 |
--------------------------------------------------------------------------------
/test_app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/__init__.py
--------------------------------------------------------------------------------
/test_app/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class TestAppConfig(AppConfig):
5 | name = "test_app"
6 |
--------------------------------------------------------------------------------
/test_app/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/migrations/__init__.py
--------------------------------------------------------------------------------
/test_app/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/__init__.py
--------------------------------------------------------------------------------
/test_app/tests/celery/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/celery/__init__.py
--------------------------------------------------------------------------------
/test_app/tests/celery/test_steps.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import patch
2 |
3 | from django.test import TestCase
4 |
5 | from django_structlog.celery import steps
6 |
7 |
8 | class TestDjangoStructLogInitStep(TestCase):
9 | def test_call(self) -> None:
10 | with patch(
11 | "django_structlog.celery.receivers.CeleryReceiver.connect_worker_signals",
12 | autospec=True,
13 | ) as mock_connect:
14 | step = steps.DjangoStructLogInitStep(None)
15 |
16 | mock_connect.assert_called_once()
17 |
18 | self.assertIsNotNone(step.receiver)
19 |
--------------------------------------------------------------------------------
/test_app/tests/middlewares/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrobichaud/django-structlog/6e7bad89cfaceeaf1e5d459380e70864c4dff093/test_app/tests/middlewares/__init__.py
--------------------------------------------------------------------------------
/test_app/tests/test_app_settings.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from django_structlog import app_settings
4 |
5 |
6 | class TestAppSettings(TestCase):
7 | def test_celery_enabled(self) -> None:
8 | settings = app_settings.AppSettings()
9 |
10 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True):
11 | self.assertTrue(settings.CELERY_ENABLED)
12 |
13 | def test_celery_disabled(self) -> None:
14 | settings = app_settings.AppSettings()
15 |
16 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False):
17 | self.assertFalse(settings.CELERY_ENABLED)
18 |
--------------------------------------------------------------------------------
/test_app/tests/test_apps.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import create_autospec, patch
2 |
3 | from django.test import TestCase
4 |
5 | from django_structlog import apps, commands
6 | from django_structlog.celery import receivers
7 |
8 |
9 | class TestAppConfig(TestCase):
10 | def test_celery_enabled(self) -> None:
11 | app = apps.DjangoStructLogConfig(
12 | "django_structlog", __import__("django_structlog")
13 | )
14 | mock_receiver = create_autospec(spec=receivers.CeleryReceiver)
15 | with patch(
16 | "django_structlog.celery.receivers.CeleryReceiver",
17 | return_value=mock_receiver,
18 | ):
19 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True):
20 | app.ready()
21 | mock_receiver.connect_signals.assert_called_once()
22 |
23 | self.assertTrue(hasattr(app, "_celery_receiver"))
24 | self.assertIsNotNone(app._celery_receiver)
25 |
26 | def test_celery_disabled(self) -> None:
27 | app = apps.DjangoStructLogConfig(
28 | "django_structlog", __import__("django_structlog")
29 | )
30 |
31 | mock_receiver = create_autospec(spec=receivers.CeleryReceiver)
32 | with patch(
33 | "django_structlog.celery.receivers.CeleryReceiver",
34 | return_value=mock_receiver,
35 | ):
36 | with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False):
37 | app.ready()
38 | mock_receiver.connect_signals.assert_not_called()
39 |
40 | self.assertFalse(hasattr(app, "_celery_receiver"))
41 |
42 | def test_command_enabled(self) -> None:
43 | app = apps.DjangoStructLogConfig(
44 | "django_structlog", __import__("django_structlog")
45 | )
46 | mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver)
47 | with patch(
48 | "django_structlog.commands.DjangoCommandReceiver",
49 | return_value=mock_receiver,
50 | ):
51 | with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=True):
52 | app.ready()
53 | mock_receiver.connect_signals.assert_called_once()
54 |
55 | self.assertTrue(hasattr(app, "_django_command_receiver"))
56 | self.assertIsNotNone(app._django_command_receiver)
57 |
58 | def test_command_disabled(self) -> None:
59 | app = apps.DjangoStructLogConfig(
60 | "django_structlog", __import__("django_structlog")
61 | )
62 |
63 | mock_receiver = create_autospec(spec=commands.DjangoCommandReceiver)
64 | with patch(
65 | "django_structlog.commands.DjangoCommandReceiver",
66 | return_value=mock_receiver,
67 | ):
68 | with self.settings(DJANGO_STRUCTLOG_COMMAND_LOGGING_ENABLED=False):
69 | app.ready()
70 | mock_receiver.connect_signals.assert_not_called()
71 |
72 | self.assertFalse(hasattr(app, "_django_command_receiver"))
73 |
--------------------------------------------------------------------------------
/test_app/tests/test_commands.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Any
3 |
4 | import structlog
5 | from django.core.management import BaseCommand, call_command
6 | from django.test import TestCase
7 | from django_extensions.management.utils import (
8 | signalcommand,
9 | )
10 |
11 |
12 | class TestCommands(TestCase):
13 | def test_command(self) -> None:
14 | class Command(BaseCommand):
15 |
16 | @signalcommand # type: ignore[misc]
17 | def handle(self, *args: Any, **options: Any) -> Any:
18 | structlog.getLogger("command").info("command_event")
19 |
20 | with (
21 | self.assertLogs("command", logging.INFO) as command_log_results,
22 | self.assertLogs(
23 | "django_structlog.commands", logging.INFO
24 | ) as django_structlog_commands_log_results,
25 | ):
26 | call_command(Command())
27 |
28 | self.assertEqual(1, len(command_log_results.records))
29 | record: Any
30 | record = command_log_results.records[0]
31 | self.assertEqual("command_event", record.msg["event"])
32 | self.assertIn("command_id", record.msg)
33 |
34 | self.assertEqual(2, len(django_structlog_commands_log_results.records))
35 | record = django_structlog_commands_log_results.records[0]
36 | self.assertEqual("command_started", record.msg["event"])
37 | self.assertIn("command_id", record.msg)
38 | record = django_structlog_commands_log_results.records[1]
39 | self.assertEqual("command_finished", record.msg["event"])
40 | self.assertIn("command_id", record.msg)
41 |
42 | def test_nested_command(self) -> None:
43 | class Command(BaseCommand):
44 | @signalcommand # type: ignore[misc]
45 | def handle(self, *args: Any, **options: Any) -> None:
46 | logger = structlog.getLogger("command")
47 | logger.info("command_event_1")
48 | call_command(NestedCommand())
49 | logger.info("command_event_2")
50 |
51 | class NestedCommand(BaseCommand):
52 | @signalcommand # type: ignore[misc]
53 | def handle(self, *args: Any, **options: Any) -> None:
54 | structlog.getLogger("nested_command").info("nested_command_event")
55 |
56 | with (
57 | self.assertLogs("command", logging.INFO) as command_log_results,
58 | self.assertLogs("nested_command", logging.INFO),
59 | self.assertLogs(
60 | "django_structlog.commands", logging.INFO
61 | ) as django_structlog_commands_log_results,
62 | ):
63 | call_command(Command())
64 |
65 | self.assertEqual(2, len(command_log_results.records))
66 | command_event_1: Any = command_log_results.records[0]
67 | self.assertEqual("command_event_1", command_event_1.msg["event"])
68 | self.assertIn("command_id", command_event_1.msg)
69 | command_event_2: Any = command_log_results.records[1]
70 | self.assertEqual("command_event_2", command_event_2.msg["event"])
71 | self.assertIn("command_id", command_event_2.msg)
72 | self.assertEqual(
73 | command_event_1.msg["command_id"], command_event_2.msg["command_id"]
74 | )
75 |
76 | self.assertEqual(4, len(django_structlog_commands_log_results.records))
77 | command_started_1: Any = django_structlog_commands_log_results.records[0]
78 | self.assertEqual("command_started", command_started_1.msg["event"])
79 | self.assertIn("command_id", command_started_1.msg)
80 |
81 | command_started_2: Any = django_structlog_commands_log_results.records[1]
82 | self.assertEqual("command_started", command_started_2.msg["event"])
83 | self.assertIn("command_id", command_started_2.msg)
84 | self.assertIn("parent_command_id", command_started_2.msg)
85 | self.assertEqual(
86 | command_started_1.msg["command_id"],
87 | command_started_2.msg["parent_command_id"],
88 | )
89 |
90 | command_finished_1: Any = django_structlog_commands_log_results.records[2]
91 | self.assertEqual("command_finished", command_finished_1.msg["event"])
92 | self.assertIn("command_id", command_finished_1.msg)
93 | self.assertIn("parent_command_id", command_finished_1.msg)
94 | self.assertEqual(
95 | command_started_1.msg["command_id"],
96 | command_finished_1.msg["parent_command_id"],
97 | )
98 |
99 | command_finished_2: Any = django_structlog_commands_log_results.records[3]
100 | self.assertEqual("command_finished", command_finished_2.msg["event"])
101 | self.assertIn("command_id", command_finished_2.msg)
102 | self.assertNotIn("parent_command_id", command_finished_2.msg)
103 | self.assertEqual(
104 | command_event_1.msg["command_id"], command_finished_2.msg["command_id"]
105 | )
106 |
--------------------------------------------------------------------------------