Skip to content

Drop event dict keys colliding with LogRecord attributes in render_to_log_* - #842

Open
dualfroz wants to merge 1 commit into
hynek:mainfrom
dualfroz:dualfroz/fix-render-to-log-kwargs-reserved-keys
Open

Drop event dict keys colliding with LogRecord attributes in render_to_log_*#842
dualfroz wants to merge 1 commit into
hynek:mainfrom
dualfroz:dualfroz/fix-render-to-log-kwargs-reserved-keys

Conversation

@dualfroz

@dualfroz dualfroz commented Sep 5, 2026

Copy link
Copy Markdown

Problem

structlog.stdlib.render_to_log_kwargs() and
structlog.stdlib.render_to_log_args_and_kwargs() are documented, standard
processors for "rendering your log entries entirely within logging" (see
docs/standard-library.md, "Processors" section). Both build the extra
kwarg they hand to the standard library out of whatever event dict keys are
left after event, positional_args, and the exc_info/stack_info/
stacklevel trio have been extracted.

If a remaining key happens to share a name with an attribute that
logging.LogRecord already carries -- filename, module, process,
args, name, message, and so on are all plausible field names for
someone's structured business data -- the standard library's own
Logger.makeRecord() rejects the call outright:

>>> import logging, structlog
>>> structlog.configure(
...     processors=[structlog.stdlib.render_to_log_kwargs],
...     logger_factory=structlog.stdlib.LoggerFactory(),
...     wrapper_class=structlog.stdlib.BoundLogger,
... )
>>> logging.basicConfig(level=logging.DEBUG)
>>> log = structlog.get_logger("test")
>>> log.debug("hello", filename="not-a-file.py")
Traceback (most recent call last):
  ...
  File ".../logging/__init__.py", line 1656, in makeRecord
    raise KeyError("Attempt to overwrite %r in LogRecord" % key)
KeyError: "Attempt to overwrite 'filename' in LogRecord"

No ProcessorFormatter involved -- this is the plain "defer formatting to
logging" configuration from the docs. The log call itself raises, taking
down whatever code path tried to log.

Root cause

src/structlog/stdlib.py, render_to_log_kwargs() (around line 1001,
pre-fix) and render_to_log_args_and_kwargs() (around line 961, pre-fix):
after popping event/positional_args/LOG_KWARG_NAMES, both functions
put the entire remaining event dict under extra unconditionally. They
never check it against the names logging.LogRecord already reserves for
itself.

This is exactly the reverse of what structlog.stdlib.ExtraAdder already
does a few dozen lines above in the same file: ExtraAdder merges a
LogRecord's own extra fields into an event dict, and it explicitly
filters out _LOG_RECORD_KEYS (a LogRecord's standard attribute names) so
that record-internal fields do not leak in as noise. render_to_log_kwargs/
render_to_log_args_and_kwargs do the same round trip in the opposite
direction (event dict -> extra on a soon-to-be-built LogRecord) but had
no equivalent filter.

Two names collide that are not in a fresh LogRecord.__dict__ at all:
"message" and "asctime". Logger.makeRecord() special-cases both and
rejects them regardless, because Formatter.format() adds them later:

if (key in ["message", "asctime"]) or (key in rv.__dict__):
    raise KeyError("Attempt to overwrite %r in LogRecord" % key)

Issue and prior state

This is #486, open since the
report against a Gunicorn integration. The maintainer identified the same
root cause in the issue thread and proposed the same fix direction ("this
needs to be fixed in render_to_log_kwargs and
render_to_log_args_and_kwargs by not putting these fields under these
names into extra"), but the only change that shipped
(commit 5400612, "docs/stdlib: add warning about ProcessorFormatter")
only documents a different, narrower footgun: not to combine
render_to_log_kwargs/render_to_log_args_and_kwargs with
ProcessorFormatter (a real but separate problem, since ProcessorFormatter
adds its own extra={"_logger": ..., "_name": ...} on top). It does not
touch the underlying collision, which reproduces with a plain, non-
ProcessorFormatter configuration as shown above.

Fix

Reuse the existing _LOG_RECORD_KEYS constant (already computed from a
fresh LogRecord for ExtraAdder), extended with "message" and
"asctime" as _RESERVED_LOG_RECORD_KEYS. Both render_to_log_kwargs()
and render_to_log_args_and_kwargs() now delete any event dict key that
intersects with that set before handing the remainder to logging as
extra. Non-colliding data is passed through exactly as before.

This drops the colliding field's value rather than renaming or raising.
That mirrors ExtraAdder's own established precedent for the reverse
direction in this same file, and it means the fix trades a crash for a
silent, best-effort log entry instead of no log entry (and a raised
exception) at all -- see "Flags" below for the alternative the maintainer's
wording also allows.

@codspeed-hq

codspeed-hq Bot commented Sep 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 10 untouched benchmarks


Comparing dualfroz:dualfroz/fix-render-to-log-kwargs-reserved-keys (6c697cd) with main (bf3cfd0)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (73393f3) during the generation of this report, so bf3cfd0 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@dualfroz
dualfroz force-pushed the dualfroz/fix-render-to-log-kwargs-reserved-keys branch from c7f841c to e8a3055 Compare September 5, 2026 23:10
…_log_*

render_to_log_kwargs() and render_to_log_args_and_kwargs() build the
extra kwarg straight out of whatever is left in the event dict once the
event, positional_args, and the exc_info/stack_info/stacklevel trio have
been extracted. If a remaining key happens to share a name with an
attribute that logging.LogRecord already carries (filename, module,
process, message, ...), logging.Logger.makeRecord() rejects the whole
call with a raw KeyError, crashing a documented, non-ProcessorFormatter
configuration on an entirely plausible business field name.

structlog.stdlib.ExtraAdder already filters LogRecord's own attribute
names out of the event dict when merging a record's extra fields in the
other direction. Apply the same filtering here so a colliding key is
dropped from extra instead of reaching logging.Logger.makeRecord() and
crashing it.

Fixes hynek#486.
@dualfroz
dualfroz force-pushed the dualfroz/fix-render-to-log-kwargs-reserved-keys branch from e8a3055 to 6c697cd Compare September 5, 2026 23:25
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.

1 participant