@@ -182,11 +182,16 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
182
182
183
183
from __future__ import annotations
184
184
185
+ import functools
185
186
import logging
187
+ import types
186
188
from typing import Collection , Literal
187
189
188
190
import fastapi
191
+ from starlette .applications import Starlette
192
+ from starlette .middleware .errors import ServerErrorMiddleware
189
193
from starlette .routing import Match
194
+ from starlette .types import ASGIApp
190
195
191
196
from opentelemetry .instrumentation ._semconv import (
192
197
_get_schema_url ,
@@ -203,9 +208,9 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
203
208
from opentelemetry .instrumentation .fastapi .package import _instruments
204
209
from opentelemetry .instrumentation .fastapi .version import __version__
205
210
from opentelemetry .instrumentation .instrumentor import BaseInstrumentor
206
- from opentelemetry .metrics import get_meter
211
+ from opentelemetry .metrics import MeterProvider , get_meter
207
212
from opentelemetry .semconv .attributes .http_attributes import HTTP_ROUTE
208
- from opentelemetry .trace import get_tracer
213
+ from opentelemetry .trace import TracerProvider , get_tracer
209
214
from opentelemetry .util .http import (
210
215
get_excluded_urls ,
211
216
parse_excluded_urls ,
@@ -226,13 +231,13 @@ class FastAPIInstrumentor(BaseInstrumentor):
226
231
227
232
@staticmethod
228
233
def instrument_app (
229
- app ,
234
+ app : fastapi . FastAPI ,
230
235
server_request_hook : ServerRequestHook = None ,
231
236
client_request_hook : ClientRequestHook = None ,
232
237
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 ,
236
241
http_capture_headers_server_request : list [str ] | None = None ,
237
242
http_capture_headers_server_response : list [str ] | None = None ,
238
243
http_capture_headers_sanitize_fields : list [str ] | None = None ,
@@ -284,21 +289,56 @@ def instrument_app(
284
289
schema_url = _get_schema_url (sem_conv_opt_in_mode ),
285
290
)
286
291
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 ,
301
340
)
341
+
302
342
app ._is_instrumented_by_opentelemetry = True
303
343
if app not in _InstrumentedFastAPI ._instrumented_fastapi_apps :
304
344
_InstrumentedFastAPI ._instrumented_fastapi_apps .add (app )
@@ -309,11 +349,12 @@ def instrument_app(
309
349
310
350
@staticmethod
311
351
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
317
358
app .middleware_stack = app .build_middleware_stack ()
318
359
app ._is_instrumented_by_opentelemetry = False
319
360
@@ -341,12 +382,7 @@ def _instrument(self, **kwargs):
341
382
_InstrumentedFastAPI ._http_capture_headers_sanitize_fields = (
342
383
kwargs .get ("http_capture_headers_sanitize_fields" )
343
384
)
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" )
350
386
_InstrumentedFastAPI ._meter_provider = kwargs .get ("meter_provider" )
351
387
_InstrumentedFastAPI ._exclude_spans = kwargs .get ("exclude_spans" )
352
388
fastapi .FastAPI = _InstrumentedFastAPI
@@ -365,43 +401,29 @@ class _InstrumentedFastAPI(fastapi.FastAPI):
365
401
_server_request_hook : ServerRequestHook = None
366
402
_client_request_hook : ClientRequestHook = None
367
403
_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
+
368
409
_instrumented_fastapi_apps = set ()
369
410
_sem_conv_opt_in_mode = _StabilityMode .DEFAULT
370
411
371
412
def __init__ (self , * args , ** kwargs ):
372
413
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 ,
403
426
)
404
- self ._is_instrumented_by_opentelemetry = True
405
427
_InstrumentedFastAPI ._instrumented_fastapi_apps .add (self )
406
428
407
429
def __del__ (self ):
0 commit comments