From 9c37fd47f9554d99ee5f2046e5ef982918300418 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 31 Mar 2022 18:47:49 +0530 Subject: [PATCH 1/8] Code changes for https://github.com/open-telemetry/opentelemetry-python-contrib/issues/913 --- CHANGELOG.md | 6 +- .../instrumentation/asgi/__init__.py | 12 +- .../instrumentation/django/__init__.py | 37 ++++++ .../instrumentation/django/middleware.py | 39 +++++- .../tests/test_middleware.py | 113 +++++++++++++++- .../tests/test_middleware_asgi.py | 123 +++++++++++++++++- .../tests/views.py | 14 ++ 7 files changed, 327 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62122d051..5a8b097fab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) ### Added - +- `opentelemetry-instrumentation-asgi` Capture custom request/response headers in span attributes + ([#1004])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004) - `opentelemetry-instrumentation-psycopg2` extended the sql commenter support of dbapi into psycopg2 ([#940](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/940)) - `opentelemetry-instrumentation-flask` Fix non-recording span bug @@ -22,9 +23,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.10.0-0.29b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.10.0-0.29b0) - 2022-03-10 -- `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes - ([#1004])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004) - - `opentelemetry-instrumentation-wsgi` Capture custom request/response headers in span attributes ([#925])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/925) - `opentelemetry-instrumentation-flask` Flask: Capture custom request/response headers in span attributes diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index d8932da996..cfa8c5291f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -149,11 +149,13 @@ def get( # asgi header keys are in lower case key = key.lower() - decoded = [ - _value.decode("utf8") - for (_key, _value) in headers - if _key.decode("utf8") == key - ] + decoded = [] + for _key, _value in headers: + if not isinstance(_key, str) and _key.decode("utf8") == key: + decoded.append(_value.decode("utf8")) + elif isinstance(_key, str) and _key == key: + decoded.append(_value) + if not decoded: return None return decoded diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index d5e1f07279..9c1a60b6d8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -76,6 +76,43 @@ def response_hook(span, request, response): Django Request object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httprequest-objects Django Response object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httpresponse-objects +Capture HTTP request and response headers +***************************************** +You can configure the agent to capture predefined HTTP headers as span attributes, according to the `semantic convention `_. + +Request headers +*************** +To capture predefined HTTP request headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` +to a comma-separated list of HTTP header names. + +For example, +:: + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content_type,custom_request_header" +will extract content_type and custom_request_header from request headers and add them as span attributes. + +The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.request.header.custom_request_header = [","]`` + +Response headers +**************** +To capture predefined HTTP response headers as span attributes, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` +to a comma-separated list of HTTP header names. + +For example, +:: + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content_type,custom_response_header" + +will extract content_type and custom_response_header from response headers and add them as span attributes. + +The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). +The value of the attribute will be single item list containing all the header values. + +Example of the added span attribute, +``http.response.header.custom_response_header = [","]`` + API --- """ diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 6d756c665a..f5817e2a83 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -28,13 +28,16 @@ _start_internal_or_server_span, extract_attributes_from_object, ) -from opentelemetry.instrumentation.wsgi import add_response_attributes from opentelemetry.instrumentation.wsgi import ( collect_request_attributes as wsgi_collect_request_attributes, + add_custom_request_headers as wsgi_add_custom_request_headers, + add_custom_response_headers as wsgi_add_custom_response_headers, + add_response_attributes, + wsgi_getter, ) -from opentelemetry.instrumentation.wsgi import wsgi_getter + from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import Span, use_span +from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs try: @@ -77,12 +80,13 @@ def __call__(self, request): # try/except block exclusive for optional ASGI imports. try: - from opentelemetry.instrumentation.asgi import asgi_getter from opentelemetry.instrumentation.asgi import ( collect_request_attributes as asgi_collect_request_attributes, + collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes, + collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes, + set_status_code, + asgi_getter, ) - from opentelemetry.instrumentation.asgi import set_status_code - _is_asgi_supported = True except ImportError: asgi_getter = None @@ -213,6 +217,13 @@ def process_request(self, request): self._traced_request_attrs, attributes, ) + if span.is_recording() and span.kind == SpanKind.SERVER: + attributes.update( + asgi_collect_custom_request_attributes(carrier) + ) + else: + if span.is_recording() and span.kind == SpanKind.SERVER: + wsgi_add_custom_request_headers(span, carrier) for key, value in attributes.items(): span.set_attribute(key, value) @@ -271,12 +282,28 @@ def process_response(self, request, response): if activation and span: if is_asgi_request: set_status_code(span, response.status_code) + if span.is_recording() and span.kind == SpanKind.SERVER: + """ + asgi_getter inside asgi_collect_custom_response_attributes + requires 'headers' key to fetch the actuals headers where + Django response object has 'headers' properties. + Thats why I have to create a separate dict with 'headers' + key to make it compatible with asgi_getter + """ + custom_headers = {"headers": response.items()} + custom_res_attributes = ( + asgi_collect_custom_response_attributes(custom_headers) + ) + for key, value in custom_res_attributes.items(): + span.set_attribute(key, value) else: add_response_attributes( span, f"{response.status_code} {response.reason_phrase}", response.items(), ) + if span.is_recording() and span.kind == SpanKind.SERVER: + wsgi_add_custom_response_headers(span, response.items()) propagator = get_global_response_propagator() if propagator: diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py index 32bd1d03fa..855cf3e389 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py @@ -43,7 +43,12 @@ format_span_id, format_trace_id, ) -from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + get_excluded_urls, + get_traced_request_attrs, +) # pylint: disable=import-error from .views import ( @@ -51,6 +56,7 @@ excluded, excluded_noarg, excluded_noarg2, + response_with_custom_header, route_span_name, traced, traced_template, @@ -67,6 +73,7 @@ urlpatterns = [ re_path(r"^traced/", traced), + re_path(r"^traced_custom_header/", response_with_custom_header), re_path(r"^route/(?P[0-9]{4})/template/$", traced_template), re_path(r"^error/", error), re_path(r"^excluded_arg/", excluded), @@ -451,3 +458,107 @@ def test_django_with_wsgi_instrumented(self): parent_span.get_span_context().span_id, span_list[0].parent.span_id, ) + + +class TestMiddlewareWsgiWithCustomHeaders(TestBase, WsgiTestBase): + @classmethod + def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + super().setUpClass() + + def setUp(self): + super().setUp() + setup_test_environment() + tracer_provider, exporter = self.create_tracer_provider() + self.exporter = exporter + _django_instrumentor.instrument(tracer_provider=tracer_provider) + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + + def tearDown(self): + super().tearDown() + self.env_patch.stop() + teardown_test_environment() + _django_instrumentor.uninstrument() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + + def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + Client( + HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1", + HTTP_CUSTOM_TEST_HEADER_2="test-header-value-2", + ).get("/traced/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + Client(HTTP_CUSTOM_TEST_HEADER_1="test-header-value-1").get("/traced/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() + + def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + Client().get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + Client().get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index baf1a92894..ffe7bbc69d 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -15,6 +15,7 @@ # pylint: disable=E0611 from sys import modules +from unittest import mock from unittest.mock import Mock, patch import pytest @@ -42,7 +43,12 @@ format_span_id, format_trace_id, ) -from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs +from opentelemetry.util.http import ( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + get_excluded_urls, + get_traced_request_attrs, +) # pylint: disable=import-error from .views import ( @@ -53,6 +59,7 @@ async_route_span_name, async_traced, async_traced_template, + async_with_custom_header, ) DJANGO_2_0 = VERSION >= (2, 0) @@ -65,6 +72,7 @@ urlpatterns = [ re_path(r"^traced/", async_traced), + re_path(r"^traced_custom_header/", async_with_custom_header), re_path(r"^route/(?P[0-9]{4})/template/$", async_traced_template), re_path(r"^error/", async_error), re_path(r"^excluded_arg/", async_excluded), @@ -415,3 +423,116 @@ async def test_tracer_provider_traced(self): self.assertEqual( span.resource.attributes["resource-key"], "resource-value" ) + + +class TestMiddlewareAsgiWithCustomHeaders(SimpleTestCase, TestBase): + @classmethod + def setUpClass(cls): + conf.settings.configure(ROOT_URLCONF=modules[__name__]) + super().setUpClass() + + def setUp(self): + super().setUp() + setup_test_environment() + + tracer_provider, exporter = self.create_tracer_provider() + self.exporter = exporter + _django_instrumentor.instrument(tracer_provider=tracer_provider) + self.env_patch = patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "Custom-Test-Header-1,Custom-Test-Header-2,Custom-Test-Header-3", + }, + ) + self.env_patch.start() + + def tearDown(self): + super().tearDown() + self.env_patch.stop() + teardown_test_environment() + _django_instrumentor.uninstrument() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + conf.settings = conf.LazySettings() + + async def test_http_custom_request_headers_in_span_attributes(self): + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + await self.async_client.get( + "/traced/", + **{ + "custom-test-header-1": "test-header-value-1", + "custom-test-header-2": "test-header-value-2", + }, + ) + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + async def test_http_custom_request_headers_not_in_span_attributes(self): + not_expected = { + "http.request.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + await self.async_client.get( + "/traced/", + **{ + "custom-test-header-1": "test-header-value-1", + }, + ) + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() + + async def test_http_custom_response_headers_in_span_attributes(self): + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1", + ), + "http.response.header.custom_test_header_2": ( + "test-header-value-2", + ), + } + await self.async_client.get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + self.memory_exporter.clear() + + async def test_http_custom_response_headers_not_in_span_attributes(self): + not_expected = { + "http.response.header.custom_test_header_3": ( + "test-header-value-3", + ), + } + await self.async_client.get("/traced_custom_header/") + spans = self.exporter.get_finished_spans() + self.assertEqual(len(spans), 1) + + span = spans[0] + self.assertEqual(span.kind, SpanKind.SERVER) + for key, _ in not_expected.items(): + self.assertNotIn(key, span.attributes) + self.memory_exporter.clear() diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 0bcc7e95be..18147baa50 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -31,6 +31,13 @@ def route_span_name( return HttpResponse() +def response_with_custom_header(request): + response = HttpResponse() + response.headers["custom-test-header-1"] = "test-header-value-1" + response.headers["custom-test-header-2"] = "test-header-value-2" + return response + + async def async_traced(request): # pylint: disable=unused-argument return HttpResponse() @@ -61,3 +68,10 @@ async def async_route_span_name( request, *args, **kwargs ): # pylint: disable=unused-argument return HttpResponse() + + +async def async_with_custom_header(request): + response = HttpResponse() + response.headers["custom-test-header-1"] = "test-header-value-1" + response.headers["custom-test-header-2"] = "test-header-value-2" + return response From 2f51696efc3dbe6fd0aa564a1ce245539f1448a5 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 31 Mar 2022 18:59:02 +0530 Subject: [PATCH 2/8] added entry in changlog.md file --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8b097fab..1ab82330fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.10.0-0.29b0...HEAD) ### Added +- `opentelemetry-instrumentation-django` Capture custom request/response headers in span attributes + ([#1024])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1024) - `opentelemetry-instrumentation-asgi` Capture custom request/response headers in span attributes ([#1004])(https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1004) - `opentelemetry-instrumentation-psycopg2` extended the sql commenter support of dbapi into psycopg2 From 50c4e395ebdf6f68a506cdef6f4dbbe36dd62cfb Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 31 Mar 2022 19:41:15 +0530 Subject: [PATCH 3/8] changes according to lint and generate command --- .../instrumentation/django/middleware.py | 22 +++++++++++++------ .../tests/test_middleware_asgi.py | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index f5817e2a83..7b34d969b7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -29,13 +29,16 @@ extract_attributes_from_object, ) from opentelemetry.instrumentation.wsgi import ( - collect_request_attributes as wsgi_collect_request_attributes, add_custom_request_headers as wsgi_add_custom_request_headers, +) +from opentelemetry.instrumentation.wsgi import ( add_custom_response_headers as wsgi_add_custom_response_headers, - add_response_attributes, - wsgi_getter, ) - +from opentelemetry.instrumentation.wsgi import add_response_attributes +from opentelemetry.instrumentation.wsgi import ( + collect_request_attributes as wsgi_collect_request_attributes, +) +from opentelemetry.instrumentation.wsgi import wsgi_getter from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs @@ -80,13 +83,18 @@ def __call__(self, request): # try/except block exclusive for optional ASGI imports. try: + from opentelemetry.instrumentation.asgi import asgi_getter from opentelemetry.instrumentation.asgi import ( - collect_request_attributes as asgi_collect_request_attributes, collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes, + ) + from opentelemetry.instrumentation.asgi import ( collect_custom_response_headers_attributes as asgi_collect_custom_response_attributes, - set_status_code, - asgi_getter, ) + from opentelemetry.instrumentation.asgi import ( + collect_request_attributes as asgi_collect_request_attributes, + ) + from opentelemetry.instrumentation.asgi import set_status_code + _is_asgi_supported = True except ImportError: asgi_getter = None diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py index ffe7bbc69d..14a1ce82a9 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py @@ -15,7 +15,6 @@ # pylint: disable=E0611 from sys import modules -from unittest import mock from unittest.mock import Mock, patch import pytest From a90d8f4ac1bc81743dd1ec8b70a6770626ffd89e Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 31 Mar 2022 22:19:21 +0530 Subject: [PATCH 4/8] resolved lint comments --- .../instrumentation/django/middleware.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 7b34d969b7..9f95323a71 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -276,6 +276,7 @@ def process_exception(self, request, exception): if self._environ_activation_key in request.META.keys(): request.META[self._environ_exception_key] = exception + # pylint: disable=too-many-branches def process_response(self, request, response): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return response @@ -291,13 +292,12 @@ def process_response(self, request, response): if is_asgi_request: set_status_code(span, response.status_code) if span.is_recording() and span.kind == SpanKind.SERVER: - """ - asgi_getter inside asgi_collect_custom_response_attributes - requires 'headers' key to fetch the actuals headers where - Django response object has 'headers' properties. - Thats why I have to create a separate dict with 'headers' - key to make it compatible with asgi_getter - """ + # asgi_getter inside asgi_collect_custom_response_attributes + # requires 'headers' key to fetch the actuals headers where + # Django response object has 'headers' properties. + # Thats why I have to create a separate dict with 'headers' + # key to make it compatible with asgi_getter + custom_headers = {"headers": response.items()} custom_res_attributes = ( asgi_collect_custom_response_attributes(custom_headers) From 71b2d51e6e591e32a3452c9c78a78ad7c4633d24 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Thu, 31 Mar 2022 22:35:53 +0530 Subject: [PATCH 5/8] resolved docs errors and change the way to set headers in http response in django tests --- .../src/opentelemetry/instrumentation/django/__init__.py | 3 +++ .../opentelemetry-instrumentation-django/tests/views.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 9c1a60b6d8..38e109cec7 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -87,7 +87,9 @@ def response_hook(span, request, response): For example, :: + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content_type,custom_request_header" + will extract content_type and custom_request_header from request headers and add them as span attributes. The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). @@ -103,6 +105,7 @@ def response_hook(span, request, response): For example, :: + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content_type,custom_response_header" will extract content_type and custom_response_header from response headers and add them as span attributes. diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py index 18147baa50..f97933cfd8 100644 --- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py +++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py @@ -33,8 +33,8 @@ def route_span_name( def response_with_custom_header(request): response = HttpResponse() - response.headers["custom-test-header-1"] = "test-header-value-1" - response.headers["custom-test-header-2"] = "test-header-value-2" + response["custom-test-header-1"] = "test-header-value-1" + response["custom-test-header-2"] = "test-header-value-2" return response From 2a3e6d60354d0cfa883d9d4e78d9e8c35fa14df9 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 1 Apr 2022 22:46:28 +0530 Subject: [PATCH 6/8] adding the recommandation info for custom request and response headers --- .../src/opentelemetry/instrumentation/django/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py index 38e109cec7..ad6fa7bf36 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py @@ -92,6 +92,9 @@ def response_hook(span, request, response): will extract content_type and custom_request_header from request headers and add them as span attributes. +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Request header names in django are case insensitive. So, giving header name as ``CUStom_Header`` in environment variable will be able capture header with name ``custom-header``. + The name of the added span attribute will follow the format ``http.request.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). The value of the attribute will be single item list containing all the header values. @@ -110,6 +113,9 @@ def response_hook(span, request, response): will extract content_type and custom_response_header from response headers and add them as span attributes. +It is recommended that you should give the correct names of the headers to be captured in the environment variable. +Response header names captured in django are case insensitive. So, giving header name as ``CUStomHeader`` in environment variable will be able capture header with name ``customheader``. + The name of the added span attribute will follow the format ``http.response.header.`` where ```` being the normalized HTTP header name (lowercase, with - characters replaced by _ ). The value of the attribute will be single item list containing all the header values. From ecf4f868f137736783f4b00512390c8c62222a63 Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Fri, 1 Apr 2022 23:50:53 +0530 Subject: [PATCH 7/8] Removed unnecesary comments --- .../src/opentelemetry/instrumentation/django/middleware.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 9f95323a71..09c6ff51c1 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -291,13 +291,8 @@ def process_response(self, request, response): if activation and span: if is_asgi_request: set_status_code(span, response.status_code) - if span.is_recording() and span.kind == SpanKind.SERVER: - # asgi_getter inside asgi_collect_custom_response_attributes - # requires 'headers' key to fetch the actuals headers where - # Django response object has 'headers' properties. - # Thats why I have to create a separate dict with 'headers' - # key to make it compatible with asgi_getter + if span.is_recording() and span.kind == SpanKind.SERVER: custom_headers = {"headers": response.items()} custom_res_attributes = ( asgi_collect_custom_response_attributes(custom_headers) From 7aa8b1d55ea83ce61caf1f38c784d8468b906afa Mon Sep 17 00:00:00 2001 From: sanket Mehta Date: Tue, 5 Apr 2022 17:41:06 +0530 Subject: [PATCH 8/8] Using Asgi_setter to collect custom headers from response object --- .../opentelemetry/instrumentation/asgi/__init__.py | 12 +++++------- .../instrumentation/django/middleware.py | 7 +++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py index cfa8c5291f..d8932da996 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -149,13 +149,11 @@ def get( # asgi header keys are in lower case key = key.lower() - decoded = [] - for _key, _value in headers: - if not isinstance(_key, str) and _key.decode("utf8") == key: - decoded.append(_value.decode("utf8")) - elif isinstance(_key, str) and _key == key: - decoded.append(_value) - + decoded = [ + _value.decode("utf8") + for (_key, _value) in headers + if _key.decode("utf8") == key + ] if not decoded: return None return decoded diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py index 09c6ff51c1..4524caf5a1 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py @@ -83,7 +83,7 @@ def __call__(self, request): # try/except block exclusive for optional ASGI imports. try: - from opentelemetry.instrumentation.asgi import asgi_getter + from opentelemetry.instrumentation.asgi import asgi_getter, asgi_setter from opentelemetry.instrumentation.asgi import ( collect_custom_request_headers_attributes as asgi_collect_custom_request_attributes, ) @@ -293,7 +293,10 @@ def process_response(self, request, response): set_status_code(span, response.status_code) if span.is_recording() and span.kind == SpanKind.SERVER: - custom_headers = {"headers": response.items()} + custom_headers = {} + for key, value in response.items(): + asgi_setter.set(custom_headers, key, value) + custom_res_attributes = ( asgi_collect_custom_response_attributes(custom_headers) )