@@ -179,10 +179,14 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
179179from __future__ import annotations
180180
181181import logging
182+ import types
182183from typing import Collection , Literal
183184
184185import fastapi
186+ from starlette .applications import Starlette
187+ from starlette .middleware .error import ServerErrorMiddleware
185188from starlette .routing import Match
189+ from starlette .types import ASGIApp
186190
187191from opentelemetry .instrumentation ._semconv import (
188192 _get_schema_url ,
@@ -280,21 +284,38 @@ def instrument_app(
280284 schema_url = _get_schema_url (sem_conv_opt_in_mode ),
281285 )
282286
283- app .add_middleware (
284- OpenTelemetryMiddleware ,
285- excluded_urls = excluded_urls ,
286- default_span_details = _get_default_span_details ,
287- server_request_hook = server_request_hook ,
288- client_request_hook = client_request_hook ,
289- client_response_hook = client_response_hook ,
290- # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
291- tracer = tracer ,
292- meter = meter ,
293- http_capture_headers_server_request = http_capture_headers_server_request ,
294- http_capture_headers_server_response = http_capture_headers_server_response ,
295- http_capture_headers_sanitize_fields = http_capture_headers_sanitize_fields ,
296- exclude_spans = exclude_spans ,
297- )
287+ # Instead of using `app.add_middleware` we monkey patch `build_middleware_stack` to insert our middleware
288+ # as the outermost middleware.
289+ # Otherwise `OpenTelemetryMiddleware` would have unhandled exceptions tearing through it and would not be able
290+ # to faithfully record what is returned to the client since it technically cannot know what `ServerErrorMiddleware` is going to do.
291+
292+ def build_middleware_stack (self : Starlette ) -> ASGIApp :
293+ stack = super ().build_middleware_stack ()
294+ stack = OpenTelemetryMiddleware (
295+ stack ,
296+ excluded_urls = excluded_urls ,
297+ default_span_details = _get_default_span_details ,
298+ server_request_hook = server_request_hook ,
299+ client_request_hook = client_request_hook ,
300+ client_response_hook = client_response_hook ,
301+ # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
302+ tracer = tracer ,
303+ meter = meter ,
304+ http_capture_headers_server_request = http_capture_headers_server_request ,
305+ http_capture_headers_server_response = http_capture_headers_server_response ,
306+ http_capture_headers_sanitize_fields = http_capture_headers_sanitize_fields ,
307+ exclude_spans = exclude_spans ,
308+ )
309+ # Wrap in an outer layer of ServerErrorMiddleware so that any exceptions raised in OpenTelemetryMiddleware
310+ # are handled.
311+ # This should not happen unless there is a bug in OpenTelemetryMiddleware, but if there is we don't want that
312+ # to impact the user's application just because we wrapped the middlewares in this order.
313+ stack = ServerErrorMiddleware (stack )
314+ return stack
315+
316+ app ._original_build_middleware_stack = app .build_middleware_stack
317+ app .build_middleware_stack = types .MethodType (build_middleware_stack , app )
318+
298319 app ._is_instrumented_by_opentelemetry = True
299320 if app not in _InstrumentedFastAPI ._instrumented_fastapi_apps :
300321 _InstrumentedFastAPI ._instrumented_fastapi_apps .add (app )
@@ -305,11 +326,8 @@ def instrument_app(
305326
306327 @staticmethod
307328 def uninstrument_app (app : fastapi .FastAPI ):
308- app .user_middleware = [
309- x
310- for x in app .user_middleware
311- if x .cls is not OpenTelemetryMiddleware
312- ]
329+ app .build_middleware_stack = app ._original_build_middleware_stack
330+ del app ._original_build_middleware_stack
313331 app .middleware_stack = app .build_middleware_stack ()
314332 app ._is_instrumented_by_opentelemetry = False
315333
0 commit comments