← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~lifeless/launchpad/useoops into lp:launchpad

 

Robert Collins has proposed merging lp:~lifeless/launchpad/useoops into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~lifeless/launchpad/useoops/+merge/71978

Migrate loggerhead's oops integration to oops-wsgi.
-- 
https://code.launchpad.net/~lifeless/launchpad/useoops/+merge/71978
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~lifeless/launchpad/useoops into lp:launchpad.
=== modified file 'lib/launchpad_loggerhead/app.py'
--- lib/launchpad_loggerhead/app.py	2011-08-09 14:53:07 +0000
+++ lib/launchpad_loggerhead/app.py	2011-08-18 03:43:26 +0000
@@ -11,18 +11,19 @@
 
 from bzrlib import errors, lru_cache, urlutils
 from bzrlib.transport import get_transport
-
 from loggerhead.apps import favicon_app, static_app
 from loggerhead.apps.branch import BranchWSGIApp
-
+import oops_wsgi
+from openid.consumer.consumer import CANCEL, Consumer, FAILURE, SUCCESS
 from openid.extensions.sreg import SRegRequest, SRegResponse
-from openid.consumer.consumer import CANCEL, Consumer, FAILURE, SUCCESS
-
 from paste import httpserver
 from paste.fileapp import DataApp
+from paste.httpexceptions import (
+    HTTPMovedPermanently,
+    HTTPNotFound,
+    HTTPUnauthorized,
+    )
 from paste.request import construct_url, parse_querystring, path_info_pop
-from paste.httpexceptions import (
-    HTTPMovedPermanently, HTTPNotFound, HTTPUnauthorized)
 
 from canonical.config import config
 from canonical.launchpad.xmlrpc import faults
@@ -234,13 +235,6 @@
             lp_server.stop_server()
 
 
-def make_oops_logging_exception_hook(error_utility, request):
-    """Make a hook for logging OOPSes."""
-    def log_oops():
-        error_utility.raising(sys.exc_info(), request)
-    return log_oops
-
-
 def make_error_utility():
     """Make an error utility for logging errors from codebrowse."""
     error_utility = ErrorReportingUtility()
