Skip to content

Commit

Permalink
Merge pull request #2 from sprockets/improve-sentry
Browse files Browse the repository at this point in the history
Log more information to sentry
  • Loading branch information
pianoman19372 committed Jul 13, 2015
2 parents 6d0c13b + 548f8f2 commit 0df6fd2
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 126 deletions.
30 changes: 16 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,43 @@ Installation
`Python Package Index <https://pypi.python.org/pypi/sprockets.mixins.sentry>`_
and can be installed via ``pip`` or ``easy_install``:

.. code:: bash
.. code-block:: bash
pip install sprockets.mixins.sentry
pip install sprockets.mixins.sentry
Documentation
-------------
https://sprocketsmixinssentry.readthedocs.org

Requirements
------------
- `sprockets <https://github.com/sprockets/sprockets>`_

- `raven <https://raven.readthedocs.org/>`_
- `tornado <https://tornadoweb.org/>`_

Example
-------
This examples demonstrates how to use ``sprockets.mixins.sentry``.

.. code:: python
.. code-block:: python
from sprockets.mixins import sentry
from tornado import web
from sprockets.mixins import sentry
from tornado import web
class RequestHandler(sentry.SentryMixin, web.RequestHandler):
"""Requires a ``SENTRY_DSN`` environment variable is set with the
DSN value provided by sentry.
class RequestHandler(sentry.SentryMixin, web.RequestHandler):
"""Requires a ``SENTRY_DSN`` environment variable is set with the
DSN value provided by sentry.
The Mixin should catch unhandled exceptions and report them to Sentry.
The Mixin should catch unhandled exceptions and report them to Sentry.
"""
def get(self, *args, **kwargs):
raise ValueError("This should send an error to sentry")
"""
def get(self, *args, **kwargs):
raise ValueError("This should send an error to sentry")
Version History
---------------
Available at https://sprocketsmixinssentry.readthedocs.org/en/latest/history.html
Available at https://sprocketsmixinssentry.readthedocs.org/en/latest/

.. |Version| image:: https://img.shields.io/pypi/v/sprockets.mixins.sentry.svg?
:target: http://badge.fury.io/py/sprockets.mixins.sentry
Expand Down
2 changes: 0 additions & 2 deletions docs/api.rst

This file was deleted.

4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'requests': ('https://requests.readthedocs.org/en/latest/', None),
'sprockets': ('https://sprockets.readthedocs.org/en/latest/', None),
'tornado': ('http://tornadoweb.org/en/stable/', None),
'raven': ('https://raven.readthedocs.org/en/latest/', None),
}
34 changes: 34 additions & 0 deletions docs/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import sys
import signal

from tornado import ioloop, web

from sprockets.mixins import sentry


class Handler(sentry.SentryMixin, web.RequestHandler):

def initialize(self, **kwargs):
tags = kwargs.pop('tags', dict())
super(Handler, self).initialize(**kwargs)
self.sentry_tags.update(tags)

def get(self, status_code):
self.set_status(int(status_code))


def stop(signo, frame):
iol = ioloop.IOLoop.current()
iol.add_callback_from_signal(iol.stop)


if __name__ == '__main__':
tags = {}
for arg in sys.argv[1:]:
name, _, value = arg.partition('=')
tags[name] = value

signal.signal(signal.SIGINT, stop)
app = web.Application([web.url(r'/(\S+)', Handler, {'tags': tags})])
app.listen(8000)
ioloop.IOLoop.current().start()
29 changes: 0 additions & 29 deletions docs/examples.rst

This file was deleted.

5 changes: 0 additions & 5 deletions docs/history.rst

This file was deleted.

47 changes: 33 additions & 14 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,44 @@ Installation
`Python Package Index <https://pypi.python.org/pypi/sprockets.mixins.sentry>`_
and can be installed via ``pip`` or ``easy_install``:

.. code:: bash
.. code-block:: bash
pip install sprockets.mixins.sentry
pip install sprockets.mixins.sentry
Requirements
------------
@TODO: Put full requirements list here, should match requirements.txt
- `sprockets <https://github.com/sprockets/sprockets>`_
- `raven`_
- `tornado`_

