diff --git a/CHANGELOG.md b/CHANGELOG.md index e206925782..fd7d11f282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3882](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3882)) - `opentelemetry-instrumentation-aiohttp-server`: delay initialization of tracer, meter and excluded urls to instrumentation for testability ([#3836](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3836)) +- `opentelemetry-instrumentation-asgi` remove high cardinal path from span name + ([#2650](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2650)) + ## Version 1.38.0/0.59b0 (2025-10-16) 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 bb232b39d3..1497af6b1f 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py @@ -506,15 +506,12 @@ def get_default_span_details(scope: dict) -> Tuple[str, dict]: Returns: a tuple of the span name, and any attributes to attach to the span. """ - path = scope.get("path", "").strip() - method = sanitize_method(scope.get("method", "").strip()) - if method == "_OTHER": - method = "HTTP" - if method and path: # http - return f"{method} {path}", {} - if path: # websocket - return path, {} - return method, {} # http with no path + if scope.get("type") == "http": + method = sanitize_method(scope.get("method", "").strip()) + if method == "_OTHER": + method = "HTTP" + return method, {} + return scope.get("type", ""), {} def _collect_target_attribute( @@ -863,7 +860,7 @@ def _get_otel_receive(self, server_span_name, scope, receive): @wraps(receive) async def otel_receive(): with self.tracer.start_as_current_span( - " ".join((server_span_name, scope["type"], "receive")) + " ".join((server_span_name, "receive")) ) as receive_span: message = await receive() if callable(self.client_request_hook): @@ -894,7 +891,7 @@ def _set_send_span( ): """Set send span attributes and status code.""" with self.tracer.start_as_current_span( - " ".join((server_span_name, scope["type"], "send")) + " ".join((server_span_name, "send")) ) as send_span: if callable(self.client_response_hook): self.client_response_hook(send_span, scope, message) diff --git a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py index 0da3014f5f..8c93bee7b8 100644 --- a/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-asgi/tests/test_asgi_middleware.py @@ -277,6 +277,53 @@ async def error_asgi(scope, receive, send): await send({"type": "http.response.body", "body": b"*"}) +# New ASGI app for user update +async def user_update_app(scope, receive, send): + assert scope["type"] == "http" + assert scope["method"] == "PUT" + assert scope["path"].startswith("/api/v3/io/users/") + + user_id = scope["path"].split("/")[-1] + + message = await receive() + if message["type"] == "http.request": + await send( + { + "type": "http.response.start", + "status": 200, + "headers": [ + [b"Content-Type", b"application/json"], + ], + } + ) + await send( + { + "type": "http.response.body", + "body": f'{{"status": "updated", "user_id": "{user_id}"}}'.encode(), + } + ) + + +# New ASGI app for WebSocket +async def websocket_session_app(scope, receive, send): + assert scope["type"] == "websocket" + assert scope["path"].startswith("/ws/") + + session_id = scope["path"].split("/")[-1] + + await send({"type": "websocket.accept"}) + + while True: + event = await receive() + if event["type"] == "websocket.disconnect": + break + if event["type"] == "websocket.receive": + if event.get("text") == "ping": + await send( + {"type": "websocket.send", "text": f"pong:{session_id}"} + ) + + class UnhandledException(Exception): pass @@ -357,12 +404,12 @@ def validate_outputs( span_list = self.memory_exporter.get_finished_spans() expected_old = [ { - "name": "GET / http receive", + "name": "GET receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { SpanAttributes.HTTP_STATUS_CODE: 200, @@ -370,12 +417,12 @@ def validate_outputs( }, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { - "name": "GET /", + "name": "GET", "kind": trace_api.SpanKind.SERVER, "attributes": { SpanAttributes.HTTP_METHOD: "GET", @@ -393,12 +440,12 @@ def validate_outputs( ] expected_new = [ { - "name": "GET / http receive", + "name": "GET receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { HTTP_RESPONSE_STATUS_CODE: 200, @@ -406,12 +453,12 @@ def validate_outputs( }, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { - "name": "GET /", + "name": "GET", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_REQUEST_METHOD: "GET", @@ -428,12 +475,12 @@ def validate_outputs( ] expected_both = [ { - "name": "GET / http receive", + "name": "GET receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.request"}, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { SpanAttributes.HTTP_STATUS_CODE: 200, @@ -442,12 +489,12 @@ def validate_outputs( }, }, { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, }, { - "name": "GET /", + "name": "GET", "kind": trace_api.SpanKind.SERVER, "attributes": { HTTP_REQUEST_METHOD: "GET", @@ -563,7 +610,7 @@ async def test_long_response(self): def add_more_body_spans(expected: list): more_body_span = { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, } @@ -625,12 +672,12 @@ async def test_trailers(self): def add_body_and_trailer_span(expected: list): body_span = { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.body"}, } trailer_span = { - "name": "GET / http send", + "name": "GET send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "http.response.trailers"}, } @@ -661,7 +708,7 @@ def update_expected_span_name(expected): entry["name"] = span_name else: entry["name"] = " ".join( - [span_name] + entry["name"].split(" ")[2:] + [span_name] + entry["name"].split(" ")[1:] ) return expected @@ -938,17 +985,17 @@ async def test_websocket(self): self.assertEqual(len(span_list), 6) expected = [ { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", @@ -956,7 +1003,7 @@ async def test_websocket(self): }, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", @@ -964,12 +1011,12 @@ async def test_websocket(self): }, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { - "name": "GET /", + "name": "websocket", "kind": trace_api.SpanKind.SERVER, "attributes": { SpanAttributes.HTTP_SCHEME: self.scope["scheme"], @@ -1012,17 +1059,17 @@ async def test_websocket_new_semconv(self): self.assertEqual(len(span_list), 6) expected = [ { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", @@ -1030,7 +1077,7 @@ async def test_websocket_new_semconv(self): }, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", @@ -1038,12 +1085,12 @@ async def test_websocket_new_semconv(self): }, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { - "name": "GET /", + "name": "websocket", "kind": trace_api.SpanKind.SERVER, "attributes": { URL_SCHEME: self.scope["scheme"], @@ -1085,17 +1132,17 @@ async def test_websocket_both_semconv(self): self.assertEqual(len(span_list), 6) expected = [ { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.connect"}, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.accept"}, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.receive", @@ -1104,7 +1151,7 @@ async def test_websocket_both_semconv(self): }, }, { - "name": "GET / websocket send", + "name": "websocket send", "kind": trace_api.SpanKind.INTERNAL, "attributes": { "asgi.event.type": "websocket.send", @@ -1113,12 +1160,12 @@ async def test_websocket_both_semconv(self): }, }, { - "name": "GET / websocket receive", + "name": "websocket receive", "kind": trace_api.SpanKind.INTERNAL, "attributes": {"asgi.event.type": "websocket.disconnect"}, }, { - "name": "GET /", + "name": "websocket", "kind": trace_api.SpanKind.SERVER, "attributes": { SpanAttributes.HTTP_SCHEME: self.scope["scheme"], @@ -1210,9 +1257,9 @@ def update_expected_hook_results(expected): for entry in expected: if entry["kind"] == trace_api.SpanKind.SERVER: entry["name"] = "name from server hook" - elif entry["name"] == "GET / http receive": + elif entry["name"] == "GET receive": entry["name"] = "name from client request hook" - elif entry["name"] == "GET / http send": + elif entry["name"] == "GET send": entry["attributes"].update({"attr-from-hook": "value"}) return expected @@ -1690,6 +1737,64 @@ async def test_no_metric_for_websockets(self): await self.get_all_output() self.assertIsNone(self.memory_metrics_reader.get_metrics_data()) + async def test_put_request_with_user_id(self): + self.scope["method"] = "PUT" + self.scope["path"] = "/api/v3/io/users/123" + + app = otel_asgi.OpenTelemetryMiddleware(user_update_app) + self.seed_app(app) + await self.send_input( + {"type": "http.request", "body": b'{"name": "John Doe"}'} + ) + + outputs = await self.get_all_output() + self.assertEqual(len(outputs), 2) + self.assertEqual(outputs[0]["type"], "http.response.start") + self.assertEqual(outputs[0]["status"], 200) + self.assertEqual(outputs[1]["type"], "http.response.body") + self.assertEqual( + outputs[1]["body"], b'{"status": "updated", "user_id": "123"}' + ) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 4) # 3 internal spans + 1 server span + server_span = span_list[-1] + self.assertEqual(server_span.name, "PUT") + self.assertEqual(server_span.kind, trace_api.SpanKind.SERVER) + self.assertEqual( + server_span.attributes[SpanAttributes.HTTP_METHOD], "PUT" + ) + self.assertEqual( + server_span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200 + ) + + async def skip_test_websocket_connection_with_session_id(self): + app = otel_asgi.OpenTelemetryMiddleware(websocket_session_app) + self.seed_app(app) + self.scope["type"] = "websocket" + self.scope["path"] = "/ws/05b55f3f66aa31cbe6a25e7027f7c2cc" + + await self.send_input({"type": "websocket.connect"}) + await self.send_input({"type": "websocket.receive", "text": "ping"}) + await self.send_input({"type": "websocket.disconnect"}) + + outputs = await self.get_all_output() + self.assertEqual(len(outputs), 2) + self.assertEqual(outputs[0]["type"], "websocket.accept") + self.assertEqual(outputs[1]["type"], "websocket.send") + self.assertEqual( + outputs[1]["text"], "pong:05b55f3f66aa31cbe6a25e7027f7c2cc" + ) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 6) # 5 internal spans + 1 server span + server_span = span_list[-1] + self.assertEqual(server_span.name, "websocket") + self.assertEqual(server_span.kind, trace_api.SpanKind.SERVER) + self.assertEqual( + server_span.attributes[SpanAttributes.HTTP_SCHEME], "ws" + ) + async def test_excluded_urls(self): self.scope["path"] = "/test_excluded_urls" app = otel_asgi.OpenTelemetryMiddleware( diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 661c7097cd..2befdc1d8a 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -298,7 +298,7 @@ def test_fastapi_unhandled_exception(self): spans = self.memory_exporter.get_finished_spans() self.assertEqual(len(spans), 3) span = spans[0] - assert span.name == "GET /error http send" + assert span.name == "GET /error send" assert span.attributes[HTTP_STATUS_CODE] == 500 span = spans[2] assert span.name == "GET /error"