Merge lp:~elachuni/ubuntu-webcatalog/amqp-oopses into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Merged at revision: 178
Proposed branch: lp:~elachuni/ubuntu-webcatalog/amqp-oopses
Merge into: lp:ubuntu-webcatalog
Diff against target: 338 lines (+153/-24)
11 files modified
.bzrignore (+1/-0)
django_project/config/main.cfg (+2/-12)
django_project/urls.py (+4/-0)
setup.py (+1/-1)
src/webcatalog/management/commands/runserver.py (+36/-0)
src/webcatalog/schema.py (+2/-0)
src/webcatalog/templates/500.html (+3/-2)
src/webcatalog/tests/test_oops_wsgi_config.py (+12/-4)
src/webcatalog/urls.py (+1/-0)
src/webcatalog/views.py (+58/-2)
src/webcatalog/wsgi.py (+33/-3)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/amqp-oopses
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Approve
Review via email: mp+151051@code.launchpad.net

Commit message

Display the oops id on every error page, and generate oops reports in dev.

Description of the change

This branch includes several improvements related to ampq oops publishing:
 - A custom wsgi handler was added that generates an oops id for every exception.
 - Switched to oops-dictconfig 0.0.5 to use the 'inherit_id' and 'new_only' settings.
 - A custom runserver command was added that uses webcatalog's wsgi stack in dev (to generate oopses in dev)

To post a comment you must log in.
Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

Why there's render_html_error but no render_json_error (which should be extracted from __call__)?

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :
Download full text (40.7 KiB)

The attempt to merge lp:~elachuni/ubuntu-webcatalog/amqp-oopses into lp:ubuntu-webcatalog failed. Below is the output from the failed tests.

[localhost] local: /usr/bin/python /usr/bin/virtualenv --no-site-packages virtualenv
The --no-site-packages flag is deprecated; it is now the default behavior.
New python executable in virtualenv/bin/python
Installing distribute.............................................................................................................................................................................................done.
Installing pip...............done.
[localhost] local: virtualenv/bin/pip install -r test_requirements.txt
Downloading/unpacking django<1.4 (from -r test_requirements.txt (line 6))
  Running setup.py egg_info for package django

Downloading/unpacking coverage (from -r test_requirements.txt (line 7))
  Running setup.py egg_info for package coverage

    warning: no previously-included files matching '*.pyc' found anywhere in distribution
Downloading/unpacking mock (from -r test_requirements.txt (line 8))
  Running setup.py egg_info for package mock

    warning: no files found matching '*.png' under directory 'docs'
    warning: no files found matching '*.css' under directory 'docs'
    warning: no files found matching '*.html' under directory 'docs'
    warning: no files found matching '*.js' under directory 'docs'
Downloading/unpacking piston-mini-client (from -r test_requirements.txt (line 9))
  Downloading piston-mini-client-0.7.5.tar.gz
  Running setup.py egg_info for package piston-mini-client

Downloading/unpacking oauthlib (from piston-mini-client->-r test_requirements.txt (line 9))
  Running setup.py egg_info for package oauthlib

Downloading/unpacking httplib2 (from piston-mini-client->-r test_requirements.txt (line 9))
  Running setup.py egg_info for package httplib2

Installing collected packages: django, coverage, mock, piston-mini-client, oauthlib, httplib2
  Running setup.py install for django
    changing mode of build/scripts-2.7/django-admin.py from 644 to 755

    changing mode of /mnt/tarmac/cache/ubuntu-webcatalog/trunk/virtualenv/bin/django-admin.py to 755
  Running setup.py install for coverage
    building 'coverage.tracer' extension
    gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I/usr/include/python2.7 -c coverage/tracer.c -o build/temp.linux-x86_64-2.7/coverage/tracer.o
    gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro build/temp.linux-x86_64-2.7/coverage/tracer.o -o build/lib.linux-x86_64-2.7/coverage/tracer.so

    warning: no previously-included files matching '*.pyc' found anywhere in distribution
    Installing coverage2 script to /mnt/tarmac/cache/ubuntu-webcatalog/trunk/virtualenv/bin
    Installing coverage-2.7 script to /mnt/tarmac/cache/ubuntu-webcatalog/trunk/virtualenv/bin
    Installing coverage script to /mnt/tarmac/cache/ubuntu-webcatalog/trunk/virtualenv/bin
  Running setup.py install for mock

    warning: no files found matching '*.png' under directory 'docs'
    warning: no files found matching '*.css' under directory ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2012-04-18 09:37:51 +0000
