Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.1.1](https://github.com/nhairs/python-json-logger/compare/v4.1.0...v4.1.1) - UNRELEASED

### Fixed
- 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)
- The `dict` is shallow copied; nested values are still shared with the caller.

## [4.1.0](https://github.com/nhairs/python-json-logger/compare/v4.0.0...v4.1.0) - 2026-03-29

### Added
Expand Down
5 changes: 5 additions & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ logger.info({
!!! warning
Be aware that if you log using a `dict`, other formatters may not be able to handle it.

!!! note
Your `dict` is not modified - the formatter takes a shallow copy of it before adding fields
such as `exc_info`. Nested values are shared with your `dict` rather than copied, so avoid
mutating them until the record has been handled.

You can also add additional message fields using the `extra` argument.

```python
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "python-json-logger"
version = "4.1.0"
version = "4.1.1.dev1"
description = "JSON Log Formatter for the Python Logging Package"
authors = [
{name = "Zakaria Zajac", email = "zak@madzak.com"},
Expand Down
10 changes: 9 additions & 1 deletion src/pythonjsonlogger/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,22 @@ def __init__(
def format(self, record: logging.LogRecord) -> str:
"""Formats a log record and serializes to json

When `record.msg` is a `dict` it is shallow copied, so adding `exc_info` and
`stack_info` does not modify the caller's dict. Nested values are shared with
the caller rather than copied, and must not be mutated until the record has
been handled.

Args:
record: the record to format

*Changed in 4.1.1*: a `dict` `record.msg` is shallow copied instead of modified
in place.
"""
message_dict: dict[str, Any] = {}
# TODO: logging.LogRecord.msg and logging.LogRecord.message in typeshed
# are always type of str. We shouldn't need to override that.
if isinstance(record.msg, dict):
message_dict = record.msg
message_dict = record.msg.copy()
record.message = ""
else:
record.message = record.getMessage()
Expand Down
37 changes: 37 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,43 @@ def test_log_dict_defaults(env: LoggingEnvironment, class_: type[BaseJsonFormatt
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_log_dict_not_modified(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_())

msg = {"text": "testing logging", "nested": {"more": "data"}}
try:
raise ValueError("test")
except ValueError:
env.logger.exception(msg, stack_info=True)
log_json = env.load_json()

assert log_json["exc_info"]
assert log_json["stack_info"]
assert msg == {"text": "testing logging", "nested": {"more": "data"}}
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_log_dict_shallow_copied(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
captured: list[Any] = []

class CaptureJsonFormatter(class_): # type: ignore[valid-type,misc]

def process_log_record(self, log_data):
captured.append(log_data)
return super().process_log_record(log_data)

env.set_formatter(CaptureJsonFormatter())

nested = {"more": "data"}
env.logger.info({"nested": nested})

# nested values are shared with the caller, not copied
assert captured[0]["nested"] is nested
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_log_extra(env: LoggingEnvironment, class_: type[BaseJsonFormatter]):
env.set_formatter(class_())
Expand Down
Loading