diff --git a/CHANGELOG.md b/CHANGELOG.md index 024990c91d..5c3a9b9cfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-instrumentation-aiohttp-server`: Use `canonical` attribute of the `Resource` as a span name. + ## Version 1.38.0/0.59b0 (2025-10-16) ### Fixed 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 1074298fc7..4de1da843d 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 @@ -123,6 +123,10 @@ def get_default_span_details(request: web.Request) -> Tuple[str, dict]: a tuple of the span name, and any attributes to attach to the span. """ span_name = request.path.strip() or f"HTTP {request.method}" + if request.match_info and request.match_info.route.resource: + resource = request.match_info.route.resource + if resource.canonical: + span_name = resource.canonical return span_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 b85348c18b..ba10d01e82 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 @@ -27,6 +27,7 @@ from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_METHOD, HTTP_STATUS_CODE, + HTTP_TARGET, HTTP_URL, ) from opentelemetry.test.globals_test import reset_trace_globals @@ -81,7 +82,15 @@ async def fixture_server_fixture(tracer, aiohttp_server, suppress): AioHttpServerInstrumentor().instrument() app = aiohttp.web.Application() - app.add_routes([aiohttp.web.get("/test-path", default_handler)]) + app.add_routes( + [ + aiohttp.web.get("/test-path", default_handler), + aiohttp.web.get("/test-path/{url_param}", default_handler), + aiohttp.web.get( + "/object/{object_id}/action/{another_param}", default_handler + ), + ] + ) if suppress: with suppress_http_instrumentation(): server = await aiohttp_server(app) @@ -139,6 +148,53 @@ async def test_status_code_instrumentation( ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "url, example_paths", + [ + ( + "/test-path/{url_param}", + ( + "/test-path/foo", + "/test-path/bar", + ), + ), + ( + "/object/{object_id}/action/{another_param}", + ( + "/object/1/action/bar", + "/object/234/action/baz", + ), + ), + ], +) +async def test_url_params_instrumentation( + tracer, + server_fixture, + aiohttp_client, + url, + example_paths, +): + _, memory_exporter = tracer + server, _ = server_fixture + + assert len(memory_exporter.get_finished_spans()) == 0 + + client = await aiohttp_client(server) + for path in example_paths: + await client.get(path) + + assert len(memory_exporter.get_finished_spans()) == 2 + + for request_path, span in zip( + example_paths, memory_exporter.get_finished_spans() + ): + assert url == span.name + assert request_path == span.attributes[HTTP_TARGET] + full_url = f"http://{server.host}:{server.port}{request_path}" + assert full_url == span.attributes[HTTP_URL] + + @pytest.mark.asyncio @pytest.mark.parametrize("suppress", [True]) async def test_suppress_instrumentation(