Skip to content

Commit

Permalink
Generalize usage of request correlation id to all integrations (#101)
Browse files Browse the repository at this point in the history
* Generalize usage of request correlation id to all integrations

* Use json.loads()

* Leverage caplog handler as suggested by @grahamalama
  • Loading branch information
leplatrem authored Mar 4, 2024
1 parent 6a6c78a commit a953916
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 62 deletions.
8 changes: 8 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
---------

2024.2.1
~~~~~~~~~~~~~~~~~~~~~

- Add request correlation ID support (#101).
In order to add a ``rid`` field to all log messages, add the ``dockerflow.logging.RequestIdLogFilter`` filter to your logging handlers.
See: :ref:`Django <django-logging>`, :ref:`FastAPI <fastapi-logging>`, :ref:`Flask <flask-logging>`, :ref:`Sanic <sanic-logging>` for details.


2024.2.0
~~~~~~~~~~~~~~~~~~~~~

Expand Down
19 changes: 18 additions & 1 deletion docs/django.rst
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,17 @@ configure **at least** the ``request.summary`` logger that way::
'logger_name': 'myproject'
}
},
'filters': {
'request_id': {
'()': 'dockerflow.logging.RequestIdFilter',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'json'
'formatter': 'json',
'filters': ['request_id']
},
},
'loggers': {
Expand All @@ -408,6 +414,17 @@ In order to include querystrings in the request summary log, set this flag in se
DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.

Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.

The header name to obtain the request ID can be customized in settings:

.. code-block:: python
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
.. _django-static:

Static content
Expand Down
19 changes: 15 additions & 4 deletions docs/fastapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ The package ``dockerflow.fastapi`` package implements various tools to support
For more information see the :doc:`API documentation <api/fastapi>` for
the ``dockerflow.fastapi`` module.

.. _fastapi-setup:

Setup
-----

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

from fastapi import FastAPI
from dockerflow.fastapi import router
from dockerflow.fastapi.middleware import MozlogRequestSummaryLogger, CorrelationIdMiddleware
from dockerflow.fastapi.middleware import MozlogRequestSummaryLogger, RequestIdMiddleware

app = FastAPI()
app.include_router(router)
app.add_middleware(MozlogRequestSummaryLogger)
app.add_middleware(CorrelationIdMiddleware) # see snok/asgi-correlation-id
app.add_middleware(RequestIdMiddleware) # see snok/asgi-correlation-id

#. Make sure the app root path is set correctly as this will be used
to locate the ``version.json`` file that is generated by
Expand Down Expand Up @@ -298,7 +300,7 @@ for at least the ``request.summary`` logger:
},
'filters': {
'request_id': {
'()': 'dockerflow.fastapi.RequestIdFilter',
'()': 'dockerflow.logging.RequestIdFilter',
},
},
'handlers': {
Expand All @@ -324,7 +326,16 @@ In order to include querystrings in the request summary log, set this flag in th
app.state.DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
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.
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.

Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.

The header name to obtain the request ID can be customized in settings:

.. code-block:: python
app.state.DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
.. _fastapi-static:

Expand Down
19 changes: 18 additions & 1 deletion docs/flask.rst
Original file line number Diff line number Diff line change
Expand Up @@ -441,11 +441,17 @@ for at least the ``request.summary`` logger::
'logger_name': 'myproject'
}
},
'filters': {
'request_id': {
'()': 'dockerflow.logging.RequestIdFilter',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'json'
'formatter': 'json',
'filters': ['request_id']
},
},
'loggers': {
Expand All @@ -460,6 +466,17 @@ In order to include querystrings in the request summary log, set this flag in :r

DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True

A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.

Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.

The header name to obtain the request ID can be customized in settings:

.. code-block:: python
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
.. _flask-static:

Static content
Expand Down
15 changes: 14 additions & 1 deletion docs/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ this::
'logger_name': 'myproject'
}
},
'filters': {
'request_id': {
'()': 'dockerflow.logging.RequestIdFilter',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'json'
'formatter': 'json',
'filters': ['request_id']
},
},
'loggers': {
Expand Down Expand Up @@ -80,6 +86,12 @@ thing as the dictionary based configuratio above:
[formatters]
keys = json
[filters]
keys = request_id
[filter_request_id]
class = dockerflow.logging.RequestIdFilter
[logger_root]
level = INFO
handlers = console
Expand All @@ -94,6 +106,7 @@ thing as the dictionary based configuratio above:
level = DEBUG
args = (sys.stdout,)
formatter = json
filters = request_id
[formatter_json]
class = dockerflow.logging.JsonLogFormatter
Expand Down
18 changes: 17 additions & 1 deletion docs/sanic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -421,11 +421,17 @@ for at least the ``request.summary`` logger::
'logger_name': 'myproject'
}
},
'filters': {
'request_id': {
'()': 'dockerflow.logging.RequestIdFilter',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'json'
'formatter': 'json',
'filters': ['request_id']
},
},
'loggers': {
Expand Down Expand Up @@ -459,6 +465,16 @@ In order to include querystrings in the request summary log, set this flag in :r

DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True

A unique request ID is read from the `X-Request-ID` request header, and a UUID4 value is generated if unset.

Leveraging the `RequestIdFilter` in logging configuration as shown above will add a ``rid`` attribute to all log messages.

The header name to obtain the request ID can be customized in settings:

.. code-block:: python
DOCKERFLOW_REQUEST_ID_HEADER_NAME = "X-Cloud-Trace-Context"
.. _sanic-static:

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def read(*parts):
"django": ["django"],
"flask": ["flask", "blinker"],
"sanic": ["sanic"],
"fastapi": ["fastapi", "asgiref", "asgi-correlation-id"],
"fastapi": ["fastapi", "asgiref"],
},
zip_safe=False,
python_requires=">=3.7,<4",
Expand Down
19 changes: 15 additions & 4 deletions src/dockerflow/django/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@
import time
import typing
import urllib
import uuid

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin

