Merge lp:~james-w/pkgme-service/oops-integration into lp:pkgme-service

Proposed by James Westby
Status: Rejected
Rejected by: James Westby
Proposed branch: lp:~james-w/pkgme-service/oops-integration
Merge into: lp:pkgme-service
Prerequisite: lp:~james-w/pkgme-service/log-oopses
Diff against target: 311 lines (+123/-42)
11 files modified
dev_config/manifests/pkgme_service.pp (+8/-14)
dev_config/templates/django.wsgi.erb (+42/-10)
dev_config/templates/production_credentials.cfg.erb (+15/-0)
dev_config/templates/production_paths.py.erb (+0/-13)
django_project/dev.cfg (+10/-0)
django_project/main.cfg (+1/-0)
django_project/manage.py (+22/-4)
django_project/production.cfg (+5/-0)
django_project/production_credentials.cfg.example (+15/-0)
fabtasks/deploy.py (+3/-1)
src/djpkgme/views.py (+2/-0)
To merge this branch: bzr merge lp:~james-w/pkgme-service/oops-integration
Reviewer Review Type Date Requested Status
Canonical Consumer Applications Hackers Pending
Review via email: mp+90033@code.launchpad.net

Description of the change

Hi,

Here's a sketch of how oops integration works for a django app.

As it stands this hooks in at the wsgi layer, so it doesn't have
any effect on a local environment (with runserver.)

As it is written it just writes the oops to the filesystem. It's
easy to have it write the oops to rabbit instead I think.

You can spin up this branch in ec2, hit /pkgme/+oops and see
an oops appear in ~/pkgme-service/oopses on the instance.

As it stands the caller doesn't get told the oops id, that's a bug in django.
I'm putting an irc log from the end where lifeless and jamesh discuss
ways of doing it.

I'm going to set this to "Work in progress" as I don't think we should
land it as-is. I mainly did this to have a better understanding for discussing
it on the IS call tomorrow.

Thanks,

James

