Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5322.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-instrumentation-aiohttp-server`: capture all values of repeated headers
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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]]:
Expand All @@ -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,
)
Expand All @@ -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,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand Down
Loading