Skip to content

feat: support configurable exclusion of JSON log fields - #129

Merged
suppagoddo merged 4 commits into
masterfrom
feat/98-field-exclusion
Oct 20, 2026
Merged

suppagoddo merged 4 commits into
masterfrom
feat/98-field-exclusion

Conversation

@suppagoddo

@suppagoddo suppagoddo commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Final status (head 08350d7, rebased on master 6835303)

What it does. json_logging.EXCLUDED_FIELDS (default ()) names fields to omit from every JSON record: application, web and request logs, including extra/props entries. It accepts a list, tuple, set or frozenset of strings. A bare string, or a non-string entry, raises TypeError. Fixes #98.

Why filtering is done in BaseJSONFormatter.format().

  • It is the single path every built-in JSON formatter, and every subclass that inherits or calls it, already goes through. One place covers all record types.
  • Fields are removed from the dict being serialised, after it is built. The LogRecord itself is never modified, so other handlers see the original record.
  • Each formatter takes a snapshot of the setting when it is constructed, like COMPONENT_*.
  • init_*() and init_request_instrument() validate the setting before changing any global state, so an invalid value can be corrected and the call retried.

This round:

  • Issue Disabling unnesesary fields in output #98 end-to-end test. An application logger gets a StreamHandler(io.StringIO()) before init_flask(enable_json=True) and init_request_instrument(app, ...), as in the issue. A real Flask request then logs through that handler. The test does not set a formatter itself, and checks that:
    • the library installed JSONLogWebFormatter on the existing handler;
    • no handler was added;
    • X-Correlation-ID is preserved;
    • msg, level, type and written_at remain.
  • Its cases: ["written_ts"], the default () (where written_ts is kept), and ["correlation_id", "no_such_field"] (an unknown name is ignored, and get_correlation_id() still returns the header value).
  • Mutation checks: removing the filtering in format() fails the two exclusion cases. Not installing formatters on existing handlers fails all three.
  • Test isolation. The request-logger fixture is replaced by an autouse fixture. After each test, including failing ones, it restores logging._defaultFormatter and, for every logger, its handlers, handler formatters, level, parent, propagation and disabled state. Loggers created during a test are reset.
    • Before: 8 tests leaked _defaultFormatter and changed the handler formatters of unrelated library loggers (urllib3, requests, charset_normalizer, concurrent.futures).
    • After: 0 leaks, measured by a temporary plugin (not committed) that compares logging state before and after every test. That holds for the module alone, before and after tests/test_flask.py, and in forward and reversed order of the full suite.
    • All 32 tests pass alone, and 10 repeated runs alongside test_flask.py had 0 failures. The only remaining leaks come from tests/test_flask.py on master, which this PR doesn't touch.
  • Removed one test fully covered by test_formatters_built_at_different_times_keep_their_own_snapshots.
  • README 2.8:
    • exclusions apply only to formatters that use BaseJSONFormatter.format(); an independent logging.Formatter must filter fields itself;
    • excluding fields changes the JSON schema;
    • this is not a security redaction or data-loss-prevention control.
  • Rebase: onto master 6835303, which includes fix: prevent fabricated correlation IDs outside active requests #127, fix: safely handle correlation ID lookup in non-web mode #128 and ci: add Python 3.14 unit and framework smoke-test coverage #133. Only CHANGELOG.md conflicted; all master entries are kept and the Disabling unnesesary fields in output #98 entry is added. The diff against master contains only this PR's five files. fix: prevent fabricated correlation IDs outside active requests #127's request-context guards, fix: safely handle correlation ID lookup in non-web mode #128's non-web fallback and ci: add Python 3.14 unit and framework smoke-test coverage #133's Sanic pop("format", None) are intact.
  • Also restored a space accidentally dropped in init_request_instrument() in an earlier round of this PR.

Results:

  • Locally: 32/32 in tests/test_excluded_fields.py; 88/88 full suite on Python 3.9–3.13; blocking flake8 0. Default JSON fields are identical to master.
  • GitHub Actions on 08350d7: 15/15 (unit tests on Python 3.9–3.14; Flask/Quart/FastAPI/Sanic smoke tests on 3.9 and 3.14; Flake8). Run: https://github.com/bobbui/json-logging-python/actions/runs/39516295453

Remaining limits:

  • Python 2 behaviour (the basestring validator) is not tested; no Python 2 interpreter was available.
  • EXCLUDED_FIELDS is a new public setting and needs maintainer approval.

