From 022a623c11c308012df29858a441ce90ce6b3e72 Mon Sep 17 00:00:00 2001 From: Ali Abouelazm Date: Thu, 22 Oct 2026 14:46:42 -0700 Subject: [PATCH 1/3] fix(logging): preserve Django request context in async code --- .../logging_v2/handlers/middleware/request.py | 14 ++- .../unit/handlers/middleware/test_request.py | 94 +++++++++++++++++++ .../tests/unit/handlers/test__helpers.py | 11 ++- 3 files changed, 111 insertions(+), 8 deletions(-) diff --git a/packages/google-cloud-logging/google/cloud/logging_v2/handlers/middleware/request.py b/packages/google-cloud-logging/google/cloud/logging_v2/handlers/middleware/request.py index 3d289938bb67..9ecb2f5c38bf 100644 --- a/packages/google-cloud-logging/google/cloud/logging_v2/handlers/middleware/request.py +++ b/packages/google-cloud-logging/google/cloud/logging_v2/handlers/middleware/request.py @@ -14,17 +14,21 @@ """Django middleware helper to capture a request. -The request is stored on a thread-local so that it can be +The request is stored in local context so that it can be inspected by other helpers. """ -import threading +try: + from asgiref.local import Local +except ImportError: + # Django is optional, so do not require asgiref for other integrations. + from threading import local as Local -_thread_locals = threading.local() +_thread_locals = Local() def _get_django_request(): - """Get Django request from thread local. + """Get Django request from local context. Returns: str: Django request @@ -33,7 +37,7 @@ def _get_django_request(): def RequestMiddleware(get_response): - """Saves the request in thread local""" + """Saves the request in thread- and async-local context.""" def middleware(request): """Called on each request, before Django decides which view to execute. diff --git a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py index d195e7f3dd89..54eeb5158c7c 100644 --- a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py +++ b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py @@ -12,7 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import asyncio +import importlib.util +import sys +import threading import unittest +from concurrent.futures import ThreadPoolExecutor import mock @@ -35,6 +40,15 @@ def tearDownClass(cls): class TestRequestMiddleware(DjangoBase): + def setUp(self): + from google.cloud.logging_v2.handlers.middleware import request + + patch = mock.patch.object( + request, "_thread_locals", type(request._thread_locals)() + ) + patch.start() + self.addCleanup(patch.stop) + def _get_target_class(self): from google.cloud.logging_v2.handlers.middleware import request @@ -71,6 +85,86 @@ def test_can_instantiate_middleware_with_kwargs(self): mock_request = "test_req" self.assertEqual(middleware(mock_request), mock_request) + def test_concurrent_async_requests(self): + from google.cloud.logging_v2.handlers.middleware import request + + middleware = self._make_one() + requests = [object(), object()] + + async def handle_requests(): + ready = [asyncio.Event(), asyncio.Event()] + + async def handle_request(index): + middleware(requests[index]) + ready[index].set() + await ready[1 - index].wait() + return request._get_django_request() + + return await asyncio.gather(handle_request(0), handle_request(1)) + + self.assertEqual(asyncio.run(handle_requests()), requests) + self.assertIsNone(request._get_django_request()) + + def test_async_view(self): + from asgiref.sync import async_to_sync + + from google.cloud.logging_v2.handlers.middleware import request + + async def get_response(req): + return request._get_django_request() + + middleware = self._make_one(async_to_sync(get_response)) + mock_request = object() + self.assertIs(middleware(mock_request), mock_request) + + def test_sync_middleware_in_async_context(self): + from asgiref.sync import sync_to_async + + from google.cloud.logging_v2.handlers.middleware import request + + middleware = self._make_one() + mock_request = object() + + async def handle_request(): + await sync_to_async(middleware)(mock_request) + return request._get_django_request() + + self.assertIs(asyncio.run(handle_request()), mock_request) + + def test_requests_are_thread_local(self): + from google.cloud.logging_v2.handlers.middleware import request + + middleware = self._make_one() + main_request = object() + worker_request = object() + middleware(main_request) + + with ThreadPoolExecutor(max_workers=1) as executor: + self.assertIsNone(executor.submit(request._get_django_request).result()) + executor.submit(middleware, worker_request).result() + self.assertIs( + executor.submit(request._get_django_request).result(), worker_request + ) + + self.assertIs(request._get_django_request(), main_request) + + +class TestRequestStorage(unittest.TestCase): + def test_without_asgiref(self): + from google.cloud.logging_v2.handlers.middleware import request + + spec = importlib.util.spec_from_file_location("request", request.__file__) + module = importlib.util.module_from_spec(spec) + with mock.patch.dict(sys.modules, {"asgiref.local": None}): + spec.loader.exec_module(module) + + self.assertIsInstance(module._thread_locals, threading.local) + self.assertIsNone(module._get_django_request()) + mock_request = object() + middleware = module.RequestMiddleware(None) + middleware(mock_request) + self.assertIs(module._get_django_request(), mock_request) + class Test__get_django_request(DjangoBase): @staticmethod diff --git a/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py b/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py index fe284be5a050..8e44316cefdf 100644 --- a/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py +++ b/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py @@ -147,6 +147,14 @@ def setUp(self): from django.conf import settings from django.test.utils import setup_test_environment + from google.cloud.logging_v2.handlers.middleware import request + + patch = mock.patch.object( + request, "_thread_locals", type(request._thread_locals)() + ) + patch.start() + self.addCleanup(patch.stop) + if not settings.configured: settings.configure() setup_test_environment() @@ -154,10 +162,7 @@ def setUp(self): def tearDown(self): from django.test.utils import teardown_test_environment - from google.cloud.logging_v2.handlers.middleware import request - teardown_test_environment() - request._thread_locals.__dict__.clear() def test_no_context_header(self): from django.test import RequestFactory From fcf136fc4c3736e91a26ee213371fc1b68f84f74 Mon Sep 17 00:00:00 2001 From: Daniel Sanche Date: Thu, 12 Nov 2026 19:16:46 -0800 Subject: [PATCH 2/3] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../tests/unit/handlers/middleware/test_request.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py index 54eeb5158c7c..40ce111f75d9 100644 --- a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py +++ b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py @@ -139,13 +139,16 @@ def test_requests_are_thread_local(self): worker_request = object() middleware(main_request) - with ThreadPoolExecutor(max_workers=1) as executor: - self.assertIsNone(executor.submit(request._get_django_request).result()) - executor.submit(middleware, worker_request).result() + def worker(): + self.assertIsNone(request._get_django_request()) + middleware(worker_request) self.assertIs( - executor.submit(request._get_django_request).result(), worker_request + request._get_django_request(), worker_request ) + with ThreadPoolExecutor(max_workers=1) as executor: + executor.submit(worker).result() + self.assertIs(request._get_django_request(), main_request) From 64fefbb1ff3c38230f4accc56027fc7c76ebde9a Mon Sep 17 00:00:00 2001 From: Ali Abouelazm Date: Thu, 12 Nov 2026 22:05:27 -0600 Subject: [PATCH 3/3] test(logging): construct request context through public Local API --- .../tests/unit/handlers/middleware/test_request.py | 8 ++------ .../tests/unit/handlers/test__helpers.py | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py index 40ce111f75d9..70cd72419531 100644 --- a/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py +++ b/packages/google-cloud-logging/tests/unit/handlers/middleware/test_request.py @@ -43,9 +43,7 @@ class TestRequestMiddleware(DjangoBase): def setUp(self): from google.cloud.logging_v2.handlers.middleware import request - patch = mock.patch.object( - request, "_thread_locals", type(request._thread_locals)() - ) + patch = mock.patch.object(request, "_thread_locals", request.Local()) patch.start() self.addCleanup(patch.stop) @@ -142,9 +140,7 @@ def test_requests_are_thread_local(self): def worker(): self.assertIsNone(request._get_django_request()) middleware(worker_request) - self.assertIs( - request._get_django_request(), worker_request - ) + self.assertIs(request._get_django_request(), worker_request) with ThreadPoolExecutor(max_workers=1) as executor: executor.submit(worker).result() diff --git a/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py b/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py index 8e44316cefdf..46f88651085e 100644 --- a/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py +++ b/packages/google-cloud-logging/tests/unit/handlers/test__helpers.py @@ -149,9 +149,7 @@ def setUp(self): from google.cloud.logging_v2.handlers.middleware import request - patch = mock.patch.object( - request, "_thread_locals", type(request._thread_locals)() - ) + patch = mock.patch.object(request, "_thread_locals", request.Local()) patch.start() self.addCleanup(patch.stop)