diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b2102a591..0c0f788197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3679)) - `opentelemetry-instrumentation`: Avoid calls to `context.detach` with `None` token. ([#3673](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3673)) +- `opentelemetry-instrumentation-starlette`/`opentelemetry-instrumentation-fastapi`: Fixes a crash when host-based routing is used + ([#3507](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3507)) ### Added diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py index c5c87f9878..87ab1b2e74 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -516,7 +516,11 @@ def _get_route_details(scope): for starlette_route in app.routes: match, _ = starlette_route.matches(scope) if match == Match.FULL: - route = starlette_route.path + try: + route = starlette_route.path + except AttributeError: + # routes added via host routing won't have a path attribute + route = scope.get("path") break if match == Match.PARTIAL: route = starlette_route.path diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 08a4668f5b..ffc85b35b9 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -234,6 +234,7 @@ async def _(): raise UnhandledException("This is an unhandled exception") app.mount("/sub", app=sub_app) + app.host("testserver2", sub_app) return app @@ -310,6 +311,26 @@ def test_sub_app_fastapi_call(self): span.attributes[HTTP_URL], ) + def test_host_fastapi_call(self): + client = TestClient(self._app, base_url="https://testserver2") + client.get("/") + spans = self.memory_exporter.get_finished_spans() + + spans_with_http_attributes = [ + span + for span in spans + if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) + ] + + self.assertEqual(1, len(spans_with_http_attributes)) + + for span in spans_with_http_attributes: + self.assertEqual("/", span.attributes[HTTP_TARGET]) + self.assertEqual( + "https://testserver2:443/", + span.attributes[HTTP_URL], + ) + class TestBaseAutoFastAPI(TestBaseFastAPI): @classmethod diff --git a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py index 3a88582ecf..93f2424323 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/src/opentelemetry/instrumentation/starlette/__init__.py @@ -354,7 +354,11 @@ def _get_route_details(scope: dict[str, Any]) -> str | None: for starlette_route in app.routes: match, _ = starlette_route.matches(scope) if match == Match.FULL: - route = starlette_route.path + try: + route = starlette_route.path + except AttributeError: + # routes added via host routing won't have a path attribute + route = scope.get("path") break if match == Match.PARTIAL: route = starlette_route.path diff --git a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py index 17f79073a6..8c326e83ba 100644 --- a/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-starlette/tests/test_starlette_instrumentation.py @@ -18,7 +18,7 @@ from starlette import applications from starlette.responses import PlainTextResponse -from starlette.routing import Mount, Route +from starlette.routing import Host, Mount, Route from starlette.testclient import TestClient from starlette.websockets import WebSocket @@ -140,6 +140,24 @@ def test_sub_app_starlette_call(self): span.attributes[HTTP_URL], ) + def test_host_starlette_call(self): + client = TestClient(self._app, base_url="http://testserver2") + client.get("/home") + spans = self.memory_exporter.get_finished_spans() + + spans_with_http_attributes = [ + span + for span in spans + if (HTTP_URL in span.attributes or HTTP_TARGET in span.attributes) + ] + + for span in spans_with_http_attributes: + self.assertEqual("/home", span.attributes[HTTP_TARGET]) + self.assertEqual( + "http://testserver2/home", + span.attributes[HTTP_URL], + ) + def test_starlette_route_attribute_added(self): """Ensure that starlette routes are used as the span name.""" self._client.get("/user/123") @@ -294,6 +312,7 @@ def sub_home(_): Route("/user/{username}", home), Route("/healthzz", health), Mount("/sub", app=sub_app), + Host("testserver2", sub_app), ], )