Skip to content

Commit

Permalink
Refactored Django settings and the logging integration, as well as a …
Browse files Browse the repository at this point in the history
…few minor fixes
  • Loading branch information
dcramer committed Oct 10, 2011
1 parent 3523679 commit d03b1e7
Show file tree
Hide file tree
Showing 16 changed files with 129 additions and 90 deletions.
4 changes: 2 additions & 2 deletions docs/config/django.rst
Expand Up @@ -38,7 +38,7 @@ Django 1.3
'handlers': {
'sentry': {
'level': 'DEBUG',
'class': 'sentry.client.handlers.SentryHandler',
'class': 'raven.contrib.django.logging.SentryHandler',
'formatter': 'verbose'
},
'console': {
Expand Down Expand Up @@ -171,7 +171,7 @@ To work around this, you can either disable your error handling middleware, or a

Or, alternatively, you can just enable Sentry responses::

from sentry.client.models import sentry_exception_handler
from raven.contrib.django.models import sentry_exception_handler
class MyMiddleware(object):
def process_exception(self, request, exception):
# Make sure the exception signal is fired for Sentry
Expand Down
5 changes: 5 additions & 0 deletions docs/config/index.rst
Expand Up @@ -54,3 +54,8 @@ auto_log_stacks
~~~~~~~~~~~~~~~

Should Raven automatically log frame stacks (including locals) for ``create_from_record`` (``logging``) calls as it would for exceptions. Defaults to ``False``.

timeout
~~~~~~~

If supported, the timeout value for sending messages to remote.
4 changes: 2 additions & 2 deletions raven/base.py
Expand Up @@ -31,7 +31,7 @@ class Client(object):
def __init__(self, *args, **kwargs):
self.include_paths = kwargs.get('include_paths') or settings.INCLUDE_PATHS
self.exclude_paths = kwargs.get('exclude_paths') or settings.EXCLUDE_PATHS
self.remote_timeout = kwargs.get('remote_timeout') or settings.REMOTE_TIMEOUT
self.timeout = kwargs.get('timeout') or settings.TIMEOUT
self.servers = kwargs.get('servers') or settings.SERVERS
self.name = kwargs.get('name') or settings.NAME
self.auto_log_stacks = kwargs.get('auto_log_stacks') or settings.AUTO_LOG_STACKS
Expand Down Expand Up @@ -115,7 +115,7 @@ def process(self, **kwargs):
def send_remote(self, url, data, headers={}):
req = urllib2.Request(url, headers=headers)
try:
response = urllib2.urlopen(req, data, self.remote_timeout).read()
response = urllib2.urlopen(req, data, self.timeout).read()
except:
response = urllib2.urlopen(req, data).read()
return response
Expand Down
3 changes: 1 addition & 2 deletions raven/conf/defaults.py
Expand Up @@ -8,7 +8,6 @@
:license: BSD, see LICENSE for more details.
"""

import logging
import os
import os.path
import socket
Expand All @@ -23,7 +22,7 @@
# This should be the full URL to sentries store view
SERVERS = None

REMOTE_TIMEOUT = 5
TIMEOUT = 5

# TODO: this is specific to Django
CLIENT = 'raven.contrib.django.DjangoClient'
Expand Down
17 changes: 8 additions & 9 deletions raven/contrib/django/__init__.py
Expand Up @@ -11,16 +11,14 @@
import logging
import sys

from django.http import HttpRequest
from django.template import TemplateSyntaxError
from django.template.loader import LoaderOrigin

from raven.base import Client

logger = logging.getLogger('sentry.errors.client')

class DjangoClient(Client):
logger = logging.getLogger('sentry.errors.client')

def process(self, **kwargs):
from django.http import HttpRequest

request = kwargs.pop('request', None)
is_http_request = isinstance(request, HttpRequest)
if is_http_request:
Expand Down Expand Up @@ -56,8 +54,6 @@ def process(self, **kwargs):

if not kwargs.get('url'):
kwargs['url'] = request.build_absolute_uri()
else:
kwargs['request'] = request

message_id, checksum = super(DjangoClient, self).process(**kwargs)

Expand All @@ -74,6 +70,9 @@ def create_from_exception(self, exc_info=None, **kwargs):
"""
Creates an error log from an exception.
"""
from django.template import TemplateSyntaxError
from django.template.loader import LoaderOrigin

new_exc = bool(exc_info)
if not exc_info or exc_info is True:
exc_info = sys.exc_info()
Expand All @@ -100,7 +99,7 @@ def create_from_exception(self, exc_info=None, **kwargs):
try:
del exc_info
except Exception, e:
logger.exception(e)
self.logger.exception(e)

def create_from_record(self, record, **kwargs):
"""
Expand Down
39 changes: 39 additions & 0 deletions raven/contrib/django/logging.py
@@ -0,0 +1,39 @@
"""
raven.contrib.django.logging
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import

import logging
import sys
import traceback

class SentryHandler(logging.Handler):
def emit(self, record):
from raven.contrib.django.middleware import SentryLogMiddleware
from raven.contrib.django.models import get_client

# Fetch the request from a threadlocal variable, if available
request = getattr(SentryLogMiddleware.thread, 'request', None)

self.format(record)

# Avoid typical config issues by overriding loggers behavior
if record.name.startswith('sentry.errors'):
print >> sys.stderr, "Recursive log message sent to SentryHandler"
print >> sys.stderr, record.message
return

self.format(record)
try:
get_client().create_from_record(record, request=request)
except Exception:
print >> sys.stderr, "Top level Sentry exception caught - failed creating log record"
print >> sys.stderr, record.msg
print >> sys.stderr, traceback.format_exc()
return

117 changes: 54 additions & 63 deletions raven/contrib/django/models.py
Expand Up @@ -18,18 +18,8 @@
from django.conf import settings as django_settings
from django.utils.hashcompat import md5_constructor

from raven.conf import settings

logger = logging.getLogger('sentry.errors.client')

_client = (None, None)
def get_client():
global _client
if _client[0] != settings.CLIENT:
module, class_name = settings.CLIENT.rsplit('.', 1)
_client = (settings.CLIENT, getattr(__import__(module, {}, {}, class_name), class_name)())
return _client[1]
client = get_client()

def get_installed_apps():
"""
Expand All @@ -40,72 +30,73 @@ def get_installed_apps():
out.add(app)
return out

def configure_settings():
# Some sane overrides to better mix with Django
values = {}
for k in (k for k in dir(django_settings) if k.startswith('SENTRY_')):
print k, values
values[k.split('SENTRY_', 1)[1]] = getattr(django_settings, k)

if 'KEY' not in values:
values['KEY'] = md5_constructor(django_settings.SECRET_KEY).hexdigest()
if 'DEBUG' not in values:
values['DEBUG'] = django_settings.DEBUG

if 'REMOTE_URL' in values:
v = values['REMOTE_URL']
if isinstance(v, basestring):
values['REMOTES'] = [v]
elif not isinstance(v, (list, tuple)):
raise ValueError("Sentry setting 'REMOTE_URL' must be of type list.")

if 'INCLUDE_PATHS' not in values:
values['INCLUDE_PATHS'] = get_installed_apps()
else:
values['INCLUDE_PATHS'] = set(values['INCLUDE_PATHS']) + get_installed_apps()
_client = (None, None)
def get_client():
global _client

settings.configure(**values)
client = getattr(django_settings, 'SENTRY_CLIENT', 'raven.contrib.django.DjangoClient')

if _client[0] != client:
module, class_name = client.rsplit('.', 1)
_client = (client, getattr(__import__(module, {}, {}, class_name), class_name)(
include_paths=getattr(django_settings, 'SENTRY_INCLUDE_PATHS', set()) | get_installed_apps(),
exclude_paths=getattr(django_settings, 'SENTRY_EXCLUDE_PATHS', None),
timeout=getattr(django_settings, 'SENTRY_TIMEOUT', None),
servers=getattr(django_settings, 'SENTRY_SERVERS', None),
name=getattr(django_settings, 'SENTRY_NAME', None),
auto_log_stacks=getattr(django_settings, 'SENTRY_AUTO_LOG_STACKS', None),
key=getattr(django_settings, 'SENTRY_KEY', md5_constructor(django_settings.SECRET_KEY).hexdigest()),
string_max_length=getattr(django_settings, 'MAX_LENGTH_STRING', None),
list_max_length=getattr(django_settings, 'MAX_LENGTH_LIST', None),
))
return _client[1]
client = get_client()

configure_settings()
def get_transaction_wrapper(client):
if client.servers:
class MockTransaction(object):
def commit_on_success(self, func):
return func

if settings.REMOTE_URL:
class MockTransaction(object):
def commit_on_success(self, func):
return func
def is_dirty(self):
return False

def is_dirty(self):
return False
def rollback(self):
pass

def rollback(self):
pass
transaction = MockTransaction()
else:
from django.db import transaction

transaction = MockTransaction()
else:
from django.db import transaction
return transaction

@transaction.commit_on_success
def sentry_exception_handler(request=None, **kwargs):
exc_info = sys.exc_info()
try:
transaction = get_transaction_wrapper(get_client())

if django_settings.DEBUG or getattr(exc_info[0], 'skip_sentry', False):
return
@transaction.commit_on_success
def actually_do_stuff(request=None, **kwargs):
exc_info = sys.exc_info()
try:
if django_settings.DEBUG or getattr(exc_info[0], 'skip_sentry', False):
return

if transaction.is_dirty():
transaction.rollback()
if transaction.is_dirty():
transaction.rollback()

extra = dict(
request=request,
)
extra = dict(
request=request,
)

get_client().create_from_exception(**extra)
except Exception, exc:
try:
logger.exception(u'Unable to process log entry: %s' % (exc,))
get_client().create_from_exception(**extra)
except Exception, exc:
warnings.warn(u'Unable to process log entry: %s' % (exc,))
finally:
del exc_info
try:
logger.exception(u'Unable to process log entry: %s' % (exc,))
except Exception, exc:
warnings.warn(u'Unable to process log entry: %s' % (exc,))
finally:
del exc_info

return actually_do_stuff(request, **kwargs)

got_request_exception.connect(sentry_exception_handler)

2 changes: 1 addition & 1 deletion raven/contrib/log/__init__.py
@@ -1,5 +1,5 @@
"""
sentry.client.log
raven.contrib.log
~~~~~~~~~~~~~~~~~
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
Expand Down
4 changes: 2 additions & 2 deletions raven/handlers/__init__.py
@@ -1,6 +1,6 @@
"""
sentry.client.handlers
~~~~~~~~~~~~~~~~~~~~~~
raven.handlers
~~~~~~~~~~~~~~
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
Expand Down
6 changes: 3 additions & 3 deletions raven/handlers/logbook.py
@@ -1,6 +1,6 @@
"""
sentry.client.handlers.logbook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
raven.handlers.logbook
~~~~~~~~~~~~~~~~~~~~~~
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
Expand All @@ -20,7 +20,7 @@ def emit(self, record):
self.format(record)

# Avoid typical config issues by overriding loggers behavior
if record.name == 'sentry.errors':
if record.name.startswith('sentry.errors'):
print >> sys.stderr, "Recursive log message sent to SentryHandler"
print >> sys.stderr, record.message
return
Expand Down
4 changes: 2 additions & 2 deletions raven/handlers/logging.py
@@ -1,6 +1,6 @@
"""
sentry.client.handlers.logging
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
raven.handlers.logging
~~~~~~~~~~~~~~~~~~~~~~
:copyright: (c) 2010 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
Expand Down
2 changes: 1 addition & 1 deletion raven/utils/json.py
Expand Up @@ -10,7 +10,7 @@
import simplejson
import uuid

class BetterJSONEncoder(simplejson.JSONDecoder):
class BetterJSONEncoder(simplejson.JSONEncoder):
def default(self, obj):
if isinstance(obj, uuid.UUID):
return obj.hex
Expand Down
7 changes: 4 additions & 3 deletions tests/contrib/django/models.py
@@ -1,15 +1,16 @@
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)

def __unicode__(self):
return unicode(self.foo)

1 change: 1 addition & 0 deletions tests/contrib/django/tests.py
Expand Up @@ -16,6 +16,7 @@
from raven.contrib.django import DjangoClient
from raven.contrib.django.models import get_client

django_settings.SENTRY_CLIENT = 'tests.contrib.django.tests.TempStoreClient'
settings.CLIENT = 'tests.contrib.django.tests.TempStoreClient'

class TempStoreClient(DjangoClient):
Expand Down
2 changes: 2 additions & 0 deletions tests/contrib/django/urls.py
@@ -1,3 +1,5 @@
from __future__ import absolute_import

from django.conf.urls.defaults import *

urlpatterns = patterns('',
Expand Down
2 changes: 2 additions & 0 deletions tests/contrib/django/views.py
@@ -1,3 +1,5 @@
from __future__ import absolute_import

from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render_to_response

Expand Down

0 comments on commit d03b1e7

Please sign in to comment.