Skip to content

Commit 6cd4207

Browse files
authored
Merge pull request Raekkeri#5 from Raekkeri/request-id
Request-id
2 parents 0cbb892 + 67111e0 commit 6cd4207

File tree

7 files changed

+387
-19
lines changed

7 files changed

+387
-19
lines changed

README.md

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,20 @@ MIDDLEWARE = [
6060
]
6161
```
6262

63-
This will start storing the request logs using the default `STORAGE_CLASS`, which in fact just uses Python logger named `requestlogs`. Now you can, for example, redirect these logs to a file with the following `LOGGING` configuration:
63+
Set `'requestlogs.views.exception_handler'` as rest_framework's exception handler
64+
(this will make sure requestlog entry has all possible data available about the
65+
request in case of a 500 error):
66+
67+
```python
68+
REST_FRAMEWORK={
69+
...
70+
'EXCEPTION_HANDLER': 'requestlogs.views.exception_handler',
71+
}
72+
```
73+
74+
The middleware is now ready to start storing requestlog entries using the default
75+
`STORAGE_CLASS`, which in fact just uses Python logger named `requestlogs`. Now you can,
76+
for example, redirect these logs to a file with the following `LOGGING` configuration:
6477

6578
```python
6679
LOGGING = {
@@ -77,6 +90,7 @@ LOGGING = {
7790
'requestlogs': {
7891
'handlers': ['requestlogs_to_file'],
7992
'level': 'INFO',
93+
'propagate': False,
8094
},
8195
},
8296
}
@@ -90,6 +104,7 @@ Requestlogs can be customized using Django settings. The following shows the def
90104
REQUESTLOGS = {
91105
'STORAGE_CLASS': 'requestlogs.storages.LoggingStorage',
92106
'ENTRY_CLASS': 'requestlogs.entries.RequestLogEntry',
107+
'SERIALIZER_CLASS': 'requestlogs.storages.BaseEntrySerializer',
93108
'SECRETS': ['password', 'token'],
94109
'ATTRIBUTE_NAME': '_requestlog',
95110
}
@@ -99,7 +114,123 @@ REQUESTLOGS = {
99114
- Path to the Python class which will handle storing the log entries. Override this if you only need to reimplement the storage mechanism. This may be the case e.g. when choosing what data to store.
100115
- **ENTRY_CLASS**
101116
- Path to the Python class which handles the construction of the complete requestlogs entry. Override this for full customization of the requestlog entry behaviour.
117+
- **SERIALIZER_CLASS**
118+
- Path to the serializer class which is used to serialize the requestlog entry before storage. By default this is a subclass of `rest_framework.serializers.Serializer`.
102119
- **SECRETS**
103120
- List of keys in request/response data which will be replaced with `'***'` in the stored entry.
104121
- **ATTRIBUTE_NAME**
105122
- django-requestlogs internally attaches the entry object to the Django request object, and uses this attribute name. Override if it causes collisions.
123+
124+
125+
# Logging with Request ID
126+
127+
django-requestlogs also contains a middleware and logging helpers to associate a
128+
request-specific identifier (uuid) to logging messages. This aims to help
129+
distinguishing messages to certain request-response cycle, which can be useful
130+
in an application that receives a high number of requests.
131+
132+
The request id is added to the standard logging messages (Django application logs)
133+
by specifying a custom formatter and using the provided logging filter.
134+
The request id can be stored to requestlog entries as well.
135+
The middleware to enable the request id logging does not require the core requestlogs
136+
middleware to be installed.
137+
138+
Under the hood the request id is implemented with help of `threading.local()`.
139+
140+
## Installation
141+
142+
The feature is enabled by adding `requestlogs.middleware.RequestLogsMiddleware`
143+
to the `MIDDLEWARE` setting:
144+
145+
```python
146+
MIDDLEWARE = [
147+
...
148+
'requestlogs.middleware.RequestLogsMiddleware',
149+
'requestlogs.middleware.RequestIdMiddleware',
150+
]
151+
```
152+
153+
Once installed, the application logs should start showing messages with a format such as
154+
the following:
155+
156+
```
157+
2019-07-18 11:56:07,261 INFO 954fb004fb404751a2fa33326101442c urls:31 Handling GET request
158+
2019-07-18 11:56:07,262 DEBUG 954fb004fb404751a2fa33326101442c urls:32 No parameters given
159+
2019-07-18 11:56:07,262 INFO 954fb004fb404751a2fa33326101442c urls:33 All good
160+
```
161+
162+
To add the request id to requestlog entries as well, you can use the provided serializer
163+
class as a starting point:
164+
165+
```python
166+
REQUESTLOGS = {
167+
...
168+
'SERIALIZER_CLASS': 'requestlogs.storages.RequestIdEntrySerializer',
169+
}
170+
```
171+
172+
## Configuration
173+
174+
The middleware has some additional configuration possiblities:
175+
176+
177+
```python
178+
REQUESTLOGS = {
179+
...
180+
'REQUEST_ID_HTTP_HEADER': 'X_DJANGO_REQUEST_ID',
181+
'REQUEST_ID_ATTRIBUTE_NAME': 'request_id',
182+
}
183+
```
184+
- **REQUEST_ID_HTTP_HEADER**
185+
- If set, the value of this request header is used as request id (instead of it being
186+
randomly generated). This must be a valid uuid. One use case for this feature is in
187+
microservice architecture, where a micreservice calls another, internal microservice.
188+
Having the log messages of both applications to be formatted with same request id
189+
might be the preferred outcome.
190+
- **REQUEST_ID_ATTRIBUTE_NAME**
191+
- The attribute name which is used internally to attach request id to
192+
`threading.locals()`. Override if it causes collisions.
193+
194+
To add the request id to logging messages of your Django application, use the provided
195+
logging filter and include `request_id` to the log formatter.
196+
Here is the complete logging configuration:
197+
198+
```python
199+
LOGGING = {
200+
'version': 1,
201+
'disable_existing_loggers': False,
202+
'handlers': {
203+
'requestlogs_to_file': {
204+
'level': 'INFO',
205+
'class': 'logging.FileHandler',
206+
'filename': '/tmp/requestlogs.log',
207+
},
208+
'root': {
209+
'class': 'logging.StreamHandler',
210+
'filters': ['request_id_context'],
211+
'formatter': 'verbose',
212+
},
213+
},
214+
'loggers': {
215+
'': {
216+
'handlers': ['root'],
217+
'level': 'DEBUG',
218+
},
219+
'requestlogs': {
220+
'handlers': ['requestlogs_to_file'],
221+
'level': 'INFO',
222+
'propagate': False,
223+
},
224+
},
225+
'filters': {
226+
'request_id_context': {
227+
'()': 'requestlogs.logging.RequestIdContext',
228+
},
229+
},
230+
'formatters': {
231+
'verbose': {
232+
'format': '%(asctime)s %(levelname)s %(request_id)s %(module)s:%(lineno)s %(message)s'
233+
},
234+
},
235+
}
236+
```

requestlogs/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
'ATTRIBUTE_NAME': '_requestlog',
88
'ENTRY_CLASS': 'requestlogs.entries.RequestLogEntry',
99
'STORAGE_CLASS': 'requestlogs.storages.LoggingStorage',
10+
'SERIALIZER_CLASS': 'requestlogs.storages.BaseEntrySerializer',
1011
'SECRETS': ['password', 'token'],
12+
'REQUEST_ID_ATTRIBUTE_NAME': 'request_id',
13+
'REQUEST_ID_HTTP_HEADER': None,
1114
}
1215

