oops_wsgi-0.0.10/0000755000175000017500000000000011712140505013457 5ustar robertcrobertcoops_wsgi-0.0.10/README0000644000175000017500000001012311712137446014346 0ustar robertcrobertc************************************************************ python-oops-wsgi: Error report integration into wsgi servers ************************************************************ Copyright (c) 2011, Canonical Ltd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . GNU Lesser General Public License version 3 (see the file LICENSE). The oops_wsgi package provides integration glue between wsgi web servers and the oops error reporting system (http://pypi.python.org/pypi/oops). Dependencies ============ * Python 2.6+ * oops (http://pypi.python.org/pypi/oops) * paste Testing Dependencies ==================== * subunit (http://pypi.python.org/pypi/python-subunit) (optional) * testtools (http://pypi.python.org/pypi/testtools) Usage ===== oops_wsgi provides integration with an oops.Config, permitting errors in your web application to be gathered centrally, with tracebacks and other diagnostic information. Typically, something like this: * Setup your configuration:: >>> from oops import Config >>> config = Config() Note that you will probably want at least one publisher, or your reports will be discarded. * Add in wsgi specific hooks to the config:: >>> oops_wsgi.install_hooks(config) This is a convenience function - you are welcome to pick and choose the creation or filter hooks you want from oops_wsgi.hooks. * Create your wsgi app as normal, and then wrap it:: >>> app = oops_wsgi.make_app(app, config) If any exception bubbles up through this middleware, an oops will be logged. If the body of the request had not started, then a custom page is shown that shows the OOPS id, and the exception is swallowed. Exceptions that indicate normal situations like end-of-file on a socket do not trigger OOPSes. If the OOPS is filtered, or no publishers are configured, then the exception will propogate up the stack - the oops middleware cannot do anything useful in these cases. (For instance, if you have a custom 404 middleware above the oops middleware in the wsgi stack, and filter 404 exceptions so they do not create reports, then if the oops middleware did anything other than propogate the exception, your custom 404 middleware would not work. If the body had started, then there is no way to communicate the OOPS id to the client and the exception will propogate up the wsgi app stack. You can customise the error page if you supply a helper that accepts (environ, report) and returns HTML to be sent to the client. >>> def myerror_html(environ, report): ... return '

OOPS! %s

' % report['id'] >>> app = oops_wsgi.make_app(app, config, error_render=myerror_html) Or you can supply a string template to be formatted with the report. >>> json_template='{"oopsid" : "%(id)s"}' >>> app = oops_wsgi.make_app(app, config, error_template=json_template) For more information see pydoc oops_wsgi. Installation ============ Either run setup.py in an environment with all the dependencies available, or add the working directory to your PYTHONPATH. Development =========== Upstream development takes place at https://launchpad.net/python-oops-wsgi. To setup a working area for development, if the dependencies are not immediately available, you can use ./bootstrap.py to create bin/buildout, then bin/py to get a python interpreter with the dependencies available. To run the tests use the runner of your choice, the test suite is oops_wsgi.tests.test_suite. For instance:: $ bin/py -m testtools.run oops_wsgi.tests.test_suite If you have testrepository you can run the tests with that:: $ testr run oops_wsgi-0.0.10/PKG-INFO0000644000175000017500000001304111712140505014553 0ustar robertcrobertcMetadata-Version: 1.0 Name: oops_wsgi Version: 0.0.10 Summary: OOPS wsgi middleware. Home-page: https://launchpad.net/python-oops-wsgi Author: Launchpad Developers Author-email: launchpad-dev@lists.launchpad.net License: UNKNOWN Description: ************************************************************ python-oops-wsgi: Error report integration into wsgi servers ************************************************************ Copyright (c) 2011, Canonical Ltd This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, version 3 only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . GNU Lesser General Public License version 3 (see the file LICENSE). The oops_wsgi package provides integration glue between wsgi web servers and the oops error reporting system (http://pypi.python.org/pypi/oops). Dependencies ============ * Python 2.6+ * oops (http://pypi.python.org/pypi/oops) * paste Testing Dependencies ==================== * subunit (http://pypi.python.org/pypi/python-subunit) (optional) * testtools (http://pypi.python.org/pypi/testtools) Usage ===== oops_wsgi provides integration with an oops.Config, permitting errors in your web application to be gathered centrally, with tracebacks and other diagnostic information. Typically, something like this: * Setup your configuration:: >>> from oops import Config >>> config = Config() Note that you will probably want at least one publisher, or your reports will be discarded. * Add in wsgi specific hooks to the config:: >>> oops_wsgi.install_hooks(config) This is a convenience function - you are welcome to pick and choose the creation or filter hooks you want from oops_wsgi.hooks. * Create your wsgi app as normal, and then wrap it:: >>> app = oops_wsgi.make_app(app, config) If any exception bubbles up through this middleware, an oops will be logged. If the body of the request had not started, then a custom page is shown that shows the OOPS id, and the exception is swallowed. Exceptions that indicate normal situations like end-of-file on a socket do not trigger OOPSes. If the OOPS is filtered, or no publishers are configured, then the exception will propogate up the stack - the oops middleware cannot do anything useful in these cases. (For instance, if you have a custom 404 middleware above the oops middleware in the wsgi stack, and filter 404 exceptions so they do not create reports, then if the oops middleware did anything other than propogate the exception, your custom 404 middleware would not work. If the body had started, then there is no way to communicate the OOPS id to the client and the exception will propogate up the wsgi app stack. You can customise the error page if you supply a helper that accepts (environ, report) and returns HTML to be sent to the client. >>> def myerror_html(environ, report): ... return '

OOPS! %s

' % report['id'] >>> app = oops_wsgi.make_app(app, config, error_render=myerror_html) Or you can supply a string template to be formatted with the report. >>> json_template='{"oopsid" : "%(id)s"}' >>> app = oops_wsgi.make_app(app, config, error_template=json_template) For more information see pydoc oops_wsgi. Installation ============ Either run setup.py in an environment with all the dependencies available, or add the working directory to your PYTHONPATH. Development =========== Upstream development takes place at https://launchpad.net/python-oops-wsgi. To setup a working area for development, if the dependencies are not immediately available, you can use ./bootstrap.py to create bin/buildout, then bin/py to get a python interpreter with the dependencies available. To run the tests use the runner of your choice, the test suite is oops_wsgi.tests.test_suite. For instance:: $ bin/py -m testtools.run oops_wsgi.tests.test_suite If you have testrepository you can run the tests with that:: $ testr run Platform: UNKNOWN Classifier: Development Status :: 2 - Pre-Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python oops_wsgi-0.0.10/oops_wsgi/0000755000175000017500000000000011712140505015470 5ustar robertcrobertcoops_wsgi-0.0.10/oops_wsgi/hooks.py0000644000175000017500000000510511712137446017200 0ustar robertcrobertc# Copyright (c) 2010, 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # GNU Lesser General Public License version 3 (see the file LICENSE). """oops creation and filtering hooks for working with WSGI.""" __all__ = [ 'copy_environ', 'hide_cookie', 'install_hooks', 'update_report', ] _wsgi_standard_env_keys = set([ 'REQUEST_METHOD', 'SCRIPT_NAME', 'PATH_INFO', 'QUERY_STRING', 'CONTENT_TYPE', 'CONTENT_LENGTH', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'wsgi.version', 'wsgi.url_scheme', ]) def copy_environ(report, context): """Copy useful variables from the wsgi environment if it is present. This should be in the context as 'wsgi_environ'. e.g. report = config.create(context=dict(wsgi_environ=environ)) """ environ = context.get('wsgi_environ', {}) if 'req_vars' not in report: report['req_vars'] = {} req_vars = report['req_vars'] for key, value in sorted(environ.items()): if (key in _wsgi_standard_env_keys or key.startswith('HTTP_')): req_vars[key] = value def hide_cookie(report, context): """If there is an HTTP_COOKIE entry in the report, hide its value. The entry is looked for either as a top level key or in the req_vars dict. The COOKIE header is often used to carry session tokens and thus permits folk analyzing crash reports to log in as an arbitrary user (e.g. your sysadmin users). """ if 'HTTP_COOKIE' in report: report['HTTP_COOKIE'] = '' if 'HTTP_COOKIE' in report.get('req_vars', {}): report['req_vars']['HTTP_COOKIE'] = '' def install_hooks(config): """Install the default wsgi hooks into config.""" config.on_create.extend([copy_environ, hide_cookie]) config.on_create.insert(0, update_report) def update_report(report, context): """Copy the oops.report contents from the wsgi environment to report.""" report.update(context.get('wsgi_environ', {}).get('oops.report', {})) oops_wsgi-0.0.10/oops_wsgi/django.py0000644000175000017500000000340711712137446017322 0ustar robertcrobertc# Copyright (c) 2011 Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # GNU Lesser General Public License version 3 (see the file LICENSE). """Django glue for OOPS integration. To use: * Use OOPSWSGIHandler rather than than WSGIHandler. * Create an oops wrapper with oops_wsgi.make_app(..., oops_on_status=['500']) This is not needed if you have https://code.djangoproject.com/ticket/16674 fixed in your Django. """ from __future__ import absolute_import from django.core.handlers import wsgi __all__ = [ 'OOPSWSGIHandler', ] class OOPSWSGIHandler(wsgi.WSGIHandler): def handle_uncaught_exception(self, request, resolver, exc_info): if 'oops.context' in request.environ: # We are running under python-oops-wsgi - inject the exception into # its context. This will provide the exception to the handler, and # if you use oops_on_status=['500'] OOPS reports will be created # when Django has suffered a failure. request.environ['oops.context']['exc_info'] = exc_info # Now perform the default django uncaught exception behaviour. return super(OOPSWSGIHandler, self).handle_uncaught_exception( request, resolver, exc_info) oops_wsgi-0.0.10/oops_wsgi/__init__.py0000644000175000017500000001204311712140406017601 0ustar robertcrobertc# # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # GNU Lesser General Public License version 3 (see the file LICENSE). """oops <-> wsgi integration. oops_wsgi provides integration with an oops.Config, permitting errors in your web application to be gathered centrally, with tracebacks and other diagnostic information. Typically, something like this: * Setup your configuration:: >>> from oops import Config >>> config = Config() Note that you will probably want at least one publisher, or your reports will be discarded. * Add in wsgi specific hooks to the config:: >>> oops_wsgi.install_hooks(config) This is a convenience function - you are welcome to pick and choose the creation or filter hooks you want from oops_wsgi.hooks. * Create your wsgi app as normal, and then wrap it:: >>> app = oops_wsgi.make_app(app, config) If any exception bubbles up through this middleware, an oops will be logged. If the body of the request had not started, then a custom page is shown that shows the OOPS id, and the exception is swallowed. Exceptions that indicate normal situations like end-of-file on a socket do not trigger OOPSes. If the OOPS is filtered, or no publishers are configured, then the exception will propogate up the stack - the oops middleware cannot do anything useful in these cases. (For instance, if you have a custom 404 middleware above the oops middleware in the wsgi stack, and filter 404 exceptions so they do not create reports, then if the oops middleware did anything other than propogate the exception, your custom 404 middleware would not work. If the body had started, then there is no way to communicate the OOPS id to the client and the exception will propogate up the wsgi app stack. You can customise the error page if you supply a helper that accepts (environ, report) and returns HTML to be sent to the client. >>> def myerror_html(environ, report): ... return '

OOPS! %s

' % report['id'] >>> app = oops_wsgi.make_app(app, config, error_render=myerror_html) Or you can supply a string template to be formatted with the report. >>> json_template='{"oopsid" : "%(id)s"}' >>> app = oops_wsgi.make_app(app, config, error_template=json_template) If the wrapped app errors by sending exc_info to start_response, that will be used to create an OOPS report, and the id added to the headers under the X-Oops-Id header. This is also present when an OOPS is triggered by catching an exception in the wrapped app (as long as the body hasn't started). You can request that reports be created when a given status code is used (e.g. to gather stats on the number of 404's occuring without doing log processing). >>> app = oops_wsgi.make_app(app, config, oops_on_status=['404']) The oops middleware injects two variables into the WSGI environ to make it easy for cooperating code to report additional data. The `oops.report` variable is a dict which is copied into the report. See the `oops` package documentation for documentation on what should be present in an oops report. This requires the update_report hook to be installed (which `install_hooks` will do for you). The `oops.context` variable is a dict used for generating the report - keys and values added to that can be used in the `config.on_create` hooks to populate custom data without needing to resort to global variables. If a timeline is present in the WSGI environ (as 'timeline.timeline') it is automatically captured to the oops context when generating an OOPS. See the oops-timeline module for hooks to use this. `pydoc oops_wsgi.make_app` describes the entire capabilities of the middleware. """ # same format as sys.version_info: "A tuple containing the five components of # the version number: major, minor, micro, releaselevel, and serial. All # values except releaselevel are integers; the release level is 'alpha', # 'beta', 'candidate', or 'final'. The version_info value corresponding to the # Python version 2.0 is (2, 0, 0, 'final', 0)." Additionally we use a # releaselevel of 'dev' for unreleased under-development code. # # If the releaselevel is 'alpha' then the major/minor/micro components are not # established at this point, and setup.py will use a version of next-$(revno). # If the releaselevel is 'final', then the tarball will be major.minor.micro. # Otherwise it is major.minor.micro~$(revno). __version__ = (0, 0, 10, 'beta', 0) __all__ = [ 'install_hooks', 'make_app' ] from oops_wsgi.middleware import make_app from oops_wsgi.hooks import install_hooks oops_wsgi-0.0.10/oops_wsgi/middleware.py0000644000175000017500000002557211712137665020207 0ustar robertcrobertc# Copyright (c) 2010, 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # GNU Lesser General Public License version 3 (see the file LICENSE). """WSGI middleware to integrate with an oops.Config.""" __metaclass__ = type import socket import sys import time from paste.request import construct_url __all__ = [ 'default_map_environ', 'generator_tracker', 'make_app', ] default_error_template=''' Oops! - %(id)s

Oops!

Something broke while generating the page. Please try again in a few minutes, and if the problem persists file a bug or contact customer support. Please quote OOPS-ID %(id)s

''' default_map_environ = { # Map timeline objects into the oops context as 'timeline' 'timeline.timeline': 'timeline', } class SoftRequestTimeout(Exception): """Soft request timeout expired""" def make_app(app, config, template=default_error_template, content_type='text/html', error_render=None, oops_on_status=None, map_environ=None, tracker=None, soft_start_timeout=None): """Construct a middleware around app that will forward errors via config. Any errors encountered by the app will be forwarded to config and an error page shown. If the body of a reply has already started the error will be forwarded to config and also re-raised. If there are no publishers, or an error is filtered, the error will be re-raised rather than an error page shown. This permits containing middleware to show custom errors (for 404's, for instance), perhaps even for just some occurences of the issue. :param app: A WSGI app. :param config: An oops.Config. :param template: Optional string template to use when reporting the oops to the client. If not supplied a default template is used (unless an error_render function has been supplied). :param content_type: The content type for error pages. Defaults to text/html. :param error_render: Optional custom renderer for presenting error reports to clients. Should be a callable taking the report as its only parameter. :param oops_on_status: Optional list of HTTP status codes that should generate OOPSes. OOPSes triggered by sniffing these codes will not interfere with the response being sent. For instance, if you do not expect any 404's from your application, you might set oops_on_status=['404']. :param map_environ: A dictionary of environment keys to look for, and if present map into the OOPS context when generating an OOPS. The value of the key determines the name given in the OOPS context. If None is passed the default_map_environ is used. Pass {} in to entirely disable mapping. :param tracker: A factory function to create a tracker. Trackers are used to allow variations on the WSGI environment to still use oops_wsgi. See generator_tracker for the reference tracker used in regular WSGI environments. generator_tracker is used by default or when tracker=None. :param soft_start_timeout: A duration in milliseconds for the creation of reports on slow requests. If this is set and the duration between calling into the app and start_response being called is greater than the timeout value, then an OOPS will be created and the OOPS id added to the response HTTP headers as normal. A backtrace leading into the middleware is generated (this can be informative as start_response is a callback) and the exception type is set to SoftRequestTimeout. :return: A WSGI app. """ def oops_middleware(environ, start_response): """OOPS inserting middleware. This has the following WSGI properties: * start_response is buffered until either write() is called, or the wrapped app starts yielding content. * Exceptions that are ignored by the oops config get re-raised. * socket errors and GeneratorExit errors are passed through without * being forward to the oops system. """ environ['oops.report'] = {} environ['oops.context'] = {} if soft_start_timeout: start_time = time.time() state = {} def make_context(exc_info=None): context = dict(url=construct_url(environ), wsgi_environ=environ) context.update(environ.get('oops.context', {})) mapper = map_environ if mapper is None: mapper = default_map_environ for environ_key, context_key in mapper.items(): if environ_key in environ: context[context_key] = environ[environ_key] if exc_info is not None: context['exc_info'] = exc_info return context def oops_write(bytes): write = state.get('write') if write is None: status, headers = state.pop('response') # Signal that we have called start_response state['write'] = start_response(status, headers) write = state['write'] write(bytes) def oops_start_response(status, headers, exc_info=None): if exc_info is not None: # The app is explicitly signalling an error (rather than # returning a page describing the error). Capture that and then # forward to the containing element untouched except for the # addition of the X-Oops-Id header. We don't touch the body # because the application is handling the error and generating # the body itself. We may in future provide an option to # replace the body in this situation. report = config.create(make_context(exc_info=exc_info)) ids = config.publish(report) try: if ids: headers = list(headers) headers.append(('X-Oops-Id', str(report['id']))) state['write'] = start_response(status, headers, exc_info) return state['write'] finally: del exc_info else: do_oops = False if oops_on_status: for sniff_status in oops_on_status: if status.startswith(sniff_status): do_oops = True if (soft_start_timeout and (time.time()-start_time)*1000 > soft_start_timeout): try: raise SoftRequestTimeout( "Start_response over timeout %s." % soft_start_timeout) except SoftRequestTimeout: exc_info = sys.exc_info() do_oops = True if do_oops: report = config.create(make_context(exc_info=exc_info)) report['HTTP_STATUS'] = status.split(' ')[0] config.publish(report) state['response'] = (status, headers) return oops_write try: def ensure_start_response(): if 'write' not in state: status, headers = state.pop('response') # Signal that we have called start_response state['write'] = start_response(status, headers) def on_exception(exc_info): report = config.create(make_context(exc_info=exc_info)) ids = config.publish(report) if not ids or 'write' in state: # No OOPS generated, no oops publisher, or we have already # transmitted the wrapped apps headers - either way we can't # replace the content with a clean error, so let the wsgi # server figure it out. raise headers = [('Content-Type', content_type)] headers.append(('X-Oops-Id', str(report['id']))) start_response( '500 Internal Server Error', headers, exc_info) del exc_info if error_render is not None: return error_render(report) else: return template % report if tracker is None: tracker_factory = generator_tracker else: tracker_factory = tracker return tracker_factory( ensure_start_response, ensure_start_response, on_exception, app(environ, oops_start_response)) except socket.error: raise except Exception: exc_info = sys.exc_info() return [on_exception(exc_info)] return oops_middleware def generator_tracker(on_first_bytes, on_finish, on_error, app_body): """A wrapper for generators that calls the OOPS hooks as needed. :param on_first_bytes: Called as on_first_bytes() when the first bytes from the app body are available but before they are yielded. :param on_finish: Called as on_finish() when the app body is fully consumed. :param on_error: Called as on_error(sys.exc_info()) if a handleable error has occured while consuming the generator. Errors like GeneratorExit are not handleable. :param app_body: The iterable body for the WSGI app. This may be a simple list or a generator - it is merely known to meet the iterator protocol. """ try: called_first = False for bytes in app_body: if not called_first: called_first = True on_first_bytes() yield bytes on_finish() except socket.error: # start_response, which iteration can trigger a call into, may raise # socket.error when writing if the client has disconnected: thats not # an OOPS condition. This does potentially mask socket.error issues in # the appserver code, so we may want to change this to callback to # determine if start_response has been called upstream, and if so, to # still generate an OOPS. raise except GeneratorExit: # Python 2.4 raise except Exception: exc_info = sys.exc_info() yield on_error(exc_info) oops_wsgi-0.0.10/setup.py0000755000175000017500000000330111712140376015177 0ustar robertcrobertc#!/usr/bin/env python # # Copyright (c) 2011, Canonical Ltd # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, version 3 only. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . # GNU Lesser General Public License version 3 (see the file LICENSE). from distutils.core import setup import os.path description = file( os.path.join(os.path.dirname(__file__), 'README'), 'rb').read() setup(name="oops_wsgi", version="0.0.10", description=\ "OOPS wsgi middleware.", long_description=description, maintainer="Launchpad Developers", maintainer_email="launchpad-dev@lists.launchpad.net", url="https://launchpad.net/python-oops-wsgi", packages=['oops_wsgi'], package_dir = {'':'.'}, classifiers = [ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Operating System :: OS Independent', 'Programming Language :: Python', ], install_requires = [ 'oops', 'paste', ], extras_require = dict( test=[ 'testtools', ] ), )