Skip to content

Commit 23d5e85

Browse files
Punit Maheshwaripunitmahes
authored andcommitted
fix(middleware): Add handling for Middleware wrapped app in FastAPI
1 parent 8fa0c1b commit 23d5e85

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
> Use [this search for a list of all CHANGELOG.md files in this repo](https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python-contrib+path%3A**%2FCHANGELOG.md&type=code).
1111
1212
## Unreleased
13+
- `opentelemetry-instrumentation-fastapi` Support for Middleware Wrapped FastAPI Application [#4041](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4031)
1314

1415
## Version 1.39.0/0.60b0 (2025-12-03)
1516

instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ def instrument_app(
265265
http_capture_headers_sanitize_fields: Optional list of HTTP headers to sanitize.
266266
exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
267267
"""
268+
# unwraps any middleware to get to the FastAPI or Starlette app
269+
app = _unwrap_middleware(app)
268270
if not hasattr(app, "_is_instrumented_by_opentelemetry"):
269271
app._is_instrumented_by_opentelemetry = False
270272

@@ -391,6 +393,12 @@ async def __call__(
391393
app=otel_middleware,
392394
)
393395

396+
# add check if the app object has build_middleware_stack method
397+
if not hasattr(app, "build_middleware_stack"):
398+
_logger.error(
399+
"Skipping FastAPI instrumentation due to missing build_middleware_stack method on app object."
400+
)
401+
return
394402
app._original_build_middleware_stack = app.build_middleware_stack
395403
app.build_middleware_stack = types.MethodType(
396404
functools.wraps(app.build_middleware_stack)(
@@ -409,6 +417,9 @@ async def __call__(
409417

410418
@staticmethod
411419
def uninstrument_app(app: fastapi.FastAPI):
420+
# Unwraps any middleware to get to the FastAPI or Starlette app
421+
app = _unwrap_middleware(app)
422+
412423
original_build_middleware_stack = getattr(
413424
app, "_original_build_middleware_stack", None
414425
)
@@ -514,3 +525,17 @@ def _get_default_span_details(scope):
514525
else: # fallback
515526
span_name = method
516527
return span_name, attributes
528+
529+
530+
def _unwrap_middleware(app):
531+
"""
532+
Unwraps the middleware stack to find the underlying FastAPI or Starlette app.
533+
534+
Args:
535+
app: The ASGI application potentially wrapped in middleware.
536+
Returns:
537+
The unwrapped FastAPI or Starlette application.
538+
"""
539+
while hasattr(app, "app"):
540+
app = app.app
541+
return app

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import fastapi
2727
import pytest
28+
from fastapi.middleware.cors import CORSMiddleware
2829
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
2930
from fastapi.responses import JSONResponse, PlainTextResponse
3031
from fastapi.routing import APIRoute
@@ -1487,6 +1488,38 @@ def test_mark_span_internal_in_presence_of_span_from_other_framework(self):
14871488
)
14881489

14891490

1491+
class TestMiddlewareWrappedApplication(TestBase):
1492+
def setUp(self):
1493+
super().setUp()
1494+
self.fastapi_app = fastapi.FastAPI()
1495+
1496+
@self.fastapi_app.get("/foobar")
1497+
async def _():
1498+
return {"message": "hello world"}
1499+
1500+
self.app = CORSMiddleware(self.fastapi_app, allow_origins=["*"])
1501+
1502+
otel_fastapi.FastAPIInstrumentor().instrument_app(self.app)
1503+
self.client = TestClient(self.app)
1504+
1505+
def tearDown(self) -> None:
1506+
super().tearDown()
1507+
with self.disable_logging():
1508+
otel_fastapi.FastAPIInstrumentor().uninstrument_app(self.app)
1509+
1510+
def test_instrumentation_with_existing_middleware(self):
1511+
resp = self.client.get("/foobar")
1512+
self.assertEqual(200, resp.status_code)
1513+
1514+
span_list = self.memory_exporter.get_finished_spans()
1515+
self.assertEqual(len(span_list), 3)
1516+
1517+
server_span = [
1518+
span for span in span_list if span.kind == trace.SpanKind.SERVER
1519+
][0]
1520+
self.assertEqual(server_span.name, "GET /foobar")
1521+
1522+
14901523
class TestFastAPIGarbageCollection(unittest.TestCase):
14911524
def test_fastapi_app_is_collected_after_instrument(self):
14921525
app = fastapi.FastAPI()

0 commit comments

Comments
 (0)