1316

@@ -18,6 +21,8 @@ def populate_settings(_settings):
1821
_settings[k] = v
1922
_settings['ENTRY_CLASS'] = import_string(_settings['ENTRY_CLASS'])
2023
_settings['STORAGE_CLASS'] = import_string(_settings['STORAGE_CLASS'])
24+
_settings['SERIALIZER_CLASS'] = import_string(
25+
_settings['SERIALIZER_CLASS'])
2126

2227

2328
SETTINGS = {}

requestlogs/entries.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from rest_framework.request import Request
66

77
from .base import SETTINGS
8+
from .logging import get_request_id
89
from .utils import remove_secrets, get_client_ip
910

1011

@@ -28,6 +29,10 @@ def query_params(self):
2829
def full_path(self):
2930
return self.request.get_full_path()
3031

32+
@property
33+
def request_id(self):
34+
return get_request_id()
35+
3136

3237
class DRFRequestHandler(RequestHandler):
3338
@property

requestlogs/logging.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import logging
2+
import uuid
3+
import threading
4+
5+
from .base import SETTINGS
6+
7+
local = threading.local()
8+
9+
10+
def get_request_id():
11+
return getattr(local, SETTINGS['REQUEST_ID_ATTRIBUTE_NAME'], '')
12+
13+
14+
def set_request_id(_uuid=None):
15+
_uuid = _uuid or uuid.uuid4().hex
16+
setattr(local, SETTINGS['REQUEST_ID_ATTRIBUTE_NAME'], _uuid)
17+
return _uuid
18+
19+
20+
def validate_uuid(_uuid):
21+
try:
22+
val = uuid.UUID(_uuid, version=4)
23+
except (ValueError, TypeError):
24+
return None
25+
26+
return _uuid if val.hex == _uuid else None
27+
28+
29+
class RequestIdContext(logging.Filter):
30+
def filter(self, record):
31+
setattr(record, SETTINGS['REQUEST_ID_ATTRIBUTE_NAME'],
32+
get_request_id())
33+
return True

