Merge lp:~elachuni/rnr-server/amqp-oopses into lp:rnr-server

Proposed by Anthony Lenton
Status: Rejected
Rejected by: Natalia Bidart
Proposed branch: lp:~elachuni/rnr-server/amqp-oopses
Merge into: lp:rnr-server
Diff against target: 462 lines (+189/-71)
12 files modified
.bzrignore (+1/-0)
django_project/config_dev/config/main.cfg (+6/-2)
django_project/config_dev/settings.py (+0/-3)
requirements.txt (+4/-2)
src/reviewsapp/management/commands/runserver.py (+36/-0)
src/reviewsapp/schema.py (+17/-0)
src/reviewsapp/templates/500.html (+1/-3)
src/reviewsapp/tests/test_views.py (+1/-1)
src/reviewsapp/tests/test_wsgi.py (+19/-32)
src/reviewsapp/urls.py (+1/-1)
src/reviewsapp/views/moderation.py (+34/-11)
src/reviewsapp/wsgi.py (+69/-16)
To merge this branch: bzr merge lp:~elachuni/rnr-server/amqp-oopses
Reviewer Review Type Date Requested Status
Michael Nelson (community) Approve
Review via email: mp+151302@code.launchpad.net

Description of the change

This branch switches to configglue 0.0.5 for setting amqp oops publishing.

There is also a custom wsgi handler now that will generate an oops id for every exception, and display the oops id on the error page correctly.

Finally, a custom runserver command is provided to run the custom wsgi stack in dev, to generate oopses in dev.

To post a comment you must log in.
lp:~elachuni/rnr-server/amqp-oopses updated
234. By Anthony Lenton

Removed storm from requirements

Revision history for this message
Anthony Lenton (elachuni) wrote :

Also, storm is no longer required since we moved from wsgi_oops to oops_wsgi...

Revision history for this message
Michael Nelson (michael.nelson) wrote :

If we update to Django 1.4, the custom runserver command isn't needed:

https://docs.djangoproject.com/en/1.4/ref/settings/#std:setting-WSGI_APPLICATION

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Setting MP as Rejected to reduce numbers of landing candidates in @reviewlist. Change status again if this MP is still current.

Unmerged revisions

234. By Anthony Lenton

Removed storm from requirements

233. By Anthony Lenton

