Skip to content

Commit c2de5c3

Browse files
camillolpre-commit-ci[bot]hynek
authored
Include notes when logging exceptions (#684)
* 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. * update docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * skip notes tests before 3.11 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update api.rst example * link PR in changelog * make exc_notes a tuple * fix docs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * update next version * shorter * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Make exc_notes a list for consistency ref #684 (comment) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack <[email protected]>
1 parent d8f4e6e commit c2de5c3

File tree

4 files changed

+84
-1
lines changed

4 files changed

+84
-1
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/
1515

1616
## [Unreleased](https://github.com/hynek/structlog/compare/25.1.0...HEAD)
1717

18+
### Added
19+
20+
- `structlog.tracebacks.Stack` now includes an `exc_notes` field reflecting the notes attached to the exception.
21+
[#684](https://github.com/hynek/structlog/pull/684)
22+
23+
1824
### Changed
1925

2026
- `structlog.stdlib.BoundLogger`'s binding-related methods now also return `Self`.

docs/api.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ API Reference
197197
... 1 / 0
198198
... except ZeroDivisionError:
199199
... log.exception("Cannot compute!")
200-
{"event": "Cannot compute!", "exception": [{"exc_type": "ZeroDivisionError", "exc_value": "division by zero", "syntax_error": null, "is_cause": false, "frames": [{"filename": "<doctest default[3]>", "lineno": 2, "name": "<module>", "locals": {..., "var": "'spam'"}}]}]}
200+
{"event": "Cannot compute!", "exception": [{"exc_type": "ZeroDivisionError", "exc_value": "division by zero", "exc_notes": [], "syntax_error": null, "is_cause": false, "frames": [{"filename": "<doctest default[3]>", "lineno": 2, "name": "<module>", "locals": {..., "var": "'spam'"}}]}]}
201201

202202
.. autoclass:: KeyValueRenderer
203203

src/structlog/tracebacks.py

+7
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,14 @@ class SyntaxError_: # noqa: N801
8181
class Stack:
8282
"""
8383
Represents an exception and a list of stack frames.
84+
85+
.. versionchanged:: 25.2.0
86+
Added the *exc_notes* field.
8487
"""
8588

8689
exc_type: str
8790
exc_value: str
91+
exc_notes: list[str] = field(default_factory=list)
8892
syntax_error: SyntaxError_ | None = None
8993
is_cause: bool = False
9094
frames: list[Frame] = field(default_factory=list)
@@ -230,6 +234,9 @@ def extract(
230234
stack = Stack(
231235
exc_type=safe_str(exc_type.__name__),
232236
exc_value=safe_str(exc_value),
237+
exc_notes=[
238+
safe_str(note) for note in getattr(exc_value, "__notes__", ())
239+
],
233240
is_cause=is_cause,
234241
)
235242

tests/test_tracebacks.py

+70
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,40 @@ def test_simple_exception():
160160
] == trace.stacks
161161

162162

163+
@pytest.mark.skipif(
164+
sys.version_info < (3, 11), reason="Requires Python 3.11 or higher"
165+
)
166+
def test_simple_exception_with_notes():
167+
"""
168+
Notes are included in the traceback.
169+
"""
170+
try:
171+
lineno = get_next_lineno()
172+
1 / 0
173+
except Exception as e:
174+
e.add_note("This is a note.")
175+
e.add_note("This is another note.")
176+
trace = tracebacks.extract(type(e), e, e.__traceback__)
177+
178+
assert [
179+
tracebacks.Stack(
180+
exc_type="ZeroDivisionError",
181+
exc_value="division by zero",
182+
exc_notes=["This is a note.", "This is another note."],
183+
syntax_error=None,
184+
is_cause=False,
185+
frames=[
186+
tracebacks.Frame(
187+
filename=__file__,
188+
lineno=lineno,
189+
name="test_simple_exception_with_notes",
190+
locals=None,
191+
),
192+
],
193+
),
194+
] == trace.stacks
195+
196+
163197
def test_raise_hide_cause():
164198
"""
165199
If "raise ... from None" is used, the trace looks like from a simple
@@ -588,6 +622,7 @@ def test_json_traceback():
588622
{
589623
"exc_type": "ZeroDivisionError",
590624
"exc_value": "division by zero",
625+
"exc_notes": [],
591626
"frames": [
592627
{
593628
"filename": __file__,
@@ -601,6 +636,40 @@ def test_json_traceback():
601636
] == result
602637

603638

639+
@pytest.mark.skipif(
640+
sys.version_info < (3, 11), reason="Requires Python 3.11 or higher"
641+
)
642+
def test_json_traceback_with_notes():
643+
"""
644+
Tracebacks are formatted to JSON with all information.
645+
"""
646+
try:
647+
lineno = get_next_lineno()
648+
1 / 0
649+
except Exception as e:
650+
e.add_note("This is a note.")
651+
e.add_note("This is another note.")
652+
format_json = tracebacks.ExceptionDictTransformer(show_locals=False)
653+
result = format_json((type(e), e, e.__traceback__))
654+
655+
assert [
656+
{
657+
"exc_type": "ZeroDivisionError",
658+
"exc_value": "division by zero",
659+
"exc_notes": ["This is a note.", "This is another note."],
660+
"frames": [
661+
{
662+
"filename": __file__,
663+
"lineno": lineno,
664+
"name": "test_json_traceback_with_notes",
665+
}
666+
],
667+
"is_cause": False,
668+
"syntax_error": None,
669+
},
670+
] == result
671+
672+
604673
def test_json_traceback_locals_max_string():
605674
"""
606675
Local variables in each frame are trimmed to locals_max_string.
@@ -617,6 +686,7 @@ def test_json_traceback_locals_max_string():
617686
{
618687
"exc_type": "ZeroDivisionError",
619688
"exc_value": "division by zero",
689+
"exc_notes": [],
620690
"frames": [
621691
{
622692
"filename": __file__,

0 commit comments

Comments
 (0)