from dockerflow.logging import get_or_generate_request_id, request_id_context

from . import views


def extract_request_id(request):
"""Extract request ID from request."""
rid = get_or_generate_request_id(
request.headers,
header_name=getattr(settings, "DOCKERFLOW_REQUEST_ID_HEADER_NAME", None),
)
request_id_context.set(rid)
request._id = rid # Used in tests.


class DockerflowMiddleware(MiddlewareMixin):
"""
Emits a request.summary type log entry for every request.
Expand All @@ -34,7 +45,8 @@ def process_request(self, request):
if pattern.match(request.path_info):
return view(request)

request._id = str(uuid.uuid4())
extract_request_id(request)

request._start_timestamp = time.time()
return None

Expand All @@ -57,8 +69,7 @@ def _build_extra_meta(self, request):
# attributes before trying to use them.
if hasattr(request, "user"):
out["uid"] = request.user.is_authenticated and request.user.pk or ""
if hasattr(request, "_id"):
out["rid"] = request._id
out["rid"] = request_id_context.get()
if hasattr(request, "_start_timestamp"):
# Duration of request, in milliseconds.
out["t"] = int(1000 * (time.time() - request._start_timestamp))
Expand Down
14 changes: 0 additions & 14 deletions src/dockerflow/fastapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from logging import Filter, LogRecord

from asgi_correlation_id import correlation_id
from fastapi import APIRouter
from fastapi.routing import APIRoute

Expand All @@ -15,14 +12,3 @@
],
)
"""This router adds the Dockerflow views."""


class RequestIdLogFilter(Filter):
"""Logging filter to attach request IDs to log records"""

def filter(self, record: "LogRecord") -> bool:
"""
Attach the request ID to the log record.
"""
record.rid = correlation_id.get(None)
return True
35 changes: 32 additions & 3 deletions src/dockerflow/fastapi/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import urllib
from typing import Any, Dict

from asgi_correlation_id import CorrelationIdMiddleware, correlation_id # noqa
from asgiref.typing import (
ASGI3Application,
ASGIReceiveCallable,
Expand All @@ -15,7 +14,37 @@
HTTPScope,
)

from ..logging import JsonLogFormatter
from ..logging import JsonLogFormatter, get_or_generate_request_id, request_id_context


class RequestIdMiddleware:
def __init__(
self,
app: ASGI3Application,
) -> None:
self.app = app

async def __call__(
self, scope: HTTPScope, receive: ASGIReceiveCallable, send: ASGISendCallable
) -> None:
if scope["type"] != "http":
return await self.app(scope, receive, send)

headers = {}
for name, value in scope["headers"]:
header_key = name.decode("latin1").lower()
header_val = value.decode("latin1")
headers[header_key] = header_val

rid = get_or_generate_request_id(
headers,
header_name=getattr(
scope["app"].state, "DOCKERFLOW_REQUEST_ID_HEADER_NAME", None
),
)
request_id_context.set(rid)

await self.app(scope, receive, send)


class MozlogRequestSummaryLogger:
Expand Down Expand Up @@ -75,7 +104,7 @@ def _format(self, scope: HTTPScope, info) -> Dict[str, Any]:
"code": info["response"]["status"],
"lang": info["request_headers"].get("accept-language"),
"t": int(request_duration_ms),
"rid": correlation_id.get(),
"rid": request_id_context.get(),
}

if getattr(scope["app"].state, "DOCKERFLOW_SUMMARY_LOG_QUERYSTRING", False):
Expand Down
Loading

0 comments on commit a953916

Please sign in to comment.