-
Notifications
You must be signed in to change notification settings - Fork 798
fastapi: fix wrapping of middlewares #3012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
83d4863
fastapi: fix wrapping of middlewares
adriangb d635026
fix import, super
adriangb 23a04a5
add test
adriangb a979966
changelog
adriangb 8057faa
lint
adriangb c1c4300
lint
adriangb 121813e
fix
adriangb 04cf5d5
ci
adriangb fdba820
fix wip
adriangb 0da827b
fix
adriangb af0bf2c
fix
adriangb d3b9910
lint
adriangb bfe110c
lint
adriangb 0c32f4d
Exit?
adriangb c75b280
Update test_fastapi_instrumentation.py
adriangb 3d7d537
remove break
adriangb f75dbef
fix
adriangb 834392f
remove dunders
adriangb 14d8159
add test
adriangb 128cf89
lint
adriangb d8ca85e
add endpoint to class
adriangb 4c4ac68
fmt
adriangb d7f7cc6
pr feedback
adriangb 00840d7
move type ignores
adriangb a5db685
fix sphinx?
adriangb 1a485a8
Merge branch 'main' into fix-asgi-middleware
xrmx 0870986
Update CHANGELOG.md
xrmx b3bea3c
Merge branch 'main' into fix-asgi-middleware
adriangb 8a2bfc7
Merge branch 'main' into fix-asgi-middleware
adriangb 2498309
update fastapi versions
adriangb 887475b
Merge branch 'main' into fix-asgi-middleware
adriangb d2cd389
Merge branch 'main' into fix-asgi-middleware
adriangb 289f788
fix?
adriangb 7ac54ae
generate
adriangb 5fa64a5
stop passing on user-supplied error handler
outergod f6dd589
Merge pull request #2 from outergod/fix-asgi-middleware
adriangb faf0cef
Merge branch 'main' into fix-asgi-middleware
emdneto 17e08fa
fix ci
emdneto 8fcf4ec
fix ruff
emdneto fcc62f4
remove unused funcs
adriangb 5a84bcc
fix lint,ruff
emdneto 87aa3f6
Merge branch 'main' into fix-asgi-middleware
emdneto 2a150fd
fix changelog
emdneto 808d42a
add changelog note
emdneto 18b8962
Merge branch 'main' into fix-asgi-middleware
adriangb 60e1706
fix conflicts with main
emdneto a648666
Merge branch 'main' into fix-asgi-middleware
xrmx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -182,11 +182,16 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A | |
|
||
from __future__ import annotations | ||
|
||
import functools | ||
import logging | ||
import types | ||
from typing import Collection, Literal | ||
|
||
import fastapi | ||
from starlette.applications import Starlette | ||
from starlette.middleware.errors import ServerErrorMiddleware | ||
from starlette.routing import Match | ||
from starlette.types import ASGIApp | ||
|
||
from opentelemetry.instrumentation._semconv import ( | ||
_get_schema_url, | ||
|
@@ -203,9 +208,9 @@ def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, A | |
from opentelemetry.instrumentation.fastapi.package import _instruments | ||
from opentelemetry.instrumentation.fastapi.version import __version__ | ||
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor | ||
from opentelemetry.metrics import get_meter | ||
from opentelemetry.metrics import MeterProvider, get_meter | ||
from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE | ||
from opentelemetry.trace import get_tracer | ||
from opentelemetry.trace import TracerProvider, get_tracer | ||
from opentelemetry.util.http import ( | ||
get_excluded_urls, | ||
parse_excluded_urls, | ||
|
@@ -226,13 +231,13 @@ class FastAPIInstrumentor(BaseInstrumentor): | |
|
||
@staticmethod | ||
def instrument_app( | ||
app, | ||
app: fastapi.FastAPI, | ||
server_request_hook: ServerRequestHook = None, | ||
client_request_hook: ClientRequestHook = None, | ||
client_response_hook: ClientResponseHook = None, | ||
tracer_provider=None, | ||
meter_provider=None, | ||
excluded_urls=None, | ||
tracer_provider: TracerProvider | None = None, | ||
meter_provider: MeterProvider | None = None, | ||
excluded_urls: str | None = None, | ||
http_capture_headers_server_request: list[str] | None = None, | ||
http_capture_headers_server_response: list[str] | None = None, | ||
http_capture_headers_sanitize_fields: list[str] | None = None, | ||
|
@@ -284,21 +289,56 @@ def instrument_app( | |
schema_url=_get_schema_url(sem_conv_opt_in_mode), | ||
) | ||
|
||
app.add_middleware( | ||
OpenTelemetryMiddleware, | ||
excluded_urls=excluded_urls, | ||
default_span_details=_get_default_span_details, | ||
server_request_hook=server_request_hook, | ||
client_request_hook=client_request_hook, | ||
client_response_hook=client_response_hook, | ||
# Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation | ||
tracer=tracer, | ||
meter=meter, | ||
http_capture_headers_server_request=http_capture_headers_server_request, | ||
http_capture_headers_server_response=http_capture_headers_server_response, | ||
http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields, | ||
exclude_spans=exclude_spans, | ||
# Instead of using `app.add_middleware` we monkey patch `build_middleware_stack` to insert our middleware | ||
# as the outermost middleware. | ||
# Otherwise `OpenTelemetryMiddleware` would have unhandled exceptions tearing through it and would not be able | ||
# to faithfully record what is returned to the client since it technically cannot know what `ServerErrorMiddleware` is going to do. | ||
|
||
def build_middleware_stack(self: Starlette) -> ASGIApp: | ||
inner_server_error_middleware: ASGIApp = ( # type: ignore | ||
self._original_build_middleware_stack() # type: ignore | ||
) | ||
otel_middleware = OpenTelemetryMiddleware( | ||
inner_server_error_middleware, | ||
excluded_urls=excluded_urls, | ||
default_span_details=_get_default_span_details, | ||
server_request_hook=server_request_hook, | ||
client_request_hook=client_request_hook, | ||
client_response_hook=client_response_hook, | ||
# Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation | ||
tracer=tracer, | ||
meter=meter, | ||
http_capture_headers_server_request=http_capture_headers_server_request, | ||
http_capture_headers_server_response=http_capture_headers_server_response, | ||
http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields, | ||
exclude_spans=exclude_spans, | ||
) | ||
# Wrap in an outer layer of ServerErrorMiddleware so that any exceptions raised in OpenTelemetryMiddleware | ||
# are handled. | ||
# This should not happen unless there is a bug in OpenTelemetryMiddleware, but if there is we don't want that | ||
# to impact the user's application just because we wrapped the middlewares in this order. | ||
if isinstance( | ||
inner_server_error_middleware, ServerErrorMiddleware | ||
): # usually true | ||
outer_server_error_middleware = ServerErrorMiddleware( | ||
app=otel_middleware, | ||
) | ||
else: | ||
# Something else seems to have patched things, or maybe Starlette changed. | ||
# Just create a default ServerErrorMiddleware. | ||
outer_server_error_middleware = ServerErrorMiddleware( | ||
app=otel_middleware | ||
) | ||
Comment on lines
+320
to
+331
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these branches are the same now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel stupid for not realizing this when I changed the code |
||
return outer_server_error_middleware | ||
|
||
app._original_build_middleware_stack = app.build_middleware_stack | ||
emdneto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
app.build_middleware_stack = types.MethodType( | ||
functools.wraps(app.build_middleware_stack)( | ||
build_middleware_stack | ||
), | ||
app, | ||
) | ||
|
||
app._is_instrumented_by_opentelemetry = True | ||
if app not in _InstrumentedFastAPI._instrumented_fastapi_apps: | ||
_InstrumentedFastAPI._instrumented_fastapi_apps.add(app) | ||
|
@@ -309,11 +349,12 @@ def instrument_app( | |
|
||
@staticmethod | ||
def uninstrument_app(app: fastapi.FastAPI): | ||
app.user_middleware = [ | ||
x | ||
for x in app.user_middleware | ||
if x.cls is not OpenTelemetryMiddleware | ||
] | ||
original_build_middleware_stack = getattr( | ||
app, "_original_build_middleware_stack", None | ||
) | ||
if original_build_middleware_stack: | ||
app.build_middleware_stack = original_build_middleware_stack | ||
del app._original_build_middleware_stack | ||
app.middleware_stack = app.build_middleware_stack() | ||
app._is_instrumented_by_opentelemetry = False | ||
|
||
|
@@ -341,12 +382,7 @@ def _instrument(self, **kwargs): | |
_InstrumentedFastAPI._http_capture_headers_sanitize_fields = ( | ||
kwargs.get("http_capture_headers_sanitize_fields") | ||
) | ||
_excluded_urls = kwargs.get("excluded_urls") | ||
_InstrumentedFastAPI._excluded_urls = ( | ||
_excluded_urls_from_env | ||
if _excluded_urls is None | ||
else parse_excluded_urls(_excluded_urls) | ||
) | ||
_InstrumentedFastAPI._excluded_urls = kwargs.get("excluded_urls") | ||
_InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider") | ||
_InstrumentedFastAPI._exclude_spans = kwargs.get("exclude_spans") | ||
fastapi.FastAPI = _InstrumentedFastAPI | ||
|
@@ -365,43 +401,29 @@ class _InstrumentedFastAPI(fastapi.FastAPI): | |
_server_request_hook: ServerRequestHook = None | ||
_client_request_hook: ClientRequestHook = None | ||
_client_response_hook: ClientResponseHook = None | ||
_http_capture_headers_server_request: list[str] | None = None | ||
_http_capture_headers_server_response: list[str] | None = None | ||
_http_capture_headers_sanitize_fields: list[str] | None = None | ||
_exclude_spans: list[Literal["receive", "send"]] | None = None | ||
|
||
_instrumented_fastapi_apps = set() | ||
_sem_conv_opt_in_mode = _StabilityMode.DEFAULT | ||
|
||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
tracer = get_tracer( | ||
__name__, | ||
__version__, | ||
_InstrumentedFastAPI._tracer_provider, | ||
schema_url=_get_schema_url( | ||
_InstrumentedFastAPI._sem_conv_opt_in_mode | ||
), | ||
) | ||
meter = get_meter( | ||
__name__, | ||
__version__, | ||
_InstrumentedFastAPI._meter_provider, | ||
schema_url=_get_schema_url( | ||
_InstrumentedFastAPI._sem_conv_opt_in_mode | ||
), | ||
) | ||
self.add_middleware( | ||
OpenTelemetryMiddleware, | ||
excluded_urls=_InstrumentedFastAPI._excluded_urls, | ||
default_span_details=_get_default_span_details, | ||
server_request_hook=_InstrumentedFastAPI._server_request_hook, | ||
client_request_hook=_InstrumentedFastAPI._client_request_hook, | ||
client_response_hook=_InstrumentedFastAPI._client_response_hook, | ||
# Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation | ||
tracer=tracer, | ||
meter=meter, | ||
http_capture_headers_server_request=_InstrumentedFastAPI._http_capture_headers_server_request, | ||
http_capture_headers_server_response=_InstrumentedFastAPI._http_capture_headers_server_response, | ||
http_capture_headers_sanitize_fields=_InstrumentedFastAPI._http_capture_headers_sanitize_fields, | ||
exclude_spans=_InstrumentedFastAPI._exclude_spans, | ||
FastAPIInstrumentor.instrument_app( | ||
self, | ||
server_request_hook=self._server_request_hook, | ||
client_request_hook=self._client_request_hook, | ||
client_response_hook=self._client_response_hook, | ||
tracer_provider=self._tracer_provider, | ||
meter_provider=self._meter_provider, | ||
excluded_urls=self._excluded_urls, | ||
http_capture_headers_server_request=self._http_capture_headers_server_request, | ||
http_capture_headers_server_response=self._http_capture_headers_server_response, | ||
http_capture_headers_sanitize_fields=self._http_capture_headers_sanitize_fields, | ||
exclude_spans=self._exclude_spans, | ||
) | ||
self._is_instrumented_by_opentelemetry = True | ||
_InstrumentedFastAPI._instrumented_fastapi_apps.add(self) | ||
|
||
def __del__(self): | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.