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
1=== modified file 'django_project/urls.py'
2--- django_project/urls.py 2012-02-10 13:22:04 +0000
3+++ django_project/urls.py 2012-03-07 16:30:26 +0000
4@@ -6,6 +6,8 @@
5 admin.autodiscover()
6 preflight.autodiscover()
7
8+handler500 = 'recommender.views.server_error'
9+
10 urlpatterns = patterns('',
11 # Django's admin site:
12 (r'^admin/', include(admin.site.urls)),
13
14=== modified file 'requirements.txt'
15--- requirements.txt 2012-02-29 00:21:56 +0000
16+++ requirements.txt 2012-03-07 16:30:26 +0000
17@@ -15,3 +15,7 @@
18 south==0.7.3
19 django-factory
20 psycopg2==2.4.1
21+oops-wsgi==0.0.10
22+oops-dictconfig==0.0.1
23+# XXX 2012-03-07 bug=948857 Currently this pulls in lazr.restfulclient and co.
24+oops_datedir_repo==0.0.17
25
26=== modified file 'src/recommender/schema.py'
27--- src/recommender/schema.py 2012-03-02 14:41:35 +0000
28+++ src/recommender/schema.py 2012-03-07 16:30:26 +0000
29@@ -27,6 +27,7 @@
30 import django
31 from configglue import schema
32 from django_configglue.schema import schemas
33+from oops_dictconfig.configglue_options import OopsOption
34
35 DjangoSchema = schemas.get(django.get_version())
36
37@@ -93,3 +94,16 @@
38
39 class logging(schema.Section):
40 webapp_logging_config = schema.StringOption(fatal=True)
41+
42+ class piston(schema.Section):
43+ piston_display_errors = schema.BoolOption(default=False)
44+ piston_email_errors = schema.BoolOption(default=False)
45+
46+ class oops_wsgi(schema.Section):
47+ oopses = OopsOption(default={
48+ 'publishers': [{
49+ 'type': 'datedir',
50+ 'error_dir': 'oopses',
51+ 'instance_id': 'dev',
52+ }],
53+ })
54
55=== added directory 'src/recommender/templates/recommender'
56=== added file 'src/recommender/templates/recommender/500_api.txt'
57--- src/recommender/templates/recommender/500_api.txt 1970-01-01 00:00:00 +0000
58+++ src/recommender/templates/recommender/500_api.txt 2012-03-07 16:30:26 +0000
59@@ -0,0 +1,6 @@
60+API Error - OOPS ID: {{ oops_id }}
61+
62+There was an error while processing this request. It has been reported to the developers with the above OOPS ID.
63+
64+Traceback:
65+{{ traceback }}
66
67=== modified file 'src/recommender/tests/__init__.py'
68--- src/recommender/tests/__init__.py 2012-02-28 10:15:15 +0000
69+++ src/recommender/tests/__init__.py 2012-03-07 16:30:26 +0000
70@@ -1,9 +1,10 @@
71 from .test_api import *
72 from .test_auth import *
73+from .test_commands import *
74 from .test_managers import *
75 from .test_models_oauthtoken import *
76 from .test_models_data import *
77+from .test_oops_wsgi_setup import *
78 from .test_pep8 import *
79-from .test_commands import *
80 from .test_slopeone import *
81 from .test_utilities import *
82
83=== added file 'src/recommender/tests/test_oops_wsgi_setup.py'
84--- src/recommender/tests/test_oops_wsgi_setup.py 1970-01-01 00:00:00 +0000
85+++ src/recommender/tests/test_oops_wsgi_setup.py 2012-03-07 16:30:26 +0000
86@@ -0,0 +1,162 @@
87+# -*- coding: utf-8 -*-
88+# This file is part of the Ubuntu Recommender
89+# Copyright (C) 2011-2012 Canonical Ltd.
90+#
91+# This program is free software: you can redistribute it and/or modify
92+# it under the terms of the GNU Affero General Public License as
93+# published by the Free Software Foundation, either version 3 of the
94+# License, or (at your option) any later version.
95+#
96+# This program is distributed in the hope that it will be useful,
97+# but WITHOUT ANY WARRANTY; without even the implied warranty of
98+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
99+# GNU Affero General Public License for more details.
100+#
101+# You should have received a copy of the GNU Affero General Public License
102+# along with this program. If not, see <http://www.gnu.org/licenses/>.
103+
104+"""Tests for the oops_wsgi setup."""
105+
106+from __future__ import absolute_import
107+
108+__metaclass__ = type
109+__all__ = [
110+ 'OopsWsgiTestCase',
111+ ]
112+
113+import bson
114+import os
115+from shutil import rmtree
116+from tempfile import mkdtemp
117+
118+from django.core.urlresolvers import reverse
119+from django.conf import settings
120+from django_factory import TestCase
121+from mock import (
122+ Mock,
123+ patch,
124+ )
125+import oops_dictconfig
126+# This is needed to workaround a bug in Django, see the file it is
127+# implemented in for more details.
128+from oops_wsgi.django import OOPSWSGIHandler
129+from oops_wsgi import (
130+ make_app,
131+ install_hooks,
132+ )
133+
134+from recommender.tests.helpers import patch_settings
135+
136+
137+def make_oops_app():
138+ """Create an oops_wsgi handler."""
139+
140+ application = OOPSWSGIHandler()
141+
142+ ###
143+ # Wrap the application in the Oops wsgi app to catch unhandled exceptions
144+ # and create oops for them.
145+ #
146+ # First we create the config that defines what to do with the oopses.
147+
148+ config = oops_dictconfig.config_from_dict(settings.OOPSES)
149+ install_hooks(config)
150+
151+ # Then we wrap the django app in the oops one
152+ return make_app(application, config, oops_on_status=['500'])
153+
154+
155+class OopsWsgiTestCase(TestCase):
156+
157+ def setUp(self):
158+ super(OopsWsgiTestCase, self).setUp()
159+ self.oops_dir = mkdtemp()
160+ self.addCleanup(rmtree, self.oops_dir)
161+ self.environ = {
162+ 'PATH_INFO': '/',
163+ 'QUERY_STRING': '',
164+ 'REMOTE_ADDR': '127.0.0.1',
165+ 'REQUEST_METHOD': 'GET',
166+ 'SCRIPT_NAME': '',
167+ 'SERVER_NAME': 'testserver',
168+ 'SERVER_PORT': '80',
169+ 'SERVER_PROTOCOL': 'HTTP/1.1',
170+ 'wsgi.version': (1, 0),
171+ 'wsgi.url_scheme': 'http',
172+ 'wsgi.multiprocess': True,
173+ 'wsgi.multithread': False,
174+ 'wsgi.run_once': False,
175+ 'wsgi.input': '',
176+ }
177+
178+ def request_api_exception(self):
179+ self.environ['PATH_INFO'] = reverse('api-server-status')
180+ read_fn_path = 'recommender.api.handlers.StatusHandler.read'
181+ with patch(read_fn_path) as mock_read:
182+ mock_read.side_effect = ZeroDivisionError
183+ response = self.request_wsgi_response(self.environ)
184+ return response
185+
186+ def request_wsgi_response(self, environ):
187+ start_response = Mock()
188+ with patch_settings(OOPSES={
189+ 'publishers': [{
190+ 'type': 'datedir',
191+ 'error_dir': self.oops_dir,
192+ 'instance_id': 'dev',
193+ }],
194+ }):
195+ app = make_oops_app()
196+ response = app(environ, start_response).next()
197+ return response
198+
199+ def read_oops_from_disk(self):
200+ # Helper to read in the oops written to disk.
201+ # There is only one date directory in the oops directory.
202+ dated_dirs = os.listdir(self.oops_dir)
203+ self.assertEqual(1, len(dated_dirs))
204+
205+ # There is only one oops.
206+ todays_oops_path = dated_dirs[0]
207+ oops_list = os.listdir(self.oops_dir + '/' + todays_oops_path)
208+ self.assertEqual(1, len(oops_list))
209+
210+ oops_path = self.oops_dir + '/' + todays_oops_path + '/' + oops_list[0]
211+ with open(oops_path, "r") as oops_file:
212+ oops_file_contents = oops_file.read()
213+ return bson.loads(oops_file_contents)
214+
215+ def test_traceback_in_api_response(self):
216+ # The API response should contain the traceback.
217+ response = self.request_api_exception()
218+
219+ self.assertIn('raise effect', response)
220+
221+ # XXX 2012-03-07 bug=949013 michaeln Can't access oops-id from django.
222+ def disabled_test_oops_id_in_api_response(self):
223+ # The API response should contain the oops id.
224+ response = self.request_api_exception()
225+
226+ oops = self.read_oops_from_disk()
227+ self.assertIn(oops['id'], response)
228+
229+ def test_traceback_in_api_oops_on_disk(self):
230+ # A request resulting in an api oops writes the traceback to the file.
231+ response = self.request_api_exception()
232+
233+ oops = self.read_oops_from_disk()
234+ self.assertIn('tb_text', oops)
235+ self.assertEqual('ZeroDivisionError', oops['type'])
236+
237+ def request_non_api_exception(self):
238+ self.environ['PATH_INFO'] = reverse('preflight-overview')
239+ view_fn_path = 'preflight.views.render_to_response'
240+ with patch(view_fn_path) as mock_render:
241+ mock_render.side_effect = ZeroDivisionError
242+ response = self.request_wsgi_response(self.environ)
243+ return response
244+
245+ def test_normal_template_for_non_api(self):
246+ response = self.request_non_api_exception()
247+
248+ self.assertIn('Internal Server Error', response)
249
250=== modified file 'src/recommender/urls.py'
251--- src/recommender/urls.py 2011-12-07 04:01:01 +0000
252+++ src/recommender/urls.py 2012-03-07 16:30:26 +0000
253@@ -23,6 +23,7 @@
254 url,
255 )
256
257+
258 urlpatterns = patterns('',
259 url(r'^api/1.0/', include('recommender.api.urls')),
260 )
261
262=== modified file 'src/recommender/views.py'
263--- src/recommender/views.py 2011-12-01 15:32:25 +0000
264+++ src/recommender/views.py 2012-03-07 16:30:26 +0000
265@@ -1,1 +1,51 @@
266-# Create your views here.
267+# -*- coding: utf-8 -*-
268+# This file is part of the Ubuntu Recommender
269+# Copyright (C) 2011-2012 Canonical Ltd.
270+#
271+# This program is free software: you can redistribute it and/or modify
272+# it under the terms of the GNU Affero General Public License as
273+# published by the Free Software Foundation, either version 3 of the
274+# License, or (at your option) any later version.
275+#
276+# This program is distributed in the hope that it will be useful,
277+# but WITHOUT ANY WARRANTY; without even the implied warranty of
278+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
279+# GNU Affero General Public License for more details.
280+#
281+# You should have received a copy of the GNU Affero General Public License
282+# along with this program. If not, see <http://www.gnu.org/licenses/>.
283+
284+"""Views for Ubuntu Recommender."""
285+
286+import traceback
287+
288+from django.http import HttpResponseServerError
289+from django.template import (
290+ Context,
291+ loader,
292+ )
293+from django.views.defaults import server_error as default_server_error
294+
295+
296+def server_error(request):
297+ if request.META['PATH_INFO'].startswith('/api'):
298+ t = loader.get_template('recommender/500_api.txt')
299+ traceback_formatted = 'Traceback not available.'
300+ oops_id = 'OOPS ID not available.'
301+ oops_context = request.META.get('oops.context')
302+ if oops_context:
303+ exc_info = oops_context.get('exc_info')
304+ if exc_info and len(exc_info) >= 3:
305+ tb_object = exc_info[2]
306+ if isinstance(tb_object, traceback.types.TracebackType):
307+ traceback_formatted = "\n".join(
308+ traceback.format_tb(exc_info[2]))
309+
310+ response = HttpResponseServerError(
311+ t.render(Context({
312+ 'traceback': traceback_formatted,
313+ 'oops_id': oops_id,
314+ })))
315+ return response
316+ else:
317+ return default_server_error(request)

Subscribers

People subscribed via source and target branches