<james_w> lifeless, have you looked in to how to encourage django to use a response with the oops id on an uncaught exception?
<james_w> if you don't provide a 500 template then you get the oops one, but the oops it points to is an exception complaining that you don't have a 500 template
<lifeless> jamesh: has
<lifeless> oh, for oops-tools itself I know we need to provide a 500 template
<lifeless> there is a patch for django, last I checked it was a bit stalled
<lifeless> https://code.djangoproject.com/ticket/16674
<james_w> oh, I thought that was what the django.py fixed
<lifeless> it works around it
<james_w> http://ec2-107-22-92-8.compute-1.amazonaws.com/pkgme/+oops is what I'm getting currently
<lifeless> https://code.djangoproject.com/attachment/ticket/16674/wsgi-expose-exc-info-v2.patch
<lifeless> thats the actual fix
<lifeless> the django module in oops-wsgi sniffs the exception to ensure you get an oops
<lifeless> but it can't influence django to make it expose the error to the wsgi stack
<jamesh> james_w: you mean to display the OOPS ID on the error page?
<lifeless> so the main oops module just sees a successful reponse
<james_w> jamesh, yeah
<james_w> lifeless, ah, I see
<jamesh> james_w: I haven't been able to work out a good way to solve that with the new infrastructure
<james_w> so if it were properly fixed then oops' response would take over
<james_w> ok, it's not so important to us at this point
<lifeless> james_w: do you get an OOPS emitted to (rabbit|disk)
<james_w> lifeless, yep
<jamesh> the old oops code we have in U1 can preallocate the oops IDs on demand, so we can use that in the error page
<lifeless> jamesh: we could allow that too, if you wanted to use a timeuuid or something, and put the id in the context
<jamesh> one idea I had for the new code might be to set a short lived cookie in the WSGI middleware, and look that up with JS in the error page
<lifeless> jamesh: ooh, nice.
<jamesh> that has the downside that cookies are shared between all tabs in a browser
<james_w> lifeless, for this patch, how much do you care about a test?
<james_w> it's not the easiest thing to test after all
<lifeless> 'meh'
<jamesh> so if you open 20 pages at once and five fail, you might see the same OOPS ID on each
<lifeless> yeah
<lifeless> uhm
* poolie has quit (Quit: Ex-Chat)
<lifeless> wonder if you can inspect the response headers from the page itself
<lifeless> jamesh: shoving id in the context would be fine.
<lifeless> james_w: should be easy to test if you want to; just have an outer start_response(*args, **kwargs) and check kwargs is empty
<james_w> lifeless, ah, good point
<jamesh> I don't think there is a way to inspect response headers unfortunately
<lifeless> so, a django error template that puts content['oops.context']['id'] = str(uuid.uuid1()) => win
<lifeless> jamesh: ^
<wgrant> Hmm
<wgrant> Regression
<wgrant> 5633 AssertionError: Ambiguous view name.
<wgrant> GET: 5629 Other: 4 Robots: 98 Local: 239
<wgrant> 168 http://feeds.launchpad.net/%7Ebarry/latest-bugs.atom (Person:latest-bugs.atom)
<wgrant> OOPS-00033c67366d1e7513397953f71c9b29, OOPS-001af265e7d5322fdd12c9b12b4fed8c, OOPS-00ae6fedb93aa14a7fc508d51b9fa6a7, OOPS-010f606986cdc5008d999fabe085e6aa, OOPS-01432c961a9928b26bb0d210d723a53b
<wgrant> Ah
<wgrant> I bet it's the bug listings release
<wgrant> It doesn't work with feeds.
<wgrant> Which are always anonymous, so were never tested.
<wgrant> lifeless: ^^
<lifeless> grah
<lifeless> I guess we need to rollback
<jamesh> lifeless: I guess that would work. Do we lose anything by having the view assign an ID instead of the publisher doing so?
<wgrant> lifeless: It's been broken for a day, and it's a major thing.
<wgrant> "it" == bug listings
<wgrant> So I think we should keep it on.
<wgrant> It's not as if anybody uses feeds anyway.
<lifeless> well, 5633 polls for them
<lifeless> wgrant: lets start with a bug
<lifeless> jamesh: I think its fine to allow the earliest point that knows an error is on the way to assign an id - the hash based aproach is just one way to get a unique id
<lifeless> jamesh: oops can render errors itself
<lifeless> jamesh: or you could pass a django template into oops
<lifeless> jamesh: or you can assign the id earlier, if the framework really wants to render its own errors
<lifeless> jamesh: the existing facilities are the trivial string template that oops has itself or a callback you can supply to the middleware constructor
<jamesh> lifeless: I think giving frameworks the ability to render their own error pages makes python-oops-wsgi more appealing, since it can be dropped into an existing app
<lifeless> jamesh: I think the first thing I would attempt would be to discard the django error page but wire up the callback to let me use a django template to render the error page
<james_w> so, I can't do this patch as I am yet to get close to having a working buildout
<wgrant> lifeless: Hm
<lifeless> james_w: oh? what happens?
<wgrant> 'bugs.dynamic_bug_listings.enabled pageid:Person:latest-bugs.atom 2 '?
<lifeless> wgrant: might work, we should try
<wgrant> That seems to be the only popular affected page.
<jamesh> lifeless: well, once we've got that django bug report I filed fixed, that should be possible: if start_response() is called with an exc_info, just re-raise it
<lifeless> jamesh: that seems to be slightly different
<jamesh> then present an error page after the exception bubbles back up to the middleware
<lifeless> oh right
<lifeless> so yes, get the middleware's start_response to be called with exc_info
<jamesh> with that in mind, the debug error page Django presents is pretty nice
<lifeless> ah, make_app(template=...)
<jamesh> so when developing an app, I'd prefer to see that than an oops-wsgi error page
<lifeless> jamesh: sure, but not for deployed apps ?
<lifeless> jamesh: so have dev mode eat the exceptions and render itself
<lifeless> jamesh: anyhow, I'm happy to let the app allocate ids
<lifeless> pragmatic
<james_w> lifeless, confusing error messages
<jamesh> lifeless: I'm of two minds there. The error pages we've got wired up in U1 don't do much , so would be pretty easy to reimplement in the middleware
<lifeless> james_w: pastebin ?
<james_w> apparently when it said I might have misspelled a dependency, it really meant that it couldn't install from the cache because the cache is currently empty
<lifeless> james_w: you're using the LP cache, right ?
<jamesh> on the other hand, if I could just get/create an OOPS ID in the existing error page code, that would be a much smaller change
<james_w> lifeless, nope
<lifeless> james_w: use the LP cache
<jamesh> and I suspect other people trying to integrate python-oops would feel the same
<lifeless> jamesh: these are not exclusive options
<jamesh> lifeless: I understand that.
<lifeless> :)
<lifeless> I think we should make it easy to get going
<jamesh> I'm just thinking out loud about why I might pick one option over the other
<lifeless> with better/more organised stuff being something you can grow into
<lifeless> jamesh: I'm glad you are doing so :) I was just adding commentary

