Skip to content
This repository has been archived by the owner on Jan 31, 2018. It is now read-only.

Commit

Permalink
Merge pull request #509 from willkg/1136840-error-emails
Browse files Browse the repository at this point in the history
[bug 1136840] Fix error handling for better debugging: WIP
  • Loading branch information
willkg committed Mar 4, 2015
2 parents ab706c6 + ceb53eb commit 55bbcea
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 16 deletions.
18 changes: 10 additions & 8 deletions fjord/base/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,8 @@ def connect(self):
finally:
views.test_memcached = test_memcached

@override_settings(SHOW_STAGE_NOTICE=True)
def test_500(self):
with self.assertRaises(IntentionalException) as cm:
self.client.get('/services/throw-error')

eq_(type(cm.exception), IntentionalException)


class ErrorTesting(ElasticTestCase):
class FileNotFoundTesting(TestCase):
client_class = LocalizingClient

def test_404(self):
Expand All @@ -96,6 +89,15 @@ def test_404(self):
self.assertTemplateUsed(request, '404.html')


class ServerErrorTesting(TestCase):
@override_settings(SHOW_STAGE_NOTICE=True)
def test_500(self):
with self.assertRaises(IntentionalException) as cm:
self.client.get('/services/throw-error')

eq_(type(cm.exception), IntentionalException)


class TestRobots(TestCase):
def test_robots(self):
resp = self.client.get('/robots.txt')
Expand Down
2 changes: 2 additions & 0 deletions fjord/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.shortcuts import render
from django.utils.http import is_safe_url
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt

from celery.messaging import establish_connection
from elasticsearch.exceptions import ConnectionError, NotFoundError
Expand Down Expand Up @@ -228,6 +229,7 @@ class IntentionalException(Exception):


@dev_or_authorized
@csrf_exempt
def throw_error(request):
"""Throw an error for testing purposes."""
raise IntentionalException("Error raised for testing purposes.")
1 change: 1 addition & 0 deletions fjord/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ def JINJA_CONFIG():
]
}

WSGI_APPLICATION = 'fjord.wsgi.application'

# When set to True, this will cause a message to be displayed on all
# pages that this is not production.
Expand Down
30 changes: 30 additions & 0 deletions fjord/wsgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file gets run by ./manage.py (runserver|test) and also gets
# imported for the WSGI application building by wsgi/playdoh.wsgi
# for stage/prod.
#
# It holds the setup that's common to both environments.

import os

import django
from django.core.handlers.wsgi import WSGIHandler

from fjord.wsgi_utils import BetterDebugMixin


os.environ.setdefault('CELERY_LOADER', 'django')


class DebuggableWSGIHandler(BetterDebugMixin, WSGIHandler):
pass


def get_debuggable_wsgi_application():
# This does the same thing as
# django.core.wsgi.get_wsgi_application except it returns a
# different WSGIHandler.
django.setup()
return DebuggableWSGIHandler()


application = get_debuggable_wsgi_application()
87 changes: 87 additions & 0 deletions fjord/wsgi_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from django.conf import settings


class BetterDebugMixin(object):
"""Provides better debugging data
Developing API endpoints and tired of wading through HTML for HTTP
500 errors?
Working on POST API debugging and not seeing the POST data show up
in the error logs/emails?
Then this mixin is for you!
It:
* spits out text rather than html when DEBUG = True (OMG! THANK
YOU!)
* adds a "HTTP_X_POSTBODY" META variable so you can see the raw post
data in error emails which is gross, but I couldn't figure out
a better way to do it
Usage:
Create a WSGIHandler subclass and bind that to ``application`` in
your wsgi file. For example::
import django
from django.core.handlers.wsgi import WSGIHandler
from fjord.wsgi_utils import BetterDebugMixin
class MyWSGIHandler(BetterDebugMixin, WSGIHandler):
pass
def get_debuggable_wsgi_application():
# This does the same thing as
# django.core.wsgi.get_wsgi_application except
# it returns a different WSGIHandler.
django.setup()
return MyWSGIHandler()
application = get_debuggable_wsgi_application()
"""
def handle_uncaught_exception(self, request, resolver, exc_info):
if settings.DEBUG_PROPAGATE_EXCEPTIONS:
raise

# First, grab the raw POST body and put it somewhere that's
# guaranteed to show up in the error email.

# This should be "bytes" which is str type in Python 2.
#
# FIXME: This is probably broken with Python 3.
postbody = getattr(request, 'body', '')
try:
# For string-ish data, we truncate and decode/re-encode in
# utf-8.
postbody = postbody[:10000].decode('utf-8').encode('utf-8')
except (UnicodeDecodeError, UnicodeEncodeError):
# For binary, we say, 'BINARY CONTENT'
postbody = 'BINARY OR NON-UTF-8 CONTENT'

# The logger.error generates a record which can get handled by
# the AdminEmailHandler. Overriding all that machinery is
# daunting, so we're instead going to shove it in the META
# section which shows up when the machinery does a repr on
# WSGIRequest.
request.META['HTTP_X_POST_BODY'] = postbody

# Second, check the Accept header and if it's not text/html,
# pretend this is an AJAX request so that we get the output in
# text rather than html when DEBUG=True.
if settings.DEBUG:
# request.is_ajax() == True will push this into doing text
# instead of html which is waaaaaayyy more useful from an
# API perspective. So if the Accept header is anything other
# than html, we'll say it's an ajax request to return text.
if 'html' not in request.META.get('HTTP_ACCEPT', 'text/html'):
request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'

return super(BetterDebugMixin, self).handle_uncaught_exception(
request, resolver, exc_info)
25 changes: 17 additions & 8 deletions wsgi/playdoh.wsgi
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# This gets used by stage/prod to set up the WSGI application for stage/prod
# use. We do some minor environment setup and then have `fjord/wsgi.py` do
# the rest.

import os
import site

# Set up NewRelic stuff.
try:
import newrelic.agent
except ImportError:
Expand All @@ -15,7 +20,6 @@ if newrelic:
newrelic = False


os.environ.setdefault('CELERY_LOADER', 'django')
# NOTE: you can also set DJANGO_SETTINGS_MODULE in your environment to override
# the default value in manage.py

Expand All @@ -24,18 +28,23 @@ wsgidir = os.path.dirname(__file__)
site.addsitedir(os.path.abspath(os.path.join(wsgidir, '../')))

# Explicitly set these so that fjord.manage_utils does the right
# thing.
# thing in production.
os.environ['USING_VENDOR'] = '1'
os.environ['SKIP_CHECK'] = '1'

# manage adds vendor to the Python path and otherwise sets up the
# environment.
# Importing manage has the side-effect of adding vendor/ stuff and
# doing other environment setup.
import manage

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

# This is the original Django WSGIHandler preserved here in case
# we ever have to back out the debuggable one.
# from django.core.wsgi import get_wsgi_application
# application = get_wsgi_application()

from fjord.wsgi import get_debuggable_wsgi_application
application = get_debuggable_wsgi_application()


if newrelic:
application = newrelic.agent.wsgi_application()(application)

# vim: ft=python

0 comments on commit 55bbcea

Please sign in to comment.