# -*- 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()))