To post a comment you must log in.
41. By James Westby

Try out the new oops stuff.

Unmerged revisions

41. By James Westby

Try out the new oops stuff.

40. By James Westby

Install the extra packages (until pkgme-service-dependencies depends on them)

39. By James Westby

Merge the log oops branch.

38. By James Westby

Add a basic oops integration to the wsgi for devstaging deployments.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dev_config/manifests/pkgme_service.pp'
2--- dev_config/manifests/pkgme_service.pp 2012-01-19 19:13:04 +0000
3+++ dev_config/manifests/pkgme_service.pp 2012-01-28 21:41:26 +0000
4@@ -23,7 +23,7 @@
5 ensure => 'present',
6 gid => $unix_user,
7 require => Group[$unix_user],
8-}
9+}
10
11 exec { "psql-create-user-$postgres_user":
12 command => "psql -c \"create user $postgres_user with password '$::postgres_password'\"",
13@@ -58,17 +58,6 @@
14 ],
15 }
16
17-file { "$::basedir/django_project/production_paths.py":
18- content => template("production_paths.py.erb"),
19- owner => $unix_user,
20- group => $unix_user,
21- mode => 644,
22- require => [
23- User[$unix_user],
24- Group[$unix_user],
25- ],
26-}
27-
28 file { "$::basedir/pkgme.log":
29 owner => $unix_user,
30 group => $unix_user,
31@@ -76,6 +65,13 @@
32 ensure => present,
33 }
34
35+file { "$::basedir/oopses":
36+ owner => $unix_user,
37+ group => $unix_user,
38+ mode => 755,
39+ ensure => directory,
40+}
41+
42 file { "$::basedir/celery.log":
43 owner => $unix_user,
44 group => $unix_user,
45@@ -97,7 +93,6 @@
46 user => $unix_user,
47 require => [
48 File["$::basedir/django_project/production_credentials.cfg"],
49- File["$::basedir/django_project/production_paths.py"],
50 File["$::basedir/pkgme.log"],
51 ],
52 logoutput => on_failure,
53@@ -117,7 +112,6 @@
54 content => template("django.wsgi.erb"),
55 require => [
56 File["$::basedir/django_project/production_credentials.cfg"],
57- File["$::basedir/django_project/production_paths.py"],
58 ]
59 }
60
61
62=== modified file 'dev_config/templates/django.wsgi.erb'
63--- dev_config/templates/django.wsgi.erb 2012-01-18 23:45:14 +0000
64+++ dev_config/templates/django.wsgi.erb 2012-01-28 21:41:26 +0000
65@@ -1,19 +1,51 @@
66 import os
67 import sys
68
69-extra_paths = [
70- '<%= basedir %>',
71- '<%= basedir %>/src',
72- '<%= basedir %>/django_project',
73- '<%= basedir %>/sourcecode/pkgme',
74- '<%= basedir %>/sourcecode/pkgme-binary',
75-]
76+basepath = '<%= basedir %>'
77+extra_paths = [basepath, '<%= basedir %>/src', '<%= basedir %>/django_project']
78+
79+sourcecode_path = os.path.abspath(os.path.join(basepath, 'sourcecode'))
80+for filename in os.listdir(sourcecode_path):
81+ dependency_path = os.path.join(sourcecode_path, filename)
82+ if os.path.isdir(dependency_path):
83+ extra_paths.append(dependency_path)
84+
85 for path in extra_paths:
86 if path not in sys.path:
87- sys.path.append(path)
88+ sys.path.insert(0, path)
89
90 os.environ['PKGME_LOG_DIR'] = '<%= basedir %>'
91 os.environ['DJANGO_SETTINGS_MODULE'] = 'django_project.settings'
92
93-import django.core.handlers.wsgi
94-application = django.core.handlers.wsgi.WSGIHandler()
95+from django.conf import settings
96+
97+# This is needed to workaround a bug in DJango, see the file it is
98+# implemented in for more details.
99+from oops_wsgi.django import OOPSWSGIHandler
100+
101+application = OOPSWSGIHandler()
102+
103+###
104+# Wrap the application in the Oops wsgi app to catch unhandled exceptions
105+# and create oops for them.
106+#
107+# First we create the config that defines what to do with the oopses.
108+
109+import oops_dictconfig
110+import oops_timeline
111+from oops_wsgi import make_app, install_hooks
112+from timeline import wsgi as timeline_wsgi
113+from timeline_django import hooks, middleware, timeline_cursor
114+
115+config = oops_dictconfig.config_from_dict(settings.OOPSES)
116+install_hooks(config)
117+oops_timeline.install_hooks(config)
118+
119+receiver = hooks.TimelineReceiver(middleware.get_timeline)
120+receiver.connect_to_signals()
121+timeline_cursor.insert_timeline_cursors(middleware.get_timeline)
122+
123+application = timeline_wsgi.make_app(application)
124+
125+# Then we wrap the django app in the oops one
126+application = make_app(application, config, oops_on_status=['500'])
127
128=== modified file 'dev_config/templates/production_credentials.cfg.erb'
129--- dev_config/templates/production_credentials.cfg.erb 2012-01-24 21:11:22 +0000
130+++ dev_config/templates/production_credentials.cfg.erb 2012-01-28 21:41:26 +0000
131@@ -31,3 +31,18 @@
132 class = logging.handlers.WatchedFileHandler
133 formatter = verbose
134 filename = <%= basedir %>/django.log
135+
136+[oops_amqp_publisher]
137+type = amqp
138+host = <%= rabbit_host %>:<%= rabbit_port %>
139+user = <%= rabbit_user %>
140+password = <%= rabbit_password %>
141+vhost = <%= rabbit_vhost %>
142+exchange_name = oopses
143+routing_key = oopses
144+
145+[oops_datedir_publisher]
146+type = datedir
147+error_dir = <%= basedir %>/oopses
148+instance_id = production
149+only_new = True
150
151=== removed file 'dev_config/templates/production_paths.py.erb'
152--- dev_config/templates/production_paths.py.erb 2012-01-18 23:45:14 +0000
153+++ dev_config/templates/production_paths.py.erb 1970-01-01 00:00:00 +0000
154@@ -1,13 +0,0 @@
155-import os
156-import sys
157-
158-for path in [
159- '<%= basedir %>/sourcecode/pkgme-binary',
160- '<%= basedir %>/sourcecode/pkgme',
161- '<%= basedir %>/django_project',
162- '<%= basedir %>/src',
163- '<%= basedir %>',
164- ]:
165- sys.path.insert(0, path)
166-
167-os.environ['PKGME_LOG_DIR'] = '<%= basedir %>'
168
169=== modified file 'django_project/dev.cfg'
170--- django_project/dev.cfg 2012-01-24 20:49:55 +0000
171+++ django_project/dev.cfg 2012-01-28 21:41:26 +0000
172@@ -14,6 +14,16 @@
173 django_configglue
174 djkombu
175 databases = databases
176+debug = False
177+oopses = oops_config
178+
179+[oops_config]
180+publishers = oops_dev_publisher
181+
182+[oops_dev_publisher]
183+type = datedir
184+error_dir = oopses
185+instance_id = dev
186
187 [django_file_logging_handler]
188 level = WARNING
189
190=== modified file 'django_project/main.cfg'
191--- django_project/main.cfg 2012-01-24 21:16:59 +0000
192+++ django_project/main.cfg 2012-01-28 21:41:26 +0000
193@@ -28,6 +28,7 @@
194 middleware_classes =
195 django.middleware.common.CommonMiddleware
196 django.contrib.sessions.middleware.SessionMiddleware
197+ timeline_django.request_tracker.TimelineMiddleware
198 django.middleware.csrf.CsrfViewMiddleware
199 django.contrib.auth.middleware.AuthenticationMiddleware
200 django.contrib.messages.middleware.MessageMiddleware
201
202=== modified file 'django_project/manage.py'
203--- django_project/manage.py 2012-01-13 01:22:19 +0000
204+++ django_project/manage.py 2012-01-28 21:41:26 +0000
205@@ -1,8 +1,12 @@
206 #!/usr/bin/env python
207-try:
208- import production_paths
209-except ImportError:
210- pass
211+import os
212+import sys
213+
214+sourcecode_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'sourcecode'))
215+for filename in os.listdir(sourcecode_path) + ['../src', '../django_project']:
216+ dependency_path = os.path.join(sourcecode_path, filename)
217+ if os.path.isdir(dependency_path):
218+ sys.path.insert(0, dependency_path)
219
220 from django.core.management import execute_manager
221 import imp
222@@ -15,5 +19,19 @@
223
224 import settings
225
226+from oops_celery import setup_oops_reporter
227+from oops_dictconfig import config_from_dict
228+from timeline import Timeline
229+from timeline_django import hooks, timeline_cursor
230+
231+# TODO: OOPSES in settings
232+config = config_from_dict(settings.OOPSES)
233+setup_oops_reporter(config)
234+timeline = Timeline()
235+receiver = hooks.TimelineReceiver(lambda: timeline)
236+receiver.connect_to_signals()
237+timeline_cursor.insert_timeline_cursors(lambda: timeline)
238+
239 if __name__ == "__main__":
240+ # TODO: catch errors and generate oopses for manage.py commands.
241 execute_manager(settings)
242
243=== modified file 'django_project/production.cfg'
244--- django_project/production.cfg 2012-01-23 15:38:33 +0000
245+++ django_project/production.cfg 2012-01-28 21:41:26 +0000
246@@ -6,6 +6,11 @@
247 #static_root =
248 #static_url =
249 #admin_media_prefix =
250+oopses = oops_config
251+
252+[oops_config]
253+publishers = oops_amqp_publisher
254+ oops_datedir_publisher
255
256 [djpkgme]
257 pkgme_output_directory = /srv/pkgme-service.canonical.com/var/packaged-applications/
258
259=== modified file 'django_project/production_credentials.cfg.example'
260--- django_project/production_credentials.cfg.example 2012-01-24 20:49:55 +0000
261+++ django_project/production_credentials.cfg.example 2012-01-28 21:41:26 +0000
262@@ -37,3 +37,18 @@
263 class = logging.handlers.WatchedFileHandler
264 formatter = verbose
265 filename = /var/log/pkgme-service/django.log
266+
267+[oops_amqp_publisher]
268+type = amqp
269+host = #amqp host and optional port, e.g. localhost:5867
270+user = #amqp user
271+password = #amqp password
272+vhost = #amqp vhost
273+exchange_name = #amqp exchange name
274+routing_key = #amqp routing key
275+
276+[oops_datedir_publisher]
277+type = datedir
278+error_dir = /var/cache/oopses
279+instance_id = production
280+only_new = True
281
282=== modified file 'fabtasks/deploy.py'
283--- fabtasks/deploy.py 2012-01-24 23:38:20 +0000
284+++ fabtasks/deploy.py 2012-01-28 21:41:26 +0000
285@@ -218,12 +218,14 @@
286 run('echo "postfix postfix/main_mailer_type select No configuration" | sudo debconf-set-selections')
287 # Install the dependencies needed to get puppet going
288 # TODO: move the rest of the dependencies to puppet
289- run('sudo apt-get install -q -y --force-yes pkgme-service-dependencies bzr apache2 libapache2-mod-wsgi rabbitmq-server postgresql-8.4 puppet')
290+ run('sudo apt-get install -q -y --force-yes pkgme-service-dependencies bzr apache2 libapache2-mod-wsgi rabbitmq-server postgresql-8.4 puppet python-oops-wsgi python-oops-datedir-repo python-oops-celery python-oops-amqp python-timeline python-oops-timeline')
291 # Grab the branches
292 # TODO: investigate re-using IS' config-manager config
293 run('bzr branch -q %s pkgme-service' % branch)
294 run('bzr branch -q %s pkgme-service/sourcecode/pkgme' % pkgme_branch)
295 run('bzr branch -q %s pkgme-service/sourcecode/pkgme-binary' % pkgme_binary_branch)
296+ run('bzr branch -q lp:~james-w/+junk/python-timeline-django pkgme-service/sourcecode/python-timeline-django')
297+ run('bzr branch -q lp:~james-w/python-oops-dictconfig/amqp-config pkgme-service/sourcecode/python-oops-dictconfig')
298 run('cd pkgme-service/sourcecode/pkgme && python setup.py build')
299 run('cd pkgme-service/sourcecode/pkgme-binary && python setup.py build')
300 # Grab canonical-memento and use it?
301
302=== modified file 'src/djpkgme/views.py'
303--- src/djpkgme/views.py 2012-01-24 20:49:55 +0000
304+++ src/djpkgme/views.py 2012-01-28 21:41:26 +0000
305@@ -6,4 +6,6 @@
306 return HttpResponse(ALL_CLEAR)
307
308 def oops(request):
309+ from django.contrib.auth.models import User
310+ [a for a in User.objects.filter(first_name__contains="a")]
311 raise AssertionError("Manually generated OOPS")

Subscribers

People subscribed via source and target branches