Repository navigation
docs: add tested Gunicorn and Flask integration example - #131
Conversation
|
ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
Adds example/gunicorn_flask (a Flask app plus a gunicorn.conf.py) and a README that explains what each output looks like: master-process messages stay plain text, workers emit JSON application and request logs, Gunicorn's access log is disabled to avoid a duplicate line per request, and records outside a request carry '-' once #125 is merged. tests/test_gunicorn_flask.py starts the example with a real Gunicorn worker on a Unix socket, sends a supplied-ID request and a generated-ID request, and checks that application and request records carry the same IDs and that no gunicorn.access JSON line is written. gunicorn is added to test-requirements.txt for this test; it is skipped when gunicorn is not installed. Local status: the sandbox this change was prepared in refuses Gunicorn's socket connection (EPERM), so the automated test was not run to completion there. The launch and record layout were verified manually earlier. Fixes #25
- Readiness: the test waited only for the Unix socket file to exist, which does
not mean the worker accepts requests. It now polls GET /ready over the socket
until 200, with a 30 s monotonic deadline and a 2 s per-attempt timeout. It
fails early, with gunicorn's full output, if the process exits during startup.
- /ready is a route excluded from request logging, so probing adds no records.
- Gunicorn's output goes to a file, not a pipe, so a large log cannot block the
server. The server is terminated and reaped in a finally block.
- The socket lives in a short temporary directory, because macOS limits Unix
socket paths to about 104 bytes and pytest's tmp_path can exceed that.
- Every line starting with '{' must be valid JSON. Previously malformed lines
were skipped silently.
- In CI (CI=true) a missing gunicorn fails the test instead of skipping it.
- README: states the dependency on #127 explicitly, documents the readiness
probe and the --preload assumption.
Local status: this sandbox refuses gunicorn's own socket check (EPERM), so the
end-to-end run fails here. It fails fast with the output attached.
Refs #25
…docs - The README said removing accesslog = None turns Gunicorn's access log on. None is already Gunicorn's default, so nothing changes. Document how to turn it on (accesslog = "-" or --access-logfile -) and the duplicate records. - Detect Gunicorn access lines in the raw output, in plain text or wrapped in a JSON record, instead of only checking JSON records for gunicorn.access. - Add a negative control that enables --access-logfile - and checks that the detector finds the extra lines. - Assert that only the two / requests are request-logged (/ready is excluded), that the worker's 'Worker exiting' gunicorn.error record is JSON, and that the workers have exited. - Keep Gunicorn 25+'s control socket in the test's temporary directory. - Narrow the gunicorn.error wording: '-' outside requests needs PR #127. - Drop a reference to a log file that is not in the repository.
- Add a two-worker test (the configured count). It waits until both workers have logged their startup record, then checks per worker that each booted, logged 'worker ready' with its own PID and correlation_id '-', and exited with a JSON 'Worker exiting' record with '-'. Request checks are per request, so they do not depend on which worker served it. - app.py logs 'worker ready' at import time, outside any request; the tests assert it gets json_logging.EMPTY_VALUE. Removing #127's Flask guard fails the one- and two-worker tests. - Send an X-Request-ID request as well, and check X-Correlation-ID, X-Request-ID and generated IDs across responses, application logs and request records. - README: remove the 'depends on PR #127' wording (merged); give the stream for each kind of output (errorlog '-' is stderr, accesslog '-' is stdout); say when the 'Update all existing logger' line appears; add the scope of the example (sync worker; gevent, --preload and a custom logconfig not tested) and how init_flask() treats handlers from a custom logging configuration. - gunicorn.conf.py: errorlog '-' writes to stderr, not stdout.
bcc0de2 to
72d0dfb
Compare
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
Summary
Fixes #25 ("how can gunicorn, flask and json-logging combine together?") with a runnable, tested example in
example/gunicorn_flask/. Nojson_logging/library code changes.What the example does
app.pycallsjson_logging.init_flask(enable_json=True)andinit_request_instrument(app, exclude_url_patterns=[r"^/ready$"])at import time.app.pyin each worker after the fork, so json-logging is initialised once per worker process, where requests are handled. The master never runs it.gunicorn.conf.py:workers = 2,accesslog = None(no duplicate access records),errorlog = "-".Output. Each kind of output was checked against a real Gunicorn 26.2.0 run, with stdout and stderr captured separately.
json_logging"Update all existing logger" line (when JSON is enabled and the root logger has no handlers)worker ready(logged outside any request) and request-time application logsgunicorn.errorrecords, such asWorker exitingcorrelation_id-errorlog = "-"is stderr andaccesslog = "-"is stdout; checked in Gunicorn'sglogging.py.accesslog = Nonemeans no access log. With Gunicorn's access log on, each request has two access records, Gunicorn's and json-logging's.Correlation IDs, checked in a real worker:
X-Correlation-IDis preserved;X-Request-IDis used whenX-Correlation-IDis absent;-(json_logging.EMPTY_VALUE).Tests (
tests/test_gunicorn_flask.py, real Gunicorn on a Unix socket, 3 tests). Each run sends 3 requests: withX-Correlation-ID, withX-Request-ID, and with no header.worker readyandWorker exitingrecords are JSON with-./readyis not request-logged, and there is no Gunicorn access line, in plain text or JSON.worker ready, which is synchronisation on the log rather than a sleep. Then, per worker: boot,worker readywith its own PID and-, and a JSONWorker exitingrecord with-. Request checks are per request, independent of which worker served it.--access-logfile -, the detector finds Gunicorn's 3GET /lines and the/readylines, alongside json-logging's 3 request records.Robustness:
/tmpdirectory keeps the Unix-socket path under macOS's limit;{lines fail the test;CI=true, a missing gunicorn is a hard failure, not a skip.Mutation checks:
Scope. Only the default sync worker, Gunicorn's built-in logging, and the default startup without
--preloadare tested. Issue #25 uses gevent and a customlogging.conf; neither is exercised here. The README explains whatinit_flask()does to handlers from a custom logging configuration:config_root_logger().It also says how to adapt the example to that setup.
Rebase. Onto master
237b46f, which includes #127–#130 and #133. OnlyCHANGELOG.mdconflicted; all entries are kept. The diff against master is onlyexample/gunicorn_flask/*,tests/test_gunicorn_flask.py,test-requirements.txt(+gunicorn) andCHANGELOG.md.Results (head
72d0dfb):test-requirements.txt(including gunicorn) and setsCI=true, so the Gunicorn tests run rather than skip in each Tests job.bind()sometimes fails with EPERM, outside the test's control.Earlier review rounds are in the PR's commit history.