diff --git a/CHANGELOG.md b/CHANGELOG.md index 805bcc8f..260345fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ - `structlog.BytesLogger`, `structlog.PrintLogger`, and `structlog.WriteLogger` now hold *weak* references to the files they use for output. This prevents their leakage in long-running processes that open many logfiles, such as task executors that create a per-task `BytesLogger` or `WriteLogger`. [#807](https://github.com/hynek/structlog/pull/807) +- `structlog.stdlib.ProcessorFormatter` no longer clears `LogRecord.args` before running its processor chain, so processors that inspect `event_dict["_record"].args` can see the original positional arguments. + `args` is still cleared before the record is handed to the underlying `logging.Formatter`. + [#771](https://github.com/hynek/structlog/issues/771) ### Changed diff --git a/src/structlog/stdlib.py b/src/structlog/stdlib.py index bf0a2083..652228c8 100644 --- a/src/structlog/stdlib.py +++ b/src/structlog/stdlib.py @@ -1141,8 +1141,6 @@ def format(self, record: logging.LogRecord) -> str: if self.pass_foreign_args: ed["positional_args"] = record.args - record.args = () - # Add stack-related attributes to the event dict if record.exc_info: ed["exc_info"] = record.exc_info @@ -1176,6 +1174,12 @@ def format(self, record: logging.LogRecord) -> str: ) ed = cast(str, ed) + # Clear args after processors have run so user processors can access + # ``record.args`` via ``_record``. We render our log records before + # sending them back to logging, so ``LogRecord.args`` must be cleared, + # otherwise stdlib's formatter would raise ``TypeError: not all + # arguments converted during string formatting``. + record.args = () record.msg = ed return super().format(record) diff --git a/tests/test_stdlib.py b/tests/test_stdlib.py index 862e6f60..cb06d80d 100644 --- a/tests/test_stdlib.py +++ b/tests/test_stdlib.py @@ -1105,6 +1105,23 @@ def test_pass_foreign_args_true_sets_positional_args_key(self): assert "positional_args" in event_dict assert positional_args == event_dict["positional_args"] + def test_record_args_accessible_in_processor(self): + """ + Processors can access ``record.args`` via ``_record``: args are not + cleared on the record before the formatter's processors run. + """ + seen = [] + + def capture_args(_, __, event_dict): + seen.append(event_dict["_record"].args) + return event_dict + + configure_logging((capture_args,)) + + logging.getLogger().info("test a=%s b=%s", "a", "b") + + assert [("a", "b")] == seen + def test_log_dict(self, capsys): """ dicts can be logged with std library loggers.