API Documentation
-----------------
.. toctree::
:maxdepth: 2
.. automodule:: sprockets.mixins.sentry
:members:

api
examples
Examples
--------
The following application will report errors to sentry if you export the
:envvar:`SENTRY_DSN` environment variable and make a request to
http://localhost:8000/whatever provided that *whatever* is not an integer.

.. literalinclude:: example.py

Version History
---------------
See :doc:`history`
* `0.3.0`_ (13-Jul-2015)

- Add ``sprockets.mixins.sentry.SentryMixin.sentry_extra``
- Add ``sprockets.mixins.sentry.SentryMixin.sentry_tags``
- Improve module reporting in Sentry messages
- Improved documentation

* `0.2.0`_ (22-Jun-2015)

- Stop reporting :class:`tornado.web.HTTPError`s
* `0.1.0`_ (13-May-2015)
- Initial public release
Issues
------
Expand All @@ -43,12 +61,12 @@ License
-------
``sprockets.mixins.sentry`` is released under the `3-Clause BSD license <https://github.com/sprockets/sprockets.mixins.sentry/blob/master/LICENSE>`_.

Indices and tables
------------------
.. _raven: https://raven.readthedocs.org/
.. _tornado: https://tornadoweb.org/

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. _0.1.0: https://github.com/sprockets/sprockets.mixins.sentry/compare/e01c264...0.1.0
.. _0.2.0: https://github.com/sprockets/sprockets.mixins.sentry/compare/0.1.0...0.2.0
.. _0.3.0: https://github.com/sprockets/sprockets.mixins.sentry/compare/0.2.0...0.3.0

.. |Version| image:: https://badge.fury.io/py/sprockets.mixins.sentry.svg?
:target: http://badge.fury.io/py/sprockets.mixins.sentry
Expand All @@ -64,3 +82,4 @@ Indices and tables

.. |License| image:: https://pypip.in/license/sprockets.mixins.sentry/badge.svg?
:target: https://sprocketsmixinssentry.readthedocs.org

6 changes: 4 additions & 2 deletions setup.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env python

import codecs
import sys

Expand Down Expand Up @@ -29,13 +31,13 @@ def read_requirements_file(req_name):

