Skip to content

Commit 9c1f625

Browse files
committed
Minimal implementation of structlog for EmbedAPI
Use structlog _only_ for EmbedAPI v2/v3 for now. We will do a test in production to this and decide the migration path for the whole codebase.
1 parent d65241d commit 9c1f625

File tree

10 files changed

+73
-61
lines changed

10 files changed

+73
-61
lines changed

readthedocs/core/apps.py

+3
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@ class CoreAppConfig(AppConfig):
1111

1212
def ready(self):
1313
import readthedocs.core.signals # noqa
14+
15+
# Import `readthedocs.core.logs` to set up structlog
16+
import readthedocs.core.logs # noqa

readthedocs/core/logs.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import structlog
2+
3+
from django_structlog.middlewares.request import RequestMiddleware
4+
5+
logger = structlog.getLogger(__name__)
6+
7+
8+
class ReadTheDocsRequestMiddleware(RequestMiddleware):
9+
"""
10+
``ReadTheDocsRequestMiddleware`` adds request metadata to ``structlog``'s logger context.
11+
12+
This middleware overrides the original ``format_request`` to log the full
13+
URL instead of just its path.
14+
15+
>>> MIDDLEWARE = [
16+
... # ...
17+
... 'readthedocs.core.logs.ReadTheDocsRequestMiddleware',
18+
... ]
19+
20+
"""
21+
22+
def format_request(self, request):
23+
return request.build_absolute_uri()
24+
25+
structlog.configure(
26+
processors=[
27+
structlog.stdlib.filter_by_level,
28+
structlog.processors.TimeStamper(fmt='iso'),
29+
structlog.stdlib.add_logger_name,
30+
structlog.stdlib.add_log_level,
31+
structlog.stdlib.PositionalArgumentsFormatter(),
32+
structlog.processors.StackInfoRenderer(),
33+
structlog.processors.format_exc_info,
34+
structlog.processors.UnicodeDecoder(),
35+
# TODO: research a little how to combine both (``wrap_for_formatter``
36+
# for regular logging and ``render_to_log_kwargs`` for NewRelic)
37+
# structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
38+
structlog.stdlib.render_to_log_kwargs,
39+
],
40+
context_class=structlog.threadlocal.wrap_dict(dict),
41+
logger_factory=structlog.stdlib.LoggerFactory(),
42+
wrapper_class=structlog.stdlib.BoundLogger,
43+
cache_logger_on_first_use=True,
44+
)

readthedocs/core/signals.py

-11
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from django.db.models.signals import pre_delete
99
from django.dispatch import Signal, receiver
1010

11-
from django_structlog.signals import bind_extra_request_metadata
1211
from rest_framework.permissions import SAFE_METHODS
1312
from simple_history.models import HistoricalRecords
1413
from simple_history.signals import pre_create_historical_record
@@ -147,14 +146,4 @@ def add_extra_historical_fields(sender, **kwargs):
147146
history_instance.extra_history_browser = request.headers.get('User-Agent')
148147

149148

150-
@receiver(bind_extra_request_metadata)
151-
def bind_user_username(request, logger, **kwargs):
152-
logger.bind(user_username=getattr(request.user, 'username', ''))
153-
154-
155-
@receiver(bind_extra_request_metadata)
156-
def rewrite_request(request, logger, **kwargs):
157-
logger.bind(absolute_url=request.build_absolute_uri())
158-
159-
160149
signals.check_request_enabled.connect(decide_if_cors)

readthedocs/embed/v3/views.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,8 @@ def get(self, request): # noqa
344344
}
345345
log.info(
346346
'EmbedAPI successful response.',
347-
project=self.unresolved_url.project.slug if not external else domain,
347+
project_slug=self.unresolved_url.project.slug if not external else None,
348+
domain=domain if external else None,
348349
url=url,
349350
referer=request.META.get('HTTP_REFERER'),
350351
external=external,

readthedocs/embed/views.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,9 @@ def get(self, request):
143143
)
144144

