From c7f28eb000087e0c368ae979cee984d8af24031b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 23 Feb 2024 18:05:29 +0000 Subject: [PATCH 01/14] avoid loosing repeated HTTP headers --- .../instrumentation/asgi/__init__.py | 16 ++++-- .../tests/test_asgi_custom_headers.py | 55 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 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 dba7414cb1..ffa2a815f6 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -347,11 +347,17 @@ def collect_custom_headers_attributes( - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers """ # Decode headers before processing. - headers: dict[str, str] = { - _key.decode("utf8"): _value.decode("utf8") - for (_key, _value) in scope_or_response_message.get("headers") - or cast("list[tuple[bytes, bytes]]", []) - } + headers: dict[str, str] = {} + raw_headers = scope_or_response_message.get("headers") + if raw_headers: + for _key, _value in raw_headers: + key = _key.decode().lower() + value = _value.decode() + if key in headers: + headers[key] += f",{value}" + else: + headers[key] = value + return sanitize.sanitize_header_values( headers, header_regexes, diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py index c50839f72f..7a9c91a3e7 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -40,6 +40,24 @@ async def http_app_with_custom_headers(scope, receive, send): await send({"type": "http.response.body", "body": b"*"}) +async def http_app_with_repeat_headers(scope, receive, send): + message = await receive() + assert scope["type"] == "http" + if message.get("type") == "http.request": + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + (b"Content-Type", b"text/plain"), + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-1", b"test-header-value-2"), + ], + } + ) + await send({"type": "http.response.body", "body": b"*"}) + + async def websocket_app_with_custom_headers(scope, receive, send): assert scope["type"] == "websocket" while True: @@ -121,6 +139,25 @@ def test_http_custom_request_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) + def test_http_repeat_request_headers_in_span_attributes(self): + self.scope["headers"].extend( + [ + (b"custom-test-header-1", b"test-header-value-1"), + (b"custom-test-header-1", b"test-header-value-2"), + ] + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.request.header.custom_test_header_1": ( + "test-header-value-1,test-header-value-2", + ), + } + span = next(span for span in span_list if span.kind == SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_http_custom_request_headers_not_in_span_attributes(self): self.scope["headers"].extend( [ @@ -176,6 +213,24 @@ def test_http_custom_response_headers_in_span_attributes(self): if span.kind == SpanKind.SERVER: self.assertSpanHasAttributes(span, expected) + def test_http_repeat_response_headers_in_span_attributes(self): + self.app = otel_asgi.OpenTelemetryMiddleware( + http_app_with_repeat_headers, + tracer_provider=self.tracer_provider, + **self.constructor_params, + ) + self.seed_app(self.app) + self.send_default_request() + self.get_all_output() + span_list = self.exporter.get_finished_spans() + expected = { + "http.response.header.custom_test_header_1": ( + "test-header-value-1,test-header-value-2", + ), + } + span = next(span for span in span_list if span.kind == SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_http_custom_response_headers_not_in_span_attributes(self): self.app = otel_asgi.OpenTelemetryMiddleware( http_app_with_custom_headers, From 2f6a9980634c19e8371e2b792058659ae5d24eff Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 23 Feb 2024 18:35:13 +0000 Subject: [PATCH 02/14] fix fof wsgi, test in falcon --- .../tests/test_falcon.py | 20 +++++++++++++++++++ .../instrumentation/wsgi/__init__.py | 20 +++++++++++++------ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 2245dbfd80..474da0925e 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -490,6 +490,26 @@ def test_custom_request_header_added_in_server_span(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) + def test_repeated_request_header_added_in_server_span(self): + headers = [ + ("Custom-Test-Header-1", "Test Value 1"), + ("Custom-Test-Header-1", "Test Value 2"), + ] + self.client().simulate_request( + method="GET", path="/hello", headers=headers + ) + span = self.memory_exporter.get_finished_spans()[0] + assert span.status.is_ok + + expected = { + "http.request.header.custom_test_header_1": ( + "Test Value 1,Test Value 2", + ), + } + + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 87c73cc737..9fac866480 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -358,12 +358,15 @@ def collect_custom_request_headers_attributes(environ): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) + headers = {} - headers = { - key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val - for key, val in environ.items() - if key.startswith(_CARRIER_KEY_PREFIX) - } + for key, val in environ.items(): + if key.startswith(_CARRIER_KEY_PREFIX): + header_key = key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-").lower() + if header_key in headers: + headers[header_key] += "," + val + else: + headers[header_key] = val return sanitize.sanitize_header_values( headers, @@ -387,7 +390,12 @@ def collect_custom_response_headers_attributes(response_headers): ) response_headers_dict = {} if response_headers: - response_headers_dict = dict(response_headers) + for key, val in response_headers: + key = key.lower() + if key in response_headers_dict: + response_headers_dict[key] += "," + val + else: + response_headers_dict[key] = val return sanitize.sanitize_header_values( response_headers_dict, From 88a65532652f72c8347bae60ef76be0947ab7ba2 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 26 Feb 2024 10:46:20 +0000 Subject: [PATCH 03/14] add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbe579232..ede003f6d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1756](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1756)) - `opentelemetry-instrumentation-flask` Add importlib metadata default for deprecation warning flask version ([#2297](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2297)) +- Avoid losing repeated HTTP headers + ([#2266](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2266)) ## Version 1.23.0/0.44b0 (2024-02-23) From 109ac8c53f5fd3e03fb96ae62d61c3227b44302f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 26 Feb 2024 11:00:33 +0000 Subject: [PATCH 04/14] add more tests --- .../tests/base_test.py | 13 ++++++++ .../tests/test_programmatic.py | 31 ++++++++++++++++++ .../tests/test_wsgi_middleware.py | 32 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py index 6117521bb9..5ca15de1de 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py @@ -88,6 +88,16 @@ def _custom_response_headers(): resp.headers["my-secret-header"] = "my-secret-value" return resp + @staticmethod + def _repeat_custom_response_headers(): + headers = { + "content-type": "text/plain; charset=utf-8", + "my-custom-header": [ + "my-custom-value-1", "my-custom-header-2" + ], + } + return flask.Response("test response", headers=headers) + def _common_initialization(self): def excluded_endpoint(): return "excluded" @@ -106,6 +116,9 @@ def excluded2_endpoint(): self.app.route("/test_custom_response_headers")( self._custom_response_headers ) + self.app.route("/test_repeat_custom_response_headers")( + self._repeat_custom_response_headers + ) # pylint: disable=attribute-defined-outside-init self.client = Client(self.app, Response) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 3bd2f74ba8..dec265907f 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -671,6 +671,22 @@ def test_custom_request_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) + def test_repeat_custom_request_header_added_in_server_span(self): + headers = [ + ("Custom-Test-Header-1", "Test Value 1"), + ("Custom-Test-Header-1", "Test Value 2"), + ] + resp = self.client.get("/hello/123", headers=headers) + self.assertEqual(200, resp.status_code) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.request.header.custom_test_header_1": ( + "Test Value 1, Test Value 2", + ), + } + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): @@ -724,6 +740,21 @@ def test_custom_response_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) + def test_repeat_custom_response_header_added_in_server_span(self): + resp = self.client.get("/test_repeat_custom_response_headers") + self.assertEqual(resp.status_code, 200) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.response.header.content_type": ( + "text/plain; charset=utf-8", + ), + "http.response.header.my_custom_header": ( + "my-custom-value-1,my-custom-header-2", + ), + } + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_response_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index d71e584ca8..54810c2f0b 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -115,6 +115,18 @@ def wsgi_with_custom_response_headers(environ, start_response): return [b"*"] +def wsgi_with_repeat_custom_response_headers(environ, start_response): + assert isinstance(environ, dict) + start_response( + "200 OK", + [ + ("my-custom-header", "my-custom-value-1"), + ("my-custom-header", "my-custom-value-2"), + ], + ) + return [b"*"] + + _expected_metric_names = [ "http.server.active_requests", "http.server.duration", @@ -711,6 +723,26 @@ def test_custom_response_headers_not_added_in_internal_span(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) + @mock.patch.dict( + "os.environ", + { + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "my-custom-header", + }, + ) + def test_custom_response_headers_added_in_server_span(self): + app = otel_wsgi.OpenTelemetryMiddleware( + wsgi_with_repeat_custom_response_headers + ) + response = app(self.environ, self.start_response) + self.iterate_response(response) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.response.header.my_custom_header": ( + "my-custom-value-1,my-custom-value-2", + ), + } + self.assertSpanHasAttributes(span, expected) + if __name__ == "__main__": unittest.main() From d71e09349c6c963249d049e8ae137c8f4e45891e Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 26 Feb 2024 11:43:18 +0000 Subject: [PATCH 05/14] linting --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 2 +- .../opentelemetry-instrumentation-flask/tests/base_test.py | 4 +--- .../src/opentelemetry/instrumentation/wsgi/__init__.py | 4 +++- .../tests/test_wsgi_middleware.py | 2 +- 4 files changed, 6 insertions(+), 6 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 ffa2a815f6..5d4c626b3a 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -195,7 +195,7 @@ def client_response_hook(span: Span, message: dict): import urllib from functools import wraps from timeit import default_timer -from typing import Any, Awaitable, Callable, Tuple, cast +from typing import Any, Awaitable, Callable, Tuple from asgiref.compatibility import guarantee_single_callable diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py index 5ca15de1de..3c8073f261 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py @@ -92,9 +92,7 @@ def _custom_response_headers(): def _repeat_custom_response_headers(): headers = { "content-type": "text/plain; charset=utf-8", - "my-custom-header": [ - "my-custom-value-1", "my-custom-header-2" - ], + "my-custom-header": ["my-custom-value-1", "my-custom-header-2"], } return flask.Response("test response", headers=headers) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 9fac866480..81f70abc42 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -362,7 +362,9 @@ def collect_custom_request_headers_attributes(environ): for key, val in environ.items(): if key.startswith(_CARRIER_KEY_PREFIX): - header_key = key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-").lower() + header_key = ( + key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-").lower() + ) if header_key in headers: headers[header_key] += "," + val else: diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index 54810c2f0b..f74dd67867 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -729,7 +729,7 @@ def test_custom_response_headers_not_added_in_internal_span(self): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE: "my-custom-header", }, ) - def test_custom_response_headers_added_in_server_span(self): + def test_repeat_custom_response_headers_added_in_server_span(self): app = otel_wsgi.OpenTelemetryMiddleware( wsgi_with_repeat_custom_response_headers ) From 3ad758744db8a6ce052edd3562a5241c9c6b2189 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 18 Mar 2024 18:14:38 +0000 Subject: [PATCH 06/14] fix falcon and flask --- .../tests/test_falcon.py | 20 ------------------- .../tests/test_programmatic.py | 16 +++++++++++++++ .../instrumentation/wsgi/__init__.py | 16 +++++---------- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py index 474da0925e..2245dbfd80 100644 --- a/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py +++ b/instrumentation/opentelemetry-instrumentation-falcon/tests/test_falcon.py @@ -490,26 +490,6 @@ def test_custom_request_header_added_in_server_span(self): for key, _ in not_expected.items(): self.assertNotIn(key, span.attributes) - def test_repeated_request_header_added_in_server_span(self): - headers = [ - ("Custom-Test-Header-1", "Test Value 1"), - ("Custom-Test-Header-1", "Test Value 2"), - ] - self.client().simulate_request( - method="GET", path="/hello", headers=headers - ) - span = self.memory_exporter.get_finished_spans()[0] - assert span.status.is_ok - - expected = { - "http.request.header.custom_test_header_1": ( - "Test Value 1,Test Value 2", - ), - } - - self.assertEqual(span.kind, trace.SpanKind.SERVER) - self.assertSpanHasAttributes(span, expected) - def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index dec265907f..061a0d94a1 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -687,6 +687,22 @@ def test_repeat_custom_request_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) + def test_repeat_similar_custom_request_header_added_in_server_span(self): + headers = [ + ("Custom-Test-Header-1", "Test Value 1"), + ("Custom-Test-Header_1", "Test Value 2"), + ] + resp = self.client.get("/hello/123", headers=headers) + self.assertEqual(200, resp.status_code) + span = self.memory_exporter.get_finished_spans()[0] + expected = { + "http.request.header.custom_test_header_1": ( + "Test Value 1, Test Value 2", + ), + } + self.assertEqual(span.kind, trace.SpanKind.SERVER) + self.assertSpanHasAttributes(span, expected) + def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 06cbaa67d1..50d4f03dff 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -358,17 +358,11 @@ def collect_custom_request_headers_attributes(environ): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) - headers = {} - - for key, val in environ.items(): - if key.startswith(_CARRIER_KEY_PREFIX): - header_key = ( - key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-").lower() - ) - if header_key in headers: - headers[header_key] += "," + val - else: - headers[header_key] = val + headers = { + key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val + for key, val in environ.items() + if key.startswith(_CARRIER_KEY_PREFIX) + } return sanitize.sanitize_header_values( headers, From f29c5b9cd37d497b0891d9c3b9942439d608617b Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 18 Mar 2024 18:16:31 +0000 Subject: [PATCH 07/14] remove unused test --- .../tests/test_programmatic.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 061a0d94a1..dec265907f 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -687,22 +687,6 @@ def test_repeat_custom_request_header_added_in_server_span(self): self.assertEqual(span.kind, trace.SpanKind.SERVER) self.assertSpanHasAttributes(span, expected) - def test_repeat_similar_custom_request_header_added_in_server_span(self): - headers = [ - ("Custom-Test-Header-1", "Test Value 1"), - ("Custom-Test-Header_1", "Test Value 2"), - ] - resp = self.client.get("/hello/123", headers=headers) - self.assertEqual(200, resp.status_code) - span = self.memory_exporter.get_finished_spans()[0] - expected = { - "http.request.header.custom_test_header_1": ( - "Test Value 1, Test Value 2", - ), - } - self.assertEqual(span.kind, trace.SpanKind.SERVER) - self.assertSpanHasAttributes(span, expected) - def test_custom_request_header_not_added_in_internal_span(self): tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("test", kind=trace.SpanKind.SERVER): From 0fa0a36e7463e279862f8d74a5dac27683dfc85c Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 20 Mar 2024 16:04:41 +0000 Subject: [PATCH 08/14] Use a list for repeated HTTP headers --- .../instrumentation/asgi/__init__.py | 16 ++++----- .../tests/test_asgi_custom_headers.py | 6 ++-- .../tests/test_programmatic.py | 3 +- .../instrumentation/wsgi/__init__.py | 10 +++--- .../tests/test_wsgi_middleware.py | 3 +- .../src/opentelemetry/util/http/__init__.py | 36 +++++++++---------- 6 files changed, 36 insertions(+), 38 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 d28d85f975..62524b1a77 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -193,9 +193,10 @@ def client_response_hook(span: Span, message: dict): import typing import urllib +from collections import defaultdict from functools import wraps from timeit import default_timer -from typing import Any, Awaitable, Callable, Tuple +from typing import Any, Awaitable, Callable, Tuple, DefaultDict from asgiref.compatibility import guarantee_single_callable @@ -339,7 +340,7 @@ def collect_custom_headers_attributes( sanitize: SanitizeValue, header_regexes: list[str], normalize_names: Callable[[str], str], -) -> dict[str, str]: +) -> dict[str, list[str]]: """ Returns custom HTTP request or response headers to be added into SERVER span as span attributes. @@ -347,16 +348,11 @@ def collect_custom_headers_attributes( - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers """ # Decode headers before processing. - headers: dict[str, str] = {} + headers: DefaultDict[str, list[str]] = defaultdict(list) raw_headers = scope_or_response_message.get("headers") if raw_headers: - for _key, _value in raw_headers: - key = _key.decode().lower() - value = _value.decode() - if key in headers: - headers[key] += f",{value}" - else: - headers[key] = value + for key, value in raw_headers: + headers[key.decode()].append(value.decode()) return sanitize.sanitize_header_values( headers, diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py index 7a9c91a3e7..5394d62ff0 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_custom_headers.py @@ -152,7 +152,8 @@ def test_http_repeat_request_headers_in_span_attributes(self): span_list = self.exporter.get_finished_spans() expected = { "http.request.header.custom_test_header_1": ( - "test-header-value-1,test-header-value-2", + "test-header-value-1", + "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) @@ -225,7 +226,8 @@ def test_http_repeat_response_headers_in_span_attributes(self): span_list = self.exporter.get_finished_spans() expected = { "http.response.header.custom_test_header_1": ( - "test-header-value-1,test-header-value-2", + "test-header-value-1", + "test-header-value-2", ), } span = next(span for span in span_list if span.kind == SpanKind.SERVER) diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index dec265907f..16f4334108 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -749,7 +749,8 @@ def test_repeat_custom_response_header_added_in_server_span(self): "text/plain; charset=utf-8", ), "http.response.header.my_custom_header": ( - "my-custom-value-1,my-custom-header-2", + "my-custom-value-1", + "my-custom-header-2", ), } self.assertEqual(span.kind, trace.SpanKind.SERVER) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 50d4f03dff..1568271703 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -210,7 +210,9 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he import functools import typing import wsgiref.util as wsgiref_util +from collections import defaultdict from timeit import default_timer +from typing import DefaultDict from opentelemetry import context, trace from opentelemetry.instrumentation.utils import ( @@ -384,14 +386,10 @@ def collect_custom_response_headers_attributes(response_headers): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) - response_headers_dict = {} + response_headers_dict: DefaultDict[str, list[str]] = defaultdict(list) if response_headers: for key, val in response_headers: - key = key.lower() - if key in response_headers_dict: - response_headers_dict[key] += "," + val - else: - response_headers_dict[key] = val + response_headers_dict[key.lower()].append(val) return sanitize.sanitize_header_values( response_headers_dict, diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index f74dd67867..74c54f0c54 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -738,7 +738,8 @@ def test_repeat_custom_response_headers_added_in_server_span(self): span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.my_custom_header": ( - "my-custom-value-1,my-custom-value-2", + "my-custom-value-1", + "my-custom-value-2", ), } self.assertSpanHasAttributes(span, expected) diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 523f9400b1..4b9dae2e87 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -18,7 +18,7 @@ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile from re import search -from typing import Callable, Iterable, Optional +from typing import Callable, Iterable, Optional, Mapping from urllib.parse import urlparse, urlunparse from opentelemetry.semconv.trace import SpanAttributes @@ -87,32 +87,32 @@ def sanitize_header_value(self, header: str, value: str) -> str: def sanitize_header_values( self, - headers: dict[str, str], + headers: Mapping[str, str | list[str]], header_regexes: list[str], normalize_function: Callable[[str], str], - ) -> dict[str, str]: - values: dict[str, str] = {} + ) -> dict[str, list[str]]: + values: dict[str, list[str]] = {} if header_regexes: header_regexes_compiled = re_compile( - "|".join("^" + i + "$" for i in header_regexes), + "|".join(header_regexes), RE_IGNORECASE, ) - for header_name in list( - filter( - header_regexes_compiled.match, - headers.keys(), - ) - ): - header_values = headers.get(header_name) - if header_values: + for header_name, header_value in headers.items(): + if header_regexes_compiled.fullmatch(header_name): key = normalize_function(header_name.lower()) - values[key] = [ - self.sanitize_header_value( - header=header_name, value=header_values - ) - ] + if isinstance(header_value, str): + values[key] = [ + self.sanitize_header_value( + header_name, header_value + ) + ] + else: + values[key] = [ + self.sanitize_header_value(header_name, value) + for value in header_value + ] return values From 8f7ff4838457ccef21aef1b309d3a7e673450c3a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 20 Mar 2024 16:06:37 +0000 Subject: [PATCH 09/14] linting --- .../src/opentelemetry/instrumentation/asgi/__init__.py | 2 +- .../src/opentelemetry/util/http/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 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 62524b1a77..4084b4ba2a 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -196,7 +196,7 @@ def client_response_hook(span: Span, message: dict): from collections import defaultdict from functools import wraps from timeit import default_timer -from typing import Any, Awaitable, Callable, Tuple, DefaultDict +from typing import Any, Awaitable, Callable, DefaultDict, Tuple from asgiref.compatibility import guarantee_single_callable diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 4b9dae2e87..246845ef0f 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -18,7 +18,7 @@ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile from re import search -from typing import Callable, Iterable, Optional, Mapping +from typing import Callable, Iterable, Mapping, Optional from urllib.parse import urlparse, urlunparse from opentelemetry.semconv.trace import SpanAttributes From dc39259649b1cca50d474a02b47dcd11de37a17e Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 1 May 2024 12:43:50 +0100 Subject: [PATCH 10/14] add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 498eb44a2a..37f3dde9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) - `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) +- Record repeated HTTP headers in lists, rather than a comma separate strings + ([#2361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2361)) ### Added From 2ea4ebd77a58724468f2780d7946b35a45cf115d Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 1 May 2024 13:26:53 +0100 Subject: [PATCH 11/14] update docs and improve fastapi tests --- .../instrumentation/asgi/__init__.py | 8 ++-- .../instrumentation/fastapi/__init__.py | 6 +-- .../tests/test_fastapi_instrumentation.py | 42 ++++++++++++++----- .../instrumentation/starlette/__init__.py | 8 ++-- 4 files changed, 43 insertions(+), 21 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 90717f77fe..1d941edc1d 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -129,10 +129,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -163,10 +163,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index 10b73c7a5b..68a0fe4282 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -115,7 +115,7 @@ def client_response_hook(span: Span, message: dict): single item list containing all the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -146,10 +146,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 4269dfa2e4..cebf764e6e 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -11,9 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import unittest from timeit import default_timer +from typing import Mapping, Tuple from unittest.mock import patch import fastapi @@ -549,6 +549,24 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self): ) +class MultiMapping(Mapping): + + def __init__(self, *items: Tuple[str, str]): + self._items = items + + def __len__(self): + return len(self._items) + + def __getitem__(self, __key): + raise NotImplementedError('use .items() instead') + + def __iter__(self): + raise NotImplementedError('use .items() instead') + + def items(self): + return self._items + + @patch.dict( "os.environ", { @@ -575,13 +593,15 @@ def _create_app(): @app.get("/foobar") async def _(): - headers = { - "custom-test-header-1": "test-header-value-1", - "custom-test-header-2": "test-header-value-2", - "my-custom-regex-header-1": "my-custom-regex-value-1,my-custom-regex-value-2", - "My-Custom-Regex-Header-2": "my-custom-regex-value-3,my-custom-regex-value-4", - "My-Secret-Header": "My Secret Value", - } + headers = MultiMapping( + ("custom-test-header-1", "test-header-value-1"), + ("custom-test-header-2", "test-header-value-2"), + ("my-custom-regex-header-1", "my-custom-regex-value-1"), + ("my-custom-regex-header-1", "my-custom-regex-value-2"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-3"), + ("My-Custom-Regex-Header-2", "my-custom-regex-value-4"), + ("My-Secret-Header", "My Secret Value"), + ) content = {"message": "hello world"} return JSONResponse(content=content, headers=headers) @@ -657,10 +677,12 @@ def test_http_custom_response_headers_in_span_attributes(self): "test-header-value-2", ), "http.response.header.my_custom_regex_header_1": ( - "my-custom-regex-value-1,my-custom-regex-value-2", + "my-custom-regex-value-1", + "my-custom-regex-value-2", ), "http.response.header.my_custom_regex_header_2": ( - "my-custom-regex-value-3,my-custom-regex-value-4", + "my-custom-regex-value-3", + "my-custom-regex-value-4", ), "http.response.header.my_secret_header": ("[REDACTED]",), } diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 1ebc3348d4..f8e9ed5515 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -108,10 +108,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.request.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.request.header.custom_request_header = [","]`` +``http.request.header.custom_request_header = ["", ""]`` Response headers **************** @@ -142,10 +142,10 @@ def client_response_hook(span: Span, message: dict): The name of the added span attribute will follow the format ``http.response.header.`` where ```` is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a -single item list containing all the header values. +list containing the header values. For example: -``http.response.header.custom_response_header = [","]`` +``http.response.header.custom_response_header = ["", ""]`` Sanitizing headers ****************** From 94ad9c18f6ad05799752de1ab98abdffefe9ee6f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 1 May 2024 13:29:52 +0100 Subject: [PATCH 12/14] revert changes in wsgi based webframeworks --- CHANGELOG.md | 3 ++- .../tests/test_programmatic.py | 3 +-- .../src/opentelemetry/instrumentation/wsgi/__init__.py | 10 ++++++---- .../tests/test_wsgi_middleware.py | 3 +-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37f3dde9ce..2375eaf1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2425](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2425)) - `opentelemetry-instrumentation-flask` Add `http.method` to `span.name` ([#2454](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2454)) -- Record repeated HTTP headers in lists, rather than a comma separate strings +- Record repeated HTTP headers in lists, rather than a comma separate strings for ASGI based web frameworks ([#2361](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2361)) ### Added @@ -75,6 +75,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - AwsLambdaInstrumentor sets `cloud.account.id` span attribute ([#2367](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2367)) + ## Version 1.23.0/0.44b0 (2024-02-23) - Drop support for 3.7 diff --git a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py index 86ff3a78cd..d30a100b0e 100644 --- a/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py +++ b/instrumentation/opentelemetry-instrumentation-flask/tests/test_programmatic.py @@ -1003,8 +1003,7 @@ def test_repeat_custom_response_header_added_in_server_span(self): "text/plain; charset=utf-8", ), "http.response.header.my_custom_header": ( - "my-custom-value-1", - "my-custom-header-2", + "my-custom-value-1,my-custom-header-2", ), } self.assertEqual(span.kind, trace.SpanKind.SERVER) diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py index 19065f7bcc..0a873d0fc3 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/src/opentelemetry/instrumentation/wsgi/__init__.py @@ -210,9 +210,7 @@ def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_he import functools import typing import wsgiref.util as wsgiref_util -from collections import defaultdict from timeit import default_timer -from typing import DefaultDict from opentelemetry import context, trace from opentelemetry.instrumentation._semconv import ( @@ -422,10 +420,14 @@ def collect_custom_response_headers_attributes(response_headers): OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ) - response_headers_dict: DefaultDict[str, list[str]] = defaultdict(list) + response_headers_dict = {} if response_headers: for key, val in response_headers: - response_headers_dict[key.lower()].append(val) + key = key.lower() + if key in response_headers_dict: + response_headers_dict[key] += "," + val + else: + response_headers_dict[key] = val return sanitize.sanitize_header_values( response_headers_dict, diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py index ed4afbd44e..2b26cbb5f9 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/test_wsgi_middleware.py @@ -1032,8 +1032,7 @@ def test_repeat_custom_response_headers_added_in_server_span(self): span = self.memory_exporter.get_finished_spans()[0] expected = { "http.response.header.my_custom_header": ( - "my-custom-value-1", - "my-custom-value-2", + "my-custom-value-1,my-custom-value-2", ), } self.assertSpanHasAttributes(span, expected) From 299ea998c395734b3664acb89936b02f7d60f877 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 1 May 2024 13:50:05 +0100 Subject: [PATCH 13/14] fix linting --- .../tests/test_fastapi_instrumentation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index cebf764e6e..135a12d412 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -558,10 +558,10 @@ def __len__(self): return len(self._items) def __getitem__(self, __key): - raise NotImplementedError('use .items() instead') + raise NotImplementedError("use .items() instead") def __iter__(self): - raise NotImplementedError('use .items() instead') + raise NotImplementedError("use .items() instead") def items(self): return self._items From fa3683f1f9f30b5fe4c8d2b5601adcaaf4712ff8 Mon Sep 17 00:00:00 2001 From: Diego Hurtado Date: Tue, 14 May 2024 15:49:18 -0500 Subject: [PATCH 14/14] Fix import path of typing symbols --- .../tests/test_fastapi_instrumentation.py | 3 ++- .../src/opentelemetry/util/http/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 135a12d412..a65e002895 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import unittest +from collections.abc import Mapping from timeit import default_timer -from typing import Mapping, Tuple +from typing import Tuple from unittest.mock import patch import fastapi diff --git a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py index 300c85721d..4a23195b65 100644 --- a/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py +++ b/util/opentelemetry-util-http/src/opentelemetry/util/http/__init__.py @@ -14,11 +14,12 @@ from __future__ import annotations +from collections.abc import Mapping from os import environ from re import IGNORECASE as RE_IGNORECASE from re import compile as re_compile from re import search -from typing import Callable, Iterable, Mapping, Optional +from typing import Callable, Iterable, Optional from urllib.parse import urlparse, urlunparse from opentelemetry.semconv.trace import SpanAttributes