diff options
author | Patrick Gerken <do3cc@patrick-gerken.de> | 2012-07-05 16:09:22 +0200 |
---|---|---|
committer | Patrick Gerken <do3cc@patrick-gerken.de> | 2012-07-05 16:09:22 +0200 |
commit | fcfa2465ff88d5404f355b830f64e2a57a3ff27d (patch) | |
tree | c780b074aa0efb6b2b691b1652e74b508d19da7b | |
parent | 359562c60f3d877662db6536e171d0fe69f05680 (diff) | |
parent | 87c77a51e42026af05c226322010c609818ad233 (diff) | |
download | raven-fcfa2465ff88d5404f355b830f64e2a57a3ff27d.tar.gz |
Merge remote-tracking branch 'upstream/master'
-rw-r--r-- | CHANGES | 12 | ||||
-rw-r--r-- | raven/base.py | 12 | ||||
-rw-r--r-- | raven/contrib/async.py | 7 | ||||
-rw-r--r-- | raven/contrib/django/models.py | 5 | ||||
-rw-r--r-- | raven/contrib/django/serializers.py | 36 | ||||
-rw-r--r-- | raven/contrib/zope/__init__.py | 3 | ||||
-rw-r--r-- | raven/transport.py | 19 | ||||
-rw-r--r-- | raven/utils/encoding.py | 70 | ||||
-rw-r--r-- | raven/utils/serializer/__init__.py | 11 | ||||
-rw-r--r-- | raven/utils/serializer/base.py | 135 | ||||
-rw-r--r-- | raven/utils/serializer/manager.py | 84 | ||||
-rwxr-xr-x | setup.py | 4 | ||||
-rw-r--r-- | tests/contrib/django/models.py | 2 | ||||
-rw-r--r-- | tests/contrib/django/tests.py | 20 | ||||
-rw-r--r-- | tests/transports/tests.py | 1 |
15 files changed, 338 insertions, 83 deletions
@@ -1,3 +1,15 @@ +2.0.0 + +* New serializers exist (and can be registered) against Raven. See ``raven.utils.serializer`` for more information. +* You can now pass ``tags`` to the ``capture`` method. This will require a Sentry server compatible with the new + tags protocol. +* A new gevent+http transport exists. +* A new tornado+http transport exists. +* A new twisted+http transport exists. +* Zope integration has been added. See docs for more information. +* PasteDeploy integration has been added. See docs for more information. +* A Django endpoint now exists for proxying requests to Sentry. See ``raven.contrib.django.views`` for more information. + 1.9.0 * Signatures are no longer sent with messages. This requires the server version to be at least 4.4.6. diff --git a/raven/base.py b/raven/base.py index 9a068b2..cb70100 100644 --- a/raven/base.py +++ b/raven/base.py @@ -205,7 +205,7 @@ class Client(object): def build_msg(self, event_type, data=None, date=None, time_spent=None, extra=None, stack=None, public_key=None, - **kwargs): + tags=None, **kwargs): """ Captures, processes and serializes an event into a dict object """ @@ -264,6 +264,7 @@ class Client(object): data['level'] = logging.ERROR data['modules'] = get_versions(self.include_paths) data['server_name'] = self.name + data['tags'] = tags data.setdefault('extra', {}) data.setdefault('level', logging.ERROR) @@ -295,7 +296,7 @@ class Client(object): data.update(processor.process(data)) # Make sure all data is coerced - data = transform(data) + data = self.transform(data) if 'message' not in data: data['message'] = handler.to_string(data) @@ -311,8 +312,11 @@ class Client(object): return data + def transform(self, data): + return transform(data) + def capture(self, event_type, data=None, date=None, time_spent=None, - extra=None, stack=None, public_key=None, **kwargs): + extra=None, stack=None, public_key=None, tags=None, **kwargs): """ Captures and processes an event and pipes it off to SentryClient.send. @@ -367,7 +371,7 @@ class Client(object): """ data = self.build_msg(event_type, data, date, time_spent, - extra, stack, public_key=public_key, **kwargs) + extra, stack, public_key=public_key, tags=tags, **kwargs) self.send(**data) diff --git a/raven/contrib/async.py b/raven/contrib/async.py index 0b343ae..4f83c57 100644 --- a/raven/contrib/async.py +++ b/raven/contrib/async.py @@ -97,11 +97,10 @@ class SentryWorker(object): >>> from raven.base import Client >>> application = SentryWorker(application) """ - def __init__(self, application): + def __init__(self, application, worker=None): self.application = application - self.worker = AsyncWorker() + self.worker = worker or AsyncWorker() def __call__(self, environ, start_response): environ['raven.worker'] = self.worker - for event in self.application(environ, start_response): - yield event + return iter(self.application(environ, start_response)) diff --git a/raven/contrib/django/models.py b/raven/contrib/django/models.py index a54db8d..ecd7d5d 100644 --- a/raven/contrib/django/models.py +++ b/raven/contrib/django/models.py @@ -194,5 +194,10 @@ def register_handlers(): except Exception, e: logger.exception('Failed installing django-celery hook: %s' % e) + +def register_serializers(): + import raven.contrib.django.serializers # force import so serializers can call register + if 'raven.contrib.django' in django_settings.INSTALLED_APPS: register_handlers() + register_serializers() diff --git a/raven/contrib/django/serializers.py b/raven/contrib/django/serializers.py new file mode 100644 index 0000000..41d0ebd --- /dev/null +++ b/raven/contrib/django/serializers.py @@ -0,0 +1,36 @@ +""" +raven.contrib.django.serializers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + +from django.db.models.query import QuerySet +from django.utils.functional import Promise +from raven.utils.serializer import Serializer, register + +__all__ = ('PromiseSerializer',) + + +@register +class PromiseSerializer(Serializer): + types = (Promise,) + + def serialize(self, value): + # EPIC HACK + # handles lazy model instances (which are proxy values that dont easily give you the actual function) + pre = value.__class__.__name__[1:] + value = getattr(value, '%s__func' % pre)(*getattr(value, '%s__args' % pre), **getattr(value, '%s__kw' % pre)) + return self.recurse(value) + + +@register +class QuerySetSerializer(Serializer): + types = (QuerySet,) + + def serialize(self, value): + qs_name = type(value).__name__ + if value.model: + return u'<%s: model=%s>' % (qs_name, value.model.__name__) + return u'<%s: (Unbound)>' % (qs_name,) diff --git a/raven/contrib/zope/__init__.py b/raven/contrib/zope/__init__.py index cf21372..3df1ebd 100644 --- a/raven/contrib/zope/__init__.py +++ b/raven/contrib/zope/__init__.py @@ -84,6 +84,5 @@ class ZopeSentryHandler(SentryHandler): ), email=user.getProperty('email')) setattr(record, 'sentry.interfaces.User', user_dict) except (AttributeError, KeyError): - logger.warning('Could not extract data from request' - , exc_info=True) + logger.warning('Could not extract data from request', exc_info=True) return super(ZopeSentryHandler, self).emit(record) diff --git a/raven/transport.py b/raven/transport.py index b5c4360..2b7e1e1 100644 --- a/raven/transport.py +++ b/raven/transport.py @@ -1,6 +1,5 @@ import urllib2 from socket import socket, AF_INET, SOCK_DGRAM, error as socket_error -from collections import Iterable try: from gevent import spawn @@ -16,7 +15,8 @@ except: twisted = False try: - from tornado.httpclient import AsyncHTTPClient + from tornado import ioloop + from tornado.httpclient import AsyncHTTPClient, HTTPClient tornado = True except: tornado = False @@ -228,9 +228,16 @@ class TornadoHTTPTransport(HTTPTransport): self._url = self._url.split('+', 1)[-1] def send(self, data, headers): - client = AsyncHTTPClient() - client.fetch(self._url, callback=None, - method='POST', headers=headers, body=data) + kwargs = dict(method='POST', headers=headers, body=data) + + # only use async if ioloop is running, otherwise it will never send + if ioloop.IOLoop.initialized(): + client = AsyncHTTPClient() + kwargs['callback'] = None + else: + client = HTTPClient() + + client.fetch(self._url, **kwargs) class TransportRegistry(object): @@ -244,7 +251,7 @@ class TransportRegistry(object): self.register_transport(transport) def register_transport(self, transport): - if not hasattr(transport, 'scheme') and not isinstance(transport.scheme, Iterable): + if not hasattr(transport, 'scheme') and not hasattr(transport.scheme, '__iter__'): raise AttributeError('Transport %s must have a scheme list', transport.__class__.__name__) for scheme in transport.scheme: diff --git a/raven/utils/encoding.py b/raven/utils/encoding.py index 41c7db9..525dd47 100644 --- a/raven/utils/encoding.py +++ b/raven/utils/encoding.py @@ -6,8 +6,7 @@ raven.utils.encoding :license: BSD, see LICENSE for more details. """ -import uuid -from types import ClassType, TypeType +import warnings def force_unicode(s, encoding='utf-8', errors='strict'): @@ -54,71 +53,12 @@ def force_unicode(s, encoding='utf-8', errors='strict'): return s -def _has_sentry_metadata(value): - try: - return callable(value.__getattribute__("__sentry__")) - except: - return False - - -def transform(value, stack=[], context=None): - # TODO: make this extendable - if context is None: - context = {} +def transform(value): + from raven.utils.serializer import transform - objid = id(value) - if objid in context: - return '<...>' + warnings.warn('You should switch to raven.utils.serializers.transform', DeprecationWarning) - context[objid] = 1 - transform_rec = lambda o: transform(o, stack + [value], context) - - if any(value is s for s in stack): - ret = 'cycle' - elif isinstance(value, (tuple, list, set, frozenset)): - try: - ret = type(value)(transform_rec(o) for o in value) - except Exception: - # We may be dealing with a namedtuple - class value_type(list): - __name__ = type(value).__name__ - ret = value_type(transform_rec(o) for o in value) - elif isinstance(value, uuid.UUID): - ret = repr(value) - elif isinstance(value, dict): - ret = dict((to_string(k), transform_rec(v)) for k, v in value.iteritems()) - elif isinstance(value, unicode): - ret = to_unicode(value) - elif isinstance(value, str): - ret = to_string(value) - elif not isinstance(value, (ClassType, TypeType)) and \ - _has_sentry_metadata(value): - ret = transform_rec(value.__sentry__()) - # elif isinstance(value, Promise): - # # EPIC HACK - # # handles lazy model instances (which are proxy values that dont easily give you the actual function) - # pre = value.__class__.__name__[1:] - # value = getattr(value, '%s__func' % pre)(*getattr(value, '%s__args' % pre), **getattr(value, '%s__kw' % pre)) - # return transform(value) - elif isinstance(value, bool): - ret = bool(value) - elif isinstance(value, float): - ret = float(value) - elif isinstance(value, int): - ret = int(value) - elif isinstance(value, long): - ret = long(value) - elif value is not None: - try: - ret = transform(repr(value)) - except: - # It's common case that a model's __unicode__ definition may try to query the database - # which if it was not cleaned up correctly, would hit a transaction aborted exception - ret = u'<BadRepr: %s>' % type(value) - else: - ret = None - del context[objid] - return ret + return transform(value) def to_unicode(value): diff --git a/raven/utils/serializer/__init__.py b/raven/utils/serializer/__init__.py new file mode 100644 index 0000000..c90ce6b --- /dev/null +++ b/raven/utils/serializer/__init__.py @@ -0,0 +1,11 @@ +""" +raven.utils.serializer +~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + + +from raven.utils.serializer.base import Serializer +from raven.utils.serializer.manager import register, transform diff --git a/raven/utils/serializer/base.py b/raven/utils/serializer/base.py new file mode 100644 index 0000000..dc5faf9 --- /dev/null +++ b/raven/utils/serializer/base.py @@ -0,0 +1,135 @@ +""" +raven.utils.serializer.base +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + +from raven.utils.encoding import to_string, to_unicode +from raven.utils.serializer.manager import register +from types import ClassType, TypeType +from uuid import UUID + + +def has_sentry_metadata(value): + try: + return callable(value.__getattribute__('__sentry__')) + except: + return False + + +class Serializer(object): + types = () + + def __init__(self, manager): + self.manager = manager + + def can(self, value): + """ + Given ``value``, return a boolean describing whether this + serializer can operate on the given type + """ + return isinstance(value, self.types) + + def serialize(self, value): + """ + Given ``value``, coerce into a JSON-safe type. + """ + return value + + def recurse(self, value): + """ + Given ``value``, recurse (using the parent serializer) to handle + coercing of newly defined values. + """ + return self.manager.transform(value) + + +@register +class IterableSerializer(Serializer): + types = (tuple, list, set, frozenset) + + def serialize(self, value): + try: + return type(value)(self.recurse(o) for o in value) + except Exception: + # We may be dealing with something like a namedtuple + class value_type(list): + __name__ = type(value).__name__ + return value_type(self.recurse(o) for o in value) + + +@register +class UUIDSerializer(Serializer): + types = (UUID,) + + def serialize(self, value): + return repr(value) + + +@register +class DictSerializer(Serializer): + types = (dict,) + + def serialize(self, value): + return dict((to_string(k), self.recurse(v)) for k, v in value.iteritems()) + + +@register +class UnicodeSerializer(Serializer): + types = (unicode,) + + def serialize(self, value): + return to_unicode(value) + + +@register +class StringSerializer(Serializer): + types = (str,) + + def serialize(self, value): + return to_string(value) + + +@register +class TypeSerializer(Serializer): + types = (ClassType, TypeType,) + + def can(self, value): + return not super(TypeSerializer, self).can(value) and has_sentry_metadata(value) + + def serialize(self, value): + return self.recurse(value.__sentry__()) + + +@register +class BooleanSerializer(Serializer): + types = (bool,) + + def serialize(self, value): + return bool(value) + + +@register +class FloatSerializer(Serializer): + types = (float,) + + def serialize(self, value): + return float(value) + + +@register +class IntegerSerializer(Serializer): + types = (int,) + + def serialize(self, value): + return int(value) + + +@register +class LongSerializer(Serializer): + types = (long,) + + def serialize(self, value): + return long(value) diff --git a/raven/utils/serializer/manager.py b/raven/utils/serializer/manager.py new file mode 100644 index 0000000..62cb815 --- /dev/null +++ b/raven/utils/serializer/manager.py @@ -0,0 +1,84 @@ +""" +raven.utils.serializer.manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. +""" + +import logging + +logger = logging.getLogger('sentry.errors.serializer') + + +class SerializationManager(object): + def __init__(self): + self.__registry = [] + self.__serializers = {} + + @property + def serializers(self): + # XXX: Would serializers ever need state that we shouldnt cache them? + for serializer in self.__registry: + yield serializer + + def register(self, serializer): + if serializer not in self.__registry: + self.__registry.append(serializer) + return serializer + + +class Serializer(object): + def __init__(self, manager): + self.manager = manager + self.context = {} + self.serializers = [] + for serializer in manager.serializers: + self.serializers.append(serializer(self)) + + def transform(self, value): + """ + Primary function which handles recursively transforming + values via their serializers + """ + if value is None: + return None + + objid = id(value) + if objid in self.context: + return '<...>' + self.context[objid] = 1 + + # TODO: do we still need this code? context seems to handle it + # if any(value is s for s in self.stack): + # ret = 'cycle' + # self.stack.append(value) + + try: + for serializer in self.serializers: + if serializer.can(value): + try: + return serializer.serialize(value) + except Exception, e: + logger.exception(e) + return u'<BadSerializable: %s>' % type(value) + + # if all else fails, lets use the repr of the object + try: + return self.transform(repr(value)) + except Exception, e: + logger.exception(e) + # It's common case that a model's __unicode__ definition may try to query the database + # which if it was not cleaned up correctly, would hit a transaction aborted exception + return u'<BadRepr: %s>' % type(value) + finally: + del self.context[objid] + + +manager = SerializationManager() +register = manager.register + + +def transform(value, manager=manager): + serializer = Serializer(manager) + return serializer.transform(value) @@ -33,7 +33,7 @@ tests_require = [ 'nose', 'mock', 'pep8', - 'sentry>=4.4.6', + 'sentry>=4.7.9', 'unittest2', 'webob', 'zerorpc>=0.2.0', @@ -46,7 +46,7 @@ install_requires = [ setup( name='raven', - version='1.9.4', + version='2.0.0', author='David Cramer', author_email='dcramer@gmail.com', url='http://github.com/dcramer/raven', diff --git a/tests/contrib/django/models.py b/tests/contrib/django/models.py index 41b3ea3..c678329 100644 --- a/tests/contrib/django/models.py +++ b/tests/contrib/django/models.py @@ -3,12 +3,14 @@ from __future__ import absolute_import from django.db import models from sentry.models import GzippedDictField + class TestModel(models.Model): data = GzippedDictField(blank=True, null=True) def __unicode__(self): return unicode(self.data) + class DuplicateKeyModel(models.Model): foo = models.IntegerField(unique=True, default=1) diff --git a/tests/contrib/django/tests.py b/tests/contrib/django/tests.py index 34acf62..ad6e5d8 100644 --- a/tests/contrib/django/tests.py +++ b/tests/contrib/django/tests.py @@ -25,8 +25,10 @@ from raven.contrib.django.handlers import SentryHandler from raven.contrib.django.models import client, get_client from raven.contrib.django.middleware.wsgi import Sentry from raven.contrib.django.views import is_valid_origin +from raven.utils.serializer import transform from django.test.client import Client as TestClient, ClientHandler as TestClientHandler +from .models import TestModel settings.SENTRY_CLIENT = 'tests.contrib.django.tests.TempStoreClient' @@ -588,3 +590,21 @@ class ReportViewTest(TestCase): self.assertEquals(resp.status_code, 200) event = client.events.pop(0) self.assertEquals(event, {'auth_header': 'Sentry foo/bar'}) + + +class PromiseSerializerTestCase(TestCase): + def test_basic(self): + from django.utils.functional import lazy + + obj = lazy(lambda: 'bar', str)() + res = transform(obj) + self.assertEquals(res, 'bar') + + +class QuerySetSerializerTestCase(TestCase): + def test_basic(self): + from django.db.models.query import QuerySet + obj = QuerySet(model=TestModel) + + res = transform(obj) + self.assertEquals(res, '<QuerySet: model=TestModel>') diff --git a/tests/transports/tests.py b/tests/transports/tests.py index 08d9011..5bb3034 100644 --- a/tests/transports/tests.py +++ b/tests/transports/tests.py @@ -89,6 +89,7 @@ class TransportTest(TestCase): 'extra': {}, 'modules': {}, 'site': None, + 'tags': None, 'time_spent': None, 'timestamp': 1336089600, 'message': 'foo', |