Merge lp:~michael.nelson/ubuntu-recommender/944075-python-oops-wsgi into lp:ubuntu-recommender

Proposed by Michael Nelson
Status: Merged
Approved by: James Westby
Approved revision: 58
Merged at revision: 52
Proposed branch: lp:~michael.nelson/ubuntu-recommender/944075-python-oops-wsgi
Merge into: lp:ubuntu-recommender
Diff against target: 317 lines (+242/-2)
8 files modified
django_project/urls.py (+2/-0)
requirements.txt (+4/-0)
src/recommender/schema.py (+14/-0)
src/recommender/templates/recommender/500_api.txt (+6/-0)
src/recommender/tests/__init__.py (+2/-1)
src/recommender/tests/test_oops_wsgi_setup.py (+162/-0)
src/recommender/urls.py (+1/-0)
src/recommender/views.py (+51/-1)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-recommender/944075-python-oops-wsgi
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Review via email: mp+96333@code.launchpad.net

Commit message

Adds schema config options for for oops_wsgi integration and tests.

Description of the change

Overview
========
This branch just documents and adds tests to confirm that a configuration using oops-wsgi will work (and updates our schema to support it).

Turning this on in production will simply require updating the settings and django.wsgi in our config branch as documented here. But that can't be done until https://portal.admin.canonical.com/50529 is complete (unless we want to simply store the oopses locally, which could be done immediately with a config branch?)

The new dependencies are available in cat (thanks james_w!), so we'll need to ensure they are installed on the machine: python-oops-wsgi, python-oops-dictconfig and python-oops_datedir_repo

I'll create a ca-config/ur-config branch that can land once that's done.

NOTE: lazr.restfulclient and launchpad lib are pulled in via oops_datedir_repo :/ see the XXX below with a bug.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

On Wed, 07 Mar 2012 15:15:22 -0000, Michael Nelson <email address hidden> wrote:
Non-text part: multipart/mixed
> Michael Nelson has proposed merging lp:~michael.nelson/ubuntu-recommender/944075-python-oops-wsgi into lp:ubuntu-recommender.
>
> Requested reviews:
> Canonical Consumer Applications Hackers (canonical-ca-hackers)
>
> For more details, see:
> https://code.launchpad.net/~michael.nelson/ubuntu-recommender/944075-python-oops-wsgi/+merge/96333
>
> Overview
> ========
> This branch just documents and adds tests to confirm that a configuration using oops-wsgi will work (and updates our schema to support it).

Hi Michael,

Thanks for working on this, it will be great to have this in use across
all our services.

> Turning this on in production will simply require updating the settings and django.wsgi in our config branch as documented here. But that can't be done until https://portal.admin.canonical.com/50529 is complete (unless we want to simply store the oopses locally, which could be done immediately with a config branch?)
>
> The new dependencies will need to be added via branches, as none of python-oops-wsgi, python-oops-dictconfig or python-oops_datedir_repo are available on lucid.

