@@ -182,11 +182,16 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
182182
183183from __future__ import annotations
184184
185+ import functools
185186import logging
187+ import types
186188from typing import Collection , Literal
187189
188190import fastapi
191+ from starlette .applications import Starlette
192+ from starlette .middleware .errors import ServerErrorMiddleware
189193from starlette .routing import Match
194+ from starlette .types import ASGIApp
190195
191196from opentelemetry .instrumentation ._semconv import (
192197 _get_schema_url ,
@@ -203,9 +208,9 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
203208from opentelemetry .instrumentation .fastapi .package import _instruments
204209from opentelemetry .instrumentation .fastapi .version import __version__
205210from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
206- from opentelemetry .metrics import get_meter
211+ from opentelemetry .metrics import MeterProvider , get_meter
207212from opentelemetry .semconv .attributes .http_attributes import HTTP_ROUTE
208- from opentelemetry .trace import get_tracer
213+ from opentelemetry .trace import TracerProvider , get_tracer
209214from opentelemetry .util .http import (
210215 get_excluded_urls ,
211216 parse_excluded_urls ,
@@ -226,13 +231,13 @@ class FastAPIInstrumentor(BaseInstrumentor):
226231
227232 @staticmethod
228233 def instrument_app (
229- app ,
234+ app : fastapi . FastAPI ,
230235 server_request_hook : ServerRequestHook = None ,
231236 client_request_hook : ClientRequestHook = None ,
232237 client_response_hook : ClientResponseHook = None ,
233- tracer_provider = None ,
234- meter_provider = None ,
235- excluded_urls = None ,
238+ tracer_provider : TracerProvider | None = None ,
239+ meter_provider : MeterProvider | None = None ,
240+ excluded_urls : str | None = None ,
236241 http_capture_headers_server_request : list [str ] | None = None ,
237242 http_capture_headers_server_response : list [str ] | None = None ,
238243 http_capture_headers_sanitize_fields : list [str ] | None = None ,
@@ -284,21 +289,56 @@ def instrument_app(
284289 schema_url = _get_schema_url (sem_conv_opt_in_mode ),
285290 )
286291
287- app .add_middleware (
288- OpenTelemetryMiddleware ,
289- excluded_urls = excluded_urls ,
290- default_span_details = _get_default_span_details ,
291- server_request_hook = server_request_hook ,
292- client_request_hook = client_request_hook ,
293- client_response_hook = client_response_hook ,
294- # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
295- tracer = tracer ,
296- meter = meter ,
297- http_capture_headers_server_request = http_capture_headers_server_request ,
298- http_capture_headers_server_response = http_capture_headers_server_response ,
299- http_capture_headers_sanitize_fields = http_capture_headers_sanitize_fields ,
300- exclude_spans = exclude_spans ,
292+ # Instead of using `app.add_middleware` we monkey patch `build_middleware_stack` to insert our middleware
293+ # as the outermost middleware.
294+ # Otherwise `OpenTelemetryMiddleware` would have unhandled exceptions tearing through it and would not be able
295+ # to faithfully record what is returned to the client since it technically cannot know what `ServerErrorMiddleware` is going to do.
296+
297+ def build_middleware_stack (self : Starlette ) -> ASGIApp :
298+ inner_server_error_middleware : ASGIApp = ( # type: ignore
299+ self ._original_build_middleware_stack () # type: ignore
300+ )
301+ otel_middleware = OpenTelemetryMiddleware (
302+ inner_server_error_middleware ,
303+ excluded_urls = excluded_urls ,
304+ default_span_details = _get_default_span_details ,
305+ server_request_hook = server_request_hook ,
306+ client_request_hook = client_request_hook ,
307+ client_response_hook = client_response_hook ,
308+ # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
309+ tracer = tracer ,
310+ meter = meter ,
311+ http_capture_headers_server_request = http_capture_headers_server_request ,
312+ http_capture_headers_server_response = http_capture_headers_server_response ,
313+ http_capture_headers_sanitize_fields = http_capture_headers_sanitize_fields ,
314+ exclude_spans = exclude_spans ,
315+ )
316+ # Wrap in an outer layer of ServerErrorMiddleware so that any exceptions raised in OpenTelemetryMiddleware
317+ # are handled.
318+ # This should not happen unless there is a bug in OpenTelemetryMiddleware, but if there is we don't want that
319+ # to impact the user's application just because we wrapped the middlewares in this order.
320+ if isinstance (
321+ inner_server_error_middleware , ServerErrorMiddleware
322+ ): # usually true
323+ outer_server_error_middleware = ServerErrorMiddleware (
324+ app = otel_middleware ,
325+ )
326+ else :
327+ # Something else seems to have patched things, or maybe Starlette changed.
328+ # Just create a default ServerErrorMiddleware.
329+ outer_server_error_middleware = ServerErrorMiddleware (
330+ app = otel_middleware
331+ )
332+ return outer_server_error_middleware
333+
334+ app ._original_build_middleware_stack = app .build_middleware_stack
335+ app .build_middleware_stack = types .MethodType (
336+ functools .wraps (app .build_middleware_stack )(
337+ build_middleware_stack
338+ ),
339+ app ,
301340 )
341+
302342 app ._is_instrumented_by_opentelemetry = True
303343 if app not in _InstrumentedFastAPI ._instrumented_fastapi_apps :
304344 _InstrumentedFastAPI ._instrumented_fastapi_apps .add (app )
@@ -309,11 +349,12 @@ def instrument_app(
309349
310350 @staticmethod
311351 def uninstrument_app (app : fastapi .FastAPI ):
312- app .user_middleware = [
313- x
314- for x in app .user_middleware
315- if x .cls is not OpenTelemetryMiddleware
316- ]
352+ original_build_middleware_stack = getattr (
353+ app , "_original_build_middleware_stack" , None
354+ )
355+ if original_build_middleware_stack :
356+ app .build_middleware_stack = original_build_middleware_stack
357+ del app ._original_build_middleware_stack
317358 app .middleware_stack = app .build_middleware_stack ()
318359 app ._is_instrumented_by_opentelemetry = False
319360
@@ -341,12 +382,7 @@ def _instrument(self, **kwargs):
341382 _InstrumentedFastAPI ._http_capture_headers_sanitize_fields = (
342383 kwargs .get ("http_capture_headers_sanitize_fields" )
343384 )
344- _excluded_urls = kwargs .get ("excluded_urls" )
345- _InstrumentedFastAPI ._excluded_urls = (
346- _excluded_urls_from_env
347- if _excluded_urls is None
348- else parse_excluded_urls (_excluded_urls )
349- )
385+ _InstrumentedFastAPI ._excluded_urls = kwargs .get ("excluded_urls" )
350386 _InstrumentedFastAPI ._meter_provider = kwargs .get ("meter_provider" )
351387 _InstrumentedFastAPI ._exclude_spans = kwargs .get ("exclude_spans" )
352388 fastapi .FastAPI = _InstrumentedFastAPI
@@ -365,43 +401,29 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
365401 _server_request_hook : ServerRequestHook = None
366402 _client_request_hook : ClientRequestHook = None
367403 _client_response_hook : ClientResponseHook = None
404+ _http_capture_headers_server_request : list [str ] | None = None
405+ _http_capture_headers_server_response : list [str ] | None = None
406+ _http_capture_headers_sanitize_fields : list [str ] | None = None
407+ _exclude_spans : list [Literal ["receive" , "send" ]] | None = None
408+
368409 _instrumented_fastapi_apps = set ()
369410 _sem_conv_opt_in_mode = _StabilityMode .DEFAULT
370411
371412 def __init__ (self , * args , ** kwargs ):
372413 super ().__init__ (* args , ** kwargs )
373- tracer = get_tracer (
374- __name__ ,
375- __version__ ,
376- _InstrumentedFastAPI ._tracer_provider ,
377- schema_url = _get_schema_url (
378- _InstrumentedFastAPI ._sem_conv_opt_in_mode
379- ),
380- )
381- meter = get_meter (
382- __name__ ,
383- __version__ ,
384- _InstrumentedFastAPI ._meter_provider ,
385- schema_url = _get_schema_url (
386- _InstrumentedFastAPI ._sem_conv_opt_in_mode
387- ),
388- )
389- self .add_middleware (
390- OpenTelemetryMiddleware ,
391- excluded_urls = _InstrumentedFastAPI ._excluded_urls ,
392- default_span_details = _get_default_span_details ,
393- server_request_hook = _InstrumentedFastAPI ._server_request_hook ,
394- client_request_hook = _InstrumentedFastAPI ._client_request_hook ,
395- client_response_hook = _InstrumentedFastAPI ._client_response_hook ,
396- # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
397- tracer = tracer ,
398- meter = meter ,
399- http_capture_headers_server_request = _InstrumentedFastAPI ._http_capture_headers_server_request ,
400- http_capture_headers_server_response = _InstrumentedFastAPI ._http_capture_headers_server_response ,
401- http_capture_headers_sanitize_fields = _InstrumentedFastAPI ._http_capture_headers_sanitize_fields ,
402- exclude_spans = _InstrumentedFastAPI ._exclude_spans ,
414+ FastAPIInstrumentor .instrument_app (
415+ self ,
416+ server_request_hook = self ._server_request_hook ,
417+ client_request_hook = self ._client_request_hook ,
418+ client_response_hook = self ._client_response_hook ,
419+ tracer_provider = self ._tracer_provider ,
420+ meter_provider = self ._meter_provider ,
421+ excluded_urls = self ._excluded_urls ,
422+ http_capture_headers_server_request = self ._http_capture_headers_server_request ,
423+ http_capture_headers_server_response = self ._http_capture_headers_server_response ,
424+ http_capture_headers_sanitize_fields = self ._http_capture_headers_sanitize_fields ,
425+ exclude_spans = self ._exclude_spans ,
403426 )
404- self ._is_instrumented_by_opentelemetry = True
405427 _InstrumentedFastAPI ._instrumented_fastapi_apps .add (self )
406428
407429 def __del__ (self ):
0 commit comments