requestlogs/middleware.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from .base import SETTINGS
2+
from .logging import set_request_id, validate_uuid
13
from . import get_requestlog_entry
24

35

@@ -14,3 +16,13 @@ def __call__(self, request):
1416
response = self.get_response(request)
1517
get_requestlog_entry(request).finalize(response)
1618
return response
19+
20+
21+
class RequestIdMiddleware(object):
22+
def __init__(self, get_response):
23+
self.get_response = get_response
24+
25+
def __call__(self, request):
26+
reuse_request_id = request.META.get(SETTINGS['REQUEST_ID_HTTP_HEADER'])
27+
set_request_id(validate_uuid(reuse_request_id))
28+
return self.get_response(request)

requestlogs/storages.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from rest_framework import serializers
55

6+
from .base import SETTINGS
7+
68

79
logger = logging.getLogger('requestlogs')
810

@@ -12,13 +14,14 @@ def to_representation(self, value):
1214
return json.dumps(value)
1315

1416

15-
class BaseEntrySerializer(serializers.Serializer):
16-
class RequestSerializer(serializers.Serializer):
17-
method = serializers.CharField(read_only=True)
18-
full_path = serializers.CharField(read_only=True)
19-
data = JsonDumpField(read_only=True)
20-
query_params = JsonDumpField(read_only=True)
17+
class BaseRequestSerializer(serializers.Serializer):
18+
method = serializers.CharField(read_only=True)
19+
full_path = serializers.CharField(read_only=True)
20+
data = JsonDumpField(read_only=True)
21+
query_params = JsonDumpField(read_only=True)
22+
2123

24+
class BaseEntrySerializer(serializers.Serializer):
2225
class ResponseSerializer(serializers.Serializer):
2326
status_code = serializers.IntegerField(read_only=True)
2427
data = JsonDumpField(read_only=True)
@@ -31,16 +34,29 @@ class UserSerializer(serializers.Serializer):
3134
execution_time = serializers.DurationField(read_only=True)
3235
timestamp = serializers.DateTimeField(read_only=True)
3336
ip_address = serializers.CharField(read_only=True)
34-
request = RequestSerializer(read_only=True)
37+
request = BaseRequestSerializer(read_only=True)
3538
response = ResponseSerializer(read_only=True)
3639
user = UserSerializer()
3740

3841

39-
class LoggingStorage(object):
40-
serializer_class = BaseEntrySerializer
42+
class RequestIdEntrySerializer(BaseEntrySerializer):
43+
class RequestSerializer(BaseRequestSerializer):
44+
request_id = serializers.CharField()
4145

42-
def store(self, entry):
43-
logger.info(self.prepare(entry))
46+
request = RequestSerializer()
47+
48+
49+
class BaseStorage(object):
50+
serializer_class = None
51+
52+
def get_serializer_class(self):
53+
return (self.serializer_class if self.serializer_class else
54+
SETTINGS['SERIALIZER_CLASS'])
4455

4556
def prepare(self, entry):
46-
return self.serializer_class(entry).data
57+
return self.get_serializer_class()(entry).data
58+
59+
60+
class LoggingStorage(BaseStorage):
61+
def store(self, entry):
62+
logger.info(self.prepare(entry))

0 commit comments

Comments
 (0)