Repository navigation
fix(fastapi): propagate request correlation across async tasks and sync worker threads - #123
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
…ation Restore json_logging/util.py to master so custom framework integrations that support a global request object keep logging '-' outside a request, instead of failing in the formatter. The call-stack fallback now lives in FastAPIRequestInfoExtractor.get_current_request(). Only use the ContextVar on Python 3.7+. The contextvars backport available on 3.6 is not asyncio-aware, so 3.6 keeps the previous lookup. Add tests for context reset, same-event-loop concurrency, the no-instrumentation fallback and the custom framework contract. Add a changelog entry, make httpx an explicit test dependency, fix the test module docstring and imports.
With the request now visible in worker threads, two threadpool workers of one request could both find no correlation id, each generate one, and disagree with each other and with the request log. The middleware now resolves the id once before the endpoint runs (including excluded URLs), so later calls in any thread read the same id. Errors from that call are ignored, so a failing custom generator behaves as before. Add a deterministic two-worker test (instrumented and excluded route) that forces both workers into id generation, and clarify the changelog.
…ntext is active On Python < 3.7 the request context variable is not used, so skip the eager per-request correlation-id resolution there as well and keep the previous lazy behaviour unchanged (no id is created for requests that never ask for one). Restore CORRELATION_ID_GENERATOR in the two-worker race test with monkeypatch.
Python 3.8 and earlier cannot be tested in CI, so keep their legacy behaviour unchanged: no request ContextVar, call-stack lookup only, and no eager correlation-id creation. Add a regression test that simulates Python 3.8 and checks the disabled-feature fallback, and document the compatibility boundary in the changelog.
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
There was a problem hiding this comment.
LlamaPReview — No blocking issues found
Read bounded PR-head context from json_logging/framework/fastapi/implementation.py.
Change flow
How one request's correlation ID is set once and reaches async handlers, sync worker threads, application logs, and the request log, then is reset.
sequenceDiagram
participant C as Client
participant M as Middleware
participant E as Endpoint
participant L as JSON formatter
C->>M: HTTP request
note over M: PR change — ContextVar set and ID resolved once before call_next
M->>M: Resolve ID (state, then header, then generate once)
M->>E: call_next with inherited context
alt async handler
E->>L: Log or get_correlation_id()
L-->>E: Request correlation ID
else sync def handler
E->>E: AnyIO worker with copied context
E->>L: Log or get_correlation_id()
L-->>E: Request correlation ID
end
E-->>M: Response
M->>C: Response + request log with same ID
note over M: PR change — ContextVar reset in finally
Review details and evidence
Material unknowns
- Whether the pre-3.9 code path behaves correctly on a real Python 3.8 runtime rather than a simulated one. Could change confidence in the claim that pre-3.9 behavior is unchanged; Deep assigned it no merge-posture weight because the disabled branch is the mechanically inert master path.
- Check: Optional: run the suite once on an actual Python 3.8 interpreter to confirm the simulated pre-3.9 path.
- Whether the author's older FastAPI/Starlette/AnyIO compatibility matrix holds. Only affects the breadth of the stated support claim; exact-head CI covers Python 3.9–3.13 with the installed dependency set, which is the stated acceptance boundary.
- Check: Optional: spot-check an older FastAPI/Starlette/AnyIO combination if that matrix is still supported.
- The literal contents of the built wheel manifest. A packaging-artifact detail; the source delta confines changes under json_logging/ to implementation.py and no packaging/version metadata changed.
- Check: Optional: inspect the built wheel file list if packaging provenance matters for release.
LlamaPReview checks
- Read bounded PR-head context from
json_logging/framework/fastapi/implementation.py. - Read bounded PR-head context from
tests/test_fastapi.py.
LlamaPReview is open source. Explore the code behind this review.
The Added entry pointed to a release-readiness report that is not part of the repository, and the Documentation entry described a compatibility limit, which is already listed under Compatibility. Both sections are removed. The heading no longer says 'TBD (release candidate)'; the date is added when the release is cut. The section still lists only merged work (PR #123).
Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section covering every change merged since 1.5.1: - Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98) - Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133) - Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and Flask example (#131, fixes #25) - Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage The previous 'other integrations unchanged' and 'no change to the JSON schema' statements are replaced, since they do not hold for the whole release. The date is added when the release is cut. No library code change.
* chore(release): prepare json-logging 1.6.0 Bump the version to 1.6.0 and collect the changelog into a 1.6.0 section covering every change merged since 1.5.1: - Added: json_logging.EXCLUDED_FIELDS (#129, fixes #98) - Fixed: FastAPI request-context propagation (#123, fixes #124); outside-request correlation IDs in Flask, Quart and Connexion (#127, fixes #125); non-web get_correlation_id() (#128, fixes #126); Sanic 24+ logging config (#133) - Documentation: FastAPI/Uvicorn startup (#130, fixes #106); Gunicorn and Flask example (#131, fixes #25) - Compatibility: default output unchanged unless EXCLUDED_FIELDS is set; the framework behaviour changes; Python 3.9+ FastAPI context path; CI coverage The previous 'other integrations unchanged' and 'no change to the JSON schema' statements are replaced, since they do not hold for the whole release. The date is added when the release is cut. No library code change. * chore(release): credit Bob T. and Udit Agrawal in 1.6.0 metadata
Problem
FastAPI
defendpoints execute in AnyIO worker threads.json-logginguses Python call-stack (frame) inspection to discover the active request, and that fails when the request is not in the worker thread's own stack. As a result,logger.info(...)andjson_logging.get_correlation_id()emit"-"while the same HTTP request's instrumentation log has a real correlation ID.Fixes #124. This failure was independently reproduced against the published
1.5.1package and currentmaster. #80 described a similar symptom in 2021, but the reporter later confirmed it was working and that issue was closed as resolved. This PR does not claim to fix or reopen #80.Change (FastAPI integration only)
ContextVarholds the current request. It is set inJSONLoggingASGIMiddleware.dispatchfor the request's lifetime (including URLs excluded from request logging) and reset infinally.FastAPIRequestInfoExtractorreads it through the existing extractor interface. When no request context is active, it falls back to the previous call-stack lookup inside the FastAPI integration.CORRELATION_ID_GENERATORbehaves as before.json_logging/util.pyand the shared framework code are unchanged frommaster. Custom framework integrations (README §5) that support a global request object keep logging"-"outside a request.Compatibility boundary
ContextVar-based propagation is enabled only on Python 3.9+, the versions covered by CI (3.9–3.13). Python 3.8 and earlier, including 3.6 where only the thread-local
contextvarsbackport exists, keep the original behaviour unchanged:We could not test those versions, so the PR deliberately preserves their legacy behaviour instead of enabling unverified functionality. Other behaviour that stays as before on every version:
threading.Thread,loop.run_in_executor()) and WebSocket endpoints: there, the request is found only if it is held in one of that thread's own frames, otherwise"-".init_request_instrument(app).Regression coverage
get_correlation_id()return the header ID.init_request_instrument(): the call-stack fallback still works."-"without raising.Validation
master;json_logging/framework/fastapi/implementation.pychanges in the wheel.No version bump, PyPI release or tag is included. Merging requires an independent maintainer review.
Summary by CodeRabbit
New Features
Compatibility