fix(logging): stop a bad enrichment value losing the whole JSON record - #1472
fix(logging): stop a bad enrichment value losing the whole JSON record#1472groupthinking wants to merge 2 commits into
Conversation
Closes #1452 `_format_json` passed `default=str` to `json.dumps` and its comment claimed that kept a non-serializable `extra` value from costing us the record. It does not, in two ways: * a circular container is rejected structurally, before `default` is ever consulted, so `default=str` never sees it; * a value whose `__str__` raises propagates that exception out of `default` itself. Either way `json.dumps` raises inside the logging path, `logging` swallows it via `Handler.handleError`, and the record is dropped. Measured against `main` with three `logger.info` calls and the middle one poisoned: 2 of 3 records reached the sink. With this change, 3 of 3. The fallback keeps every scalar field -- notably `level`, which routing and alerting depend on -- and reports the failure in-band as `serialization_error` rather than swallowing it. `except Exception` is deliberate: a narrow `(TypeError, ValueError, RecursionError)` looks more correct but the exploding-`__str__` case walks straight through it. Building the description is itself guarded, since rendering the exception calls the same `__str__` that raised. Reachable only through the optional `correlation_id` / `performance_ms` enrichments; every live call site passes a scalar today, and an attacker-supplied header is a string, so this is robustness rather than a vulnerability. The CWE-117 field-forgery property from #1429/#1439 is untouched. 6 regression tests; 5 fail against the pre-fix implementation. The sixth guards the fallback's blast radius by asserting scalar enrichments still reach a normal record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVnVRDHCJWNjN6pLhHFW8k
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
@coderabbitai full review Requesting manually because auto-review skipped again with "Auto reviews are limited based on label configuration" — worth recording, because this is evidence that #1425's fix did not close #1424. #1425 merged Not opening an issue for that; #1424 already owns it and should simply be reopened or have its scope corrected to "org UI, not repo YAML". On the change itself — this is a robustness fix to a logging path, so the claims worth attacking are specific:
One thing I would rather you catch than have me assert: the non-vacuity claim. 5 of the 6 new tests fail with only Generated by Claude Code |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== changed files and diff ==='
git diff --find-renames origin/main...HEAD -- \
src/youtube_extension/backend/config/logging_config.py \
tests/unit/test_logging_config_crlf.py
printf '%s\n' '=== formatter outline ==='
ast-grep outline src/youtube_extension/backend/config/logging_config.py --items all
printf '%s\n' '=== test outline ==='
ast-grep outline tests/unit/test_logging_config_crlf.py --items all
printf '%s\n' '=== logging formatter references and logger calls ==='
rg -n -C 3 --glob '*.py' \
'(_format_json\b|JSON_LOGGING\b|correlation_id\b|performance_ms\b|extra\s*=|formatException\s*\(|formatStack\s*\(|getMessage\s*\()' \
src tests || true
printf '%s\n' '=== logging configuration source ==='
cat -n src/youtube_extension/backend/config/logging_config.py
printf '%s\n' '=== complete changed test file ==='
cat -n tests/unit/test_logging_config_crlf.pyLength of output: 60986 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '=== tracked matching paths ==='
git ls-tree -r --name-only HEAD | rg '(^|/)(test_logging_config_crlf\.py|logging_config\.py)$' || true
printf '%s\n' '=== HEAD test file ==='
git show HEAD:tests/unit/test_logging_config_crlf.py 2>/dev/null || true
printf '%s\n' '=== base test file ==='
git show origin/main:tests/unit/test_logging_config_crlf.py 2>/dev/null || true
printf '%s\n' '=== formatter source on main ==='
git show origin/main:src/youtube_extension/backend/config/logging_config.py | sed -n '90,205p'
printf '%s\n' '=== all formatter test references in tracked files ==='
git grep -n -I -E 'StructuredFormatter|_make_json_logger|_format_json|serialization_error' HEAD -- ':!*.lock' || true
printf '%s\n' '=== standard-library logging LogRecord message behavior ==='
python - <<'PY'
import inspect
import logging
print(inspect.getsource(logging.LogRecord.getMessage))
print(inspect.getsource(logging.Formatter.format))
PYLength of output: 41428
The reported auto-review skip is consistent with an inherited Organization UI label requirement overriding the repository YAML. ✏️ Learnings added
|
|
Duplicate of #1477 — three PRs (#1471, #1472, #1477) implement #1452, all created within three minutes of each other. The diagnosis here is correct and the fix works; this is purely a reconciliation call under Functionally the three are equivalent — your fallback passes One thing worth carrying to #1477 if it is revised: your Recommending close in favour of #1477. Full comparison and verification evidence is there. Generated by Claude Code |
…rces Addresses CodeRabbit's review on #1472. The comment asserted "never lose a record" without qualification. `payload` is built *above* the guarded `json.dumps`, so a raise from `getMessage()` (bad %-args), `formatTime`, `formatException` or `formatStack` still drops the record. Confirmed rather than accepted on review: logger.info('user=%d', 'not-an-int') -> TypeError, record lost Shipping that sentence would have been the same defect this change exists to fix -- a comment claiming more than its code enforces. The comment now scopes the guarantee to the serialization boundary and names the pre-existing exposure it does not cover, which the line-oriented path shares. Also corrects the opposite error, in my own review comment rather than the code: I claimed `exception` and `stack_info` are dropped by the fallback. They are not. Both are rendered to `str` before the dump, so the scalar filter keeps them -- a failing enrichment does not cost the traceback. Now asserted by `test_rendered_traceback_survives_the_serialization_fallback`. Adds the nested-raise case the suite did not type: a value whose `__str__` raises an exception that itself raises when rendered. This is what makes the inner guard live code -- with it removed, that test and only that test fails. 28 tests pass; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVnVRDHCJWNjN6pLhHFW8k
|
Both substantive points addressed in 1 — The comment overclaimed. Correct, and fixed.You're right that That sentence would have shipped the exact defect this change exists to remove — a comment claiming more than its code enforces. The comment now scopes the guarantee to the serialization boundary and names what it does not cover ( 2 — I was wrong, you were right: the traceback survives.In my review request I wrote that " There was no trade-off to weigh — I invented one. Now pinned by 4 — Nested raise: you were right that it was reachable but untested.Added Exactly that test and nothing else, which is the precise form of non-vacuity I wanted. 3 and 5 — no change neededAgreed on both, and 5 is the answer I was fishing for: Head On the auto-review skip: agreed, and thank you for confirming the Organization UI reading. #1424 needs its scope corrected from repo YAML to the dashboard — no commit in this repo can close it, which is why #1425 merged without fixing it. Not something I can action from here. Noting your rate limit: I won't re-trigger a review for this push. The change is comment scoping plus two tests, with the source behaviour unchanged apart from the guarantee it claims. Generated by Claude Code |
Both salvaged from #1472, a competing implementation of #1452. They pass unmodified on this head, so they are pure coverage gain rather than a behaviour change. * An enrichment whose __str__ raises an exception that ITSELF raises when rendered. This is what makes _describe_exception's inner guard live code rather than defensive decoration -- the existing unit test calls the helper directly, which cannot show the path is reachable through the formatter. * A traceback must survive the fallback. `exception` is rendered to str before the guarded dump, so a poisoned enrichment firing the fallback must not also cost the traceback. With this, the branch carries the union of all four competing PRs: #1471's exact-type filter, #1472's two tests, and the non-finite float guard found by this run's red-team pass, which none of the others have. 32 passed. ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0146p7ZfxtbdEfJjtgETZTNf
Live defect here: the
|
|
Superseded — #1452 landed as #1491 while this was open, so the base fix here is now on The contribution worth keeping from this one is the honesty about scope: the merged comment implies a broader guarantee than the code gives (payload construction can still raise upstream of serialization — Recommend closing this in favour of #1497. Generated by Claude Code |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
Canonical issue
Closes #1452
The follow-up #1452 sequenced to land after #1439. That merged at
20:47, so_format_jsonis now onmainand this is no longer a competing implementation of #1429.Outcome
A JSON log record is no longer dropped when an optional enrichment value cannot be serialized.
_format_jsonpasseddefault=strtojson.dumpsand its comment claimed that kept a non-serializableextravalue from costing us the record. It does not, in two ways:default=strmisses itjson.dumpsrejects the cycle structurally, beforedefaultis consulted — sodefault=strnever sees it.__str__raisesdefaultis consulted, and the exception propagates straight back out of it.Either way
json.dumpsraises inside the logging path,loggingswallows it viaHandler.handleError, and the record is dropped entirely — the specific outcome the comment said was prevented.Measured against
main, threelogger.infocalls with the middle one poisoned:The fallback keeps every scalar field — notably
level, which routing and alerting depend on — and reports the failure in-band rather than swallowing it.Scope
src/youtube_extension/backend/config/logging_config.py(guardedjson.dumps+ a comment that states what the code actually enforces);tests/unit/test_logging_config_crlf.py(+6 tests)."survives the #1270 log sanitizer #1429/fix(security): build JSON log records with json.dumps (CWE-117 field forgery) #1439 — untouched, and its 19 tests pass unmodified. TheJSON_LOGGINGdefault mismatch betweenproduction_config.py:72andlogging_config.py, which fix(security): build JSON log records with json.dumps (CWE-117 field forgery) #1439 already scoped out.Two deliberate choices worth flagging:
except Exception, not a narrow tuple.(TypeError, ValueError, RecursionError)looks more correct and the exploding-__str__case walks straight through it.__str__that raised. Asserting "never lose a record" is only worth doing if the fallback cannot become the thing that loses it.Risk
git revert. No migration, config, or schema change; the emitted field set on the success path is byte-identical.Verification
Head
e138fdd.Focused tests —
tests/unit/test_logging_config_crlf.py: 26 passed. The 19 pre-existing tests are unmodified and still pass, so the fix(security): CWE-117 JSON field forgery via unescaped"survives the #1270 log sanitizer #1429 property and the line-oriented contract are both intact.Reproduced before fixing, against
main— both cases, 2 of 3 records reaching the sink.Non-vacuity checked in both directions, not asserted. Reverting only the source file and keeping the new tests:
The sixth new test passes either way by design — it guards the fallback's blast radius rather than the bug.
ruff checkon both changed files — clean.Required CI — will populate on this head.
Review threads resolved — none open yet.
Production evidence
Not applicable as a preview — backend logging, no
apps/web/**surface, so gate 4 ofMERGE_POLICY.mddoes not apply.The runtime evidence is the reproduction itself, run through a real
StreamHandlerin both directions (transcripts above). Exposure is real rather than theoretical:production_config.py:72defaultsJSON_LOGGINGto"true", so this is the live formatter path — though reaching it needs a future call site that passes a non-scalar, since every current one passes a string or int.Agent handoff
default=strin_format_jsondoes not deliver its stated guarantee — a record can still be lost (follow-up to #1439) #1452default=strin_format_jsondoes not deliver its stated guarantee — a record can still be lost (follow-up to #1439) #1452 is explicit that this is a follow-up to fix(security): build JSON log records with json.dumps (CWE-117 field forgery) #1439, which is now mergeddefault=strin_format_jsondoes not deliver its stated guarantee — a record can still be lost (follow-up to #1439) #1452Agent provenance
Agent-authored under the PR remediation runbook. The finding was independently derived by three red-team passes on #1439 and tracked zero times until #1452; this is the fourth pass reading the issue instead of re-deriving it.
Generated by Claude Code