Skip to content

Commit 81883ab

Browse files
committed
Add basic setup for django-structlog, Celery included
1 parent 0d3a001 commit 81883ab

File tree

7 files changed

+160
-5
lines changed

7 files changed

+160
-5
lines changed

config/django/base.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,12 @@
188188

189189
INSTALLED_APPS, MIDDLEWARE = DebugToolbarSetup.do_settings(INSTALLED_APPS, MIDDLEWARE)
190190

191+
from config.settings.loggers.settings import * # noqa
192+
from config.settings.loggers.setup import LoggersSetup # noqa
191193

192-
SHELL_PLUS_IMPORTS = [
193-
"from styleguide_example.blog_examples.print_qs_in_shell.utils import print_qs"
194-
]
194+
INSTALLED_APPS, MIDDLEWARE = LoggersSetup.setup_settings(INSTALLED_APPS, MIDDLEWARE)
195+
LoggersSetup.setup_structlog()
196+
LOGGING = LoggersSetup.setup_logging()
197+
198+
199+
SHELL_PLUS_IMPORTS = ["from styleguide_example.blog_examples.print_qs_in_shell.utils import print_qs"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import logging
2+
3+
DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL = logging.INFO
4+
DJANGO_STRUCTLOG_CELERY_ENABLED = True

config/settings/loggers/setup.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import logging
2+
3+
import structlog
4+
5+
6+
class IgnoreFilter(logging.Filter):
7+
def filter(self, record):
8+
return False
9+
10+
11+
class LoggersSetup:
12+
"""
13+
We use a class, just for namespacing convenience.
14+
"""
15+
16+
@staticmethod
17+
def setup_settings(INSTALLED_APPS, MIDDLEWARE, middleware_position=None):
18+
INSTALLED_APPS = INSTALLED_APPS + ["django_structlog"]
19+
20+
django_structlog_middleware = "django_structlog.middlewares.RequestMiddleware"
21+
22+
if middleware_position is None:
23+
MIDDLEWARE = MIDDLEWARE + [django_structlog_middleware]
24+
else:
25+
# Grab a new copy of the list, since insert mutates the internal structure
26+
_middleware = MIDDLEWARE[::]
27+
_middleware.insert(middleware_position, django_structlog_middleware)
28+
29+
MIDDLEWARE = _middleware
30+
31+
return INSTALLED_APPS, MIDDLEWARE
32+
33+
@staticmethod
34+
def setup_structlog():
35+
structlog.configure(
36+
processors=[
37+
structlog.contextvars.merge_contextvars,
38+
structlog.stdlib.filter_by_level,
39+
structlog.processors.TimeStamper(fmt="iso", utc=True),
40+
structlog.stdlib.add_logger_name,
41+
structlog.stdlib.add_log_level,
42+
structlog.stdlib.PositionalArgumentsFormatter(),
43+
structlog.processors.StackInfoRenderer(),
44+
structlog.dev.set_exc_info,
45+
structlog.processors.format_exc_info,
46+
# structlog.processors.dict_tracebacks,
47+
structlog.processors.UnicodeDecoder(),
48+
structlog.processors.CallsiteParameterAdder(
49+
{
50+
structlog.processors.CallsiteParameter.FILENAME,
51+
structlog.processors.CallsiteParameter.FUNC_NAME,
52+
structlog.processors.CallsiteParameter.LINENO,
53+
}
54+
),
55+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
56+
],
57+
logger_factory=structlog.stdlib.LoggerFactory(),
58+
cache_logger_on_first_use=True,
59+
)
60+
61+
@staticmethod
62+
def setup_logging():
63+
return {
64+
"version": 1,
65+
"disable_existing_loggers": False,
66+
"formatters": {
67+
"json_formatter": {
68+
"()": structlog.stdlib.ProcessorFormatter,
69+
"processor": structlog.processors.JSONRenderer(),
70+
},
71+
"plain_console": {
72+
"()": structlog.stdlib.ProcessorFormatter,
73+
"processor": structlog.dev.ConsoleRenderer(),
74+
},
75+
},
76+
"filters": {
77+
"ignore": {
78+
"()": "config.settings.loggers.setup.IgnoreFilter",
79+
},
80+
},
81+
"handlers": {
82+
# Important notes regarding handlers.
83+
#
84+
# 1. Make sure you use handlers adapted for your project.
85+
# These handlers configurations are only examples for this library.
86+
# See python's logging.handlers: https://docs.python.org/3/library/logging.handlers.html
87+
#
88+
# 2. You might also want to use different logging configurations depending of the environment.
89+
# Different files (local.py, tests.py, production.py, ci.py, etc.) or only conditions.
90+
# See https://docs.djangoproject.com/en/dev/topics/settings/#designating-the-settings
91+
"console": {
92+
"class": "logging.StreamHandler",
93+
"formatter": "plain_console",
94+
}
95+
},
96+
"loggers": {
97+
# We want to get rid of the runserver logs
98+
"django.server": {"propagate": False, "handlers": ["console"], "filters": ["ignore"]},
99+
# We want to get rid of the logs for 4XX and 5XX
100+
"django.request": {"propagate": False, "handlers": ["console"], "filters": ["ignore"]},
101+
"django_structlog": {
102+
"handlers": ["console"],
103+
"level": "INFO",
104+
},
105+
"celery": {
106+
"handlers": ["console"],
107+
"level": "INFO",
108+
},
109+
"styleguide_example": {
110+
"handlers": ["console"],
111+
"level": "INFO",
112+
},
113+
},
114+
}

requirements/base.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ django-filter==25.2
1313
django-extensions==4.1
1414
django-cors-headers==4.9.0
1515
django-storages==1.14.6
16+
django-structlog[celery]==9.1.1
1617

1718
drf-jwt==1.19.2
1819

styleguide_example/errors/apis.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import structlog
12
from rest_framework.response import Response
23
from rest_framework.views import APIView
34

@@ -8,6 +9,8 @@
89
from styleguide_example.errors.services import trigger_errors
910
from styleguide_example.users.services import user_create
1011

12+
logger = structlog.get_logger(__name__)
13+
1114

1215
class TriggerErrorApi(APIView):
1316
def get(self, request):
@@ -30,6 +33,12 @@ def get(self, request):
3033

3134
class TriggerUnhandledExceptionApi(APIView):
3235
def get(self, request):
33-
raise Exception("Oops")
36+
log = logger.bind()
37+
38+
try:
39+
raise Exception("Oops")
40+
except Exception:
41+
log.exception("unhandled_exception")
42+
raise
3443

3544
return Response()

styleguide_example/tasks/celery.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
from __future__ import absolute_import, unicode_literals
22

3+
import logging
34
import os
45

6+
import structlog
57
from celery import Celery
8+
from celery.signals import setup_logging
9+
from django.dispatch import receiver
10+
from django_structlog.celery import signals
11+
from django_structlog.celery.steps import DjangoStructLogInitStep
12+
13+
from config.settings.loggers.setup import LoggersSetup
614

715
# set the default Django settings module for the 'celery' program.
816
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.django.base")
917

1018
app = Celery("styleguide_example")
19+
app.steps["worker"].add(DjangoStructLogInitStep)
1120

1221
# Using a string here means the worker doesn't have to serialize
1322
# the configuration object to child processes.
@@ -17,3 +26,16 @@
1726

1827
# Load task modules from all registered Django app configs.
1928
app.autodiscover_tasks()
29+
30+
31+
@setup_logging.connect
32+
def receiver_setup_logging(loglevel, logfile, format, colorize, **kwargs): # pragma: no cover
33+
logging.config.dictConfig(LoggersSetup.setup_logging())
34+
LoggersSetup.setup_structlog()
35+
36+
37+
@receiver(signals.bind_extra_task_metadata)
38+
def receiver_bind_extra_request_metadata(sender, signal, task=None, logger=None, **kwargs):
39+
# We want to add the task name to the task_succeeded event
40+
if task is not None:
41+
structlog.contextvars.bind_contextvars(task=task.name)

styleguide_example/tasks/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from celery import shared_task
22

33

4-
@shared_task
4+
@shared_task(bind=True)
55
def debug_task(self):
66
print("Request: {0!r}".format(self.request))

0 commit comments

Comments
 (0)