Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include notes when logging exceptions #684

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ 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.

[#684](https://github.com/hynek/structlog/pull/684)

## Fixed

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": null, "syntax_error": null, "is_cause": false, "frames": [{"filename": "<doctest default[3]>", "lineno": 2, "name": "<module>", "locals": {..., "var": "'spam'"}}]}]}

.. autoclass:: KeyValueRenderer

Expand Down
9 changes: 9 additions & 0 deletions src/structlog/tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,14 @@ 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
exc_value: str
exc_notes: str | None = None
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey sorry for the long delay – is there a reason why you chose to make it an optional str?

You mention it in the description but without reasoning and I think it would be more ergonomic to make it a tuple[str, ...]?

(tuple > list because it would keep Stack hashable)

syntax_error: SyntaxError_ | None = None
is_cause: bool = False
frames: list[Frame] = field(default_factory=list)
Expand Down Expand Up @@ -232,6 +236,11 @@ 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,
)

Expand Down
70 changes: 70 additions & 0 deletions tests/test_tracebacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,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.\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
Expand Down Expand Up @@ -546,6 +580,7 @@ def test_json_traceback():
{
"exc_type": "ZeroDivisionError",
"exc_value": "division by zero",
"exc_notes": None,
"frames": [
{
"filename": __file__,
Expand All @@ -559,6 +594,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.\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.
Expand All @@ -575,6 +644,7 @@ def test_json_traceback_locals_max_string():
{
"exc_type": "ZeroDivisionError",
"exc_value": "division by zero",
"exc_notes": None,
"frames": [
{
"filename": __file__,
Expand Down
Loading