Skip to content

Commit

Permalink
docs(recipes): clean up request-id log recipes
Browse files Browse the repository at this point in the history
  • Loading branch information
vytas7 committed Jan 2, 2025
1 parent 2ab64a8 commit 29786cc
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 43 deletions.
14 changes: 9 additions & 5 deletions docs/user/recipes/request-id.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,29 @@ to every log entry.

If you wish to trace each request throughout your application, including
from within components that are deeply nested or otherwise live outside of the
normal request context, you can use a `contextvars`_ object to store
the request ID:
normal request context, you can use a :class:`~contextvars.ContextVar` object
to store the request ID:

.. literalinclude:: ../../../examples/recipes/request_id_context.py
:caption: *context.py*
:language: python

Then, you can create a :ref:`middleware <middleware>` class to generate a
unique ID for each request, persisting it in the `contextvars` object:

.. literalinclude:: ../../../examples/recipes/request_id_middleware.py
:caption: *middleware.py*
:language: python

Alternatively, if all of your application logic has access to the :ref:`request
<request>`, you can simply use the `context` object to store the ID:
Alternatively, if all of your application logic has access to the
:ref:`request <request>`, you can simply use the
:attr:`req.context <falcon.Request.context>` object to store the ID:

.. literalinclude:: ../../../examples/recipes/request_id_structlog.py
:caption: *middleware.py*
:language: python

.. note::

If your app is deployed behind a reverse proxy that injects a request ID
header, you can easily adapt this recipe to use the upstream ID rather than
generating a new one. By doing so, you can provide traceability across the
Expand All @@ -46,6 +49,7 @@ or by using a third-party logging library such as
In a pinch, you can also output the request ID directly:

.. literalinclude:: ../../../examples/recipes/request_id_log.py
:caption: *some_other_module.py*
:language: python

.. _contextvars: https://docs.python.org/3/library/contextvars.html
2 changes: 0 additions & 2 deletions examples/recipes/request_id_context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# context.py

import contextvars


Expand Down
5 changes: 2 additions & 3 deletions examples/recipes/request_id_log.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# some_other_module.py

import logging

from context import ctx
# Import the above context.py
from my_app.context import ctx


def create_widget_object(name: str):
Expand Down
6 changes: 2 additions & 4 deletions examples/recipes/request_id_middleware.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# middleware.py

from uuid import uuid4

from context import ctx
# Import the above context.py
from my_app.context import ctx


class RequestIDMiddleware:
def process_request(self, req, resp):
request_id = str(uuid4())
ctx.request_id = request_id
req.context.request_id = request_id

# It may also be helpful to include the ID in the response
def process_response(self, req, resp, resource, req_succeeded):
Expand Down
4 changes: 1 addition & 3 deletions examples/recipes/request_id_structlog.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# middleware.py

from uuid import uuid4

# Optional logging package (pip install structlog)
Expand All @@ -10,7 +8,7 @@ class RequestIDMiddleware:
def process_request(self, req, resp):
request_id = str(uuid4())

# Using Falcon 2.0 syntax
# Using Falcon 2.0+ context style
req.context.request_id = request_id

# Or if your logger has built-in support for contexts
Expand Down
19 changes: 16 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,31 @@ def load_module(filename, parent_dir=None, suffix=None, module_name=None):
if suffix is not None:
path = path.with_name(f'{path.stem}_{suffix}.py')
prefix = '.'.join(filename.parent.parts)
sys_module_name = module_name
module_name = module_name or f'{prefix}.{path.stem}'

spec = importlib.util.spec_from_file_location(module_name, path)
assert spec is not None, f'could not load module from {path}'
module = importlib.util.module_from_spec(spec)
if sys_module_name:
sys.modules[sys_module_name] = module
spec.loader.exec_module(module)
return module


@pytest.fixture()
def register_module():
"""Temporarily monkey-patch a module into sys.modules."""

def _register(name, module):
sys.modules[name] = module
patched_modules.add(name)

patched_modules = set()

yield _register

for name in patched_modules:
sys.modules.pop(name, None)


@pytest.fixture(scope='session')
def util():
return _SuiteUtils()
Expand Down
53 changes: 30 additions & 23 deletions tests/test_recipes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import types
import unittest.mock

import pytest

import falcon
Expand Down Expand Up @@ -163,42 +166,46 @@ def test_text_plain_basic(self, util):


class TestRequestIDContext:
@pytest.fixture
def app(self, util):
@pytest.fixture(params=['middleware', 'structlog'])
def app(self, request, util, register_module):
class RequestIDResource:
def on_get(self, req, resp):
# NOTE(vytas): Reference either ContextVar or req.context
# depending on the recipe being tested.
context = getattr(recipe, 'ctx', req.context)
resp.media = {'request_id': context.request_id}

context = util.load_module(
'examples/recipes/request_id_context.py', module_name='my_app.context'
)
# NOTE(vytas): Inject `context` into the importable system modules
# as it is referenced from other recipes.
util.load_module(
'examples/recipes/request_id_context.py', module_name='context'
)
recipe = util.load_module('examples/recipes/request_id_middleware.py')
register_module('my_app.context', context)

app = falcon.App(middleware=[recipe.RequestIDMiddleware()])
app.add_route('/test', self.RequestIDResource())
return app
# NOTE(vytas): Inject a fake structlog module because we do not want to
# introduce a new test dependency for a single recipe.
fake_structlog = types.ModuleType('structlog')
fake_structlog.get_logger = unittest.mock.MagicMock()
register_module('structlog', fake_structlog)

class RequestIDResource:
def on_get(self, req, resp):
resp.media = {'request_id': req.context.request_id}
recipe = util.load_module(f'examples/recipes/request_id_{request.param}.py')

def test_request_id_isolated(self, app):
client = falcon.testing.TestClient(app)
request_id1 = client.simulate_get('/test').json['request_id']
request_id2 = client.simulate_get('/test').json['request_id']

assert request_id1 != request_id2
app = falcon.App(middleware=[recipe.RequestIDMiddleware()])
app.add_route('/test', RequestIDResource())
return app

def test_request_id_persistence(self, app):
client = falcon.testing.TestClient(app)

response = client.simulate_get('/test')
request_id1 = response.json['request_id']
resp1 = client.simulate_get('/test')
request_id1 = resp1.json['request_id']

response = client.simulate_get('/test')
request_id2 = response.json['request_id']
resp2 = client.simulate_get('/test')
request_id2 = resp2.json['request_id']

assert request_id1 != request_id2

def test_request_id_in_response_header(self, app):
def test_request_id_header(self, app):
client = falcon.testing.TestClient(app)

response = client.simulate_get('/test')
Expand Down

0 comments on commit 29786cc

Please sign in to comment.