+++ .bzrignore 2013-03-01 12:52:22 +0000
@@ -15,3 +15,4 @@
15django_project/adminaudit15django_project/adminaudit
16django_project/local.cfg16django_project/local.cfg
17django_project/sreclient.py17django_project/sreclient.py
18oopses/
1819
=== modified file 'django_project/config/main.cfg'
--- django_project/config/main.cfg 2012-08-23 15:12:54 +0000
+++ django_project/config/main.cfg 2013-03-01 12:52:22 +0000
@@ -11,14 +11,14 @@
11debug = true11debug = true
12media_root = django_project/media_root_dev/12media_root = django_project/media_root_dev/
1313
14installed_apps = webcatalog14installed_apps = django.contrib.contenttypes
15 django.contrib.contenttypes
16 django.contrib.sessions15 django.contrib.sessions
17 django.contrib.sites16 django.contrib.sites
18 django.contrib.messages17 django.contrib.messages
19 django.contrib.staticfiles18 django.contrib.staticfiles
20 django.contrib.markup19 django.contrib.markup
21 django.contrib.admin20 django.contrib.admin
21 webcatalog
22 django_openid_auth22 django_openid_auth
23 django_configglue23 django_configglue
24 django.contrib.auth24 django.contrib.auth
@@ -106,16 +106,6 @@
106canonical-losas = admin106canonical-losas = admin
107canonical-ca-hackers = developers107canonical-ca-hackers = developers
108108
109[oops_wsgi]
110oopses = oops_publishers
111
112[oops_publishers]
113publishers = oops_datedir_publisher
114
115[oops_datedir_publisher]
116type = datedir
117error_dir = oopses
118
119[webcatalog]109[webcatalog]
120serve_site_media = True110serve_site_media = True
121sca_api_url = https://sc.staging.ubuntu.com/api/2.0/111sca_api_url = https://sc.staging.ubuntu.com/api/2.0/
122112
=== modified file 'django_project/urls.py'
--- django_project/urls.py 2012-06-21 20:18:44 +0000
+++ django_project/urls.py 2013-03-01 12:52:22 +0000
@@ -25,6 +25,10 @@
25admin.autodiscover()25admin.autodiscover()
26preflight.autodiscover()26preflight.autodiscover()
2727
28handler500 = 'webcatalog.views.server_error'
29handler404 = 'webcatalog.views.page_not_found'
30
31
28urlpatterns = patterns('',32urlpatterns = patterns('',
29 url(r'^cat/', include('webcatalog.urls')),33 url(r'^cat/', include('webcatalog.urls')),
30 url(r'^admin/', include(admin.site.urls)),34 url(r'^admin/', include(admin.site.urls)),
3135
=== modified file 'setup.py'
--- setup.py 2012-09-28 15:02:24 +0000
+++ setup.py 2013-03-01 12:52:22 +0000
@@ -53,7 +53,7 @@
53 'celery==2.5.0',53 'celery==2.5.0',
54 'django-celery==2.5.0',54 'django-celery==2.5.0',
55 'oops-wsgi==0.0.10',55 'oops-wsgi==0.0.10',
56 'oops-dictconfig==0.0.2',56 'oops-dictconfig==0.0.5',
57 'oops-datedir_repo==0.0.17',57 'oops-datedir_repo==0.0.17',
58 ],58 ],
59 package_data = find_packages_data('src'),59 package_data = find_packages_data('src'),
6060
=== added file 'src/webcatalog/management/commands/runserver.py'
--- src/webcatalog/management/commands/runserver.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/management/commands/runserver.py 2013-03-01 12:52:22 +0000
@@ -0,0 +1,36 @@
1# -*- coding: utf-8 -*-
2# This file is part of the Apps Directory
3# Copyright (C) 2011-2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as
7# published by the Free Software Foundation, either version 3 of the
8# License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18"""Customized runserver command that runs ubuntu-webcatalog's wsgi stack."""
19
20from optparse import make_option
21from django.core.management.commands.runserver import BaseRunserverCommand
22from django.core.servers.basehttp import AdminMediaHandler
23
24
25class Command(BaseRunserverCommand):
26 option_list = BaseRunserverCommand.option_list + (
27 make_option(
28 '--adminmedia', dest='admin_media_path', default='',
29 help='Specifies the directory from which to serve admin media.'),
30 )
31
32 def get_handler(self, *args, **options):
33 """Serves admin media like old-school (deprecation pending)."""
34 from webcatalog.wsgi import make_app
35 handler = make_app()
36 return AdminMediaHandler(handler, options.get('admin_media_path', ''))
037
=== modified file 'src/webcatalog/schema.py'
--- src/webcatalog/schema.py 2013-02-22 21:02:05 +0000
+++ src/webcatalog/schema.py 2013-03-01 12:52:22 +0000
@@ -38,6 +38,8 @@
38 'publishers': [{38 'publishers': [{
39 'type': 'datedir',39 'type': 'datedir',
40 'error_dir': 'oopses',40 'error_dir': 'oopses',
41 'inherit_id': True,
42 'new_only': False,
41 }],43 }],
42 })44 })
4345
4446
=== modified file 'src/webcatalog/templates/500.html'
--- src/webcatalog/templates/500.html 2012-08-23 14:59:18 +0000
+++ src/webcatalog/templates/500.html 2013-03-01 12:52:22 +0000
@@ -1,10 +1,11 @@
1{% extends "light/index.1col.html" %}1{% extends "webcatalog/base.html" %}
2{% load i18n %}2{% load i18n %}
33
4{% block title %}{% trans 'Oops!' %}{% endblock %}4{% block title %}{% trans 'Oops!' %}{% endblock %}
55
6{% block content %}6{% block content %}
7<h1>{% trans 'Server Error <em>(500)</em>' %}</h1>7<h1>{% trans 'Server Error' %}</h1>
8{% if errormsg %}<p>{{ errormsg }}</p>{% endif %}8{% if errormsg %}<p>{{ errormsg }}</p>{% endif %}
9<p>{% blocktrans %}An error occurred which prevented the page you requested from loading. If this problem continues, please consider reporting this problem by sending an e-mail to <a href="mailto:webmaster@canonical.com">webmaster@canonical.com</a>{% endblocktrans %}</p>9<p>{% blocktrans %}An error occurred which prevented the page you requested from loading. If this problem continues, please consider reporting this problem by sending an e-mail to <a href="mailto:webmaster@canonical.com">webmaster@canonical.com</a>{% endblocktrans %}</p>
10<p id="oops-id">{{ oops_id }}</p>
10{% endblock %}11{% endblock %}
1112
=== modified file 'src/webcatalog/tests/test_oops_wsgi_config.py'
--- src/webcatalog/tests/test_oops_wsgi_config.py 2012-08-23 14:59:18 +0000
+++ src/webcatalog/tests/test_oops_wsgi_config.py 2013-03-01 12:52:22 +0000
@@ -26,6 +26,7 @@
2626
27import bson27import bson
28import os28import os
29import re
29from shutil import rmtree30from shutil import rmtree
30from tempfile import mkdtemp31from tempfile import mkdtemp
3132
@@ -67,13 +68,15 @@
6768
68 def request_wsgi_response(self, environ):69 def request_wsgi_response(self, environ):
69 start_response = Mock()70 start_response = Mock()
70 with patch_settings(OOPSES={71 oops_config = {
71 'publishers': [{72 'publishers': [{
72 'type': 'datedir',73 'type': 'datedir',
73 'error_dir': self.oops_dir,74 'error_dir': self.oops_dir,
74 'instance_id': 'dev',75 'inherit_id': True,
76 'new_only': False,
75 }],77 }],
76 }):78 }
79 with patch_settings(OOPSES=oops_config):
77 app = make_app()80 app = make_app()
78 response = app(environ, start_response).next()81 response = app(environ, start_response).next()
79 return response82 return response
@@ -105,6 +108,11 @@
105 def test_oops_on_disk_on_error(self):108 def test_oops_on_disk_on_error(self):
106 response = self.request_with_exception()109 response = self.request_with_exception()
107110
108 self.assertIn('Server Error <em>(500)</em>', response)111 self.assertIn('Server Error', response)
112 oops_id_re = re.compile('<p id="oops-id">(OOPS-[0-9a-f]{32})</p>')
113 match = oops_id_re.search(response)
114 self.assertTrue(match)
115 oops_id = match.groups()[0]
109 oops_data = self.read_oops_from_disk()116 oops_data = self.read_oops_from_disk()
110 self.assertEqual('ZeroDivisionError', oops_data['type'])117 self.assertEqual('ZeroDivisionError', oops_data['type'])
118 self.assertEqual(oops_id, oops_data['id'])
111119
=== modified file 'src/webcatalog/urls.py'
--- src/webcatalog/urls.py 2012-07-02 21:25:30 +0000
+++ src/webcatalog/urls.py 2013-03-01 12:52:22 +0000
@@ -63,4 +63,5 @@
63 name='wc-task-status'),63 name='wc-task-status'),
6464
65 (r'^api/', include('webcatalog.api.urls')),65 (r'^api/', include('webcatalog.api.urls')),
66 url(r'^die/$', 'die', name='debug-die'),
66)67)
6768
=== modified file 'src/webcatalog/views.py'
--- src/webcatalog/views.py 2012-09-06 10:38:18 +0000
+++ src/webcatalog/views.py 2013-03-01 12:52:22 +0000
@@ -34,7 +34,6 @@
34from django.http import (34from django.http import (
35 HttpResponse,35 HttpResponse,
36 HttpResponseForbidden,36 HttpResponseForbidden,
37 HttpResponseNotFound,
38 HttpResponseRedirect,37 HttpResponseRedirect,
39)38)
40from django.shortcuts import (39from django.shortcuts import (
@@ -42,7 +41,10 @@
42 render_to_response,41 render_to_response,
43)42)
44from django.template import RequestContext43from django.template import RequestContext
45from django.template.loader import render_to_string44from django.template.loader import (
45 render_to_string,
46 get_template,
47)
46from django.utils.translation import ugettext as _48from django.utils.translation import ugettext as _
47from django.views.decorators.cache import never_cache49from django.views.decorators.cache import never_cache
48from django.views.decorators.http import require_GET50from django.views.decorators.http import require_GET
@@ -346,3 +348,57 @@
346def forbidden(request):348def forbidden(request):
347 return HttpResponseForbidden(349 return HttpResponseForbidden(
348 render_to_string('forbidden.html', RequestContext(request)))350 render_to_string('forbidden.html', RequestContext(request)))
351
352
353class ErrorPage(object):
354 def __init__(self, status, errormsg=None):
355 self.template_name = '%s.html' % status
356 self.status_code = status
357 self.errormsg = errormsg
358
359 def __call__(self, request):
360 accept = request.META.get('ACCEPT', '')
361 if 'application/json' in accept and not 'text/html' in accept:
362 return self.render_json_error(request)
363 else:
364 return self.render_html_error(request)
365
366 def render_html_error(self, request):
367 oops_id = self.get_oops_id(request)
368 template = get_template(self.template_name)
369 atts = {
370 'oops_id': oops_id,
371 'errormsg': self.errormsg,
372 }
373 context = RequestContext(request, atts)
374 return HttpResponse(template.render(context), status=self.status_code)
375
376 def render_json_error(self, request):
377 oops_id = self.get_oops_id(request)
378 oops_context = request.META.get('oops.context', {})
379 exc_info = oops_context.get('exc_info', (None, None, None))
380 exception_type = unicode(exc_info[0])
381 response = json.dumps({
382 'oops_id': oops_id,
383 'error_type': exception_type,
384 'error_msg': "An error occurred and has been logged.",
385 })
386 return HttpResponse(response, status=self.status_code,
387 content_type='application/json')
388
389 def get_oops_id(self, request):
390 oops_report = request.environ.get('oops.report', {})
391 return oops_report.get('id', 'OOPS ID not available.')
392
393
394server_error = ErrorPage(500)
395page_not_found = ErrorPage(404)
396
397
398class ArtificialOopsException(Exception):
399 pass
400
401
402def die(request):
403 """Just trigger an oops report"""
404 raise ArtificialOopsException()
349405
=== modified file 'src/webcatalog/wsgi.py'
--- src/webcatalog/wsgi.py 2012-08-23 14:59:18 +0000
+++ src/webcatalog/wsgi.py 2013-03-01 12:52:22 +0000
@@ -23,8 +23,40 @@
23import oops_wsgi.django23import oops_wsgi.django
24import os24import os
25import platform25import platform
26import uuid
2627
27from django.conf import settings28from django.conf import settings
29from django.core.handlers import wsgi
30
31
32class EagerOOPSWSGIHandler(wsgi.WSGIHandler):
33 """Pre-generates an oops id on exception."""
34 def handle_uncaught_exception(self, request, resolver, exc_info):
35 # This will pass the exception information back to oops. It
36 # should not be necessary once
37 # https://code.djangoproject.com/ticket/16674 has been merged.
38 if 'oops.context' in request.environ:
39 request.environ['oops.context']['exc_info'] = exc_info
40 # Generate OOPS id early so that we can render the error page right
41 # away and not depend on passing thread locals around.
42 if 'oops.report' in request.environ:
43 unique_id = uuid.uuid4().hex
44 request.environ['oops.report']['id'] = "OOPS-%s" % unique_id
45
46 return super(EagerOOPSWSGIHandler, self).handle_uncaught_exception(
47 request, resolver, exc_info)
48
49 def __call__(self, environ, start_response):
50 def start_response_with_exc_info(status, headers, exc_info=None):
51 """Custom start_response callback for wsgi."""
52 # This will pass the exception information back to oops_wgi. It
53 # should not be necessary once
54 # https://code.djangoproject.com/ticket/16674 has been merged.
55 if exc_info is None:
56 exc_info = environ['oops.context'].get('exc_info', None)
57 return start_response(status, headers, exc_info)
58 return super(EagerOOPSWSGIHandler, self).__call__(
59 environ, start_response_with_exc_info)
2860
2961
30def make_app():62def make_app():
@@ -38,9 +70,7 @@
38 if logging_config:70 if logging_config:
39 logging.config.fileConfig(logging_config)71 logging.config.fileConfig(logging_config)
4072
41 # This is needed to workaround a bug in Django, see the file it is73 non_oops_app = EagerOOPSWSGIHandler()
42 # implemented in for more details.
43 non_oops_app = oops_wsgi.django.OOPSWSGIHandler()
4474
45 config = oops_dictconfig.config_from_dict(settings.OOPSES)75 config = oops_dictconfig.config_from_dict(settings.OOPSES)
46 oops_wsgi.install_hooks(config)76 oops_wsgi.install_hooks(config)

Subscribers

People subscribed via source and target branches