diff --git a/demoproj/urls.py b/demoproj/urls.py index 05bdd00..0193010 100644 --- a/demoproj/urls.py +++ b/demoproj/urls.py @@ -15,7 +15,7 @@ """ from django.urls import path -from demoproj.views.sync_views import index_view, no_guid, rest_view +from demoproj.views.sync_views import index_view, no_guid, rest_view, no_guid_regex from demoproj.views.async_views import index_view as asgi_index_view from demoproj.views.async_views import django_guid_api_usage @@ -23,6 +23,7 @@ path('', index_view, name='index'), path('api', rest_view, name='drf'), path('no-guid', no_guid, name='no_guid'), + path('no-guid-regex', no_guid_regex, name='no_guid_regex'), path('asgi', asgi_index_view, name='asgi_index'), path('api-usage', django_guid_api_usage, name='django_guid_api_usage'), ] diff --git a/demoproj/views/sync_views.py b/demoproj/views/sync_views.py index c7b79d7..40f4763 100644 --- a/demoproj/views/sync_views.py +++ b/demoproj/views/sync_views.py @@ -35,6 +35,15 @@ def no_guid(request: 'HttpRequest') -> JsonResponse: return JsonResponse({'detail': f'It worked also! Useless function response is {useless_response}'}) +def no_guid_regex(request: 'HttpRequest') -> JsonResponse: + """ + Example view with a URL in the IGNORE_REGEX_URLS list - no GUID will be in these logs + """ + logger.info('This log message should NOT have a GUID - the URL is in IGNORE_REGEX_URLS') + useless_response = useless_function() + return JsonResponse({'detail': f'It worked also! Useless function response is {useless_response}'}) + + @api_view(('GET',)) def rest_view(request: 'Request') -> Response: """ diff --git a/django_guid/config.py b/django_guid/config.py index 8a2f590..c672599 100644 --- a/django_guid/config.py +++ b/django_guid/config.py @@ -45,6 +45,10 @@ def expose_header(self) -> bool: def ignore_urls(self) -> List[str]: return list({url.strip('/') for url in self.settings.get('IGNORE_URLS', [])}) + @property + def ignore_regex_urls(self) -> List[str]: + return list({url.strip('/') for url in self.settings.get('IGNORE_REGEX_URLS', [])}) + @property def validate_guid(self) -> bool: return self.settings.get('VALIDATE_GUID', True) diff --git a/django_guid/middleware.py b/django_guid/middleware.py index 66dc8e1..a59b4fe 100644 --- a/django_guid/middleware.py +++ b/django_guid/middleware.py @@ -6,7 +6,7 @@ from django.core.exceptions import ImproperlyConfigured from django_guid.context import guid -from django_guid.utils import get_id_from_header, ignored_url +from django_guid.utils import get_id_from_header, ignored_regex_url, ignored_url try: from django.utils.decorators import sync_and_async_middleware @@ -28,7 +28,7 @@ def process_incoming_request(request: 'HttpRequest') -> None: Processes an incoming request. This function is called before the view and later middleware. Same logic for both async and sync views. """ - if not ignored_url(request=request): + if (not ignored_url(request=request)) and (not ignored_regex_url(request=request)): # Process request and store the GUID in a contextvar guid.set(get_id_from_header(request)) @@ -42,7 +42,7 @@ def process_outgoing_request(response: 'HttpResponse', request: 'HttpRequest') - """ Process an outgoing request. This function is called after the view and before later middleware. """ - if not ignored_url(request=request): + if (not ignored_url(request=request)) and (not ignored_regex_url(request=request)): if settings.return_header: response[settings.guid_header_name] = guid.get() # Adds the GUID to the response header if settings.expose_header: diff --git a/django_guid/utils.py b/django_guid/utils.py index c65f09f..6a48cae 100644 --- a/django_guid/utils.py +++ b/django_guid/utils.py @@ -1,4 +1,5 @@ import logging +import re import uuid from typing import TYPE_CHECKING, Optional, Union @@ -91,3 +92,20 @@ def validate_guid(original_guid: str) -> bool: return bool(uuid.UUID(original_guid, version=4).hex) except ValueError: return False + + +def ignored_regex_url(request: Union['HttpRequest', 'HttpResponse']) -> bool: + """ + Support for Regex added + Checks if the current URL is defined in the `IGNORE_REGEX_URLS` setting. + + :return: Boolean + """ + endpoint = request.path.strip('/') + + IGNORE_URLS = [] + for url in settings.ignore_regex_urls: + url_regex = url.replace('*', r'[\s\S]*') # noqa + url_regex = '^' + url_regex + '$' + IGNORE_URLS.append(re.compile(url_regex)) + return any(url.match(endpoint) for url in IGNORE_URLS) diff --git a/tests/functional/test_sync_middleware.py b/tests/functional/test_sync_middleware.py index 7a7fd3b..504fd0b 100644 --- a/tests/functional/test_sync_middleware.py +++ b/tests/functional/test_sync_middleware.py @@ -307,3 +307,27 @@ def test_url_ignored(client, caplog): ('Received signal `request_finished`, clearing guid', None), ] assert [(x.message, x.correlation_id) for x in caplog.records] == expected + + +def test_url_ignored_with_regex(client, caplog, monkeypatch): + """ + Test that a URL specified in IGNORE_URLS is ignored. + :param client: Django client + :param caplog: Caplog fixture + """ + from django.conf import settings as django_settings + + mocked_settings = deepcopy(django_settings.DJANGO_GUID) + mocked_settings['IGNORE_REGEX_URLS'] = {'no-*'} + with override_settings(DJANGO_GUID=mocked_settings): + settings = Settings() + monkeypatch.setattr('django_guid.utils.settings', settings) + client.get('/no-guid-regex', **{'HTTP_Correlation-ID': 'bad-guid'}) + # No log message should have a GUID, aka `None` on index 1. + expected = [ + ('sync middleware called', None), + ('This log message should NOT have a GUID - the URL is in IGNORE_REGEX_URLS', None), + ('Some warning in a function', None), + ('Received signal `request_finished`, clearing guid', None), + ] + assert [(x.message, x.correlation_id) for x in caplog.records] == expected