diff --git a/docs/advanced.rst b/docs/advanced.rst index 3d3312cfc..9320498ef 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -302,3 +302,19 @@ A Note on uWSGI If you're using uWSGI you will need to add ``enable-threads`` to the default invocation, or you will need to switch off of the threaded default transport. + +Integration with asyncio +--------------- + +asyncio introduces additional level of exception handling: exceptions emitted by tasks and futures +must be explicitly handled and are not automatically propagated into ``sys.excepthook``. +Unhandled exceptions (exceptions that were not retrieved before object's destruction) are passed into +loop's own exception handler. + +To handle them the per-loop exception handler can be installed: + +.. code-block:: python + + client.install_asyncio_hook() + +The function accepts one optional argument: ``loop``. It defaults to ``asyncio.get_event_loop()``. diff --git a/raven/base.py b/raven/base.py index 652e5e4df..bcb7dcbe2 100644 --- a/raven/base.py +++ b/raven/base.py @@ -73,6 +73,16 @@ def get_excepthook_client(): return client +def get_loop_excepthook_client(loop=None): + import asyncio + + loop = loop or asyncio.get_event_loop() + hook = loop.get_exception_handler() + client = getattr(hook, 'raven_client', None) + if client is not None: + return client + + class ModuleProxyCache(dict): def __missing__(self, key): module, class_name = key.rsplit('.', 1) @@ -283,6 +293,55 @@ def install_logging_hook(self): from raven.breadcrumbs import install_logging_hook install_logging_hook() + def install_asyncio_hook(self, loop=None): + import asyncio + + loop = loop or asyncio.get_event_loop() + + try: + loop_except_handler = loop.get_exception_handler() or type(loop).default_exception_handler + except AttributeError: + # No get_exception_handler before Python 3.5.2 + loop_except_handler = getattr(loop, '_exception_handler', None) or type(loop).default_exception_handler + + def handle_exception(loop, context): + if 'exception' in context: + exception = context['exception'] + exc_info = type(exception), exception, exception.__traceback__ + self.captureException(exc_info=exc_info, level='exception') # asyncio exceptions are non-fatal + else: + data = {} + + if 'source_traceback' in context: + tb = context['source_traceback'] + elif 'handle' in context and getattr(context['handle'], '_source_traceback', None): + tb = context['handle']._source_traceback + elif 'future' in context and getattr(context['future'], '_source_traceback', None): + tb = context['future']._source_traceback + else: + tb = None + + if tb: + frames = [] + + for file_name, lineno, function_name, text in tb: + frames.append({ + 'filename': file_name, + 'lineno': lineno, + 'function': function_name, + }) + + if frames: + data = {'stacktrace': {'frames': frames}} + + message = context.get('message', 'Unhandled exception in event loop') + self.captureMessage(message, data=data, level='exception') + + loop_except_handler(loop, context) + + handle_exception.raven_client = self + loop.set_exception_handler(handle_exception) + def hook_libraries(self, libraries): from raven.breadcrumbs import hook_libraries hook_libraries(libraries) diff --git a/tests/base/tests.py b/tests/base/tests.py index 5644db03d..bfe18ba2c 100644 --- a/tests/base/tests.py +++ b/tests/base/tests.py @@ -656,3 +656,25 @@ def test_repos_configuration(self): 'name': 'getsentry/raven-python', }, } + + def test_install_asyncio_hook(self): + try: + import asyncio + + loop = asyncio.new_event_loop() + client = TempStoreClient() + client.install_asyncio_hook(loop=loop) + + f = asyncio.Future(loop=loop) + f.set_exception(RuntimeError('foobar')) + del f + + assert len(client.events) == 1 + event = client.events[0] + assert event['level'] == 'exception' + exception = event['exception']['values'][-1] + assert exception['type'] == 'RuntimeError' + assert exception['value'] == 'foobar' + except ImportError: + pass +