setuptools.setup(
name='sprockets.mixins.sentry',
version='0.2.0',
version='0.3.0',
description='A RequestHandler mixin for sending exceptions to Sentry',
long_description=codecs.open('README.rst', encoding='utf-8').read(),
url='https://github.com/sprockets/sprockets.mixins.sentry.git',
author='AWeber Communications',
author_email='api@aweber.com',
license=codecs.open('LICENSE', encoding='utf-8').read(),
license='BSD',
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
Expand Down
127 changes: 70 additions & 57 deletions sprockets/mixins/sentry/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@
A RequestHandler mixin for sending exceptions to Sentry
"""
version_info = (0, 2, 0)
version_info = (0, 3, 0)
__version__ = '.'.join(str(v) for v in version_info)


import math
import os
import pkg_resources
import re
import sys
import time
try:
from urllib import parse
Expand All @@ -27,14 +25,50 @@


class SentryMixin(object):
"""
Report unexpected exceptions to Sentry.
def initialize(self):
Mix this in over a :class:`tornado.web.RequestHandler` to report
unhandled exceptions to Sentry so that you can figure out what
went wrong. In order to use this mix-in, all that you have to do
is define the **SENTRY_DSN** environment variable that contains your
projects Sentry DSN. Whenever a request comes it and the environment
variable is set, this mix-in will create a new :class:`raven.base.Client`
instance and make it available via the :attr:`sentry_client` property.
If an exception is caught by :meth:`._handle_request_exception`, then
it will be reported to Sentry in all it's glory.
.. attribute:: sentry_client
The :class:`raven.base.Client` instance or :data:`None` if sentry
reporting is disabled. You can modify attributes of the client
as required for your application -- for example, you can add new
modules by adding to ``self.sentry_client.include_paths``.
.. attribute:: sentry_extra
A :class:`dict` of extra information to pass to sentry when an
exception is reported.
.. attribute:: sentry_tags
A :class:`dict` of tag and value pairs to associated with any
reported exceptions.
"""

def __init__(self, *args, **kwargs):
self.sentry_extra = {}
self.sentry_tags = {}
if not hasattr(self.application, SENTRY_CLIENT):
sentry_dsn = os.environ.get('SENTRY_DSN')
if sentry_dsn:
setattr(self.application, SENTRY_CLIENT,
raven.Client(sentry_dsn))
super(SentryMixin, self).__init__(*args, **kwargs)

def initialize(self):
sentry_dsn = os.environ.get('SENTRY_DSN')
if self.sentry_client is None and sentry_dsn:
modules = ['raven', 'sys', 'tornado', __name__]
client = raven.Client(sentry_dsn, include_paths=modules)
setattr(self.application, SENTRY_CLIENT, client)
super(SentryMixin, self).initialize()

def _strip_uri_passwords(self, values):
Expand All @@ -45,55 +79,34 @@ def _strip_uri_passwords(self, values):
return values

def _handle_request_exception(self, e):
if isinstance(e, web.HTTPError):
if isinstance(e, web.HTTPError) or self.sentry_client is None:
return super(SentryMixin, self)._handle_request_exception(e)

if hasattr(self.application, SENTRY_CLIENT):
duration = math.ceil((time.time() - self.request._start_time) * 1000)
data = {}
if hasattr(self, 'request'):
data = {'request':
{'url': self.request.full_url(),
'method': self.request.method,
'data': self.request.body,
'query_string': self.request.query,
'cookies': self.request.headers.get('Cookie', {}),
'headers': dict(self.request.headers)},
'logger': 'sprockets.mixins.sentry',
'modules': self._get_module_data()}
kwargs = {'extra':
{'handler': '{0}.{1}'.format(__name__,
self.__class__.__name__),
'env': self._strip_uri_passwords(dict(os.environ)),
'http_host': self.request.host,
'remote_ip': self.request.remote_ip},
'time_spent': duration}
if self.sentry_tags:
kwargs.update(self.sentry_tags)
self.application.sentry_client.captureException(True, data=data,
**kwargs)
duration = math.ceil((time.time() - self.request._start_time) * 1000)
kwargs = {'extra': self.sentry_extra, 'time_spent': duration}
kwargs['extra'].setdefault(
'handler', '{0}.{1}'.format(__name__, self.__class__.__name__))
kwargs['extra'].setdefault('env',
self._strip_uri_passwords(dict(os.environ)))
if hasattr(self, 'request'):
kwargs['data'] = {
'request': {
'url': self.request.full_url(),
'method': self.request.method,
'data': self.request.body,
'query_string': self.request.query,
'cookies': self.request.headers.get('Cookie', {}),
'headers': dict(self.request.headers)},
'logger': 'sprockets.mixins.sentry'}
kwargs['extra']['http_host'] = self.request.host
kwargs['extra']['remote_ip'] = self.request.remote_ip

if self.sentry_tags:
kwargs.update({'tags': self.sentry_tags})
self.sentry_client.captureException(True, **kwargs)

super(SentryMixin, self)._handle_request_exception(e)

def _get_module_data(self):
modules = {}
for module_name in sys.modules.keys():
module = sys.modules[module_name]
if hasattr(module, '__version__'):
modules[module_name] = module.__version__
elif hasattr(module, 'version'):
modules[module_name] = module.version
else:
try:
version = self._get_version(module_name)
if version:
modules[module_name] = version
except Exception:
pass
return modules

@staticmethod
def _get_version(module_name):
try:
return pkg_resources.get_distribution(module_name).version
except pkg_resources.DistributionNotFound:
return None
@property
def sentry_client(self):
return getattr(self.application, SENTRY_CLIENT, None)

0 comments on commit 0df6fd2

Please sign in to comment.