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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `STYLE_STRING_FORMAT_REGEX`, which is no longer used to find `{` style fields. [#75](https://github.com/nhairs/python-json-logger/pull/75)

### Fixed

- Preserve an exception's own notes when encoding exception instances, including exception group summaries. Related to [#67](https://github.com/nhairs/python-json-logger/issues/67).
- `%` style formats no longer treat the escaped literal `%%` as the start of a field, so `"%%(notafield)s"` is correctly read as literal text. [#75](https://github.com/nhairs/python-json-logger/pull/75)
- `{` style formats now use `string.Formatter` (as `logging.StrFormatStyle.validate` does) to find fields, so escaped literal braces (`{{`/`}}`) are skipped and a conversion (`{message!r}`) or format spec (`{levelname:>8}`) is no longer treated as part of the field name. [#75](https://github.com/nhairs/python-json-logger/pull/75)

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.2.1.dev1"
version = "4.2.1.dev2"
description = "JSON Log Formatter for the Python Logging Package"
authors = [
{name = "Zakaria Zajac", email = "zak@madzak.com"},
Expand Down
11 changes: 10 additions & 1 deletion src/pythonjsonlogger/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,19 @@ def use_exception_default(obj: Any) -> TypeGuard[BaseException]:
def exception_default(obj: BaseException) -> str:
"""Default function for exception instances

Includes the exception's own notes, if present. Exception groups retain
their summary without recursively formatting their children.

Args:
obj: object to handle

*Changed in 4.2.1*: Include exception notes.
"""
return f"{obj.__class__.__name__}: {obj}"
result = f"{obj.__class__.__name__}: {obj}"
notes = getattr(obj, "__notes__", None)
if isinstance(notes, list):
return "\n".join([result, *(note for note in notes if isinstance(note, str))])
return result


def use_traceback_default(obj: Any) -> TypeGuard[TracebackType]:
Expand Down
94 changes: 94 additions & 0 deletions tests/test_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,100 @@ def test_common_types_encoded(
return


@pytest.mark.skipif(
sys.version_info < (3, 11), reason="Exception notes require Python 3.11 or higher"
)
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
@pytest.mark.parametrize(
["notes", "expected"],
[
([], "ValueError: my error"),
(["context"], "ValueError: my error\ncontext"),
([""], "ValueError: my error\n"),
(["first", "second"], "ValueError: my error\nfirst\nsecond"),
(["line 1\nline 2", "Привет"], "ValueError: my error\nline 1\nline 2\nПривет"),
],
)
def test_dict_exception_notes(
env: LoggingEnvironment,
class_: type[BaseJsonFormatter],
notes: list[str],
expected: str,
) -> None:
env.set_formatter(class_())
err = ValueError("my error")
for note in notes:
err.add_note(note)

env.logger.info({"error": err, "nested": {"error": err}, "errors": [err]})
log_json = env.load_json()

assert log_json["error"] == expected
assert log_json["nested"]["error"] == expected
assert log_json["errors"] == [expected]
return


@pytest.mark.skipif(
sys.version_info < (3, 11), reason="Exception notes require Python 3.11 or higher"
)
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_dict_exception_note_subclass(
env: LoggingEnvironment, class_: type[BaseJsonFormatter]
) -> None:
class Note(str):
"""A string subclass accepted by add_note."""

err = ValueError("my error")
err.add_note(Note("context"))
env.set_formatter(class_())
env.logger.info({"error": err})

assert env.load_json()["error"] == "ValueError: my error\ncontext"
return


@pytest.mark.parametrize("class_", ALL_FORMATTERS)
def test_dict_exception_message_compatibility(
env: LoggingEnvironment, class_: type[BaseJsonFormatter]
) -> None:
class CustomError(Exception):
"""An exception with a custom formatted message."""

def __format__(self, format_spec: str) -> str:
return "custom message"

env.set_formatter(class_())
env.logger.info({"empty": ValueError(), "custom": CustomError("original message")})
log_json = env.load_json()

assert log_json["empty"] == "ValueError: "
assert log_json["custom"] == "CustomError: custom message"
return


@pytest.mark.skipif(
sys.version_info < (3, 11), reason="Exception groups and notes require Python 3.11 or higher"
)
@pytest.mark.parametrize("class_", ALL_FORMATTERS)
@pytest.mark.parametrize("with_group_note", [False, True])
def test_dict_exception_group_notes(
env: LoggingEnvironment, class_: type[BaseJsonFormatter], with_group_note: bool
) -> None:
child = ValueError("child error")
child.add_note("child note")
group = ExceptionGroup("my group", [child])
expected = "ExceptionGroup: my group (1 sub-exception)"
if with_group_note:
group.add_note("group note")
expected += "\ngroup note"
env.set_formatter(class_())
env.logger.info({"error": group})

assert env.load_json()["error"] == expected
return


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