Skip to content

docs: add tested Gunicorn and Flask integration example - #131

Merged
suppagoddo merged 4 commits into
masterfrom
docs/25-gunicorn-flask
Oct 21, 2026
Merged

suppagoddo merged 4 commits into
masterfrom
docs/25-gunicorn-flask

Conversation

@suppagoddo

@suppagoddo suppagoddo commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #25 ("how can gunicorn, flask and json-logging combine together?") with a runnable, tested example in example/gunicorn_flask/. No json_logging/ library code changes.

What the example does

  • app.py calls json_logging.init_flask(enable_json=True) and init_request_instrument(app, exclude_url_patterns=[r"^/ready$"]) at import time.
  • Gunicorn's default startup imports app.py in 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.

Output Stream Format
master and boot messages stderr plain text
json_logging "Update all existing logger" line (when JSON is enabled and the root logger has no handlers) stdout JSON
worker ready (logged outside any request) and request-time application logs stderr JSON
json-logging request records stdout JSON
worker gunicorn.error records, such as Worker exiting stderr JSON, correlation_id -

errorlog = "-" is stderr and accesslog = "-" is stdout; checked in Gunicorn's glogging.py. accesslog = None means 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-ID is preserved;
  • X-Request-ID is used when X-Correlation-ID is absent;
  • without either, one ID is generated per request and shared by its application log and request record;
  • logs outside a request get - (json_logging.EMPTY_VALUE).

Tests (tests/test_gunicorn_flask.py, real Gunicorn on a Unix socket, 3 tests). Each run sends 3 requests: with X-Correlation-ID, with X-Request-ID, and with no header.

  1. One worker. Each request's response, application log and request record agree. The worker ready and Worker exiting records are JSON with -. /ready is not request-logged, and there is no Gunicorn access line, in plain text or JSON.
  2. Two workers (the configured count). The test waits until both workers have logged worker ready, which is synchronisation on the log rather than a sleep. Then, per worker: boot, worker ready with its own PID and -, and a JSON Worker exiting record with -. Request checks are per request, independent of which worker served it.
  3. Access log enabled (negative control). With --access-logfile -, the detector finds Gunicorn's 3 GET / lines and the /ready lines, alongside json-logging's 3 request records.

