diff options
author | David Cramer <dcramer@gmail.com> | 2016-07-25 10:13:33 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-25 10:13:33 -0700 |
commit | 2369418a50de098f074cbe5ccffab0227eb77b2d (patch) | |
tree | 6c23ff9271e986cdab002d258587127eb8a68e5a | |
parent | 574418413804cb4a203fce3e986d1cadba637d7d (diff) | |
parent | 63e77b9f077c1119a25ced32ec6710e0a70965b0 (diff) | |
download | raven-2369418a50de098f074cbe5ccffab0227eb77b2d.tar.gz |
Merge pull request #811 from cjerdonek/issue-550-exception-chains
Issue 550 exception chains
-rw-r--r-- | raven/events.py | 90 | ||||
-rw-r--r-- | tests/events/tests.py | 69 |
2 files changed, 129 insertions, 30 deletions
diff --git a/raven/events.py b/raven/events.py index 5398f83..67402be 100644 --- a/raven/events.py +++ b/raven/events.py @@ -32,6 +32,35 @@ class BaseEvent(object): return self.client.transform(value) +# The __suppress_context__ attribute was added in Python 3.3. +# See PEP 415 for details: +# https://www.python.org/dev/peps/pep-0415/ +if hasattr(Exception, '__suppress_context__'): + def _chained_exceptions(exc_info): + """ + Return a generator iterator over an exception's chain. + + The exceptions are yielded from outermost to innermost (i.e. last to + first when viewing a stack trace). + """ + yield exc_info + exc_type, exc, exc_traceback = exc_info + + while True: + if exc.__suppress_context__: + # Then __cause__ should be used instead. + exc = exc.__cause__ + else: + exc = exc.__context__ + if exc is None: + break + yield type(exc), exc, exc.__traceback__ +else: + # Then we do not support reporting exception chains. + def _chained_exceptions(exc_info): + yield exc_info + + class Exception(BaseEvent): """ Exceptions store the following metadata: @@ -49,6 +78,28 @@ class Exception(BaseEvent): return '%s: %s' % (exc['type'], exc['value']) return exc['type'] + def _get_value(self, exc_type, exc_value, exc_traceback): + """ + Convert exception info to a value for the values list. + """ + stack_info = get_stack_info( + iter_traceback_frames(exc_traceback), + transformer=self.transform, + capture_locals=self.client.capture_locals, + ) + + exc_module = getattr(exc_type, '__module__', None) + if exc_module: + exc_module = str(exc_module) + exc_type = getattr(exc_type, '__name__', '<unknown>') + + return { + 'value': to_unicode(exc_value), + 'type': str(exc_type), + 'module': to_unicode(exc_module), + 'stacktrace': stack_info, + } + def capture(self, exc_info=None, **kwargs): if not exc_info or exc_info is True: exc_info = sys.exc_info() @@ -56,36 +107,15 @@ class Exception(BaseEvent): if not exc_info: raise ValueError('No exception found') - exc_type, exc_value, exc_traceback = exc_info - - try: - stack_info = get_stack_info( - iter_traceback_frames(exc_traceback), - transformer=self.transform, - capture_locals=self.client.capture_locals, - ) - - exc_module = getattr(exc_type, '__module__', None) - if exc_module: - exc_module = str(exc_module) - exc_type = getattr(exc_type, '__name__', '<unknown>') - - return { - 'level': kwargs.get('level', logging.ERROR), - self.name: { - 'values': [{ - 'value': to_unicode(exc_value), - 'type': str(exc_type), - 'module': to_unicode(exc_module), - 'stacktrace': stack_info, - }], - }, - } - finally: - try: - del exc_type, exc_value, exc_traceback - except Exception as e: - self.logger.exception(e) + values = [] + for exc_info in _chained_exceptions(exc_info): + value = self._get_value(*exc_info) + values.append(value) + + return { + 'level': kwargs.get('level', logging.ERROR), + self.name: {'values': values}, + } class Message(BaseEvent): diff --git a/tests/events/tests.py b/tests/events/tests.py new file mode 100644 index 0000000..a75e326 --- /dev/null +++ b/tests/events/tests.py @@ -0,0 +1,69 @@ +import six + +from raven.base import Client +from raven.events import Exception as ExceptionEvent +from raven.utils.testutils import TestCase + + +class ExceptionTest(TestCase): + + # Handle compatibility. + if hasattr(Exception, '__suppress_context__'): + # Then exception chains are supported. + def transform_expected(self, expected): + return expected + else: + # Otherwise, we only report the first element. + def transform_expected(self, expected): + return expected[:1] + + def check_capture(self, expected): + """ + Check the return value of capture(). + + Args: + expected: the expected "type" values. + """ + c = Client() + event = ExceptionEvent(c) + result = event.capture() + info = result['exception'] + values = info['values'] + + type_names = [value['type'] for value in values] + expected = self.transform_expected(expected) + + self.assertEqual(type_names, expected) + + def test_simple(self): + try: + raise ValueError() + except Exception: + self.check_capture(['ValueError']) + + def test_nested(self): + try: + raise ValueError() + except Exception: + try: + raise KeyError() + except Exception: + self.check_capture(['KeyError', 'ValueError']) + + def test_raise_from(self): + try: + raise ValueError() + except Exception as exc: + try: + six.raise_from(KeyError(), exc) + except Exception: + self.check_capture(['KeyError', 'ValueError']) + + def test_raise_from_different(self): + try: + raise ValueError() + except Exception as exc: + try: + six.raise_from(KeyError(), TypeError()) + except Exception: + self.check_capture(['KeyError', 'TypeError']) |