@@ -265,72 +259,6 @@
 </p></body></html>'''
 
 
-_error_status = '500 Internal Server Error'
-_error_headers = [('Content-Type', 'text/html')]
-
-
-class WrappedStartResponse(object):
-    """Wraps start_response (from a WSGI request) to keep track of whether
-    start_response was called (and whether the callable it returns has been
-    called).
-
-    Used by oops_middleware.
-    """
-
-    def __init__(self, start_response):
-        self._real_start_response = start_response
-        self.response_start = None
-        self._write_callable = None
-
-    @property
-    def body_started(self):
-        return self._write_callable is not None
-
-    def start_response(self, status, headers, exc_info=None):
-        # Just keep a note of the values we were called with for now.  We don't
-        # need to invoke the real start_response until the response body
-        # starts.
-        self.response_start = (status, headers)
-        if exc_info is not None:
-            self.response_start += (exc_info,)
-        return self.write_wrapper
-
-    def ensure_started(self):
-        if not self.body_started and self.response_start is not None:
-            self._really_start()
-
-    def _really_start(self):
-        self._write_callable = self._real_start_response(*self.response_start)
-
-    def write_wrapper(self, data):
-        self.ensure_started()
-        self._write_callable(data)
-
-    def generate_oops(self, environ, error_utility):
-        """Generate an OOPS.
-
-        :returns: True if the error page was sent to the user, and False if it
-            couldn't (i.e. if the response body was already started).
-        """
-        oopsid = report_oops(environ, error_utility)
-        if self.body_started:
-            return False
-        write = self.start_response(_error_status, _error_headers)
-        write(_oops_html_template % {'oopsid': oopsid})
-        return True
-
-
-def report_oops(environ, error_utility):
-    # XXX: We should capture more per-request information to include in
-    # the OOPS here, e.g. duration, user, etc.  But even an OOPS with
-    # just a traceback and URL is better than nothing.
-    #   - Andrew Bennetts, 2010-07-27.
-    request = ScriptRequest(
-        [], URL=construct_url(environ))
-    error_utility.raising(sys.exc_info(), request)
-    return request.oopsid
-
-
 def oops_middleware(app):
     """Middleware to log an OOPS if the request fails.
 
@@ -338,43 +266,5 @@
     a basic HTML error page with the OOPS ID to the user (and status code 500).
     """
     error_utility = make_error_utility()
-    def wrapped_app(environ, start_response):
-        wrapped = WrappedStartResponse(start_response)
-        try:
-            # Start processing this request, build the app
-            app_iter = iter(app(environ, wrapped.start_response))
-            # Start yielding the response
-            stopping = False
-            while not stopping:
-                try:
-                    data = app_iter.next()
-                except StopIteration:
-                    stopping = True
-                wrapped.ensure_started()
-                if not stopping:
-                    yield data
-        except httpserver.SocketErrors, e:
-            # The Paste WSGIHandler suppresses these exceptions.
-            # Generally it means something like 'EPIPE' because the
-            # connection was closed. We don't want to generate an OOPS
-            # just because the connection was closed prematurely.
-            logger = logging.getLogger('lp-loggerhead')
-            logger.info('Caught socket exception from %s: %s %s'
-                        % (environ.get('REMOTE_ADDR', '<unknown>'),
-                           e.__class__, e,))
-            return
-        except GeneratorExit, e:
-            # This generally means a client closed early during a streaming
-            # body. Nothing to worry about. GeneratorExit doesn't usually have
-            # any context associated with it, so not worth printing to the log.
-            logger = logging.getLogger('lp-loggerhead')
-            logger.info('Caught GeneratorExit from %s'
-                        % (environ.get('REMOTE_ADDR', '<unknown>')))
-            return
-        except:
-            error_page_sent = wrapped.generate_oops(environ, error_utility)
-            if error_page_sent:
-                return
-            # Could not send error page to user, so... just give up.
-            raise
-    return wrapped_app
+    return oops_wsgi.make_app(app, error_utility._oops_config,
+            template=_oops_html_template)

=== modified file 'lib/launchpad_loggerhead/tests.py'
--- lib/launchpad_loggerhead/tests.py	2011-08-13 04:07:10 +0000
+++ lib/launchpad_loggerhead/tests.py	2011-08-18 03:43:26 +0000
@@ -20,8 +20,6 @@
 from canonical.launchpad.webapp.vhosts import allvhosts
 from canonical.testing.layers import DatabaseFunctionalLayer
 from launchpad_loggerhead.app import (
-    _oops_html_template,
-    oops_middleware,
     RootApp,
     )
 from launchpad_loggerhead.session import SessionHandler
@@ -150,154 +148,3 @@
         self.assertEqual(self.browser.url, dummy_root + '+logout')
         self.assertEqual(self.browser.contents,
                          'This is a dummy destination.\n')
-
-
-class TestOopsMiddleware(TestCase):
-
-    def setUp(self):
-        super(TestOopsMiddleware, self).setUp()
-        self.start_response_called = False
-
-    def assertContainsRe(self, haystack, needle_re, flags=0):
-        """Assert that a contains something matching a regular expression."""
-        # There is: self.assertTextMatchesExpressionIgnoreWhitespace
-        #           but it does weird things with whitespace, and gives
-        #           unhelpful error messages when it fails, so this is copied
-        #           from bzrlib
-        if not re.search(needle_re, haystack, flags):
-            if '\n' in haystack or len(haystack) > 60:
-                # a long string, format it in a more readable way
-                raise AssertionError(
-                        'pattern "%s" not found in\n"""\\\n%s"""\n'
-                        % (needle_re, haystack))
-            else:
-                raise AssertionError('pattern "%s" not found in "%s"'
-                        % (needle_re, haystack))
-
-    def catchLogEvents(self):
-        """Any log events that are triggered get written to self.log_stream"""
-        logger = logging.getLogger('lp-loggerhead')
-        logger.setLevel(logging.DEBUG)
-        self.log_stream = cStringIO.StringIO()
-        handler = logging.StreamHandler(self.log_stream)
-        handler.setLevel(logging.DEBUG)
-        logger.addHandler(handler)
-        self.addCleanup(logger.removeHandler, handler)
-
-    def runtime_failing_app(self, environ, start_response):
-        if False:
-            yield None
-        raise RuntimeError('just a generic runtime error.')
-
-    def socket_failing_app(self, environ, start_response):
-        if False:
-            yield None
-        raise socket.error(errno.EPIPE, 'Connection closed')
-
-    def logging_start_response(self, status, response_headers, exc_info=None):
-        self._response_chunks = []
-        def _write(chunk):
-            self._response_chunks.append(chunk)
-        self.start_response_called = True
-        return _write
-
-    def success_app(self, environ, start_response):
-        writer = start_response('200 OK', {})
-        writer('Successfull\n')
-        return []
-
-    def failing_start_response(self, status, response_headers, exc_info=None):
-        def fail_write(chunk):
-            raise socket.error(errno.EPIPE, 'Connection closed')
-        self.start_response_called = True
-        return fail_write
-
-    def multi_yielding_app(self, environ, start_response):
-        writer = start_response('200 OK', {})
-        yield 'content\n'
-        yield 'I want\n'
-        yield 'to give to the user\n'
-
-    def no_body_app(self, environ, start_response):
-        writer = start_response('200 OK', {})
-        return []
-
-    def _get_default_environ(self):
-        return {'wsgi.version': (1, 0),
-                'wsgi.url_scheme': 'http',
-                'PATH_INFO': '/test/path',
-                'REQUEST_METHOD': 'GET',
-                'SERVER_NAME': 'localhost',
-                'SERVER_PORT': '8080',
-               }
-
-    def wrap_and_run(self, app, failing_write=False):
-        app = oops_middleware(app)
-        # Just random env data, rather than setting up a whole wsgi stack just
-        # to pass in values for this dict
-        environ = self._get_default_environ()
-        if failing_write:
-            result = list(app(environ, self.failing_start_response))
-        else:
-            result = list(app(environ, self.logging_start_response))
-        return result
-
-    def test_exception_triggers_oops(self):
-        res = self.wrap_and_run(self.runtime_failing_app)
-        # After the exception was raised, we should also have gotten an oops
-        # event
-        self.assertEqual(1, len(self.oopses))
-        oops = self.oopses[0]
-        self.assertEqual('RuntimeError', oops['type'])
-        # runtime_failing_app doesn't call start_response, but oops_middleware
-        # does because it tries to send the OOPS information to the user.
-        self.assertTrue(self.start_response_called)
-        self.assertEqual(_oops_html_template % {'oopsid': oops['id']},
-                         ''.join(self._response_chunks))
-
-    def test_ignores_socket_exceptions(self):
-        self.catchLogEvents()
-        res = self.wrap_and_run(self.socket_failing_app)
-        self.assertEqual(0, len(self.oopses))
-        self.assertContainsRe(self.log_stream.getvalue(),
-            'Caught socket exception from <unknown>:.*Connection closed')
-        # start_response doesn't get called because the app fails first,
-        # and oops_middleware knows not to do anything with a closed socket.
-        self.assertFalse(self.start_response_called)
-
-    def test_ignores_writer_failures(self):
-        self.catchLogEvents()
-        res = self.wrap_and_run(self.success_app, failing_write=True)
-        self.assertEqual(0, len(self.oopses))
-        self.assertContainsRe(self.log_stream.getvalue(),
-            'Caught socket exception from <unknown>:.*Connection closed')
-        # success_app calls start_response, so this should get passed on.
-        self.assertTrue(self.start_response_called)
-
-    def test_stopping_early_no_oops(self):
-        # See bug #726985.
-        # If content is being streamed, and the pipe closes, we'll get a
-        # 'GeneratorExit', because it is closed before finishing. This doesn't
-        # need to become an OOPS.
-        self.catchLogEvents()
-        app = oops_middleware(self.multi_yielding_app)
-        environ = self._get_default_environ()
-        result = app(environ, self.logging_start_response)
-        self.assertEqual('content\n', result.next())
-        # At this point, we intentionally kill the app and the response, so
-        # that they will get GeneratorExit
-        del app, result
-        self.assertEqual([], self.oopses)
-        self.assertContainsRe(self.log_stream.getvalue(),
-            'Caught GeneratorExit from <unknown>')
-        # Body content was yielded, we must have called start_response
-        self.assertTrue(self.start_response_called)
-
-    def test_no_body_calls_start_response(self):
-        # See bug #732481, even if we don't have a body, if we have headers to
-        # send, we must call start_response
-        result = self.wrap_and_run(self.no_body_app)
-        self.assertEqual([], result)
-        self.assertTrue(self.start_response_called)
-        # Output content is empty because of no_body_app
-        self.assertEqual('', ''.join(self._response_chunks))

=== modified file 'setup.py'
--- setup.py	2011-08-15 09:21:48 +0000
+++ setup.py	2011-08-18 03:43:26 +0000
@@ -58,6 +58,7 @@
         'oauth',
         'oops',
         'oops_datedir_repo',
+        'oops_wsgi',
         'paramiko',
         'psycopg2',
         'python-memcached',

=== modified file 'versions.cfg'
--- versions.cfg	2011-08-17 08:09:43 +0000
+++ versions.cfg	2011-08-18 03:43:26 +0000
@@ -51,6 +51,7 @@
 oauth = 1.0
 oops = 0.0.6
 oops-datedir-repo = 0.0.3
+oops-wsgi = 0.0.1
 paramiko = 1.7.4
 Paste = 1.7.2
 PasteDeploy = 1.3.3