From 5accb8fbb1e7f0d1f4f81a0426fc3e33b4ef07b3 Mon Sep 17 00:00:00 2001 From: ghdtjdwn Date: Wed, 11 Nov 2026 10:29:31 +0900 Subject: [PATCH 1/2] opentelemetry-instrumentation-aiohttp-server: capture all values of repeated headers The request and response headers are multidicts, and passing them to sanitize_header_values as is kept only the last value of a header that appears more than once. Group the values by header name first. --- .../aiohttp_server/__init__.py | 19 +++-- .../tests/test_aiohttp_server_integration.py | 72 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py index 99df218557..c7422d7bc0 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py @@ -150,7 +150,7 @@ async def hello(request): from timeit import default_timer from aiohttp import web -from multidict import CIMultiDictProxy +from multidict import CIMultiDictProxy, MultiMapping from opentelemetry import metrics, trace from opentelemetry.instrumentation._semconv import ( @@ -334,6 +334,13 @@ def collect_request_attributes( return result +def _get_header_values(headers: MultiMapping[str]) -> dict[str, list[str]]: + values: dict[str, list[str]] = {} + for key, value in headers.items(): + values.setdefault(key.lower(), []).append(value) + return values + + def collect_request_headers_attributes( request: web.Request, ) -> dict[str, list[str]]: @@ -344,7 +351,7 @@ def collect_request_headers_attributes( sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) return sanitize.sanitize_header_values( - request.headers, + _get_header_values(request.headers), captured_headers, normalise_request_header_name, ) @@ -353,11 +360,15 @@ def collect_request_headers_attributes( def collect_response_headers_attributes( response: web.Response, ) -> dict[str, list[str]]: + captured_headers = get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE) + if not captured_headers: + return {} + sanitize = SanitizeValue(get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS)) return sanitize.sanitize_header_values( - response.headers, - get_custom_headers(OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE), + _get_header_values(response.headers), + captured_headers, normalise_response_header_name, ) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py index a49c72e4cc..3313b85cd3 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py @@ -561,6 +561,38 @@ async def test_custom_request_headers_control_sampling( assert len(exporter.get_finished_spans()) == expected_span_count +@pytest.mark.asyncio +async def test_custom_request_headers_repeated(test_base: TestBase, aiohttp_server, monkeypatch): + expected = { + "http.request.header.custom_test_header_1": ["test-header-value-1", "test-header-value-2"], + "http.request.header.my_secret_header": ["[REDACTED]", "[REDACTED]"], + } + sampler = CapturingSampler() + server, exporter = await _create_server_with_sampler(aiohttp_server, monkeypatch, sampler) + + try: + url = f"http://{server.host}:{server.port}/status/200" + async with aiohttp.ClientSession() as session: + headers = CIMultiDict( + [ + ("custom-test-header-1", "test-header-value-1"), + ("custom-test-header-1", "test-header-value-2"), + ("My-Secret-Header", "My Secret Value 1"), + ("My-Secret-Header", "My Secret Value 2"), + ] + ) + async with session.get(url, headers=headers) as response: + assert response.status == 200 + finally: + AioHttpServerInstrumentor().uninstrument() + + assert _request_header_attributes(sampler.attributes) == expected + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + assert _request_header_attributes(spans[0].attributes) == {key: tuple(value) for key, value in expected.items()} + + @pytest.mark.asyncio async def test_custom_response_headers(test_base: TestBase, aiohttp_server, monkeypatch): monkeypatch.setenv( @@ -617,6 +649,46 @@ async def handler(request): AioHttpServerInstrumentor().uninstrument() +@pytest.mark.asyncio +async def test_custom_response_headers_repeated(test_base: TestBase, aiohttp_server, monkeypatch): + monkeypatch.setenv( + OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, + "Custom-Test-Header-1", + ) + AioHttpServerInstrumentor().instrument() + + app = aiohttp.web.Application() + + async def handler(request): + headers = CIMultiDict( + [ + ("custom-test-header-1", "test-header-value-1"), + ("Custom-Test-Header-1", "test-header-value-2"), + ] + ) + return aiohttp.web.Response(text="hello", headers=headers) + + app.router.add_get("/status/200", handler) + + server = await aiohttp_server(app) + + url = f"http://{server.host}:{server.port}/status/200" + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + assert response.status == 200 + assert await response.text() == "hello" + + spans = test_base.get_finished_spans() + assert len(spans) == 1 + + assert spans[0].attributes.get("http.response.header.custom_test_header_1") == ( + "test-header-value-1", + "test-header-value-2", + ) + + AioHttpServerInstrumentor().uninstrument() + + # pylint: disable=too-many-locals @pytest.mark.asyncio async def test_semantic_conventions_metrics_old_default(test_base: TestBase, aiohttp_server, monkeypatch): From a332c23f8d6035549dcca281b5dcd1850c63f018 Mon Sep 17 00:00:00 2001 From: ghdtjdwn Date: Wed, 11 Nov 2026 10:31:11 +0900 Subject: [PATCH 2/2] Add changelog fragment --- .changelog/5322.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5322.fixed diff --git a/.changelog/5322.fixed b/.changelog/5322.fixed new file mode 100644 index 0000000000..26056675fc --- /dev/null +++ b/.changelog/5322.fixed @@ -0,0 +1 @@ +`opentelemetry-instrumentation-aiohttp-server`: capture all values of repeated headers