fastapi: fix wrapping of middlewares (#3012)

* fastapi: fix wrapping of middlewares

* fix import, super

* add test

* changelog

* lint

* lint

* fix

* ci

* fix wip

* fix

* fix

* lint

* lint

* Exit?

* Update test_fastapi_instrumentation.py

Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>

* remove break

* fix

* remove dunders

* add test

* lint

* add endpoint to class

* fmt

* pr feedback

* move type ignores

* fix sphinx?

* Update CHANGELOG.md

* update fastapi versions

* fix?

* generate

* stop passing on user-supplied error handler

This prevents potential side effects, such as logging, to be executed
more than once per request handler exception.

* fix ci

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

* fix ruff

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

* remove unused funcs

Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com>

* fix lint,ruff

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

* fix changelog

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

* add changelog note

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

* fix conflicts with main

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>

---------

Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>
Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
Co-authored-by: Alexander Dorn <ad@not.one>
Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com>
This commit is contained in:
Adrian Garcia Badaracco
2025-05-20 07:00:56 -07:00
committed by GitHub
parent dbdff31220
commit 4d6893e8fa
11 changed files with 181 additions and 109 deletions

View File

@ -15,6 +15,7 @@
# pylint: disable=too-many-lines
import unittest
from contextlib import ExitStack
from timeit import default_timer
from unittest.mock import Mock, call, patch
@ -183,9 +184,14 @@ class TestBaseFastAPI(TestBase):
self._instrumentor = otel_fastapi.FastAPIInstrumentor()
self._app = self._create_app()
self._app.add_middleware(HTTPSRedirectMiddleware)
self._client = TestClient(self._app)
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
self._exit_stack = ExitStack()
self._exit_stack.enter_context(self._client)
def tearDown(self):
self._exit_stack.close()
super().tearDown()
self.env_patch.stop()
self.exclude_patch.stop()
@ -218,11 +224,19 @@ class TestBaseFastAPI(TestBase):
async def _():
return {"message": "ok"}
@app.get("/error")
async def _():
raise UnhandledException("This is an unhandled exception")
app.mount("/sub", app=sub_app)
return app
class UnhandledException(Exception):
pass
class TestBaseManualFastAPI(TestBaseFastAPI):
@classmethod
def setUpClass(cls):
@ -233,6 +247,27 @@ class TestBaseManualFastAPI(TestBaseFastAPI):
super(TestBaseManualFastAPI, cls).setUpClass()
def test_fastapi_unhandled_exception(self):
"""If the application has an unhandled error the instrumentation should capture that a 500 response is returned."""
try:
resp = self._client.get("/error")
assert (
resp.status_code == 500
), resp.content # pragma: no cover, for debugging this test if an exception is _not_ raised
except UnhandledException:
pass
else:
self.fail("Expected UnhandledException")
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 3)
span = spans[0]
assert span.name == "GET /error http send"
assert span.attributes[HTTP_STATUS_CODE] == 500
span = spans[2]
assert span.name == "GET /error"
assert span.attributes[HTTP_TARGET] == "/error"
def test_sub_app_fastapi_call(self):
"""
This test is to ensure that a span in case of a sub app targeted contains the correct server url
@ -975,6 +1010,10 @@ class TestFastAPIManualInstrumentation(TestBaseManualFastAPI):
async def _():
return {"message": "ok"}
@app.get("/error")
async def _():
raise UnhandledException("This is an unhandled exception")
app.mount("/sub", app=sub_app)
return app
@ -1137,9 +1176,11 @@ class TestAutoInstrumentation(TestBaseAutoFastAPI):
def test_mulitple_way_instrumentation(self):
self._instrumentor.instrument_app(self._app)
count = 0
for middleware in self._app.user_middleware:
if middleware.cls is OpenTelemetryMiddleware:
app = self._app.middleware_stack
while app is not None:
if isinstance(app, OpenTelemetryMiddleware):
count += 1
app = getattr(app, "app", None)
self.assertEqual(count, 1)
def test_uninstrument_after_instrument(self):