Skip to content

Commit 9a32f09

Browse files
committed
fastapi: fix wrapping of middlewares
1 parent 803bb32 commit 9a32f09

File tree

1 file changed

+38
-20
lines changed
  • instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi

1 file changed

+38
-20
lines changed

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

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,14 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A
179179
from __future__ import annotations
180180

181181
import logging
182+
import types
182183
from typing import Collection, Literal
183184

184185
import fastapi
186+
from starlette.applications import Starlette
187+
from starlette.middleware.error import ServerErrorMiddleware
185188
from starlette.routing import Match
189+
from starlette.types import ASGIApp
186190

187191
from 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

Comments
 (0)