Skip to content

Commit e15bc80

Browse files
authored
added support for chained exceptions (#596)
If an exception has a `__context__` set, we use its traceback for the chained exception. The exception type and value are either taken from `__cause__` (if set) or `__context__`. closes #526 Also removed Python 2.7 from Windows test matrix on AppVeyor, because it proved difficult to ignore test files with Python 3 syntax only.
1 parent d0f2a39 commit e15bc80

File tree

9 files changed

+437
-266
lines changed

9 files changed

+437
-266
lines changed

.appveyor.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
environment:
22
matrix:
3-
- PYTHON: "C:\\Python27"
4-
WEBFRAMEWORK: "django-1.11"
5-
ASYNCIO: "false"
63
- PYTHON: "C:\\Python34"
74
WEBFRAMEWORK: "django-1.8"
85
ASYNCIO: "false"
@@ -12,9 +9,6 @@ environment:
129
- PYTHON: "C:\\Python35"
1310
WEBFRAMEWORK: "django-1.10"
1411
ASYNCIO: "true"
15-
- PYTHON: "C:\\Python27-x64"
16-
WEBFRAMEWORK: "flask-0.11"
17-
ASYNCIO: "false"
1812
- PYTHON: "C:\\Python34-x64"
1913
DISTUTILS_USE_SDK: "1"
2014
WEBFRAMEWORK: "flask-0.12"

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
* added `logging` filter and record factory for adding transaction, trace, and span IDs (#520, #586)
99
* added `structlog` processor for adding transaction, trace, and span IDs (#520, #586)
1010
* added new public API calls for getting transaction, trace, and span IDs (#520, #586)
11+
* added support for chained exceptions in Python 3 (#596).
12+
Note that chained exceptions will be captured and stored in Elasticsearch, but not yet
13+
visualized in the APM UI. The UI component will be released in an upcoming Kibana release (7.5 or later).
1114

1215
### Bugfixes
1316
* drop events immediately if a processor returns a falsy value (#585)

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ flake8:
88
flake8
99

1010
test:
11-
if [[ "$$PYTHON_VERSION" =~ ^(3.5|3.6|nightly|pypy3)$$ ]] ; then \
11+
if [[ "$$PYTHON_VERSION" =~ ^(3.5|3.6|3.7|3.8|nightly|pypy3)$$ ]] ; then \
1212
py.test -v $(PYTEST_ARGS) $(PYTEST_MARKER) $(PYTEST_JUNIT); \
13-
else py.test -v $(PYTEST_ARGS) $(PYTEST_MARKER) $(PYTEST_JUNIT) --ignore=tests/asyncio; fi
13+
else py.test -v $(PYTEST_ARGS) $(PYTEST_MARKER) $(PYTEST_JUNIT) --ignore=tests/asyncio --ignore-glob='*/py3_*.py'; fi
1414

1515
coverage: PYTEST_ARGS=--cov --cov-report xml:coverage.xml
1616
coverage: test

elasticapm/conf/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545

4646
MASK = 8 * "*"
4747

48+
EXCEPTION_CHAIN_MAX_DEPTH = 50
49+
4850
ERROR = "error"
4951
TRANSACTION = "transaction"
5052
SPAN = "span"

elasticapm/events.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
import random
3434
import sys
3535

36-
from elasticapm.utils import varmap
36+
from elasticapm.conf.constants import EXCEPTION_CHAIN_MAX_DEPTH
37+
from elasticapm.utils import compat, varmap
3738
from elasticapm.utils.encoding import keyword_field, shorten, to_unicode
3839
from elasticapm.utils.stacks import get_culprit, get_stack_info, iter_traceback_frames
3940

@@ -109,7 +110,9 @@ def capture(client, exc_info=None, **kwargs):
109110
),
110111
)
111112

112-
culprit = get_culprit(frames, client.config.include_paths, client.config.exclude_paths)
113+
culprit = kwargs.get("culprit", None) or get_culprit(
114+
frames, client.config.include_paths, client.config.exclude_paths
115+
)
113116

114117
if hasattr(exc_type, "__module__"):
115118
exc_module = exc_type.__module__
@@ -129,7 +132,7 @@ def capture(client, exc_info=None, **kwargs):
129132
else:
130133
message = "%s: %s" % (exc_type, to_unicode(exc_value)) if exc_value else str(exc_type)
131134

132-
return {
135+
data = {
133136
"id": "%032x" % random.getrandbits(128),
134137
"culprit": keyword_field(culprit),
135138
"exception": {
@@ -139,6 +142,30 @@ def capture(client, exc_info=None, **kwargs):
139142
"stacktrace": frames,
140143
},
141144
}
145+
if compat.PY3:
146+
depth = kwargs.get("_exc_chain_depth", 0)
147+
if depth > EXCEPTION_CHAIN_MAX_DEPTH:
148+
return
149+
cause = exc_value.__cause__
150+
chained_context = exc_value.__context__
151+
152+
# we follow the pattern of Python itself here and only capture the chained exception
153+
# if cause is not None and __suppress_context__ is False
154+
if chained_context and not (exc_value.__suppress_context__ and cause is None):
155+
if cause:
156+
chained_exc_type = type(cause)
157+
chained_exc_value = cause
158+
else:
159+
chained_exc_type = type(chained_context)
160+
chained_exc_value = chained_context
161+
chained_exc_info = chained_exc_type, chained_exc_value, chained_context.__traceback__
162+
163+
chained_cause = Exception.capture(
164+
client, exc_info=chained_exc_info, culprit="None", _exc_chain_depth=depth + 1
165+
)
166+
if chained_cause:
167+
data["exception"]["cause"] = [chained_cause["exception"]]
168+
return data
142169

143170

144171
class Message(BaseEvent):

0 commit comments

Comments
 (0)