Skip to content

Commit

Permalink
Merge branch 'master' into 2037
Browse files Browse the repository at this point in the history
  • Loading branch information
vytas7 authored Dec 30, 2024
2 parents ca349ed + 77d5e63 commit 2c515be
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 10 deletions.
6 changes: 3 additions & 3 deletions docs/user/recipes/request-id.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ 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 `thread-local`_ context object to store
normal request context, you can use a `contextvars`_ object to store
the request ID:

.. literalinclude:: ../../../examples/recipes/request_id_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 thread local:
unique ID for each request, persisting it in the `contextvars` object:

.. literalinclude:: ../../../examples/recipes/request_id_middleware.py
:language: python
Expand Down Expand Up @@ -48,4 +48,4 @@ In a pinch, you can also output the request ID directly:
.. literalinclude:: ../../../examples/recipes/request_id_log.py
:language: python

.. _thread-local: https://docs.python.org/3/library/threading.html#thread-local-data
.. _contextvars: https://docs.python.org/3/library/contextvars.html
8 changes: 4 additions & 4 deletions examples/recipes/request_id_context.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# context.py

import threading
import contextvars


class _Context:
def __init__(self):
self._thread_local = threading.local()
self._request_id_var = contextvars.ContextVar('request_id', default=None)

@property
def request_id(self):
return getattr(self._thread_local, 'request_id', None)
return self._request_id_var.get()

@request_id.setter
def request_id(self, value):
self._thread_local.request_id = value
self._request_id_var.set(value)


ctx = _Context()
4 changes: 3 additions & 1 deletion examples/recipes/request_id_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

class RequestIDMiddleware:
def process_request(self, req, resp):
ctx.request_id = str(uuid4())
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
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os
import pathlib
import sys
import urllib.parse

import pytest
Expand Down Expand Up @@ -96,7 +97,7 @@ def disable_asgi_non_coroutine_wrapping():
os.environ['FALCON_ASGI_WRAP_NON_COROUTINES'] = 'Y'

@staticmethod
def load_module(filename, parent_dir=None, suffix=None):
def load_module(filename, parent_dir=None, suffix=None, module_name=None):
if parent_dir:
filename = pathlib.Path(parent_dir) / filename
else:
Expand All @@ -105,11 +106,14 @@ def load_module(filename, parent_dir=None, suffix=None):
if suffix is not None:
path = path.with_name(f'{path.stem}_{suffix}.py')
prefix = '.'.join(filename.parent.parts)
module_name = f'{prefix}.{path.stem}'
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

Expand Down
44 changes: 44 additions & 0 deletions tests/test_recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,47 @@ def test_text_plain_basic(self, util):
assert response.status_code == 200
assert response.content_type == 'text/plain'
assert response.text == payload


class TestRequestIDContext:
@pytest.fixture
def app(self, util):
# 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')

app = falcon.App(middleware=[recipe.RequestIDMiddleware()])
app.add_route('/test', self.RequestIDResource())
return app

class RequestIDResource:
def on_get(self, req, resp):
resp.media = {'request_id': req.context.request_id}

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

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

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

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

assert request_id1 != request_id2

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

response = client.simulate_get('/test')
assert 'X-Request-ID' in response.headers
assert response.headers['X-Request-ID'] == response.json['request_id']

0 comments on commit 2c515be

Please sign in to comment.