From b78543cd2ee31b8fcd0a1745e4175f88e250dcd4 Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Thu, 5 Dec 2024 22:17:55 -0800 Subject: [PATCH 1/7] Include notes when logging exceptions This adds support for the notes feature from https://peps.python.org/pep-0678/, which was introduced in Python 3.11. --- src/structlog/tracebacks.py | 5 +++ tests/test_tracebacks.py | 65 ++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/structlog/tracebacks.py b/src/structlog/tracebacks.py index 2392b342..b683e8a2 100644 --- a/src/structlog/tracebacks.py +++ b/src/structlog/tracebacks.py @@ -87,6 +87,7 @@ class Stack: exc_type: str exc_value: str + exc_notes: str | None = None syntax_error: SyntaxError_ | None = None is_cause: bool = False frames: list[Frame] = field(default_factory=list) @@ -232,6 +233,10 @@ def extract( stack = Stack( exc_type=safe_str(exc_type.__name__), exc_value=safe_str(exc_value), + exc_notes=( + "\n".join(safe_str(note) for note in exc_value.__notes__) + if hasattr(exc_value, "__notes__") else None + ), is_cause=is_cause, ) diff --git a/tests/test_tracebacks.py b/tests/test_tracebacks.py index a086a042..ae91aa31 100644 --- a/tests/test_tracebacks.py +++ b/tests/test_tracebacks.py @@ -8,7 +8,6 @@ import inspect import json import sys - from pathlib import Path from types import ModuleType from typing import Any @@ -158,6 +157,37 @@ def test_simple_exception(): ] == trace.stacks +def test_simple_exception_with_notes(): + """ + Notes are included in the traceback. + """ + try: + lineno = get_next_lineno() + 1 / 0 + except Exception as e: + e.add_note("This is a note.") + e.add_note("This is another note.") + trace = tracebacks.extract(type(e), e, e.__traceback__) + + assert [ + tracebacks.Stack( + exc_type="ZeroDivisionError", + exc_value="division by zero", + exc_notes="This is a note.\nThis is another note.", + syntax_error=None, + is_cause=False, + frames=[ + tracebacks.Frame( + filename=__file__, + lineno=lineno, + name="test_simple_exception_with_notes", + locals=None, + ), + ], + ), + ] == trace.stacks + + def test_raise_hide_cause(): """ If "raise ... from None" is used, the trace looks like from a simple @@ -546,6 +576,7 @@ def test_json_traceback(): { "exc_type": "ZeroDivisionError", "exc_value": "division by zero", + "exc_notes": None, "frames": [ { "filename": __file__, @@ -559,6 +590,37 @@ def test_json_traceback(): ] == result +def test_json_traceback_with_notes(): + """ + Tracebacks are formatted to JSON with all information. + """ + try: + lineno = get_next_lineno() + 1 / 0 + except Exception as e: + e.add_note("This is a note.") + e.add_note("This is another note.") + format_json = tracebacks.ExceptionDictTransformer(show_locals=False) + result = format_json((type(e), e, e.__traceback__)) + + assert [ + { + "exc_type": "ZeroDivisionError", + "exc_value": "division by zero", + "exc_notes": "This is a note.\nThis is another note.", + "frames": [ + { + "filename": __file__, + "lineno": lineno, + "name": "test_json_traceback_with_notes", + } + ], + "is_cause": False, + "syntax_error": None, + }, + ] == result + + def test_json_traceback_locals_max_string(): """ Local variables in each frame are trimmed to locals_max_string. @@ -575,6 +637,7 @@ def test_json_traceback_locals_max_string(): { "exc_type": "ZeroDivisionError", "exc_value": "division by zero", + "exc_notes": None, "frames": [ { "filename": __file__, From 18fd037cd4a3366c4437d271ec756c72ac920bff Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Thu, 5 Dec 2024 22:35:23 -0800 Subject: [PATCH 2/7] update docs --- CHANGELOG.md | 1 + src/structlog/tracebacks.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0c0efd..cea6ace3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ [#647](https://github.com/hynek/structlog/pull/647) +- `structlog.tracebacks.Stack` now includes an `exc_notes` field reflecting the notes attached to the exception. ## Fixed diff --git a/src/structlog/tracebacks.py b/src/structlog/tracebacks.py index b683e8a2..cec088d2 100644 --- a/src/structlog/tracebacks.py +++ b/src/structlog/tracebacks.py @@ -83,6 +83,9 @@ class SyntaxError_: # noqa: N801 class Stack: """ Represents an exception and a list of stack frames. + + .. versionchanged:: 24.5.0 + Added the *exc_notes* field. """ exc_type: str From bf868d4416c7e8014a45262e1c368e50a37a54f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 06:36:16 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/structlog/tracebacks.py | 3 ++- tests/test_tracebacks.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/structlog/tracebacks.py b/src/structlog/tracebacks.py index cec088d2..1f965058 100644 --- a/src/structlog/tracebacks.py +++ b/src/structlog/tracebacks.py @@ -238,7 +238,8 @@ def extract( exc_value=safe_str(exc_value), exc_notes=( "\n".join(safe_str(note) for note in exc_value.__notes__) - if hasattr(exc_value, "__notes__") else None + if hasattr(exc_value, "__notes__") + else None ), is_cause=is_cause, ) diff --git a/tests/test_tracebacks.py b/tests/test_tracebacks.py index ae91aa31..846ad4a7 100644 --- a/tests/test_tracebacks.py +++ b/tests/test_tracebacks.py @@ -8,6 +8,7 @@ import inspect import json import sys + from pathlib import Path from types import ModuleType from typing import Any From 4b6a7991977036cd4ce98cc87dfa166e6e0feebf Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Thu, 5 Dec 2024 22:41:13 -0800 Subject: [PATCH 4/7] skip notes tests before 3.11 --- tests/test_tracebacks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_tracebacks.py b/tests/test_tracebacks.py index 846ad4a7..79a6b7b8 100644 --- a/tests/test_tracebacks.py +++ b/tests/test_tracebacks.py @@ -158,6 +158,7 @@ def test_simple_exception(): ] == trace.stacks +@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11 or higher") def test_simple_exception_with_notes(): """ Notes are included in the traceback. @@ -591,6 +592,7 @@ def test_json_traceback(): ] == result +@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11 or higher") def test_json_traceback_with_notes(): """ Tracebacks are formatted to JSON with all information. From ce122add7242da09fc8aab14543504ffea91f594 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 06:42:16 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_tracebacks.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/test_tracebacks.py b/tests/test_tracebacks.py index 79a6b7b8..3585a5a6 100644 --- a/tests/test_tracebacks.py +++ b/tests/test_tracebacks.py @@ -158,7 +158,9 @@ def test_simple_exception(): ] == trace.stacks -@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11 or higher") +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Requires Python 3.11 or higher" +) def test_simple_exception_with_notes(): """ Notes are included in the traceback. @@ -592,7 +594,9 @@ def test_json_traceback(): ] == result -@pytest.mark.skipif(sys.version_info < (3, 11), reason="Requires Python 3.11 or higher") +@pytest.mark.skipif( + sys.version_info < (3, 11), reason="Requires Python 3.11 or higher" +) def test_json_traceback_with_notes(): """ Tracebacks are formatted to JSON with all information. From 3bebe722f09ecde790de4526712031784e2acfeb Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Thu, 5 Dec 2024 22:47:55 -0800 Subject: [PATCH 6/7] update api.rst example --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 6879f1e0..6fbb0143 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -197,7 +197,7 @@ API Reference ... 1 / 0 ... except ZeroDivisionError: ... log.exception("Cannot compute!") - {"event": "Cannot compute!", "exception": [{"exc_type": "ZeroDivisionError", "exc_value": "division by zero", "syntax_error": null, "is_cause": false, "frames": [{"filename": "", "lineno": 2, "name": "", "locals": {..., "var": "'spam'"}}]}]} + {"event": "Cannot compute!", "exception": [{"exc_type": "ZeroDivisionError", "exc_value": "division by zero", "exc_notes": null, "syntax_error": null, "is_cause": false, "frames": [{"filename": "", "lineno": 2, "name": "", "locals": {..., "var": "'spam'"}}]}]} .. autoclass:: KeyValueRenderer From 77f02e05785a9c7fd56d5fb32e517155e5043f0d Mon Sep 17 00:00:00 2001 From: Camillo Lugaresi Date: Fri, 6 Dec 2024 10:53:44 -0800 Subject: [PATCH 7/7] link PR in changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cea6ace3..2994c36c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/ - `structlog.tracebacks.Stack` now includes an `exc_notes` field reflecting the notes attached to the exception. + [#684](https://github.com/hynek/structlog/pull/684) + ## Fixed - `structlog.traceback.ExceptionDictTransformer` now correctly handles missing exceptions.