From 0035bfee97f544c650ee51ba4279cc3df5c99c82 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Thu, 19 Dec 2024 16:45:54 +0000 Subject: [PATCH] chore(di): capture exception chain (#11771) We augment the exception fields with known exception chaining attributes to allow capturing exception chaining relations. The fields need to be added manually because they are part of the BaseException built-in fields and are not included in the object's __dict__ attribute. ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [ ] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --- ddtrace/debugging/_signal/utils.py | 9 +++++++++ tests/debugging/exception/test_replay.py | 4 ++-- tests/debugging/test_debugger.py | 20 +++++++++++++++++++- tests/debugging/test_encoding.py | 16 +++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/ddtrace/debugging/_signal/utils.py b/ddtrace/debugging/_signal/utils.py index b2e5d8e285b..09b319598ef 100644 --- a/ddtrace/debugging/_signal/utils.py +++ b/ddtrace/debugging/_signal/utils.py @@ -304,6 +304,15 @@ def capture_value( } fields = get_fields(value) + + # Capture exception chain for exceptions + if _isinstance(value, BaseException): + for attr in ("args", "__cause__", "__context__", "__suppress_context__"): + try: + fields[attr] = object.__getattribute__(value, attr) + except AttributeError: + pass + captured_fields = { n: ( capture_value(v, level=level - 1, maxlen=maxlen, maxsize=maxsize, maxfields=maxfields, stopping_cond=cond) diff --git a/tests/debugging/exception/test_replay.py b/tests/debugging/exception/test_replay.py index 8b9a2a7d830..54baeb8b826 100644 --- a/tests/debugging/exception/test_replay.py +++ b/tests/debugging/exception/test_replay.py @@ -161,8 +161,8 @@ def b_chain(bar): m = 4 try: a(bar % m) - except ValueError: - raise KeyError("chain it") + except ValueError as exc: + raise KeyError("chain it") from exc def c(foo=42): with self.trace("c"): diff --git a/tests/debugging/test_debugger.py b/tests/debugging/test_debugger.py index ed337c27f1e..0cc65bc43cf 100644 --- a/tests/debugging/test_debugger.py +++ b/tests/debugging/test_debugger.py @@ -210,7 +210,25 @@ def test_debugger_function_probe_on_function_with_exception(): return_capture = snapshot_data["captures"]["return"] assert return_capture["arguments"] == {} - assert return_capture["locals"] == {"@exception": {"fields": {}, "type": "Exception"}} + assert return_capture["locals"] == { + "@exception": { + "type": "Exception", + "fields": { + "args": { + "type": "tuple", + "elements": [ + {"type": "str", "value": "'Hello'"}, + {"type": "str", "value": "'world!'"}, + {"type": "int", "value": "42"}, + ], + "size": 3, + }, + "__cause__": {"type": "NoneType", "isNull": True}, + "__context__": {"type": "NoneType", "isNull": True}, + "__suppress_context__": {"type": "bool", "value": "False"}, + }, + } + } assert return_capture["throwable"]["message"] == "'Hello', 'world!', 42" assert return_capture["throwable"]["type"] == "Exception" diff --git a/tests/debugging/test_encoding.py b/tests/debugging/test_encoding.py index c06e5000ed8..c22851f1112 100644 --- a/tests/debugging/test_encoding.py +++ b/tests/debugging/test_encoding.py @@ -191,7 +191,21 @@ def _(): exc = context.pop("throwable") assert context["arguments"] == {} - assert context["locals"] == {"@exception": {"type": "Exception", "fields": {}}} + assert context["locals"] == { + "@exception": { + "type": "Exception", + "fields": { + "args": { + "type": "tuple", + "elements": [{"type": "str", "value": "'test'"}, {"type": "str", "value": "'me'"}], + "size": 2, + }, + "__cause__": {"type": "NoneType", "isNull": True}, + "__context__": {"type": "NoneType", "isNull": True}, + "__suppress_context__": {"type": "bool", "value": "False"}, + }, + } + } assert exc["message"] == "'test', 'me'" assert exc["type"] == "Exception"