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
=== modified file 'dev_config/manifests/pkgme_service.pp'
--- dev_config/manifests/pkgme_service.pp 2012-01-19 19:13:04 +0000
+++ dev_config/manifests/pkgme_service.pp 2012-01-28 21:41:26 +0000
@@ -23,7 +23,7 @@
23 ensure => 'present',23 ensure => 'present',
24 gid => $unix_user,24 gid => $unix_user,
25 require => Group[$unix_user],25 require => Group[$unix_user],
26} 26}
2727
28exec { "psql-create-user-$postgres_user":28exec { "psql-create-user-$postgres_user":
29 command => "psql -c \"create user $postgres_user with password '$::postgres_password'\"",29 command => "psql -c \"create user $postgres_user with password '$::postgres_password'\"",
@@ -58,17 +58,6 @@
58 ],58 ],
59}59}
6060
61file { "$::basedir/django_project/production_paths.py":
62 content => template("production_paths.py.erb"),
63 owner => $unix_user,
64 group => $unix_user,
65 mode => 644,
66 require => [
67 User[$unix_user],
68 Group[$unix_user],
69 ],
70}
71
72file { "$::basedir/pkgme.log":61file { "$::basedir/pkgme.log":
73 owner => $unix_user,62 owner => $unix_user,
74 group => $unix_user,63 group => $unix_user,
@@ -76,6 +65,13 @@
76 ensure => present,65 ensure => present,
77}66}
7867
68file { "$::basedir/oopses":
69 owner => $unix_user,
70 group => $unix_user,
71 mode => 755,
72 ensure => directory,
73}
74
79file { "$::basedir/celery.log":75file { "$::basedir/celery.log":
80 owner => $unix_user,76 owner => $unix_user,
81 group => $unix_user,77 group => $unix_user,
@@ -97,7 +93,6 @@
97 user => $unix_user,93 user => $unix_user,
98 require => [94 require => [
99 File["$::basedir/django_project/production_credentials.cfg"],95 File["$::basedir/django_project/production_credentials.cfg"],
100 File["$::basedir/django_project/production_paths.py"],
101 File["$::basedir/pkgme.log"],96 File["$::basedir/pkgme.log"],
102 ],97 ],
103 logoutput => on_failure,98 logoutput => on_failure,
@@ -117,7 +112,6 @@
117 content => template("django.wsgi.erb"),112 content => template("django.wsgi.erb"),
118 require => [113 require => [
119 File["$::basedir/django_project/production_credentials.cfg"],114 File["$::basedir/django_project/production_credentials.cfg"],
120 File["$::basedir/django_project/production_paths.py"],
121 ]115 ]
122}116}
123117
124118
=== modified file 'dev_config/templates/django.wsgi.erb'
--- dev_config/templates/django.wsgi.erb 2012-01-18 23:45:14 +0000
+++ dev_config/templates/django.wsgi.erb 2012-01-28 21:41:26 +0000
@@ -1,19 +1,51 @@
1import os1import os
2import sys2import sys
33
4extra_paths = [4basepath = '<%= basedir %>'
5 '<%= basedir %>',5extra_paths = [basepath, '<%= basedir %>/src', '<%= basedir %>/django_project']
6 '<%= basedir %>/src',6
7 '<%= basedir %>/django_project',7sourcecode_path = os.path.abspath(os.path.join(basepath, 'sourcecode'))
8 '<%= basedir %>/sourcecode/pkgme',8for filename in os.listdir(sourcecode_path):
9 '<%= basedir %>/sourcecode/pkgme-binary',9 dependency_path = os.path.join(sourcecode_path, filename)
10]10 if os.path.isdir(dependency_path):
11 extra_paths.append(dependency_path)
12
11for path in extra_paths:13for path in extra_paths:
12 if path not in sys.path:14 if path not in sys.path:
13 sys.path.append(path)15 sys.path.insert(0, path)
1416
15os.environ['PKGME_LOG_DIR'] = '<%= basedir %>'17os.environ['PKGME_LOG_DIR'] = '<%= basedir %>'
16os.environ['DJANGO_SETTINGS_MODULE'] = 'django_project.settings'18os.environ['DJANGO_SETTINGS_MODULE'] = 'django_project.settings'
1719
18import django.core.handlers.wsgi20from django.conf import settings
19application = django.core.handlers.wsgi.WSGIHandler()21
22# This is needed to workaround a bug in DJango, see the file it is
23# implemented in for more details.
24from oops_wsgi.django import OOPSWSGIHandler
25
26application = OOPSWSGIHandler()
27
28###
29# Wrap the application in the Oops wsgi app to catch unhandled exceptions
30# and create oops for them.
31#
32# First we create the config that defines what to do with the oopses.
33
34import oops_dictconfig
35import oops_timeline
36from oops_wsgi import make_app, install_hooks
37from timeline import wsgi as timeline_wsgi
38from timeline_django import hooks, middleware, timeline_cursor
39
40config = oops_dictconfig.config_from_dict(settings.OOPSES)
41install_hooks(config)
42oops_timeline.install_hooks(config)
43
44receiver = hooks.TimelineReceiver(middleware.get_timeline)
45receiver.connect_to_signals()
46timeline_cursor.insert_timeline_cursors(middleware.get_timeline)
47
48application = timeline_wsgi.make_app(application)
49
50# Then we wrap the django app in the oops one
51application = make_app(application, config, oops_on_status=['500'])
2052
=== modified file 'dev_config/templates/production_credentials.cfg.erb'
--- dev_config/templates/production_credentials.cfg.erb 2012-01-24 21:11:22 +0000
+++ dev_config/templates/production_credentials.cfg.erb 2012-01-28 21:41:26 +0000
@@ -31,3 +31,18 @@
31class = logging.handlers.WatchedFileHandler31class = logging.handlers.WatchedFileHandler
32formatter = verbose32formatter = verbose
33filename = <%= basedir %>/django.log33filename = <%= basedir %>/django.log
34
35[oops_amqp_publisher]
36type = amqp
37host = <%= rabbit_host %>:<%= rabbit_port %>
38user = <%= rabbit_user %>
39password = <%= rabbit_password %>
40vhost = <%= rabbit_vhost %>
41exchange_name = oopses
42routing_key = oopses
43
44[oops_datedir_publisher]
45type = datedir
46error_dir = <%= basedir %>/oopses
47instance_id = production
48only_new = True
3449
=== removed file 'dev_config/templates/production_paths.py.erb'
--- dev_config/templates/production_paths.py.erb 2012-01-18 23:45:14 +0000
+++ dev_config/templates/production_paths.py.erb 1970-01-01 00:00:00 +0000
@@ -1,13 +0,0 @@
1import os
2import sys
3
4for path in [
5 '<%= basedir %>/sourcecode/pkgme-binary',
6 '<%= basedir %>/sourcecode/pkgme',
7 '<%= basedir %>/django_project',
8 '<%= basedir %>/src',
9 '<%= basedir %>',
10 ]:
11 sys.path.insert(0, path)
12
13os.environ['PKGME_LOG_DIR'] = '<%= basedir %>'
140
=== modified file 'django_project/dev.cfg'
--- django_project/dev.cfg 2012-01-24 20:49:55 +0000
+++ django_project/dev.cfg 2012-01-28 21:41:26 +0000
@@ -14,6 +14,16 @@
14 django_configglue14 django_configglue
15 djkombu15 djkombu
16databases = databases16databases = databases
17debug = False
18oopses = oops_config
19
20[oops_config]
21publishers = oops_dev_publisher
22
23[oops_dev_publisher]
24type = datedir
25error_dir = oopses
26instance_id = dev
1727
18[django_file_logging_handler]28[django_file_logging_handler]
19level = WARNING29level = WARNING
2030
=== modified file 'django_project/main.cfg'
--- django_project/main.cfg 2012-01-24 21:16:59 +0000
+++ django_project/main.cfg 2012-01-28 21:41:26 +0000
@@ -28,6 +28,7 @@
28middleware_classes =28middleware_classes =
29 django.middleware.common.CommonMiddleware29 django.middleware.common.CommonMiddleware
30 django.contrib.sessions.middleware.SessionMiddleware30 django.contrib.sessions.middleware.SessionMiddleware
31 timeline_django.request_tracker.TimelineMiddleware
31 django.middleware.csrf.CsrfViewMiddleware32 django.middleware.csrf.CsrfViewMiddleware
32 django.contrib.auth.middleware.AuthenticationMiddleware33 django.contrib.auth.middleware.AuthenticationMiddleware
33 django.contrib.messages.middleware.MessageMiddleware34 django.contrib.messages.middleware.MessageMiddleware
3435
=== modified file 'django_project/manage.py'
--- django_project/manage.py 2012-01-13 01:22:19 +0000
+++ django_project/manage.py 2012-01-28 21:41:26 +0000
@@ -1,8 +1,12 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2try:2import os
3 import production_paths3import sys
4except ImportError:4
5 pass5sourcecode_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'sourcecode'))
6for filename in os.listdir(sourcecode_path) + ['../src', '../django_project']:
7 dependency_path = os.path.join(sourcecode_path, filename)
8 if os.path.isdir(dependency_path):
9 sys.path.insert(0, dependency_path)
610
7from django.core.management import execute_manager11from django.core.management import execute_manager
8import imp12import imp
@@ -15,5 +19,19 @@
1519
16import settings20import settings
1721
22from oops_celery import setup_oops_reporter
23from oops_dictconfig import config_from_dict
24from timeline import Timeline
25from timeline_django import hooks, timeline_cursor
26
27# TODO: OOPSES in settings
28config = config_from_dict(settings.OOPSES)
29setup_oops_reporter(config)
30timeline = Timeline()
31receiver = hooks.TimelineReceiver(lambda: timeline)
32receiver.connect_to_signals()
33timeline_cursor.insert_timeline_cursors(lambda: timeline)
34
18if __name__ == "__main__":35if __name__ == "__main__":
36 # TODO: catch errors and generate oopses for manage.py commands.
19 execute_manager(settings)37 execute_manager(settings)
2038
=== modified file 'django_project/production.cfg'
--- django_project/production.cfg 2012-01-23 15:38:33 +0000
+++ django_project/production.cfg 2012-01-28 21:41:26 +0000
@@ -6,6 +6,11 @@
6#static_root =6#static_root =
7#static_url =7#static_url =
8#admin_media_prefix =8#admin_media_prefix =
9oopses = oops_config
10
11[oops_config]
12publishers = oops_amqp_publisher
13 oops_datedir_publisher
914
10[djpkgme]15[djpkgme]
11pkgme_output_directory = /srv/pkgme-service.canonical.com/var/packaged-applications/16pkgme_output_directory = /srv/pkgme-service.canonical.com/var/packaged-applications/
1217
=== modified file 'django_project/production_credentials.cfg.example'
--- django_project/production_credentials.cfg.example 2012-01-24 20:49:55 +0000
+++ django_project/production_credentials.cfg.example 2012-01-28 21:41:26 +0000
@@ -37,3 +37,18 @@
37class = logging.handlers.WatchedFileHandler37class = logging.handlers.WatchedFileHandler
38formatter = verbose38formatter = verbose
39filename = /var/log/pkgme-service/django.log39filename = /var/log/pkgme-service/django.log
40
41[oops_amqp_publisher]
42type = amqp
43host = #amqp host and optional port, e.g. localhost:5867
44user = #amqp user
45password = #amqp password
46vhost = #amqp vhost
47exchange_name = #amqp exchange name
48routing_key = #amqp routing key
49
50[oops_datedir_publisher]
51type = datedir
52error_dir = /var/cache/oopses
53instance_id = production
54only_new = True
4055
=== modified file 'fabtasks/deploy.py'
--- fabtasks/deploy.py 2012-01-24 23:38:20 +0000
+++ fabtasks/deploy.py 2012-01-28 21:41:26 +0000
@@ -218,12 +218,14 @@
218 run('echo "postfix postfix/main_mailer_type select No configuration" | sudo debconf-set-selections')218 run('echo "postfix postfix/main_mailer_type select No configuration" | sudo debconf-set-selections')
219 # Install the dependencies needed to get puppet going219 # Install the dependencies needed to get puppet going
220 # TODO: move the rest of the dependencies to puppet220 # TODO: move the rest of the dependencies to puppet
221 run('sudo apt-get install -q -y --force-yes pkgme-service-dependencies bzr apache2 libapache2-mod-wsgi rabbitmq-server postgresql-8.4 puppet')221 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')
222 # Grab the branches222 # Grab the branches
223 # TODO: investigate re-using IS' config-manager config223 # TODO: investigate re-using IS' config-manager config
224 run('bzr branch -q %s pkgme-service' % branch)224 run('bzr branch -q %s pkgme-service' % branch)
225 run('bzr branch -q %s pkgme-service/sourcecode/pkgme' % pkgme_branch)225 run('bzr branch -q %s pkgme-service/sourcecode/pkgme' % pkgme_branch)
226 run('bzr branch -q %s pkgme-service/sourcecode/pkgme-binary' % pkgme_binary_branch)226 run('bzr branch -q %s pkgme-service/sourcecode/pkgme-binary' % pkgme_binary_branch)
227 run('bzr branch -q lp:~james-w/+junk/python-timeline-django pkgme-service/sourcecode/python-timeline-django')
228 run('bzr branch -q lp:~james-w/python-oops-dictconfig/amqp-config pkgme-service/sourcecode/python-oops-dictconfig')
227 run('cd pkgme-service/sourcecode/pkgme && python setup.py build')229 run('cd pkgme-service/sourcecode/pkgme && python setup.py build')
228 run('cd pkgme-service/sourcecode/pkgme-binary && python setup.py build')230 run('cd pkgme-service/sourcecode/pkgme-binary && python setup.py build')
229 # Grab canonical-memento and use it?231 # Grab canonical-memento and use it?
230232
=== modified file 'src/djpkgme/views.py'
--- src/djpkgme/views.py 2012-01-24 20:49:55 +0000
+++ src/djpkgme/views.py 2012-01-28 21:41:26 +0000
@@ -6,4 +6,6 @@
6 return HttpResponse(ALL_CLEAR)6 return HttpResponse(ALL_CLEAR)
77
8def oops(request):8def oops(request):
9 from django.contrib.auth.models import User
10 [a for a in User.objects.filter(first_name__contains="a")]
9 raise AssertionError("Manually generated OOPS")11 raise AssertionError("Manually generated OOPS")

Subscribers

People subscribed via source and target branches