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 8ba83985c6..f9925349f9 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py @@ -190,7 +190,7 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A import fastapi from starlette.applications import Starlette from starlette.middleware.errors import ServerErrorMiddleware -from starlette.routing import Match +from starlette.routing import Match, Route from starlette.types import ASGIApp from opentelemetry.instrumentation._semconv import ( @@ -448,7 +448,11 @@ def _get_route_details(scope): route = None for starlette_route in app.routes: - match, _ = starlette_route.matches(scope) + match, _ = ( + Route.matches(starlette_route, scope) + if isinstance(starlette_route, Route) + else starlette_route.matches(scope) + ) if match == Match.FULL: route = starlette_route.path break diff --git a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py index 523c165f85..8a107089c7 100644 --- a/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py +++ b/instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py @@ -22,7 +22,10 @@ import fastapi from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware from fastapi.responses import JSONResponse +from fastapi.routing import APIRoute from fastapi.testclient import TestClient +from starlette.routing import Match +from starlette.types import Receive, Scope, Send import opentelemetry.instrumentation.fastapi as otel_fastapi from opentelemetry import trace @@ -38,9 +41,7 @@ from opentelemetry.instrumentation.auto_instrumentation._load import ( _load_instrumentors, ) -from opentelemetry.instrumentation.dependencies import ( - DependencyConflict, -) +from opentelemetry.instrumentation.dependencies import DependencyConflict from opentelemetry.sdk.metrics.export import ( HistogramDataPoint, NumberDataPoint, @@ -123,6 +124,23 @@ ) +class CustomMiddleware: + def __init__(self, app: fastapi.FastAPI) -> None: + self.app = app + + async def __call__( + self, scope: Scope, receive: Receive, send: Send + ) -> None: + scope["nonstandard_field"] = "here" + await self.app(scope, receive, send) + + +class CustomRoute(APIRoute): + def matches(self, scope: Scope) -> tuple[Match, Scope]: + assert "nonstandard_field" in scope + return super().matches(scope) + + class TestBaseFastAPI(TestBase): def _create_app(self): app = self._create_fastapi_app() @@ -183,6 +201,7 @@ def setUp(self): self._instrumentor = otel_fastapi.FastAPIInstrumentor() self._app = self._create_app() self._app.add_middleware(HTTPSRedirectMiddleware) + self._app.add_middleware(CustomMiddleware) self._client = TestClient(self._app, base_url="https://testserver:443") # run the lifespan, initialize the middleware stack # this is more in-line with what happens in a real application when the server starts up @@ -202,6 +221,7 @@ def tearDown(self): def _create_fastapi_app(): app = fastapi.FastAPI() sub_app = fastapi.FastAPI() + custom_router = fastapi.APIRouter(route_class=CustomRoute) @sub_app.get("/home") async def _(): @@ -227,6 +247,12 @@ async def _(): async def _(): raise UnhandledException("This is an unhandled exception") + @custom_router.get("/success") + async def _(): + return None + + app.include_router(custom_router, prefix="/custom-router") + app.mount("/sub", app=sub_app) return app @@ -304,6 +330,14 @@ def test_sub_app_fastapi_call(self): span.attributes[HTTP_URL], ) + def test_custom_api_router(self): + """ + This test is to ensure that custom API routers the OpenTelemetryMiddleware does not cause issues with + custom API routers that depend on non-standard fields on the ASGI scope. + """ + resp = self._client.get("/custom-router/success") + self.assertEqual(resp.status_code, 200) + class TestBaseAutoFastAPI(TestBaseFastAPI): @classmethod @@ -988,6 +1022,7 @@ def test_metric_uninstrument(self): def _create_fastapi_app(): app = fastapi.FastAPI() sub_app = fastapi.FastAPI() + custom_router = fastapi.APIRouter(route_class=CustomRoute) @sub_app.get("/home") async def _(): @@ -1013,6 +1048,12 @@ async def _(): async def _(): raise UnhandledException("This is an unhandled exception") + @custom_router.get("/success") + async def _(): + return None + + app.include_router(custom_router, prefix="/custom-router") + app.mount("/sub", app=sub_app) return app