PR 3 — feat: support configurable exclusion of JSON log fields (Fixes #98)

Branch feat/98-field-exclusion, commit 63d5505.

Design. A module setting, json_logging.EXCLUDED_FIELDS = (), follows the existing COMPONENT_* pattern. The
setting is read when a formatter is constructed, so an invalid value fails at setup rather than inside a log call. A
bare string, or a collection with non-string entries, raises TypeError. The fields are removed in
BaseJSONFormatter.format(), which every JSON formatter uses, so standard, web and request records, custom subclasses,
extra and props are all covered.

Tests. tests/test_excluded_fields.py (14 tests). On master, 13 fail because the feature is absent; the empty-list
control passes. They cover the default, one and several exclusions, empty lists, the three formatters, extra/props,
custom subclasses, construction-time semantics and invalid values.

Default output. Compared with master (same record, time-varying fields removed): identical.

Local results.

  • Full unit suite, Python 3.9–3.13: 49 passed each (35 + 14).
  • Blocking flake8: 0.
  • Micro-benchmark, format() on a non-web record: master 5.35 µs, branch 5.44 µs, one run each, within noise. Not a claimed cost.

Compatibility. Default-off; no schema change. Documented in README §2.8, with a note that downstream consumers may
depend on written_ts, correlation_id and level.


Review update (round 2)

  • Head afbb073: fixed test-state pollution. test_request_log_formatter_honours_exclusion replaced the request
    logger's handlers and never restored them, leaving the logger with only the test's buffer handler and parent=None.
    Verified before and after: the test now restores handlers in finally, and a fixture requested by the tests that call init_request_instrument() restores the full
    logger state (handlers, level, parent, propagate, disabled).
  • Added tests: LogRecord is not mutated; exception text is removed only when exc_info is excluded; formatters built
    at different times keep their own snapshots.
  • Verified: 49/49 on Python 3.9-3.13 (forward order, reversed order, each test alone). Blocking flake8 clean.
  • Performance (pyperf, A-B-B-A, format() on a non-web record): master 5.90 and 5.84 us; branch 5.86 and 7.09 us
    (second branch run has std dev 1.56 us). With one excluded field the branch measured 6.00 and 5.75 us. Not a
    measurable regression at this precision, but the noise is large; treat it as no-regression-observed.
  • Semantics, documented in README 2.8: a global setting, read when a formatter is constructed; already-created formatters
    keep their setting; exclusion changes the output schema; downstream consumers may rely on removed fields; exclusion
    is not a guarantee that sensitive values cannot reach other handlers.

Review update (round 3, head 31c5074)

1. Invalid EXCLUDED_FIELDS poisoned later initialisation. Confirmed and fixed.

  • Reproduced on afbb073, in two forms.
    • EXCLUDED_FIELDS = "written_ts" followed by init_flask(enable_json=True): __init() set _current_framework and _request_util before the formatter constructor raised TypeError. After the setting was corrected, init_flask() raised RuntimeError: Can not call init more than once.
    • Through init_request_instrument(): the stdout handler was attached before the error, so a corrected retry left the request logger with 2 handlers.
  • Root cause: validation ran only inside BaseJSONFormatter.__init__, after global state had changed.
  • Fix: __init() and init_request_instrument() call _validated_excluded_fields(EXCLUDED_FIELDS) before changing any state. The setting is validated even when JSON is disabled, or when a custom formatter is used, because the request logger and the Flask/Connexion werkzeug logger always get json-logging formatters. Nothing else in the singleton initialisation changed.
  • Regression tests, each in a fresh subprocess:
    • an invalid then a corrected init_flask(enable_json=True), init_flask(enable_json=False) and init_non_web(enable_json=True);
    • after a successful init, a second init still raises the duplicate-init error;
    • an invalid then a corrected init_request_instrument(): no hooks, no handler and no disabled werkzeug are left after the failure; after the retry the request logger has exactly 1 handler, and the request record carries the supplied ID, without the excluded field.
  • Mutation check: with the early validation removed, these 3 tests fail. With it in place, all pass.

2. Python 2 string types. Fixed.

  • The validator now uses the module's existing basestring shim, so unicode field names pass on Python 2; on Python 3 basestring is str.
  • Bare str, unicode and bytes values are rejected, as are dicts, None, ints, and non-string entries (on Python 3 that includes bytes entries). Valid list, tuple, set and frozenset values, including u"" names, are tested.
  • No Python 2 interpreter was available, so Python 2 behaviour is reasoned from the code, not tested.

3. Test isolation. Fixed.

  • restored_request_logger now restores werkzeug as well as flask-request-logger: handlers, each handler's formatter, level, parent, propagate and disabled.
  • Before the fix, a pytest plugin that compares logger state before and after each test found a werkzeug leak in test_web_formatter_honours_exclusion_and_keeps_correlation_id. After: 0 leaks in this module, with each test run alone, in forward order, and in reversed order together with tests/test_flask.py. The only remaining leaks come from tests/test_flask.py, which is already on master and outside this PR's scope.

Results: 65/65 on Python 3.9–3.13 locally; blocking flake8 0; GitHub Actions 10/10 on 31c5074. Default JSON fields are unchanged from master.

Compatibility note: an invalid EXCLUDED_FIELDS now fails at init_*() even with JSON disabled. Before this, it failed later, or partway through. EXCLUDED_FIELDS is new in this PR, so no existing configuration is affected.

Public API: json_logging.EXCLUDED_FIELDS is a new public setting and needs maintainer approval.

Summary by CodeRabbit

  • New Features

    • Added configurable exclusion of selected fields from JSON log records, including request logs and additional log data.
    • Exclusions default to empty, preserving existing logging behavior.
    • Supports standard collections of field names and preserves non-excluded content.
  • Documentation

    • Added configuration guidance, supported value types, validation behavior, and setup requirements.
  • Tests

    • Added coverage for field exclusion, validation, formatter behavior, request logging, and preservation of log records.

@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: 146331f4-b824-447a-8690-860ad851ac85
📥 Commits

Reviewing files that changed from the base of the PR and between 31c5074 and 08350d7.

📒 Files selected for processing: 4
  • CHANGELOG.md
  • README.md
  • json_logging/__init__.py
  • tests/test_excluded_fields.py
🚧 Files skipped from review as they are similar to previous changes: 2
  • CHANGELOG.md
  • README.md

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 json_logging.EXCLUDED_FIELDS to configure fields omitted from JSON log records. The setting defaults to an empty tuple. The package validates it before initialization changes state, and formatters apply a validated snapshot when they format records.

Changes: JSON field exclusion

Layer / File(s) Summary
Configuration validation and initialization
json_logging/__init__.py, json_logging/formatters.py, tests/test_excluded_fields.py
Adds EXCLUDED_FIELDS and validates its container and entries. Initialization and request instrumentation validate the setting before changing state. Tests cover invalid configurations, retries, and formatter configuration snapshots.
Field filtering and documentation
json_logging/formatters.py, tests/test_excluded_fields.py, README.md, CHANGELOG.md
Formatters remove configured fields before JSON serialization. Tests cover standard, web, and request records, including extra, props, exception details, record immutability, and handler wiring. The README and changelog describe the setting and its default.

Priority: ⬇️ Low

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

Change: Feature

Merge Risk: ⚪ Minimal · up to 08350

No actionable merge-blocking issue is identified; the change appears ready for normal merge checks.

Security architecture risk: ⚪ Minimal · up to 08350

The reviewed changes do not introduce a material security risk under the documented configure-before-initialization lifecycle. Output is unchanged by default. Explicit exclusions can remove fields needed by downstream monitoring, and the documentation correctly distinguishes this option from security redaction.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The configuration can affect application and request JSON output throughout an application process wherever newly constructed formatters capture it. Downstream monitoring may lose configured fields, but the default introduces no omissions and the change does not itself grant new authority.

Security Findings and Attack Paths

  • observed — In the reviewed production path, exclusion names come from process-global Python configuration. Request-derived values enter the assembled record, not the exclusion policy. No request-controlled path to selecting excluded names was identified in these changes.

Trust Boundaries and Controls

  • observed — The new control validates field-name types and removes exact top-level keys before serialization. It does not recursively inspect nested values or establish a confidentiality boundary between the original record and other handlers.

Resilience and Maintainability Implications

  • inferred — Setup validation is not an atomic configuration transaction: constructors reread EXCLUDED_FIELDS after entrypoint validation, so a concurrent valid-to-invalid change could fail after state mutation. This requires configuration writes outside the documented configure-before-init lifecycle; it was not established as an attacker-reachable or supported-use security concern.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 3 files. (2 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: configurable exclusion of JSON log fields.
Linked Issues check Passed Issue #98 requests native exclusion of selected JSON fields, including written_ts, for Flask and request logging. The PR adds json_logging.EXCLUDED_FIELDS, validates it before initialization state…
Out of Scope Changes check Passed The changes remain connected to issue #98. Formatter changes implement field exclusion. Initialization validation prevents partial Flask and request instrumentation. Tests cover feature behavior and s…
Full details: Linked Issues check

Explanation

Issue #98 requests native exclusion of selected JSON fields, including written_ts, for Flask and request logging. The PR adds json_logging.EXCLUDED_FIELDS, validates it before initialization state changes, snapshots it per formatter, and removes configured fields in BaseJSONFormatter.format(). The supplied summary reports coverage for standard, web, request, custom, extra, and props records, plus tests for written_ts, invalid values, initialization retry, and default output compatibility.

Full details: Out of Scope Changes check

Explanation

The changes remain connected to issue #98. Formatter changes implement field exclusion. Initialization validation prevents partial Flask and request instrumentation. Tests cover feature behavior and state isolation. README and changelog entries document the new setting and its compatibility limits. No unrelated product behavior is shown.

Full details: Docstring Coverage

Explanation

Docstring coverage is 28.13% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 3 files. (2 skipped: 2 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.

@llamapreview llamapreview Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LlamaPReview — Review skipped

This repository has used its free review capacity for today (3 reviews per UTC day). Capacity resets at 2026-10-21T00:00:00Z.

LlamaPReview is free for public repositories and funded personally, so a daily bound keeps one high-volume repository from consuming the shared budget. Nothing about this pull request was judged. You can also run LlamaPReview yourself with your own provider key — see Self-hosting.

No model-driven code review was run; this result does not assess whether the PR is ready to merge.

Users wanting to drop a field such as written_ts had to subclass a formatter.
json_logging.EXCLUDED_FIELDS now names fields to omit from every JSON record:
standard, web and request log formatters, custom formatter subclasses, extra
entries and props.

The setting defaults to an empty tuple, so output is unchanged. It is read
when a formatter is constructed, like the existing COMPONENT_* settings, and
invalid values (a bare string, non-string entries) raise TypeError at that
point rather than during a log call. Removal is one dict pop per excluded
field after the record is built, so the default path adds only an empty
loop.

Adds tests for default output, single and multiple exclusions, empty lists,
the standard, web and request formatters, extra/props, custom formatters,
construction-time semantics and invalid configuration, and documents the
option in README section 2.8.

Fixes #98
test_request_log_formatter_honours_exclusion replaced the whole handler list of
the process-wide 'flask-request-logger' and never restored it. Running the two
request-logger tests left the logger with only the test's buffer handler and
parent=None, so the stdout handler that init_request_instrument() installs was
gone.

Restore the logger's handlers in a finally block, and snapshot and restore the
logger's full state (handlers, level, parent, propagate, disabled) in a
fixture used by the tests that call init_request_instrument().

Also add tests that exclusion does not mutate the LogRecord, that exception
text is removed only when exc_info is excluded, and that formatters built at
different times keep their own snapshots. The file passes in forward and
reversed order and when each test runs alone.
An invalid json_logging.EXCLUDED_FIELDS was only detected when a formatter was
constructed. By then init_flask() and the other web init functions had already
set _current_framework and _request_util, so correcting the setting and calling
init again failed with 'Can not call init more than once'. In
init_request_instrument() the stdout handler was attached before the error, so a
retry left two handlers.

- Validate the setting at the start of __init() and init_request_instrument().
- Accept unicode field names on Python 2 by using the module's basestring shim.
- Restore the werkzeug logger as well as the request logger in the tests.
- Add subprocess regression tests for recovery after invalid configuration.
- Add an end-to-end test of the setup from issue #98: an application logger has
  a StreamHandler before init_flask(). The test checks that the library installs
  JSONLogWebFormatter on that handler with the configured exclusions, adds no
  handler, and keeps the X-Correlation-ID. Cases: written_ts excluded, the
  default, and correlation_id plus an unknown field name.
- Replace the request-logger fixture with an autouse fixture that restores
  logging._defaultFormatter and, for every existing logger, its handlers,
  handler formatters, level, parent, propagation and disabled state; loggers
  created by a test are reset. Before this, init_*() left _defaultFormatter and
  the formatters of unrelated library loggers (urllib3, requests) changed.
- Remove a snapshot test fully covered by
  test_formatters_built_at_different_times_keep_their_own_snapshots.
- README 2.8: exclusions apply only to formatters that use
  BaseJSONFormatter.format(); the option changes the JSON schema and is not a
  security redaction control.
- Restore a space lost in init_request_instrument() during the earlier edit.
@suppagoddo
suppagoddo force-pushed the feat/98-field-exclusion branch from 31c5074 to 08350d7 Compare October 20, 2026 23:38
@suppagoddo
suppagoddo merged commit 0cf565b into master Oct 20, 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.

Disabling unnesesary fields in output

1 participant