Skip to content

Commit 2557bbc

Browse files
authored
fix: prevent mutation of original dict when record.msg is a dict (#66)
## Summary When `record.msg` is a dict (used as a structured log message), the `format()` method assigns it to `message_dict` by reference. Subsequent mutations to `message_dict` -- such as injecting `exc_info` or `stack_info` -- leak into the caller's original dict, causing unexpected side effects. ## Reproduction ```python import logging from pythonjsonlogger import jsonlogger logger = logging.getLogger("test") handler = logging.StreamHandler() handler.setFormatter(jsonlogger.JsonFormatter()) logger.addHandler(handler) data = {"key": "value"} try: 1 / 0 except: logger.exception(data) # data now contains "exc_info" -- the original dict was mutated! ``` ## Fix Use `record.msg.copy()` instead of a direct reference, so the original dict remains untouched. ## Tests All 135 existing tests pass. (This contribution was made under my direction and I take full responsibility for its correctness.)
1 parent 3cc30fc commit 2557bbc

5 files changed

Lines changed: 35 additions & 2 deletions

File tree

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [4.2.0](https://github.com/nhairs/python-json-logger/compare/v4.1.0...v4.2.0) - UNRELEASED
8+
9+
### Fixed
10+
- Logging a `dict` no longer modifies it. `exc_info` and `stack_info` were previously added to the caller's `dict`. [#66](https://github.com/nhairs/python-json-logger/pull/66)
11+
12+
Thanks @gaoflow
13+
714
## [4.1.0](https://github.com/nhairs/python-json-logger/compare/v4.0.0...v4.1.0) - 2026-03-29
815

916
### Added

docs/quickstart.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ logger.info({
7979
!!! warning
8080
Be aware that if you log using a `dict`, other formatters may not be able to handle it.
8181

82+
!!! note
83+
Your `dict` is not modified when the formatter adds fields such as `exc_info`.
84+
8285
You can also add additional message fields using the `extra` argument.
8386

8487
```python

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "python-json-logger"
7-
version = "4.1.0"
7+
version = "4.2.0.dev1"
88
description = "JSON Log Formatter for the Python Logging Package"
99
authors = [
1010
{name = "Zakaria Zajac", email = "zak@madzak.com"},

src/pythonjsonlogger/core.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,14 +236,20 @@ def __init__(
236236
def format(self, record: logging.LogRecord) -> str:
237237
"""Formats a log record and serializes to json
238238
239+
When `record.msg` is a `dict`, adding `exc_info` and `stack_info` does not
240+
modify the caller's dict.
241+
239242
Args:
240243
record: the record to format
244+
245+
*Changed in 4.2.0*: a `dict` `record.msg` is copied instead of modified
246+
in place.
241247
"""
242248
message_dict: dict[str, Any] = {}
243249
# TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
244250
# are always type of str. We shouldn't need to override that.
245251
if isinstance(record.msg, dict):
246-
message_dict = record.msg
252+
message_dict = record.msg.copy()
247253
record.message = ""
248254
else:
249255
record.message = record.getMessage()

tests/test_formatters.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,23 @@ def test_log_dict_defaults(env: LoggingEnvironment, class_: type[BaseJsonFormatt
393393
return
394394

395395

396+
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
397+
def test_log_dict_not_modified(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
398+
env.set_formatter(class_())
399+
400+
msg = {"text": "testing logging", "nested": {"more": "data"}}
401+
try:
402+
raise ValueError("test")
403+
except ValueError:
404+
env.logger.exception(msg, stack_info=True)
405+
log_json = env.load_json()
406+
407+
assert log_json["exc_info"]
408+
assert log_json["stack_info"]
409+
assert msg == {"text": "testing logging", "nested": {"more": "data"}}
410+
return
411+
412+
396413
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
397414
def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
398415
env.set_formatter(class_())

0 commit comments

Comments
 (0)