Skip to content

Commit a953916

Browse files
authored
Generalize usage of request correlation id to all integrations (#101)
* Generalize usage of request correlation id to all integrations * Use json.loads() * Leverage caplog handler as suggested by @grahamalama
1 parent 6a6c78a commit a953916

File tree

18 files changed

+212
-62
lines changed

18 files changed

+212
-62
lines changed

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
Changelog
22
---------
33

4+
2024.2.1
5+
~~~~~~~~~~~~~~~~~~~~~
6+
7+
- Add request correlation ID support (#101).
8+
In order to add a ``rid`` field to all log messages, add the ``dockerflow.logging.RequestIdLogFilter`` filter to your logging handlers.
9+
See: :ref:`Django <django-logging>`, :ref:`FastAPI <fastapi-logging>`, :ref:`Flask <flask-logging>`, :ref:`Sanic <sanic-logging>` for details.
10+
11+
412
2024.2.0
513
~~~~~~~~~~~~~~~~~~~~~
614

docs/django.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,17 @@ configure **at least** the ``request.summary`` logger that way::
386386
'logger_name': 'myproject'
387387
}
388388
},
389+
'filters': {
390+
'request_id': {
391+
'()': 'dockerflow.logging.RequestIdFilter',
392+
},
393+
},
389394
'handlers': {
390395
'console': {
391396
'level': 'DEBUG',
392397
'class': 'logging.StreamHandler',
393-
'formatter': 'json'
398+
'formatter': 'json',
399+
'filters': ['request_id']
394400
},
395401
},
396402
'loggers': {
@@ -408,6 +414,17 @@ In order to include querystrings in the request summary log, set this flag in se
408414
DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
409415
410416
417+
A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.
418+
419+
Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.
420+
421+
The header name to obtain the request ID can be customized in settings:
422+
423+
.. code-block:: python
424+
425+
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
426+
427+
411428
.. _django-static:
412429

413430
Static content

docs/fastapi.rst

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ The package ``dockerflow.fastapi`` package implements various tools to support
2929
For more information see the :doc:`API documentation <api/fastapi>` for
3030
the ``dockerflow.fastapi`` module.
3131

32+
.. _fastapi-setup:
33+
3234
Setup
3335
-----
3436

@@ -39,12 +41,12 @@ To install ``python-dockerflow``'s FastAPI support please follow these steps:
3941

4042
from fastapi import FastAPI
4143
from dockerflow.fastapi import router
42-
from dockerflow.fastapi.middleware import MozlogRequestSummaryLogger, CorrelationIdMiddleware
44+
from dockerflow.fastapi.middleware import MozlogRequestSummaryLogger, RequestIdMiddleware
4345

4446
app = FastAPI()
4547
app.include_router(router)
4648
app.add_middleware(MozlogRequestSummaryLogger)
47-
app.add_middleware(CorrelationIdMiddleware) # see snok/asgi-correlation-id
49+
app.add_middleware(RequestIdMiddleware) # see snok/asgi-correlation-id
4850

4951
#. Make sure the app root path is set correctly as this will be used
5052
to locate the ``version.json`` file that is generated by
@@ -298,7 +300,7 @@ for at least the ``request.summary`` logger:
298300
},
299301
'filters': {
300302
'request_id': {
301-
'()': 'dockerflow.fastapi.RequestIdFilter',
303+
'()': 'dockerflow.logging.RequestIdFilter',
302304
},
303305
},
304306
'handlers': {
@@ -324,7 +326,16 @@ In order to include querystrings in the request summary log, set this flag in th
324326
325327
app.state.DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
326328
327-
A unique ID is added to each log message, using the `asgi-correlation-id <https://github.com/snok/asgi-correlation-id>`_ package and with the `RequestIdFilter` added in logging configuration as shown above.
329+
A unique request ID is read from the `X-Request-ID` request header using the `RequestIdMiddleware` middleware (see :ref:`fastapi-setup`), and a UUID4 value is generated if unset.
330+
331+
Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.
332+
333+
The header name to obtain the request ID can be customized in settings:
334+
335+
.. code-block:: python
336+
337+
app.state.DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
338+
328339
329340
.. _fastapi-static:
330341

docs/flask.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,11 +441,17 @@ for at least the ``request.summary`` logger::
441441
'logger_name': 'myproject'
442442
}
443443
},
444+
'filters': {
445+
'request_id': {
446+
'()': 'dockerflow.logging.RequestIdFilter',
447+
},
448+
},
444449
'handlers': {
445450
'console': {
446451
'level': 'DEBUG',
447452
'class': 'logging.StreamHandler',
448-
'formatter': 'json'
453+
'formatter': 'json',
454+
'filters': ['request_id']
449455
},
450456
},
451457
'loggers': {
@@ -460,6 +466,17 @@ In order to include querystrings in the request summary log, set this flag in :r
460466

461467
DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
462468

469+
A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.
470+
471+
Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.
472+
473+
The header name to obtain the request ID can be customized in settings:
474+
475+
.. code-block:: python
476+
477+
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
478+
479+
463480
.. _flask-static:
464481

465482
Static content

docs/logging.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,17 @@ this::
3939
'logger_name': 'myproject'
4040
}
4141
},
42+
'filters': {
43+
'request_id': {
44+
'()': 'dockerflow.logging.RequestIdFilter',
45+
},
46+
},
4247
'handlers': {
4348
'console': {
4449
'level': 'DEBUG',
4550
'class': 'logging.StreamHandler',
46-
'formatter': 'json'
51+
'formatter': 'json',
52+
'filters': ['request_id']
4753
},
4854
},
4955
'loggers': {
@@ -80,6 +86,12 @@ thing as the dictionary based configuratio above:
8086
[formatters]
8187
keys = json
8288
89+
[filters]
90+
keys = request_id
91+
92+
[filter_request_id]
93+
class = dockerflow.logging.RequestIdFilter
94+
8395
[logger_root]
8496
level = INFO
8597
handlers = console
@@ -94,6 +106,7 @@ thing as the dictionary based configuratio above:
94106
level = DEBUG
95107
args = (sys.stdout,)
96108
formatter = json
109+
filters = request_id
97110
98111
[formatter_json]
99112
class = dockerflow.logging.JsonLogFormatter

docs/sanic.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,17 @@ for at least the ``request.summary`` logger::
421421
'logger_name': 'myproject'
422422
}
423423
},
424+
'filters': {
425+
'request_id': {
426+
'()': 'dockerflow.logging.RequestIdFilter',
427+
},
428+
},
424429
'handlers': {
425430
'console': {
426431
'level': 'DEBUG',
427432
'class': 'logging.StreamHandler',
428-
'formatter': 'json'
433+
'formatter': 'json',
434+
'filters': ['request_id']
429435
},
430436
},
431437
'loggers': {
@@ -459,6 +465,16 @@ In order to include querystrings in the request summary log, set this flag in :r
459465

460466
DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
461467

468+
A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.
469+
470+
Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.
471+
472+
The header name to obtain the request ID can be customized in settings:
473+
474+
.. code-block:: python
475+
476+
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
477+
462478
463479
.. _sanic-static:
464480

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def read(*parts):
5050
"django": ["django"],
5151
"flask": ["flask", "blinker"],
5252
"sanic": ["sanic"],
53-
"fastapi": ["fastapi", "asgiref", "asgi-correlation-id"],
53+
"fastapi": ["fastapi", "asgiref"],
5454
},
5555
zip_safe=False,
5656
python_requires=">=3.7,<4",

