Skip to content

Commit 94e851d

Browse files
authored
opentelemetry-instrumentation-aiohttp-server: fix span name and http.route attribute according to semconv (open-telemetry#4768)
* opentelemetry-instrumentation-aiohttp-server: fix span and http.route according to semconv Do the method sanitization in the span name and use the canonical path also for http.route instead of the handler name. * Add changelog * Use PURGE instead of QUERY
1 parent d149d6a commit 94e851d

3 files changed

Lines changed: 52 additions & 23 deletions

File tree

.changelog/4768.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-instrumentation-aiohttp-server`: fix span name and `http.route` attribute according to http semantic conventions

instrumentation/opentelemetry-instrumentation-aiohttp-server/src/opentelemetry/instrumentation/aiohttp_server/__init__.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -247,28 +247,29 @@ def get_default_span_name(request: web.Request) -> str:
247247
Returns:
248248
The span name as "{method} {canonical_name}" of a resource if possible or just "{method}".
249249
"""
250-
try:
251-
resource = request.match_info.route.resource
252-
path = resource.canonical
253-
except AttributeError:
254-
path = ""
255-
250+
path = _get_canonical_path(request)
251+
method = sanitize_method(request.method)
252+
if method == "_OTHER":
253+
method = "HTTP"
256254
if path:
257-
return f"{request.method} {path}"
258-
return f"{request.method}"
255+
return f"{method} {path}"
256+
return f"{method}"
259257

260258

261-
def _get_view_func(request: web.Request) -> str:
262-
"""Returns the name of the request handler.
259+
def _get_canonical_path(request: web.Request) -> str:
260+
"""Returns the canonical path from the request handler.
263261
Args:
264262
request: the request object itself.
265263
Returns:
266-
a string containing the name of the handler function
264+
a string containing the canonical path
267265
"""
266+
268267
try:
269-
return request.match_info.handler.__name__
268+
resource = request.match_info.route.resource
269+
path = resource.canonical
270270
except AttributeError:
271-
return "unknown"
271+
path = ""
272+
return path
272273

273274

274275
def collect_request_attributes(
@@ -323,7 +324,7 @@ def collect_request_attributes(
323324
_set_http_flavor_version(result, flavor, sem_conv_opt_in_mode)
324325

325326
# http.route for both old and new
326-
result[HTTP_ROUTE] = _get_view_func(request)
327+
result[HTTP_ROUTE] = _get_canonical_path(request)
327328

328329
if _report_old(sem_conv_opt_in_mode):
329330
http_host_value_list = (

instrumentation/opentelemetry-instrumentation-aiohttp-server/tests/test_aiohttp_server_integration.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,30 @@ async def test_url_params_instrumentation(
249249
assert full_url == span.attributes[HTTP_URL]
250250

251251

252+
@pytest.mark.asyncio
253+
async def test_span_name_handler_unknown_method(
254+
tracer,
255+
server_fixture,
256+
):
257+
_, memory_exporter = tracer
258+
server, _ = server_fixture
259+
260+
assert len(memory_exporter.get_finished_spans()) == 0
261+
262+
async with aiohttp.ClientSession(
263+
base_url=f"http://{server.host}:{server.port}"
264+
) as session:
265+
# PURGE is accepted by aiohttp but not recognized by sanitize_method.
266+
async with session.request("PURGE", "/test-path") as response:
267+
assert response.status == HTTPStatus.METHOD_NOT_ALLOWED
268+
269+
spans = memory_exporter.get_finished_spans()
270+
assert len(spans) == 1
271+
272+
assert "_OTHER" == spans[0].attributes[HTTP_METHOD]
273+
assert "HTTP" == spans[0].name
274+
275+
252276
@pytest.mark.asyncio
253277
@pytest.mark.parametrize("suppress", [True])
254278
async def test_suppress_instrumentation(
@@ -563,11 +587,11 @@ async def test_semantic_conventions_metrics_old_default(
563587

564588
AioHttpServerInstrumentor().instrument()
565589
app = aiohttp.web.Application()
566-
app.router.add_get("/test-path", default_handler)
590+
app.router.add_get("/test-path/{param}", default_handler)
567591
server = await aiohttp_server(app)
568592
client_session = aiohttp.ClientSession()
569593
try:
570-
url = f"http://{server.host}:{server.port}/test-path?query=test"
594+
url = f"http://{server.host}:{server.port}/test-path/test-param?query=test"
571595
async with client_session.get( # pylint: disable=not-async-context-manager
572596
url, headers={"User-Agent": "test-agent"}
573597
) as response:
@@ -587,11 +611,14 @@ async def test_semantic_conventions_metrics_old_default(
587611
assert span.attributes.get(HTTP_SCHEME) == "http"
588612
assert span.attributes.get(NET_HOST_NAME) == server.host
589613
assert span.attributes.get(NET_HOST_PORT) == server.port
590-
assert span.attributes.get(HTTP_TARGET) == "/test-path?query=test"
614+
assert (
615+
span.attributes.get(HTTP_TARGET)
616+
== "/test-path/test-param?query=test"
617+
)
591618
assert span.attributes.get(HTTP_USER_AGENT) == "test-agent"
592619
assert span.attributes.get(HTTP_FLAVOR) == "1.1"
593620
assert span.attributes.get(HTTP_STATUS_CODE) == 200
594-
assert span.attributes.get(HTTP_ROUTE) == "default_handler"
621+
assert span.attributes.get(HTTP_ROUTE) == "/test-path/{param}"
595622
# New semconv span attributes NOT present
596623
assert HTTP_REQUEST_METHOD not in span.attributes
597624
assert URL_SCHEME not in span.attributes
@@ -636,11 +663,11 @@ async def test_semantic_conventions_metrics_new(
636663

637664
AioHttpServerInstrumentor().instrument()
638665
app = aiohttp.web.Application()
639-
app.router.add_get("/test-path", default_handler)
666+
app.router.add_get("/test-path/{param}", default_handler)
640667
server = await aiohttp_server(app)
641668
client_session = aiohttp.ClientSession()
642669
try:
643-
url = f"http://{server.host}:{server.port}/test-path?query=test"
670+
url = f"http://{server.host}:{server.port}/test-path/test-param?query=test"
644671
async with client_session.get( # pylint: disable=not-async-context-manager
645672
url, headers={"User-Agent": "test-agent"}
646673
) as response:
@@ -660,12 +687,12 @@ async def test_semantic_conventions_metrics_new(
660687
assert span.attributes.get(URL_SCHEME) == "http"
661688
assert span.attributes.get(SERVER_ADDRESS) == server.host
662689
assert span.attributes.get(SERVER_PORT) == server.port
663-
assert span.attributes.get(URL_PATH) == "/test-path"
690+
assert span.attributes.get(URL_PATH) == "/test-path/test-param"
664691
assert span.attributes.get(URL_QUERY) == "query=test"
665692
assert span.attributes.get(USER_AGENT_ORIGINAL) == "test-agent"
666693
assert span.attributes.get(NETWORK_PROTOCOL_VERSION) == "1.1"
667694
assert span.attributes.get(HTTP_RESPONSE_STATUS_CODE) == 200
668-
assert span.attributes.get(HTTP_ROUTE) == "default_handler"
695+
assert span.attributes.get(HTTP_ROUTE) == "/test-path/{param}"
669696
# Old semconv span attributes NOT present
670697
assert HTTP_METHOD not in span.attributes
671698
assert HTTP_SCHEME not in span.attributes
@@ -755,7 +782,7 @@ async def test_semantic_conventions_metrics_both(
755782
assert span.attributes.get(NETWORK_PROTOCOL_VERSION) == "1.1"
756783
assert span.attributes.get(HTTP_STATUS_CODE) == 200
757784
assert span.attributes.get(HTTP_RESPONSE_STATUS_CODE) == 200
758-
assert span.attributes.get(HTTP_ROUTE) == "default_handler"
785+
assert span.attributes.get(HTTP_ROUTE) == "/test-path"
759786

760787
metrics = test_base.get_sorted_metrics(SCOPE)
761788
assert len(metrics) == 3 # Both duration metrics + active requests

0 commit comments

Comments
 (0)