Robustness:

  • readiness waits up to 30 seconds on the monotonic clock, with an early failure (including Gunicorn's output) if Gunicorn exits;
  • each HTTP request has a 2-second timeout;
  • Gunicorn is stopped with SIGTERM, then killed after 20 seconds, and the test checks the worker PIDs are gone;
  • output goes to a file, not a pipe, so it can't deadlock;
  • a short /tmp directory keeps the Unix-socket path under macOS's limit;
  • Gunicorn 25+'s control socket is kept in the temporary directory;
  • malformed { lines fail the test;
  • with 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 --preload are tested. Issue #25 uses gevent and a custom logging.conf; neither is exercised here. The README explains what init_flask() does to handlers from a custom logging configuration:

  • handlers on named loggers get the JSON formatter;
  • root-logger handlers are changed only by config_root_logger().

It also says how to adapt the example to that setup.

Rebase. Onto master 237b46f, which includes #127–#130 and #133. Only CHANGELOG.md conflicted; all entries are kept. The diff against master is only example/gunicorn_flask/*, tests/test_gunicorn_flask.py, test-requirements.txt (+gunicorn) and CHANGELOG.md.

Results (head 72d0dfb):

  • GitHub Actions: 15/15. Unit tests on Python 3.9–3.14, Flask/Quart/FastAPI/Sanic smoke tests on 3.9 and 3.14, and Flake8. Run: https://github.com/bobbui/json-logging-python/actions/runs/39523617759
    • CI installs test-requirements.txt (including gunicorn) and sets CI=true, so the Gunicorn tests run rather than skip in each Tests job.
    • The job logs could not be downloaded from the reviewer's environment, so per-test lines were not read.
  • Locally, Python 3.12, Gunicorn 26.2.0: all 3 Gunicorn tests passed under pytest (97 passed in the full suite) and when run directly. In this environment, Gunicorn's Unix-socket bind() sometimes fails with EPERM, outside the test's control.
  • Locally, Python 3.9/3.10/3.11/3.13: 94 passed, with the Gunicorn module skipped because gunicorn is not installed there.
  • Blocking flake8: 0.

Earlier review rounds are in the PR's commit history.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because the subscription is no longer active. Ask your workspace admin to reactivate the subscription to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7dcf0ce8-d098-4003-b7d9-e167b122841a
📥 Commits

Reviewing files that changed from the base of the PR and between 237b46f and 72d0dfb.

📒 Files selected for processing: 6
  • CHANGELOG.md
  • example/gunicorn_flask/README.md
  • example/gunicorn_flask/app.py
  • example/gunicorn_flask/gunicorn.conf.py
  • test-requirements.txt
  • tests/test_gunicorn_flask.py

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

📝 Walkthrough

Adds a documented Flask example that runs under Gunicorn with JSON request logging. Integration tests cover correlation IDs, readiness probes, worker lifecycle records, and Gunicorn access-log behavior.

Changes: Gunicorn Flask example

Layer / File(s) Summary
Configure and document the Flask example
example/gunicorn_flask/*, CHANGELOG.md
Adds Flask routes, request instrumentation, and worker-startup logging. Gunicorn uses two workers, disables access logs, and sends error logs to stderr. The README describes setup, logging behavior, correlation IDs, and test coverage.
Validate Gunicorn integration
test-requirements.txt, tests/test_gunicorn_flask.py
Adds Gunicorn as a test dependency and integration tests for supplied and generated correlation IDs, readiness-probe exclusion, worker startup and shutdown, and access-log detection.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Gunicorn
  participant FlaskApp as Flask app
  participant JSONLogging as JSON logging
  Gunicorn->>FlaskApp: Start worker and load application
  FlaskApp->>JSONLogging: Initialize request instrumentation
  Client->>Gunicorn: Send request
  Gunicorn->>FlaskApp: Route request
  FlaskApp->>JSONLogging: Record request
  FlaskApp->>Client: Return response with correlation ID
Loading
Merge Risk: ⚪ Minimal · up to 72d0d

The example and its integration tests are mergeable after normal checks; the reported intermittent test failure does not occur through the claimed buffering mechanism.

Security architecture risk: ⚪ Minimal · up to 72d0d

The example defaults to a local listener and reuses existing request-correlation behavior. No material security risk was identified in the new runtime configuration or test harness.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is bounded to the locally bound example and same-identity test subprocesses. A configured bind override can widen network reachability, but the supplied configuration does not do so by default.

Trust Boundaries and Controls

  • inferred — Caller-supplied correlation IDs cross into diagnostic output and the root response, not an authentication or authorization decision. The two route handlers perform no privileged action, so correlation-ID control does not confer additional authority in this example.

Resilience and Maintainability Implications

  • observed — Each test invocation derives its listener, log, and supported Gunicorn control-socket paths from a unique temporary directory. This avoids intentional socket sharing between concurrent runs and keeps the harness off the example’s default TCP listener.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 3 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: a tested Gunicorn and Flask integration example with supporting documentation.
Linked Issues check Passed Issue #25 asks for an example that combines Gunicorn, Flask, and json-logging-python. example/gunicorn_flask/app.py initializes Flask JSON logging and request instrumentation. gunicorn.conf.py pro…
Out of Scope Changes check Passed The changed files stay connected to issue #25. The example, README, Gunicorn test, gunicorn test dependency, and changelog entry support the requested integration example and its verification. No un…
Full details: Linked Issues check

Explanation

Issue #25 asks for an example that combines Gunicorn, Flask, and json-logging-python. example/gunicorn_flask/app.py initializes Flask JSON logging and request instrumentation. gunicorn.conf.py provides a runnable two-worker configuration and disables duplicate Gunicorn access logs. The README documents startup, output, correlation IDs, and configuration limits. tests/test_gunicorn_flask.py exercises real Gunicorn workers and verifies application/request records and lifecycle behavior. The documented synchronous-worker scope satisfies the example request; issue #25 does not require gevent or preload coverage.

Full details: Out of Scope Changes check

Explanation

The changed files stay connected to issue #25. The example, README, Gunicorn test, gunicorn test dependency, and changelog entry support the requested integration example and its verification. No unrelated change is established by the reviewed summary or file evidence.

Full details: Docstring Coverage

Explanation

Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 15 functions across 3 files. (3 skipped: 3 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.
@suppagoddo
suppagoddo force-pushed the docs/25-gunicorn-flask branch from bcc0de2 to 72d0dfb Compare October 21, 2026 00:49
@suppagoddo
suppagoddo merged commit ae87370 into master Oct 21, 2026
16 checks passed
suppagoddo added a commit that referenced this pull request Oct 21, 2026
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.
suppagoddo added a commit that referenced this pull request Oct 21, 2026
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gunicorn flask and json-logging-python combine together

1 participant