src/dockerflow/django/middleware.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,25 @@
33
import time
44
import typing
55
import urllib
6-
import uuid
76

87
from django.conf import settings
98
from django.utils.deprecation import MiddlewareMixin
109

10+
from dockerflow.logging import get_or_generate_request_id, request_id_context
11+
1112
from . import views
1213

1314

15+
def extract_request_id(request):
16+
"""Extract request ID from request."""
17+
rid = get_or_generate_request_id(
18+
request.headers,
19+
header_name=getattr(settings, "DOCKERFLOW_REQUEST_ID_HEADER_NAME", None),
20+
)
21+
request_id_context.set(rid)
22+
request._id = rid # Used in tests.
23+
24+
1425
class DockerflowMiddleware(MiddlewareMixin):
1526
"""
1627
Emits a request.summary type log entry for every request.
@@ -34,7 +45,8 @@ def process_request(self, request):
3445
if pattern.match(request.path_info):
3546
return view(request)
3647

37-
request._id = str(uuid.uuid4())
48+
extract_request_id(request)
49+
3850
request._start_timestamp = time.time()
3951
return None
4052

@@ -57,8 +69,7 @@ def _build_extra_meta(self, request):
5769
# attributes before trying to use them.
5870
if hasattr(request, "user"):
5971
out["uid"] = request.user.is_authenticated and request.user.pk or ""
60-
if hasattr(request, "_id"):
61-
out["rid"] = request._id
72+
out["rid"] = request_id_context.get()
6273
if hasattr(request, "_start_timestamp"):
6374
# Duration of request, in milliseconds.
6475
out["t"] = int(1000 * (time.time() - request._start_timestamp))

src/dockerflow/fastapi/__init__.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
from logging import Filter, LogRecord
2-
3-
from asgi_correlation_id import correlation_id
41
from fastapi import APIRouter
52
from fastapi.routing import APIRoute
63

@@ -15,14 +12,3 @@
1512
],
1613
)
1714
"""This router adds the Dockerflow views."""
18-
19-
20-
class RequestIdLogFilter(Filter):
21-
"""Logging filter to attach request IDs to log records"""
22-
23-
def filter(self, record: "LogRecord") -> bool:
24-
"""
25-
Attach the request ID to the log record.
26-
"""
27-
record.rid = correlation_id.get(None)
28-
return True

src/dockerflow/fastapi/middleware.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import urllib
77
from typing import Any, Dict
88

9-
from asgi_correlation_id import CorrelationIdMiddleware, correlation_id # noqa
109
from asgiref.typing import (
1110
ASGI3Application,
1211
ASGIReceiveCallable,
@@ -15,7 +14,37 @@
1514
HTTPScope,
1615
)
1716

18-
from ..logging import JsonLogFormatter
17+
from ..logging import JsonLogFormatter, get_or_generate_request_id, request_id_context
18+
19+
20+
class RequestIdMiddleware:
21+
def __init__(
22+
self,
23+
app: ASGI3Application,
24+
) -> None:
25+
self.app = app
26+
27+
async def __call__(
28+
self, scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable
29+
) -> None:
30+
if scope["type"] != "http":
31+
return await self.app(scope, receive, send)
32+
33+
headers = {}
34+
for name, value in scope["headers"]:
35+
header_key = name.decode("latin1").lower()
36+
header_val = value.decode("latin1")
37+
headers[header_key] = header_val
38+
39+
rid = get_or_generate_request_id(
40+
headers,
41+
header_name=getattr(
42+
scope["app"].state, "DOCKERFLOW_REQUEST_ID_HEADER_NAME", None
43+
),
44+
)
45+
request_id_context.set(rid)
46+
47+
await self.app(scope, receive, send)
1948

2049

2150
class MozlogRequestSummaryLogger:
@@ -75,7 +104,7 @@ def _format(self, scope: HTTPScope, info) -> Dict[str, Any]:
75104
"code": info["response"]["status"],
76105
"lang": info["request_headers"].get("accept-language"),
77106
"t": int(request_duration_ms),
78-
"rid": correlation_id.get(),
107+
"rid": request_id_context.get(),
79108
}
80109

81110
if getattr(scope["app"].state, "DOCKERFLOW_SUMMARY_LOG_QUERYSTRING", False):

0 commit comments

Comments
 (0)