fix(app_utils): fix apm capture fastapi calls, reorder apm middle ware#118
Merged
HosseinNejatiJavaremi merged 1 commit intoMay 31, 2026
Merged
Conversation
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Description
Elastic APM was capturing zero transactions for any FastAPI service that also enabled the Prometheus metric interceptor (
PROMETHEUS.IS_ENABLED=true). Errors were still reported, and functions (which use the@async_capture_transactiondecorator, not FastAPI middleware) were unaffected — so the breakage looked intermittent and service-specific.Root cause: in
AppUtils.create_fastapi_appthe Elastic APM middleware was registered before the Prometheus metric interceptor. BecauseFastAPI.add_middlewareprepends to the middleware list, the last-registered middleware runs outermost. This left the order as:The fix swaps the registration order so Elastic APM is registered last and therefore becomes the outermost middleware. No API or configuration changes; purely middleware registration order.
Fixes # (issue)
Type of change
How Has This Been Tested?
Minimal Starlette app using the real
elasticapm.contrib.starlette.ElasticAPMwith a queue counter (transactions queued to the agent),DISABLE_SEND=True:After the fix, services with both Prometheus and Elastic APM enabled capture request transactions again.
ELASTIC_APM.IS_ENABLED=trueandPROMETHEUS.IS_ENABLED=true; confirm request transactions appear in APM under transaction typerequest.make lint/make behavepass.Checklist:
Additional context
Why this caused every transaction to be dropped
Elastic APM tracks the "current transaction" in a
ContextVar.The Starlette
ElasticAPMmiddleware callsclient.begin_transaction("request")(which does
ContextVar.set(transaction)) on the way in, andclient.end_transaction(name, result)(which doesContextVar.get()thenfinalizes and queues the transaction) on the way out. The begin and the end
must run in the same execution context, or
end_transactioncannot find thetransaction and nothing is queued.
ContextVarwrites do not propagate across asyncio tasks.A new task receives a copy of the context at creation time; a
ContextVar.set()inside that task is invisible to the parent and to sibling tasks.
Starlette's
BaseHTTPMiddlewareruns the downstream app in a separate task.To bridge the streaming ASGI
send/receiveinto its request/response model,BaseHTTPMiddlewaredoestask_group.start_soon(self.app, ...). The PrometheusFastAPIMetricInterceptorextendsBaseHTTPMiddleware, so everything below it —including the
ElasticAPMmiddleware — executed inside a forked context.Result: the APM transaction was begun and ended in a forked context that was
disconnected from (a) the agent's top-level tracing state established by
elasticapm.instrument()patchingStarlette.middleware_stack, and (b) the realsend(BaseHTTPMiddlewaresubstitutes its ownsend, so thehttp.response.startused to deriveresult/outcomewas severed). Thetransaction object simply went out of scope without being collected — no error,
no log, just silently missing transactions.
Standalone errors (
capture_exception) do not depend on this ContextVar round-trip,which is why errors still showed up while transactions did not.
The fix
With Elastic APM outermost,
begin_transactionandend_transactionboth run inthe original request task, before any
BaseHTTPMiddlewareforks a child task. Thetransaction lives in one context for its whole lifetime, the wrapped
sendsees thegenuine
http.response.start, and the transaction is finalized and queued. ThePrometheus
BaseHTTPMiddlewarenow forks its child task below APM, which isharmless to the request transaction.
This matches Elastic's own guidance that the Starlette/FastAPI APM middleware must be
the first (outermost) middleware and is incompatible with running underneath a
BaseHTTPMiddleware.Impact
AppUtils.create_fastapi_appwithELASTIC_APM.IS_ENABLED=trueandPROMETHEUS.IS_ENABLED=true.