145145
log.info(
146-
'EmbedAPI successful response. '
147-
'project=%s version=%s doc=%s section=%s path=%s '
148-
'url=%s referer=%s hoverxref-version=%s',
149-
project=project.slug,
150-
version=version.verbose_name,
146+
'EmbedAPI successful response.',
147+
project_slug=project.slug,
148+
version_slug=version.slug,
151149
doct=doc,
152150
section=section,
153151
path=path,

readthedocs/settings/base.py

+15-43
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# pylint: disable=missing-docstring
22

3+
import logging
34
import os
45
import subprocess
56
import socket
67

7-
import requests
88
import structlog
99

1010
from celery.schedules import crontab
@@ -27,27 +27,7 @@
2727

2828

2929
_ = gettext = lambda s: s
30-
log = structlog.get_logger(__name__)
31-
32-
def send_to_newrelic(logger, log_method, event_dict):
33-
34-
if 'request' in event_dict:
35-
# WSGIRequest is not JSONSerializable
36-
event_dict.pop('request')
37-
38-
# Uses the New Relic Log API
39-
# https://docs.newrelic.com/docs/logs/log-management/log-api/introduction-log-api/
40-
headers = {"Api-Key": ''}
41-
42-
# Our log message and all the event context is sent as a JSON string
43-
# in the POST body
44-
# https://docs.newrelic.com/docs/logs/log-management/log-api/introduction-log-api/#json-content
45-
payload = {
46-
"message": f"{log_method} - {event_dict['event']}",
47-
}
48-
payload.update(event_dict)
49-
requests.post("https://log-api.newrelic.com/log/v1", json=payload, headers=headers)
50-
return event_dict
30+
log = logging.getLogger(__name__)
5131

5232

5333
class CommunityBaseSettings(Settings):
@@ -271,7 +251,7 @@ def USE_PROMOS(self): # noqa
271251
'readthedocs.core.middleware.ReferrerPolicyMiddleware',
272252
'django_permissions_policy.PermissionsPolicyMiddleware',
273253
'simple_history.middleware.HistoryRequestMiddleware',
274-
'django_structlog.middlewares.RequestMiddleware',
254+
'readthedocs.core.logs.ReadTheDocsRequestMiddleware',
275255
)
276256

277257
AUTHENTICATION_BACKENDS = (
@@ -816,6 +796,11 @@ def DOCKER_LIMITS(self):
816796
'version': 1,
817797
'disable_existing_loggers': True,
818798
'formatters': {
799+
'default': {
800+
'format': LOG_FORMAT,
801+
'datefmt': '%d/%b/%Y %H:%M:%S',
802+
},
803+
# structlog
819804
"plain_console": {
820805
"()": structlog.stdlib.ProcessorFormatter,
821806
"processor": structlog.dev.ConsoleRenderer(),
@@ -829,7 +814,7 @@ def DOCKER_LIMITS(self):
829814
'console': {
830815
'level': 'INFO',
831816
'class': 'logging.StreamHandler',
832-
'formatter': 'plain_console',
817+
'formatter': 'default',
833818
},
834819
'debug': {
835820
'level': 'DEBUG',
@@ -847,6 +832,12 @@ def DOCKER_LIMITS(self):
847832
# Always send from the root, handlers can filter levels
848833
'level': 'INFO',
849834
},
835+
'django_structlog': {
836+
'handlers': ['console'],
837+
'level': 'INFO',
838+
# Don't double log at the root logger for these.
839+
'propagate': False,
840+
},
850841
'readthedocs': {
851842
'handlers': ['debug', 'console'],
852843
'level': 'DEBUG',
@@ -859,25 +850,6 @@ def DOCKER_LIMITS(self):
859850
},
860851
},
861852
}
862-
structlog.configure(
863-
processors=[
864-
structlog.stdlib.filter_by_level,
865-
structlog.processors.TimeStamper(fmt="iso"),
866-
structlog.stdlib.add_logger_name,
867-
structlog.stdlib.add_log_level,
868-
structlog.stdlib.PositionalArgumentsFormatter(),
869-
structlog.processors.StackInfoRenderer(),
870-
structlog.processors.format_exc_info,
871-
structlog.processors.UnicodeDecoder(),
872-
send_to_newrelic,
873-
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
874-
],
875-
context_class=structlog.threadlocal.wrap_dict(dict),
876-
logger_factory=structlog.stdlib.LoggerFactory(),
877-
wrapper_class=structlog.stdlib.BoundLogger,
878-
cache_logger_on_first_use=True,
879-
)
880-
881853

882854
# MailerLite API for newsletter signups
883855
MAILERLITE_API_SUBSCRIBERS_URL = 'https://api.mailerlite.com/api/v2/subscribers'

readthedocs/settings/dev.py

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def DATABASES(self): # noqa
5757
def LOGGING(self): # noqa - avoid pep8 N802
5858
logging = super().LOGGING
5959
logging['handlers']['console']['level'] = 'DEBUG'
60+
logging['formatters']['default']['format'] = '[%(asctime)s] ' + self.LOG_FORMAT
6061
# Allow Sphinx and other tools to create loggers
6162
logging['disable_existing_loggers'] = False
6263
return logging

readthedocs/settings/docker_compose.py

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def RTD_EMBED_API_EXTERNAL_DOMAINS(self):
7777
@property
7878
def LOGGING(self):
7979
logging = super().LOGGING
80+
logging['handlers']['console']['formatter'] = 'plain_console'
8081
logging['loggers'].update({
8182
# Disable S3 logging
8283
'boto3': {

requirements/docker.txt

+3
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ tox==3.24.4
3131

3232
# AWS utilities to use against MinIO
3333
awscli==1.22.5
34+
35+
# Used together with structlog to have nicer logs locally
36+
rich==10.14.0

requirements/pip.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,4 @@ django-csp==3.7
120120
# For setting the permissions-policy security header
121121
django-permissions-policy==4.5.0
122122

123-
django-structlog==2.1.3
123+
django-structlog==2.2.0

0 commit comments

Comments
 (0)