Closed
Description
The Starlette
application class (which is the parent of the FastAPI
application class) hardcodes the ServerErrorMiddleware
to appear as the very first middleware. This means that if there is an error in some downstream middleware, handling the request with ServerErrorMiddleware
will not have an active trace.
Also, if you happen to do something like this:
app = FastAPI(...)
app.add_middleware(...)
app.add_middlware(....)
The additional middleware added after the first __init__
of FastAPI
will also not have an active trace established.
There is a relatively easy way around this to ensure that the OpenTelemetry middleware is always first:
class _InstrumentedFastAPI(fastapi.FastAPI):
_tracer_provider = None
_excluded_urls = None
_server_request_hook: _ServerRequestHookT = None
_client_request_hook: _ClientRequestHookT = None
_client_response_hook: _ClientResponseHookT = None
def build_middleware_stack(self) -> ASGIApp:
original_mw_app = super().build_middleware_stack()
return OpenTelemetryMiddleware(
original_mw_app,
excluded_urls=_InstrumentedFastAPI._excluded_urls,
default_span_details=_get_route_details,
server_request_hook=_InstrumentedFastAPI._server_request_hook,
client_request_hook=_InstrumentedFastAPI._client_request_hook,
client_response_hook=_InstrumentedFastAPI._client_response_hook,
tracer_provider=_InstrumentedFastAPI._tracer_provider,
)
FastAPI version - 0.70.0
Starlette version - 0.16.0
Python version - 3.9
Metadata
Metadata
Assignees
Labels
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
phillipuniverse commentedon Nov 9, 2021
Slightly more concrete use case:
In this case the
add_exception_handler
is hooked up as the error handler forServerErrorMiddleware
. Because of this and because the OpenTelemetry middleware is installed as "user middleware", thelogger.exception
invocation within theunhandled_exception_handler
will not have a trace id or span id as the span has already ended.phillipuniverse commentedon Nov 9, 2021
The workaround for now is to add an
opentelemetry_pre_instrument
entrypoint. Here is my patching function:Then you need an entrypoint for
fastapi
underopentelemtry_pre_instrument
. In my case I am using Poetry so mypyproject.toml
looks like this:With this change I am able to see trace/span ids in logger invocations from
ServerErrorMiddleware
.I would like to contribute this back but the problem is I don't think this will work for the non-auto-instrument case which does
add_middleware
:opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-fastapi/src/opentelemetry/instrumentation/fastapi/__init__.py
Lines 109 to 141 in 5993329
This whole process is a little shady anyway. It seems a little strange to me that Starlette/FastAPI does not give first-class support to fully control the final middleware ordering.
Also let me just say, I have been really impressed with all of the extension points that I have seen within this project. It was really nice to be able to self-serve and unblock myself in a not as nasty way. Great work!!
lskrajny-marcura commentedon Mar 11, 2023
Hi, is it possible to monkey patch it without poetry, trying to do in multiple ways, but OpenTelemetryFirstMiddlewareFastApi class is not used :/
phillipuniverse commentedon Mar 11, 2023
@lskrajny-marcura depends on how you are kicking off the instrumentation process. The poetry part above is not important per se, the important part is that you add the monkey patching as an entry_point.txt such that it runs prior to any default open telemetry instrumentation.
lskrajny-marcura commentedon Mar 11, 2023
Have a simple app with main.py where i am first creating FastApi/Starlette app and then manually adding all opentelemetry instrumentations.
Having patching on top of main.py seems "too late"
lzchen commentedon Mar 28, 2023
@phillipuniverse
Do you foresee any repercussions for adding the tracing middleware first? Would it conflict with other middlewares?
phillipuniverse commentedon Mar 28, 2023
@lzchen it has not in my experience. I have been running this patch in production since November 9, 2021 with no issues.
I did have to make one change in FastAPI 0.92/Starlette 0.24 with the change to lazily-instantiated middleware but it was pretty straightforward (I'll update my answer above to include the change, it's not there currently).
In my scenario this change is crucial for us to be able to be able to annotate unhandled exceptions in our application with a trace id in our external error reporting system.
FWIW I would consider the main problem to be a FastAPI problem where it limits full control over the entire middleware list.
lzchen commentedon Mar 28, 2023
@phillipuniverse
Makes sense to me. Would you consider creating a PR for this?
phillipuniverse commentedon Mar 28, 2023
No problem!
Kyle-sandeman-mrdfood commentedon Feb 16, 2024
Hi, this is still an issue even in
fastapi==0.103.2
.I am using both
app.add_middleware(MyCustomMW)
and custom exception handlersHere are my debugging steps, if it will help anyone understand the problem better
In your asgi instrumentation's
__init__.py
I added two print statements to show exactly when the span starts and whether it is recording (i.e. in thewith ..use_span
andif ..is_recording
)With auto instrumentation I get these logs
And after using a patch similar to the one posted earlier in this issue:
Then I get:
And just to inspect the middleware stack creation, I added prints to the various
__init__
functions:original:
patched:
alexmojaki commentedon Oct 7, 2024
+1 for this. It seems that unhandled errors cause the span to be missing response headers and a status code and that the response itself misses the
traceresponse
header.However we should assume that
OpenTelemetryMiddleware
might itself raise errors, either from bugs in OTel or from user code. For example, nothing currently handles exceptions raised by the providedserver_request_hook
. If these errors aren't handled then I think that might mean a response isn't sent at all. There could of course be a giant try/except around all the OTel code, but it seems most sensible to defer to any custom error handling via a secondServerErrorMiddleware
, i.e:Or maybe:
phillipuniverse commentedon May 28, 2025
Well looks like this got fixed by #3012!