They are all packaged and available in lucid-cat though :-)
(You can use queries like "ssh people.canonical.com ~jamesw/cat-madison
python-oops-wsgi" to see what is in cat)

I'd be happy to work with you on getting the oops id available to the
500 handler, as it's a very useful thing, and something we will need in
many places.

> + class oops_wsgi(schema.Section):
> + oopses = schema.DictOption(default={
> + 'publishers': [{
> + 'type': 'datedir',
> + 'error_dir': 'oopses',
> + 'instance_id': 'dev',
> + }],
> + })

You could use the OopsOption from python-oops-dictconfig here.

> + def test_traceback_in_api_response(self):
> + # The API response should contain the traceback.

Is the API open for use by the public? Would we want to disclose
tracebacks in that case?

Thanks,

James

58. By Michael Nelson

REFACTOR: Use OopsOption.

Revision history for this message
James Westby (james-w) wrote :

Hi,

This looks good.

I'd leave out the tracebacks, but I don't feel strongly about it at this point.

Thanks,

James

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'django_project/urls.py'
--- django_project/urls.py 2012-02-10 13:22:04 +0000
+++ django_project/urls.py 2012-03-07 16:30:26 +0000
@@ -6,6 +6,8 @@
6admin.autodiscover()6admin.autodiscover()
7preflight.autodiscover()7preflight.autodiscover()
88
9handler500 = 'recommender.views.server_error'
10
9urlpatterns = patterns('',11urlpatterns = patterns('',
10 # Django's admin site:12 # Django's admin site:
11 (r'^admin/', include(admin.site.urls)),13 (r'^admin/', include(admin.site.urls)),
1214
=== modified file 'requirements.txt'
--- requirements.txt 2012-02-29 00:21:56 +0000
+++ requirements.txt 2012-03-07 16:30:26 +0000
@@ -15,3 +15,7 @@
15south==0.7.315south==0.7.3
16django-factory16django-factory
17psycopg2==2.4.117psycopg2==2.4.1
18oops-wsgi==0.0.10
19oops-dictconfig==0.0.1
20# XXX 2012-03-07 bug=948857 Currently this pulls in lazr.restfulclient and co.
21oops_datedir_repo==0.0.17
1822
=== modified file 'src/recommender/schema.py'
--- src/recommender/schema.py 2012-03-02 14:41:35 +0000
+++ src/recommender/schema.py 2012-03-07 16:30:26 +0000
@@ -27,6 +27,7 @@
27import django27import django
28from configglue import schema28from configglue import schema
29from django_configglue.schema import schemas29from django_configglue.schema import schemas
30from oops_dictconfig.configglue_options import OopsOption
3031
31DjangoSchema = schemas.get(django.get_version())32DjangoSchema = schemas.get(django.get_version())
3233
@@ -93,3 +94,16 @@
9394
94 class logging(schema.Section):95 class logging(schema.Section):
95 webapp_logging_config = schema.StringOption(fatal=True)96 webapp_logging_config = schema.StringOption(fatal=True)
97
98 class piston(schema.Section):
99 piston_display_errors = schema.BoolOption(default=False)
100 piston_email_errors = schema.BoolOption(default=False)
101
102 class oops_wsgi(schema.Section):
103 oopses = OopsOption(default={
104 'publishers': [{
105 'type': 'datedir',
106 'error_dir': 'oopses',
107 'instance_id': 'dev',
108 }],
109 })
96110
=== added directory 'src/recommender/templates/recommender'
=== added file 'src/recommender/templates/recommender/500_api.txt'
--- src/recommender/templates/recommender/500_api.txt 1970-01-01 00:00:00 +0000
+++ src/recommender/templates/recommender/500_api.txt 2012-03-07 16:30:26 +0000
@@ -0,0 +1,6 @@
1API Error - OOPS ID: {{ oops_id }}
2
3There was an error while processing this request. It has been reported to the developers with the above OOPS ID.
4
5Traceback:
6{{ traceback }}
07
=== modified file 'src/recommender/tests/__init__.py'
--- src/recommender/tests/__init__.py 2012-02-28 10:15:15 +0000
+++ src/recommender/tests/__init__.py 2012-03-07 16:30:26 +0000
@@ -1,9 +1,10 @@
1from .test_api import *1from .test_api import *
2from .test_auth import *2from .test_auth import *
3from .test_commands import *
3from .test_managers import *4from .test_managers import *
4from .test_models_oauthtoken import *5from .test_models_oauthtoken import *
5from .test_models_data import *6from .test_models_data import *
7from .test_oops_wsgi_setup import *
6from .test_pep8 import *8from .test_pep8 import *
7from .test_commands import *
8from .test_slopeone import *9from .test_slopeone import *
9from .test_utilities import *10from .test_utilities import *
1011
=== added file 'src/recommender/tests/test_oops_wsgi_setup.py'
--- src/recommender/tests/test_oops_wsgi_setup.py 1970-01-01 00:00:00 +0000
+++ src/recommender/tests/test_oops_wsgi_setup.py 2012-03-07 16:30:26 +0000
@@ -0,0 +1,162 @@
1# -*- coding: utf-8 -*-
2# This file is part of the Ubuntu Recommender
3# Copyright (C) 2011-2012 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"""Tests for the oops_wsgi setup."""
19
20from __future__ import absolute_import
21
22__metaclass__ = type
23__all__ = [
24 'OopsWsgiTestCase',
25 ]
26
27import bson
28import os
29from shutil import rmtree
30from tempfile import mkdtemp
31
32from django.core.urlresolvers import reverse
33from django.conf import settings
34from django_factory import TestCase
35from mock import (
36 Mock,
37 patch,
38 )
39import oops_dictconfig
40# This is needed to workaround a bug in Django, see the file it is
41# implemented in for more details.
42from oops_wsgi.django import OOPSWSGIHandler
43from oops_wsgi import (
44 make_app,
45 install_hooks,
46 )
47
48from recommender.tests.helpers import patch_settings
49
50
51def make_oops_app():
52 """Create an oops_wsgi handler."""
53
54 application = OOPSWSGIHandler()
55
56 ###
57 # Wrap the application in the Oops wsgi app to catch unhandled exceptions
58 # and create oops for them.
59 #
60 # First we create the config that defines what to do with the oopses.
61
62 config = oops_dictconfig.config_from_dict(settings.OOPSES)
63 install_hooks(config)
64
65 # Then we wrap the django app in the oops one
66 return make_app(application, config, oops_on_status=['500'])
67
68
69class OopsWsgiTestCase(TestCase):
70
71 def setUp(self):
72 super(OopsWsgiTestCase, self).setUp()
73 self.oops_dir = mkdtemp()
74 self.addCleanup(rmtree, self.oops_dir)
75 self.environ = {
76 'PATH_INFO': '/',
77 'QUERY_STRING': '',
78 'REMOTE_ADDR': '127.0.0.1',
79 'REQUEST_METHOD': 'GET',
80 'SCRIPT_NAME': '',
81 'SERVER_NAME': 'testserver',
82 'SERVER_PORT': '80',
83 'SERVER_PROTOCOL': 'HTTP/1.1',
84 'wsgi.version': (1, 0),
85 'wsgi.url_scheme': 'http',
86 'wsgi.multiprocess': True,
87 'wsgi.multithread': False,
88 'wsgi.run_once': False,
89 'wsgi.input': '',
90 }
91
92 def request_api_exception(self):
93 self.environ['PATH_INFO'] = reverse('api-server-status')
94 read_fn_path = 'recommender.api.handlers.StatusHandler.read'
95 with patch(read_fn_path) as mock_read:
96 mock_read.side_effect = ZeroDivisionError
97 response = self.request_wsgi_response(self.environ)
98 return response
99
100 def request_wsgi_response(self, environ):
101 start_response = Mock()
102 with patch_settings(OOPSES={
103 'publishers': [{
104 'type': 'datedir',
105 'error_dir': self.oops_dir,
106 'instance_id': 'dev',
107 }],
108 }):
109 app = make_oops_app()
110 response = app(environ, start_response).next()
111 return response
112
113 def read_oops_from_disk(self):
114 # Helper to read in the oops written to disk.
115 # There is only one date directory in the oops directory.
116 dated_dirs = os.listdir(self.oops_dir)
117 self.assertEqual(1, len(dated_dirs))
118
119 # There is only one oops.
120 todays_oops_path = dated_dirs[0]
121 oops_list = os.listdir(self.oops_dir + '/' + todays_oops_path)
122 self.assertEqual(1, len(oops_list))
123
124 oops_path = self.oops_dir + '/' + todays_oops_path + '/' + oops_list[0]
125 with open(oops_path, "r") as oops_file:
126 oops_file_contents = oops_file.read()
127 return bson.loads(oops_file_contents)
128
129 def test_traceback_in_api_response(self):
130 # The API response should contain the traceback.
131 response = self.request_api_exception()
132
133 self.assertIn('raise effect', response)
134
135 # XXX 2012-03-07 bug=949013 michaeln Can't access oops-id from django.
136 def disabled_test_oops_id_in_api_response(self):
137 # The API response should contain the oops id.
138 response = self.request_api_exception()
139
140 oops = self.read_oops_from_disk()
141 self.assertIn(oops['id'], response)
142
143 def test_traceback_in_api_oops_on_disk(self):
144 # A request resulting in an api oops writes the traceback to the file.
145 response = self.request_api_exception()
146
147 oops = self.read_oops_from_disk()
148 self.assertIn('tb_text', oops)
149 self.assertEqual('ZeroDivisionError', oops['type'])
150
151 def request_non_api_exception(self):
152 self.environ['PATH_INFO'] = reverse('preflight-overview')
153 view_fn_path = 'preflight.views.render_to_response'
154 with patch(view_fn_path) as mock_render:
155 mock_render.side_effect = ZeroDivisionError
156 response = self.request_wsgi_response(self.environ)
157 return response
158
159 def test_normal_template_for_non_api(self):
160 response = self.request_non_api_exception()
161
162 self.assertIn('Internal Server Error', response)
0163
=== modified file 'src/recommender/urls.py'
--- src/recommender/urls.py 2011-12-07 04:01:01 +0000
+++ src/recommender/urls.py 2012-03-07 16:30:26 +0000
@@ -23,6 +23,7 @@
23 url,23 url,
24 )24 )
2525
26
26urlpatterns = patterns('',27urlpatterns = patterns('',
27 url(r'^api/1.0/', include('recommender.api.urls')),28 url(r'^api/1.0/', include('recommender.api.urls')),
28)29)
2930
=== modified file 'src/recommender/views.py'
--- src/recommender/views.py 2011-12-01 15:32:25 +0000
+++ src/recommender/views.py 2012-03-07 16:30:26 +0000
@@ -1,1 +1,51 @@
1# Create your views here.1# -*- coding: utf-8 -*-
2# This file is part of the Ubuntu Recommender
3# Copyright (C) 2011-2012 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"""Views for Ubuntu Recommender."""
19
20import traceback
21
22from django.http import HttpResponseServerError
23from django.template import (
24 Context,
25 loader,
26 )
27from django.views.defaults import server_error as default_server_error
28
29
30def server_error(request):
31 if request.META['PATH_INFO'].startswith('/api'):
32 t = loader.get_template('recommender/500_api.txt')
33 traceback_formatted = 'Traceback not available.'
34 oops_id = 'OOPS ID not available.'
35 oops_context = request.META.get('oops.context')
36 if oops_context:
37 exc_info = oops_context.get('exc_info')
38 if exc_info and len(exc_info) >= 3:
39 tb_object = exc_info[2]
40 if isinstance(tb_object, traceback.types.TracebackType):
41 traceback_formatted = "\n".join(
42 traceback.format_tb(exc_info[2]))
43
44 response = HttpResponseServerError(
45 t.render(Context({
46 'traceback': traceback_formatted,
47 'oops_id': oops_id,
48 })))
49 return response
50 else:
51 return default_server_error(request)

Subscribers

People subscribed via source and target branches