Skip to content

Instantly share code, notes, and snippets.

@zvoase
Created January 30, 2009 19:01
Show Gist options
  • Save zvoase/55210 to your computer and use it in GitHub Desktop.
Save zvoase/55210 to your computer and use it in GitHub Desktop.
There are two main issues with this at the moment:
1. The 'Backtrace' section is going to look a little odd, because Ruby formats
it with the most recent call *first*, whereas Python uses most recent call
*last*. This could be solved, but I haven't done it yet (it's probably
quite easy).
2. The 'Environment' section's going to look a little weird, because I don't
know what should go there. Suggestions are very much welcome.
Other than that, it works as it should. If you encounter any problems, contact
me at ROT13('[email protected]').
# -*- coding: utf-8 -*-
################################################################################
# getexceptional.py
# -----------------
# Python interface and Django middleware for the Exceptional
# (http://getexceptional.com) exception-tracking service.
################################################################################
#
# Copyright (c) 2008 Zachary Voase
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
################################################################################
"""
Python interface and Django middleware for Exceptional (getexceptional.com)
This module exports middleware and helper classes for the Exceptional
(http://getexceptional.com) exception-tracking service. To use it, there are
only three steps (two if you already have an Exceptional account set up):
1. Set up an account and an application at http://getexceptional.com.
2. In your Django project's ``settings.py`` file, add a setting called
``EXCEPTIONAL_API_KEY`` and set it to a string containing the API key for
the application you set up at http://getexceptional.com.
3. In your projects ``settings.py`` file, add the string
``'exceptional.ExceptionalMiddleware'`` to your ``MIDDLEWARE_CLASSES``
file. The position of the middleware should vary depending on what you
have installed. For example, if there is any fallback middleware (such
as that used for Django's flatpages app) which relies on certain
exceptions being raised, the Exceptional middleware should come *before*
it in the list.
Now visit a page that raises an exception. In the Exceptional web interface, you
should see the exception in a short amount of time, along with all its details.
"""
import datetime
import httplib
import os
import sys
import textwrap
import time
import traceback
import urllib
import zlib
import django
from django.conf import settings
from django.core import urlresolvers
from django.core.exceptions import MiddlewareNotUsed
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
from django.utils import simplejson as json
try:
import pytz
except ImportError:
pytz = None
class Exceptional(object):
"""
Represents a single exception.
This class, instantiated with the Django request and exception info from an
exception raised within a Django view, produces a dictionary of data which
is in a format compatible with the Exceptional exception-tracking system.
This dictionary is stored in a ``data`` attribute. Note that it is not
necessarily JSONifiable; when serializing an instance of this class,
non-JSONifiable data will be run through ``repr`` so as to give some
meaningful output.
"""
def __init__(self, request, exc_type, exc_instance, tb):
self.request = request
self.exc_info = (exc_type, exc_instance, tb)
self.data = {}
self.data['language'] = self.get_language()
self.data['framework'] = u'Django %s' % (django.get_version(),)
self.data['exception_class'] = self.exc_info[0].__name__
self.data['exception_message'] = str(exc_instance)
self.data['exception_backtrace'] = self.get_formatted_traceback()
self.data['occurred_at'] = self.get_occurred_at()
controller_name, action_name = self.get_controller_and_action()
self.data['controller_name'] = controller_name
self.data['action_name'] = action_name
self.data['application_root'] = self.get_application_root()
self.data['url'] = self.request.build_absolute_uri()
self.data['parameters'] = self.request.GET.copy()
self.data['session'] = {}
for key, value in self.request.session.items():
self.data['session'][key] = value
print self.data['session']
self.data['environment'] = self.get_environment()
def get_formatted_traceback(self):
return textwrap.dedent(
'\n'.join(traceback.format_tb(self.exc_info[2]))).splitlines()
def get_language(self):
major, minor, micro, release, serial = sys.version_info
version_num = '.'.join(map(str, (major, minor, micro)))
# Example: 'Python 2.6.1 (final)'
return u'Python %s (%s)' % (version_num, release)
def get_occurred_at(self):
if pytz:
tzinfo = pytz.timezone(settings.TIME_ZONE)
timestamp = datetime.datetime.now(tzinfo)
return timestamp.strftime('%Y%m%d %H:%M:%S %Z')
else:
timestamp = datetime.datetime.now()
return timestamp.strftime('%Y%m%d %H:%M:%S ') + time.tzname[0]
def get_controller_and_action(self):
view, pos_args, kwargs = urlresolvers.resolve(
self.request.path, getattr(self.request, 'urlconf', None))
return view.__module__, view.__name__
def get_application_root(self):
app, view_name = self.get_controller_and_action()
app = app.split('.')
if 'views' in app:
app = app[:app.index('views')]
if len(app) == 1:
app_obj = __import__(app[0])
else:
fromlist = [app_obj[-1]]
app_obj = __import__('.'.join(app[:-1]), fromlist=fromlist)
return os.path.dirname(app_obj.__file__)
def get_environment(self):
# There is a problem here. I don't know what the 'environment' part
# should contain. At the moment I'm just sticking all sorts of data in
# here.
environment = {}
for key in os.environ:
environment['OS::' + key] = os.environ[key]
for key in self.request.COOKIES:
environment['COOKIES::' + key] = self.request.COOKIES[key]
for key in self.request.META:
environment['META::' + key] = self.request.META[key]
for key in settings._target.__dict__:
environment['SETTINGS::' + key] = settings._target.__dict__[key]
local_vars = self.exc_info[2].tb_frame.f_locals
for key in local_vars:
environment['LOCALS::' + key] = local_vars[key]
return environment
def serialize(self):
raw_json_data = json.dumps(self.data, sort_keys=True, default=repr)
gz_json_data = zlib.compress(raw_json_data)
return urllib.pathname2url(gz_json_data)
class ExceptionalAccount(object):
"""
Represents an API key and connection to the getexceptional.com service.
This class is instantiated with an API key and has one useful method,
``post``, which takes an instance of the ``Exceptional`` class and sends it
to the getexceptional.com service.
"""
def __init__(self, api_key):
self.api_key = api_key
def post(self, exc):
body = exc.serialize()
headers = {'Accept': 'application/x-gzip',
'Content-Type': 'application/x-gzip; charset=UTF-8'}
connection = httplib.HTTPConnection('getexceptional.com')
connection.request('POST',
('/errors/?api_key=' + self.api_key + '&protocol_version=3'),
body, headers)
response = connection.getresponse()
connection.close()
return True if (response.status == 200) else response
class ExceptionalMiddleware(object):
"""
Middleware to post unhandled exceptions to the getexceptional.com service.
This middleware catches exceptions raised within a view and sends them off
the the getexceptional.com service. Note that it requires a setting within
your project, ``EXCEPTIONAL_API_KEY``. If this setting is not present, then
the middleware's ``__init__`` method will raise
``django.core.exceptions.MiddlewareNotUsed``. This deactivates the
middleware for the life of the server process.
"""
def __init__(self):
api_key = getattr(settings, 'EXCEPTIONAL_API_KEY', None)
if not api_key:
raise MiddlewareNotUsed
self.account = ExceptionalAccount(api_key)
def process_exception(self, request, exception_instance):
self.account.post(Exceptional(request, *sys.exc_info()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment