summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Gerken <do3cc@patrick-gerken.de>2012-07-05 16:09:22 +0200
committerPatrick Gerken <do3cc@patrick-gerken.de>2012-07-05 16:09:22 +0200
commitfcfa2465ff88d5404f355b830f64e2a57a3ff27d (patch)
treec780b074aa0efb6b2b691b1652e74b508d19da7b
parent359562c60f3d877662db6536e171d0fe69f05680 (diff)
parent87c77a51e42026af05c226322010c609818ad233 (diff)
downloadraven-fcfa2465ff88d5404f355b830f64e2a57a3ff27d.tar.gz
Merge remote-tracking branch 'upstream/master'
-rw-r--r--CHANGES12
-rw-r--r--raven/base.py12
-rw-r--r--raven/contrib/async.py7
-rw-r--r--raven/contrib/django/models.py5
-rw-r--r--raven/contrib/django/serializers.py36
-rw-r--r--raven/contrib/zope/__init__.py3
-rw-r--r--raven/transport.py19
-rw-r--r--raven/utils/encoding.py70
-rw-r--r--raven/utils/serializer/__init__.py11
-rw-r--r--raven/utils/serializer/base.py135
-rw-r--r--raven/utils/serializer/manager.py84
-rwxr-xr-xsetup.py4
-rw-r--r--tests/contrib/django/models.py2
-rw-r--r--tests/contrib/django/tests.py20
-rw-r--r--tests/transports/tests.py1
15 files changed, 338 insertions, 83 deletions
diff --git a/CHANGES b/CHANGES
index 0d5fdc1..d8f9552 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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)
diff --git a/setup.py b/setup.py
index 14235e7..6ca0e4f 100755
--- a/setup.py
+++ b/setup.py
@@ -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',