Plugged in oops_dictconfig 0.0.5.
Display an oops id on every error page.
Enable oops tracking in dev.
Don't track oopses for 404 responses.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2012-07-13 19:45:12 +0000
3+++ .bzrignore 2013-03-01 18:28:22 +0000
4@@ -21,3 +21,4 @@
5 sourcecode/*
6 django_project/scaclient.py
7 django_project/txstatsd
8+oopses/
9
10=== modified file 'django_project/config_dev/config/main.cfg'
11--- django_project/config_dev/config/main.cfg 2012-08-07 13:01:38 +0000
12+++ django_project/config_dev/config/main.cfg 2013-03-01 18:28:22 +0000
13@@ -1,3 +1,6 @@
14+[__main__]
15+serve_static_media = True
16+
17 # Django settings for reviews project.
18 [django]
19 debug = True
20@@ -113,6 +116,7 @@
21 openid_update_details_from_sreg = True
22 openid_sso_server_url = https://login.staging.ubuntu.com/
23 openid_launchpad_teams_mapping = lp_team_mapping
24+openid_launchpad_staff_teams = rnr-moderators
25
26 [rnr]
27 moderators_group = rnr-moderators
28@@ -158,8 +162,8 @@
29 mem
30
31 [logging]
32-logging_config_file = logging.conf
33+logging_config_file = django_project/logging.conf
34
35 [graphite]
36 txstatsd_host = localhost
37-txstatsd_port = 8125
38\ No newline at end of file
39+txstatsd_port = 8125
40
41=== modified file 'django_project/config_dev/settings.py'
42--- django_project/config_dev/settings.py 2012-03-26 21:44:24 +0000
43+++ django_project/config_dev/settings.py 2013-03-01 18:28:22 +0000
44@@ -14,6 +14,3 @@
45 if os.path.exists(path)]
46
47 configglue(RnRSchema, config_files, __name__)
48-
49-if 'LOGGING_CONFIG_FILE' in locals():
50- logging.config.fileConfig(os.path.join(current_dir, LOGGING_CONFIG_FILE))
51
52=== modified file 'requirements.txt'
53--- requirements.txt 2013-02-25 19:42:16 +0000
54+++ requirements.txt 2013-03-01 18:28:22 +0000
55@@ -4,7 +4,7 @@
56 configglue==1.0.1
57 coverage
58 django-configglue==0.6.1
59-django-openid-auth==0.2
60+django-openid-auth==0.4
61 django-piston
62 django-preflight
63 django==1.3
64@@ -15,12 +15,14 @@
65 mechanize
66 mock
67 oauthlib
68+oops-wsgi==0.0.10
69+oops-dictconfig==0.0.5
70+oops-datedir_repo==0.0.17
71 pep8
72 psycopg2==2.4.1
73 python-openid
74 pytz
75 setuptools
76 south==0.7
77-storm
78 testtools
79 wsgi_intercept==0.4
80
81=== added file 'src/reviewsapp/management/commands/runserver.py'
82--- src/reviewsapp/management/commands/runserver.py 1970-01-01 00:00:00 +0000
83+++ src/reviewsapp/management/commands/runserver.py 2013-03-01 18:28:22 +0000
84@@ -0,0 +1,36 @@
85+# -*- coding: utf-8 -*-
86+# This file is part of Software Center Ratings and Reviews
87+# Copyright (C) 2010-2013 Canonical Ltd.
88+#
89+# This program is free software: you can redistribute it and/or modify
90+# it under the terms of the GNU Affero General Public License as
91+# published by the Free Software Foundation, either version 3 of the
92+# License, or (at your option) any later version.
93+#
94+# This program is distributed in the hope that it will be useful,
95+# but WITHOUT ANY WARRANTY; without even the implied warranty of
96+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
97+# GNU Affero General Public License for more details.
98+#
99+# You should have received a copy of the GNU Affero General Public License
100+# along with this program. If not, see <http://www.gnu.org/licenses/>.
101+
102+"""Customized runserver command that runs rnr-server's wsgi stack."""
103+
104+from optparse import make_option
105+from django.core.management.commands.runserver import BaseRunserverCommand
106+from django.core.servers.basehttp import AdminMediaHandler
107+
108+
109+class Command(BaseRunserverCommand):
110+ option_list = BaseRunserverCommand.option_list + (
111+ make_option(
112+ '--adminmedia', dest='admin_media_path', default='',
113+ help='Specifies the directory from which to serve admin media.'),
114+ )
115+
116+ def get_handler(self, *args, **options):
117+ """Serves admin media like old-school (deprecation pending)."""
118+ from reviewsapp.wsgi import make_app
119+ handler = make_app()
120+ return AdminMediaHandler(handler, options.get('admin_media_path', ''))
121
122=== modified file 'src/reviewsapp/schema.py'
123--- src/reviewsapp/schema.py 2012-08-29 20:01:15 +0000
124+++ src/reviewsapp/schema.py 2013-03-01 18:28:22 +0000
125@@ -26,6 +26,7 @@
126 import django
127 from configglue import schema
128 from django_configglue.schema import schemas
129+from oops_dictconfig.configglue_options import OopsOption
130
131 DjangoSchema = schemas.get(django.get_version())
132
133@@ -115,3 +116,19 @@
134 txstatsd_host = schema.StringOption()
135 txstatsd_port = schema.IntOption()
136 txstatsd_instance_type = schema.StringOption(default='staging')
137+
138+ # oops
139+ class oops_wsgi(schema.Section):
140+ oopses = OopsOption(
141+ default={
142+ 'publishers': [
143+ {
144+ 'type': 'datedir',
145+ 'error_dir': 'oopses',
146+ 'instance_id': 'dev',
147+ 'new_only': False,
148+ 'inherit_id': True,
149+ }
150+ ],
151+ }
152+ )
153
154=== modified file 'src/reviewsapp/templates/500.html'
155--- src/reviewsapp/templates/500.html 2011-04-01 15:08:05 +0000
156+++ src/reviewsapp/templates/500.html 2013-03-01 18:28:22 +0000
157@@ -9,9 +9,7 @@
158 <h1>{% trans 'Server Error <em>(500)</em>' %}</h1>
159 {% if errormsg %}<p>{{ errormsg }}</p>{% endif %}
160 <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>
161-<p>
162- {% blocktrans %}(Error <abbr>ID</abbr>: ){% endblocktrans %} {{ oopsid }}
163-</p>
164+<p id="oops-id">{{ oops_id }}</p>
165 <pre style="overflow:auto">
166 {{ traceback }}
167 </pre>
168
169=== modified file 'src/reviewsapp/tests/test_views.py'
170--- src/reviewsapp/tests/test_views.py 2012-11-21 11:05:24 +0000
171+++ src/reviewsapp/tests/test_views.py 2013-03-01 18:28:22 +0000
172@@ -533,7 +533,7 @@
173 # We need to reload urls.py to recalculate all urls
174 reload(reviewsapp.urls)
175
176- response = self.client.get('/reviews/die/')
177+ response = self.client.get('/reviews/delay/1.0/')
178
179 self.assertEquals(404, response.status_code)
180 reload(reviewsapp.urls)
181
182=== modified file 'src/reviewsapp/tests/test_wsgi.py'
183--- src/reviewsapp/tests/test_wsgi.py 2012-06-12 17:44:57 +0000
184+++ src/reviewsapp/tests/test_wsgi.py 2013-03-01 18:28:22 +0000
185@@ -1,3 +1,4 @@
186+import bson
187 import os
188
189 from tempfile import mkdtemp
190@@ -49,16 +50,6 @@
191 rmtree(self.oops_dir)
192 settings.MIDDLEWARE_CLASSES = self.orig_middleware
193
194- def request_404_exception(self, data={}):
195- """Helper for creating a 404 exception with a referrer."""
196- self.environ['PATH_INFO'] = '/reviews/made_up_url'
197- self.environ['HTTP_REFERER'] = '/'
198- if data != {}:
199- self.environ['QUERY_STRING'] = urlencode(data, doseq=True)
200-
201- response = self.request_wsgi_response(self.environ)
202- return response
203-
204 def request_500_exception(self, data={}):
205 """Helper for creating a 500 exception."""
206 self.environ['PATH_INFO'] = reverse('rnr-overview')
207@@ -73,7 +64,15 @@
208
209 def request_wsgi_response(self, environ):
210 start_response = Mock()
211- with patch_settings(OOPS_DIR=self.oops_dir):
212+ oops_config = {
213+ 'publishers': [{
214+ 'type': 'datedir',
215+ 'error_dir': self.oops_dir,
216+ 'inherit_id': True,
217+ 'new_only': False,
218+ }],
219+ }
220+ with patch_settings(OOPSES=oops_config):
221 app = make_app()
222 response = app(environ, start_response).next()
223 return response
224@@ -92,31 +91,19 @@
225 oops_path = self.oops_dir + '/' + todays_oops_path + '/' + oops_list[0]
226 with open(oops_path, "r") as oops_file:
227 oops_file_contents = oops_file.read()
228- return oops_file_contents
229-
230- def test_oopsid_in_404_response(self):
231- # The rendered response for a view exception includes the oops id.
232- response = self.request_404_exception()
233-
234- self.assertTrue(self.environ['OOPSID'] in response)
235+ return bson.loads(oops_file_contents)
236
237 def test_oopsid_in_500_response(self):
238 # The rendered response for a 500 exception includes the oops id.
239 response = self.request_500_exception()
240
241- self.assertTrue(self.environ['OOPSID'] in response)
242-
243- def test_exception_in_404_oops_on_disk(self):
244- # A request resulting in an 404 with a referrer writes an oops to disk
245- self.request_404_exception()
246-
247- self.assertTrue("Exception-Type:" in self.read_oops_from_disk())
248-
249- def test_traceback_in_500_oops_on_disk(self):
250+ oops_id = self.environ['oops.report']['id']
251+ self.assertTrue(oops_id in response)
252+
253+ def test_oops_id_in_500_oops_on_disk(self):
254 # A request resulting in a 500 writes the oops to disk.
255- self.request_500_exception()
256+ response = self.request_500_exception()
257
258- # The oops contains the stacktrace as part of a log message.
259- self.assertTrue(
260- "'message': 'Traceback (most recent call last):" in
261- self.read_oops_from_disk())
262+ oops_id = self.environ['oops.report']['id']
263+ oops_report = self.read_oops_from_disk()
264+ self.assertEqual(oops_id, oops_report['id'])
265
266=== modified file 'src/reviewsapp/urls.py'
267--- src/reviewsapp/urls.py 2012-06-20 12:19:49 +0000
268+++ src/reviewsapp/urls.py 2013-03-01 18:28:22 +0000
269@@ -58,6 +58,7 @@
270 (r'^forbidden', never_cache(moderation.forbidden)),
271
272 url(r'^logout/$', name='logout', view=logout, kwargs={'next_page': '/'}),
273+ url(r'^die/$', 'die', name='debug-die'),
274
275 # Include the API urls
276 (r'^api/', include('reviewsapp.api.urls')),
277@@ -66,6 +67,5 @@
278 if getattr(settings, 'ENABLE_ARTIFICIAL_OOPS', False):
279 urlpatterns += patterns(
280 'reviewsapp.views',
281- (r'^die/$', 'die'),
282 (r'^delay/(?P<amount>[\d.]+)/$', 'delay'),
283 )
284
285=== modified file 'src/reviewsapp/views/moderation.py'
286--- src/reviewsapp/views/moderation.py 2013-02-20 21:39:32 +0000
287+++ src/reviewsapp/views/moderation.py 2013-03-01 18:28:22 +0000
288@@ -27,14 +27,15 @@
289 ]
290
291
292+import json
293 from urllib import urlencode
294
295-from django import http
296-from django.db.models import Count
297 from django.contrib.auth import logout
298 from django.contrib.auth.decorators import login_required, permission_required
299 from django.core.urlresolvers import reverse
300+from django.db.models import Count
301 from django.http import (
302+ HttpResponse,
303 HttpResponseForbidden,
304 HttpResponseRedirect,
305 )
306@@ -42,7 +43,8 @@
307 get_object_or_404,
308 render_to_response,
309 )
310-from django.template import loader, RequestContext
311+from django.template import RequestContext
312+from django.template.loader import get_template
313 from django.utils.safestring import mark_safe
314 from django.utils.translation import ugettext as _
315
316@@ -205,18 +207,39 @@
317 self.errormsg = errormsg
318
319 def __call__(self, request):
320- return self.render_error(request)
321-
322- def render_error(self, request):
323- template = loader.get_template(self.template_name)
324- oopsid = request.META.get('OOPSID')
325+ accept = request.META.get('ACCEPT', '')
326+ if 'application/json' in accept and not 'text/html' in accept:
327+ return self.render_json_error(request)
328+ else:
329+ return self.render_html_error(request)
330+
331+ def render_json_error(self, request):
332+ oops_id = self.get_oops_id(request)
333+ oops_context = request.META.get('oops.context', {})
334+ exc_info = oops_context.get('exc_info', (None, None, None))
335+ exception_type = unicode(exc_info[0])
336+ response = json.dumps({
337+ 'oops_id': oops_id,
338+ 'error_type': exception_type,
339+ 'error_msg': "An error occurred and has been logged.",
340+ })
341+ return HttpResponse(response, status=self.status_code,
342+ content_type='application/json')
343+
344+ def render_html_error(self, request):
345+ oops_id = self.get_oops_id(request)
346+ template = get_template(self.template_name)
347 atts = {
348- 'oopsid': oopsid,
349+ 'oops_id': oops_id,
350 'errormsg': self.errormsg,
351 }
352 context = RequestContext(request, atts)
353- return http.HttpResponse(template.render(context),
354- status=self.status_code)
355+ return HttpResponse(template.render(context), status=self.status_code)
356+
357+ def get_oops_id(self, request):
358+ oops_report = request.environ.get('oops.report', {})
359+ return oops_report.get('id', 'OOPS ID not available.')
360+
361
362 server_error = ErrorPage(500)
363 page_not_found = ErrorPage(404)
364
365=== modified file 'src/reviewsapp/wsgi.py'
366--- src/reviewsapp/wsgi.py 2012-06-20 12:19:49 +0000
367+++ src/reviewsapp/wsgi.py 2013-03-01 18:28:22 +0000
368@@ -1,25 +1,78 @@
369+# This file is part of Software Center Ratings and Reviews
370+# Copyright (C) 2010-2013 Canonical Ltd.
371+#
372+# This program is free software: you can redistribute it and/or modify
373+# it under the terms of the GNU Affero General Public License as
374+# published by the Free Software Foundation, either version 3 of the
375+# License, or (at your option) any later version.
376+#
377+# This program is distributed in the hope that it will be useful,
378+# but WITHOUT ANY WARRANTY; without even the implied warranty of
379+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
380+# GNU Affero General Public License for more details.
381+#
382+# You should have received a copy of the GNU Affero General Public License
383+# along with this program. If not, see <http://www.gnu.org/licenses/>.
384+
385+"""wsgi stack for the Ratings and Reviews app."""
386+
387+import logging.config
388+import oops_dictconfig
389+import oops_wsgi
390+import oops_wsgi.django
391+import os
392 import platform
393+import uuid
394
395 from django.conf import settings
396-from django.core.handlers.wsgi import WSGIHandler
397-
398-from canonical.oops.wsgi import OopsWare
399-
400-
401-class RatingsWSGIHandler(WSGIHandler):
402- def __init__(self):
403- """Setup logging for the SCA web app."""
404- super(RatingsWSGIHandler, self).__init__()
405-
406-app = RatingsWSGIHandler()
407+from django.core.handlers import wsgi
408+
409+
410+class EagerOOPSWSGIHandler(wsgi.WSGIHandler):
411+ """Pre-generates an oops id on exception."""
412+ def handle_uncaught_exception(self, request, resolver, exc_info):
413+ # This will pass the exception information back to oops. It
414+ # should not be necessary once
415+ # https://code.djangoproject.com/ticket/16674 has been merged.
416+ if 'oops.context' in request.environ:
417+ request.environ['oops.context']['exc_info'] = exc_info
418+ # Generate OOPS id early so that we can render the error page right
419+ # away and not depend on passing thread locals around.
420+ if 'oops.report' in request.environ:
421+ unique_id = uuid.uuid4().hex
422+ request.environ['oops.report']['id'] = "OOPS-%s" % unique_id
423+
424+ return super(EagerOOPSWSGIHandler, self).handle_uncaught_exception(
425+ request, resolver, exc_info)
426+
427+ def __call__(self, environ, start_response):
428+ def start_response_with_exc_info(status, headers, exc_info=None):
429+ """Custom start_response callback for wsgi."""
430+ # This will pass the exception information back to oops_wgi. It
431+ # should not be necessary once
432+ # https://code.djangoproject.com/ticket/16674 has been merged.
433+ if exc_info is None:
434+ exc_info = environ['oops.context'].get('exc_info', None)
435+ return start_response(status, headers, exc_info)
436+ return super(EagerOOPSWSGIHandler, self).__call__(
437+ environ, start_response_with_exc_info)
438
439
440 def make_app():
441+ """Encapsulate our webcatalog handler in an OOPS app."""
442+ if settings.FORCE_HTTPS_IN_ENVIRONMENT:
443+ os.environ['HTTPS'] = 'on'
444+
445 default_id = ''.join(x for x in platform.node() if x.isalpha())
446 appserver_id = getattr(settings, 'APPSERVER_ID', default_id)
447- oops_dir = getattr(settings, 'OOPS_DIR', '/tmp')
448- oops_app = OopsWare(
449- app, oops_dir=oops_dir, key=appserver_id, hide_meta=False,
450- serializer_factory_name=(
451- 'canonical.oops.serializer.OOPSRFC822Serializer'))
452+ logging_config = settings.LOGGING_CONFIG_FILE
453+ if logging_config:
454+ logging.config.fileConfig(logging_config)
455+
456+ non_oops_app = EagerOOPSWSGIHandler()
457+
458+ config = oops_dictconfig.config_from_dict(settings.OOPSES)
459+ oops_wsgi.install_hooks(config)
460+ oops_app = oops_wsgi.make_app(non_oops_app, config,
461+ oops_on_status=['500'])
462 return oops_app

Subscribers

People subscribed via source and target branches