diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d22a4600..8b11e4f92b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3673)) - `opentelemetry-instrumentation-starlette`/`opentelemetry-instrumentation-fastapi`: Fixes a crash when host-based routing is used ([#3507](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3507)) +- `opentelemetry-instrumentation-django`: Fixes invalid type at WSGI request headers and attributes collection. + ([#3731](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3731)) - Fix documentation order of sections and headers for Django, Flask, MySQL, mysqlclient, psycopg, psycopg2, pymysql, sqlalchemy instrumentations. ([#3719](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3719)) - `opentelemetry-instrumentation-asgi` Fixed an issue where FastAPI reports IP instead of URL. diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index f607046959..1b7d09ee45 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -165,6 +165,21 @@ class _DjangoMiddleware(MiddlewareMixin): None ) + @staticmethod + def format_request_objects_in_headers(attributes): + for _, value_list in attributes.items(): + for index, value in enumerate(value_list): + if isinstance(value, HttpRequest): + try: + method = getattr(value, "method", "UNKNOWN") + request_path = getattr(value, "path", "UNKNOWN") + value_list[index] = ( + f"HttpRequest({method} {request_path})" + ) + except Exception: # pylint: disable=broad-exception-caught + value_list[index] = "HttpRequest(...)" + return attributes + @staticmethod def _get_span_name(request): method = sanitize_method(request.method.strip()) @@ -276,6 +291,11 @@ def process_request(self, request): custom_attributes = ( wsgi_collect_custom_request_headers_attributes(carrier) ) + # Process custom attributes to handle WSGIRequest objects + custom_attributes = self.format_request_objects_in_headers( + custom_attributes + ) + if len(custom_attributes) > 0: span.set_attributes(custom_attributes) diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 960bf97bc4..a5c9dcea1d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -20,6 +20,7 @@ from unittest.mock import Mock, patch from django import VERSION, conf +from django.core.handlers.wsgi import WSGIRequest from django.http import HttpRequest, HttpResponse from django.test.client import Client from django.test.utils import setup_test_environment, teardown_test_environment @@ -1019,6 +1020,100 @@ def tearDownClass(cls): super().tearDownClass() conf.settings = conf.LazySettings() + def test_wsgi_request_in_header_is_properly_formatted(self): + mock_wsgi_request = Mock(spec=WSGIRequest) + mock_wsgi_request.method = "GET" + mock_wsgi_request.path = "/test/path" + mock_wsgi_request.__class__.__name__ = "WSGIRequest" + + input_attributes = { + "http.request.header.test_wsgirequest_header": [mock_wsgi_request] + } + expected_attributes = { + "http.request.header.test_wsgirequest_header": [ + "HttpRequest(GET /test/path)" + ] + } + + formatted_attributes = ( + _DjangoMiddleware.format_request_objects_in_headers( + input_attributes + ) + ) + + self.assertEqual(formatted_attributes, expected_attributes) + + def test_wsgi_request_handles_extraction_error(self): + mock_wsgi_request = Mock(spec=WSGIRequest) + mock_wsgi_request.__class__.__name__ = "WSGIRequest" + + type(mock_wsgi_request).method = property( + lambda self: (_ for _ in ()).throw(ValueError("Test error")) + ) + + input_attributes = { + "http.request.header.test_wsgirequest_header": [mock_wsgi_request] + } + expected_attributes = { + "http.request.header.test_wsgirequest_header": ["HttpRequest(...)"] + } + + formatted_attributes = ( + _DjangoMiddleware.format_request_objects_in_headers( + input_attributes + ) + ) + + self.assertEqual(formatted_attributes, expected_attributes) + + def test_handles_http_request_as_well(self): + mock_http_request = Mock(spec=HttpRequest) + mock_http_request.method = "POST" + mock_http_request.path = "/api/data" + mock_http_request.__class__.__name__ = "HttpRequest" + + input_attributes = { + "http.request.header.test_httprequest_header": [mock_http_request] + } + expected_attributes = { + "http.request.header.test_httprequest_header": [ + "HttpRequest(POST /api/data)" + ] + } + + formatted_attributes = ( + _DjangoMiddleware.format_request_objects_in_headers( + input_attributes + ) + ) + + self.assertEqual(formatted_attributes, expected_attributes) + + def test_regular_header_values_are_preserved(self): + mock_wsgi_request = Mock(spec=WSGIRequest) + mock_wsgi_request.method = "GET" + mock_wsgi_request.path = "/test/path" + mock_wsgi_request.__class__.__name__ = "WSGIRequest" + + input_attributes = { + "http.request.header.test_wsgirequest_header": [mock_wsgi_request], + "http.request.header.test_regular_header": ["regular-value"], + } + expected_attributes = { + "http.request.header.test_wsgirequest_header": [ + "HttpRequest(GET /test/path)" + ], + "http.request.header.test_regular_header": ["regular-value"], + } + + formatted_attributes = ( + _DjangoMiddleware.format_request_objects_in_headers( + input_attributes + ) + ) + + self.assertEqual(formatted_attributes, expected_attributes) + def test_http_custom_request_headers_in_span_attributes(self): expected = { "http.request.header.custom_test_header_1": (