Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ You can find our backwards-compatibility policy [here](https://github.com/hynek/

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

### Added

- `structlog.tracebacks.Stack` now includes an `exc_notes` field reflecting the notes attached to the exception.
[#684](https://github.com/hynek/structlog/pull/684)


### Changed

- `structlog.stdlib.BoundLogger`'s binding-related methods now also return `Self`.
Expand Down
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<doctest default[3]>", "lineno": 2, "name": "<module>", "locals": {..., "var": "'spam'"}}]}]}
{"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'"}}]}]}

.. autoclass:: KeyValueRenderer

Expand Down
7 changes: 7 additions & 0 deletions src/structlog/tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ class SyntaxError_: # noqa: N801
class Stack:
"""
Represents an exception and a list of stack frames.

.. versionchanged:: 25.2.0
Added the *exc_notes* field.
"""

exc_type: str
exc_value: str
exc_notes: list[str] = field(default_factory=list)
syntax_error: SyntaxError_ | None = None
is_cause: bool = False
frames: list[Frame] = field(default_factory=list)
Expand Down Expand Up @@ -230,6 +234,9 @@ def extract(
stack = Stack(
exc_type=safe_str(exc_type.__name__),
exc_value=safe_str(exc_value),
exc_notes=[
safe_str(note) for note in getattr(exc_value, "__notes__", ())
],
is_cause=is_cause,
)

Expand Down
70 changes: 70 additions & 0 deletions tests/test_tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,40 @@ 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.
"""
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.", "This 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
Expand Down Expand Up @@ -588,6 +622,7 @@ def test_json_traceback():
{
"exc_type": "ZeroDivisionError",
"exc_value": "division by zero",
"exc_notes": [],
"frames": [
{
"filename": __file__,
Expand All @@ -601,6 +636,40 @@ 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.
"""
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.", "This 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.
Expand All @@ -617,6 +686,7 @@ def test_json_traceback_locals_max_string():
{
"exc_type": "ZeroDivisionError",
"exc_value": "division by zero",
"exc_notes": [],
"frames": [
{
"filename": __file__,
Expand Down