Merge lp:~lifeless/python-oops-tools/amqp into lp:python-oops-tools

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 2
Proposed branch: lp:~lifeless/python-oops-tools/amqp
Merge into: lp:python-oops-tools
Diff against target: 408 lines (+242/-29)
8 files modified
buildout.cfg (+1/-1)
setup.py (+2/-0)
src/oopstools/NEWS.txt (+7/-2)
src/oopstools/README.txt (+15/-4)
src/oopstools/oops/models.py (+37/-20)
src/oopstools/oops/test/test_amqp2disk.py (+40/-0)
src/oopstools/scripts/amqp2disk.py (+137/-0)
versions.cfg (+3/-2)
To merge this branch: bzr merge lp:~lifeless/python-oops-tools/amqp
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+79505@code.launchpad.net

Commit message

Add AMQP support and improve docs.

Description of the change

Build on the recent improvements in oops-datedir-repo and oops-amqp to add an amqp queue worker that will take oopses from amqp and load them straight into OOPS-tools. Also fix docs (0.6 is the current release) and stop telling folk to edit product.cfg in-place.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

This looks fine to me.

review: Approve (code)
lp:~lifeless/python-oops-tools/amqp updated
2. By Robert Collins

Add AMQP support and improve docs.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'buildout.cfg'
--- buildout.cfg 2011-10-13 20:18:51 +0000
+++ buildout.cfg 2011-10-16 23:09:17 +0000
@@ -54,7 +54,7 @@
54recipe = djangorecipe54recipe = djangorecipe
55version = 1.355version = 1.3
56project = oopstools56project = oopstools
57projectegg = oops-tools57projectegg = oopstools
58settings = settings58settings = settings
59test = oopstools59test = oopstools
60eggs = oops-tools60eggs = oops-tools
6161
=== modified file 'setup.py'
--- setup.py 2011-10-13 20:18:51 +0000
+++ setup.py 2011-10-16 23:09:17 +0000
@@ -57,6 +57,7 @@
57 'launchpadlib',57 'launchpadlib',
58 'lazr.config',58 'lazr.config',
59 'oops',59 'oops',
60 'oops-amqp',
60 'oops-datedir-repo',61 'oops-datedir-repo',
61 'pytz',62 'pytz',
62 'setuptools',63 'setuptools',
@@ -79,6 +80,7 @@
79 ),80 ),
80 entry_points=dict(81 entry_points=dict(
81 console_scripts=[ # `console_scripts` is a magic name to setuptools82 console_scripts=[ # `console_scripts` is a magic name to setuptools
83 'amqp2disk = oopstools.scripts.amqp2disk:main',
82 'analyse_error_reports = oopstools.scripts.analyse_error_reports:main',84 'analyse_error_reports = oopstools.scripts.analyse_error_reports:main',
83 'load_sample_data = oopstools.scripts.load_sample_data:main',85 'load_sample_data = oopstools.scripts.load_sample_data:main',
84 'update_db = oopstools.scripts.update_db:main',86 'update_db = oopstools.scripts.update_db:main',
8587
=== modified file 'src/oopstools/NEWS.txt'
--- src/oopstools/NEWS.txt 2011-10-13 20:18:51 +0000
+++ src/oopstools/NEWS.txt 2011-10-16 23:09:17 +0000
@@ -2,8 +2,13 @@
2NEWS for oopstools2NEWS for oopstools
3===================3===================
44
50.5 (UNRELEASED)5NEXT
6================6====
7
8* Added AMQP support via the bin/amqp2disk script. (Robert Collins)
9
100.6
11===
712
8* Initial release13* Initial release
914
1015
=== modified file 'src/oopstools/README.txt'
--- src/oopstools/README.txt 2011-10-13 20:18:51 +0000
+++ src/oopstools/README.txt 2011-10-16 23:09:17 +0000
@@ -51,19 +51,21 @@
51Deployment using mod_wsgi51Deployment using mod_wsgi
52=========================52=========================
5353
54Update the production.cfg file with your database configuration and the paths54Create a custom cfg file - start with production.cfg and take a copy. Update
55to your OOPS directories:55your copy with your database configuration and the paths to your OOPS
56directories:
5657
57[configuration]58[configuration]
58db-name = /path/to/your/oops.db59db-name = /path/to/your/oops.db
59index-template = 'index.html'60index-template = 'index.html'
60oopsdir = /path/to/oops/reports61oopsdir = /path/where/rsynced/oopses/are/found
62 /another/such/path
6163
62Update settings.py setting a custom SECRET_KEY64Update settings.py setting a custom SECRET_KEY
6365
64To deploy oops tools make sure all the dependecies are installed.66To deploy oops tools make sure all the dependecies are installed.
6567
66 * bin/buildout -c production.cfg68 * bin/buildout -c yourfilename.cfg
6769
68 * Run bin/django syncdb70 * Run bin/django syncdb
6971
@@ -71,6 +73,15 @@
7173
72 * Copy apache/oops-tools.dev.mod_wsgi to /etc/apache2/sites-available/74 * Copy apache/oops-tools.dev.mod_wsgi to /etc/apache2/sites-available/
7375
76AMQP Integration
77================
78
79The script bin/amqp2disk is an AMQP handler that will receive OOPS reports over
80AMQP and publish them locally to disk as well as loading the metadata directly
81into the oops-tools database. To use this you will need to config your OOPS
82creation to publish over AMQP. If you are using Python then the oops-amqp
83module will help you do this.
84
74Running locally85Running locally
75===============86===============
7687
7788
=== modified file 'src/oopstools/oops/models.py'
--- src/oopstools/oops/models.py 2011-10-13 20:18:51 +0000
+++ src/oopstools/oops/models.py 2011-10-16 23:09:17 +0000
@@ -21,6 +21,8 @@
21import datetime21import datetime
22import os.path22import os.path
2323
24from pytz import utc
25
24from django.db import models26from django.db import models
25from django.db.models.signals import pre_save27from django.db.models.signals import pre_save
26import oops_datedir_repo.serializer28import oops_datedir_repo.serializer
@@ -278,7 +280,11 @@
278 prefix = oops.get('reporter')280 prefix = oops.get('reporter')
279 if not prefix:281 if not prefix:
280 # Legacy support for pre-reporter using OOPSes.282 # Legacy support for pre-reporter using OOPSes.
281 prefix = oops_re.match(oopsid).group('oopsprefix')283 prefix_match = oops_re.match(oopsid)
284 if prefix_match is not None:
285 prefix = prefix_match.group('oopsprefix')
286 else:
287 prefix = 'UNKNOWN'
282 prefix = prefix.upper()288 prefix = prefix.upper()
283 try:289 try:
284 prefix = Prefix.objects.get(value__exact=prefix)290 prefix = Prefix.objects.get(value__exact=prefix)
@@ -329,9 +335,10 @@
329 if total_time < 0:335 if total_time < 0:
330 total_time = 0336 total_time = 0
331 # Get the oops infestation337 # Get the oops infestation
332 exception_type = oops.get('type')338 exception_type = oops.get('type') or ''
339 exception_value = oops.get('value') or ''
333 exception_value = _normalize_exception_value(340 exception_value = _normalize_exception_value(
334 exception_type, oops.get('value'), prefix)341 exception_type, exception_value, prefix)
335 try:342 try:
336 infestation = Infestation.objects.get(343 infestation = Infestation.objects.get(
337 exception_type__exact=exception_type,344 exception_type__exact=exception_type,
@@ -361,10 +368,13 @@
361 most_expensive_statement = conform(most_expensive_statement, 200)368 most_expensive_statement = conform(most_expensive_statement, 200)
362 url = conform(oops.get('url') or '', MAX_URL_LEN)369 url = conform(oops.get('url') or '', MAX_URL_LEN)
363 informational = oops.get('informational', 'False').lower() == 'true'370 informational = oops.get('informational', 'False').lower() == 'true'
371 oops_date = oops.get('time')
372 if oops_date is None:
373 oops_date = datetime.datetime.now(utc)
364 data.update(374 data.update(
365 oopsid = oopsid,375 oopsid = oopsid,
366 prefix = prefix,376 prefix = prefix,
367 date = oops.get('time').replace(microsecond=0),377 date = oops_date.replace(microsecond=0),
368 # Missing pageids are urls because that suits our queries.378 # Missing pageids are urls because that suits our queries.
369 pageid = oops.get('topic') or url,379 pageid = oops.get('topic') or url,
370 url = url,380 url = url,
@@ -377,7 +387,28 @@
377 is_bot = _robot_pat.search(data['user_agent']) is not None,387 is_bot = _robot_pat.search(data['user_agent']) is not None,
378 statements_count = len(statements),388 statements_count = len(statements),
379 )389 )
380 return data, req_vars, statements, oops.get('tb_text')390 return data, req_vars, statements, oops.get('tb_text') or ''
391
392
393def parsed_oops_to_model_oops(parsed_oops, pathname):
394 """Convert an oops report dict to an Oops object."""
395 data, req_vars, statements, traceback = _get_oops_tuple(parsed_oops)
396 data['pathname'] = pathname
397 res = Oops(**data)
398 res.appinstance = res.get_appinstance()
399 res.save()
400 # Get it again. Otherwise we have discrepancies between old and
401 # new oops objects: old ones have unicode attributes, and new
402 # ones have string attributes, for instance. Ideally the message
403 # conversion would have converted everything to unicode, but it
404 # doesn't easily.
405 res = Oops.objects.get(oopsid__exact=parsed_oops['id'])
406 res.parsed_oops = parsed_oops
407 res.req_vars = req_vars
408 res.statements = statements
409 res.traceback = traceback
410 res.save()
411 return res
381412
382413
383class Oops(models.Model):414class Oops(models.Model):
@@ -432,21 +463,7 @@
432 try:463 try:
433 res = cls.objects.get(oopsid__exact=oopsid)464 res = cls.objects.get(oopsid__exact=oopsid)
434 except cls.DoesNotExist:465 except cls.DoesNotExist:
435 data, req_vars, statements, traceback = _get_oops_tuple(parsed_oops)466 res = parsed_oops_to_model_oops(parsed_oops, pathname)
436 data['pathname'] = pathname
437 res = cls(**data)
438 res.appinstance = res.get_appinstance()
439 res.save()
440 # Get it again. Otherwise we have discrepancies between old and
441 # new oops objects: old ones have unicode attributes, and new
442 # ones have string attributes, for instance. Ideally the message
443 # conversion would have converted everything to unicode, but it
444 # doesn't easily.
445 res = cls.objects.get(oopsid__exact=oopsid)
446 res.parsed_oops = parsed_oops
447 res.req_vars = req_vars
448 res.statements = statements
449 res.traceback = traceback
450 return res467 return res
451468
452 @readproperty469 @readproperty
453470
=== added file 'src/oopstools/oops/test/test_amqp2disk.py'
--- src/oopstools/oops/test/test_amqp2disk.py 1970-01-01 00:00:00 +0000
+++ src/oopstools/oops/test/test_amqp2disk.py 2011-10-16 23:09:17 +0000
@@ -0,0 +1,40 @@
1# Copyright 2005-2011 Canonical Ltd. All rights reserved.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Affero General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16
17import os.path
18
19import bson
20from fixtures import TempDir
21from testtools import TestCase
22
23from oopstools.oops.models import Oops
24from oopstools.scripts import amqp2disk
25
26
27class TestOOPSConfig(TestCase):
28
29 def test_publishes_disk_and_DB(self):
30 self.root_dir = self.useFixture(TempDir()).path
31 config = amqp2disk.make_amqp_config(self.root_dir)
32 orig_report = {'id': '12345'}
33 report = dict(orig_report)
34 ids = config.publish(report)
35 self.assertEqual(['12345', '12345'], ids)
36 with open(report['datedir_repo_filepath'], 'rb') as fp:
37 disk_report = bson.loads(fp.read())
38 self.assertEqual(disk_report, orig_report)
39 model_report = Oops.objects.get(oopsid='12345')
40 self.assertNotEqual(None, model_report)
041
=== added file 'src/oopstools/scripts/amqp2disk.py'
--- src/oopstools/scripts/amqp2disk.py 1970-01-01 00:00:00 +0000
+++ src/oopstools/scripts/amqp2disk.py 2011-10-16 23:09:17 +0000
@@ -0,0 +1,137 @@
1# Copyright 2011 Canonical Ltd. All rights reserved.
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU Affero General Public License as published by
5# the Free Software Foundation, either version 3 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Affero General Public License for more details.
12#
13# You should have received a copy of the GNU Affero General Public License
14# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16# Receive OOPS reports from AMQP and publish them into the oops-tools
17# repository.
18
19__metaclass__ = type
20
21import sys
22import optparse
23import StringIO
24from textwrap import dedent
25
26import amqplib.client_0_8 as amqp
27from oops import Config
28import oops_amqp
29import oops_datedir_repo
30
31from oopstools.oops.helpers import parsedate, load_prefixes
32from oopstools.oops.models import (
33 Oops,
34 parsed_oops_to_model_oops,
35 Prefix,
36 Report,
37 )
38from oopstools.oops.oopsstore import OopsStore
39from oopstools.oops import dbsummaries
40from oopstools.oops.summaries import (
41 WebAppErrorSummary,
42 CheckwatchesErrorSummary,
43 CodeHostingWithRemoteSectionSummary,
44 GenericErrorSummary,
45)
46
47
48def main(argv=None):
49 if argv is None:
50 argv=sys.argv
51 usage = dedent("""\
52 %prog [options]
53
54 The following options must be supplied:
55 --output
56 --host
57 --username
58 --password
59 --vhost
60 --queue
61
62 e.g.
63 amqp2disk --output /srv/oops-tools/amqpoopses --host "localhost:3472" \\
64 --username "guest" --password "guest" --vhost "/" --queue "oops"
65
66 The AMQP environment should be setup in advance with a persistent queue
67 bound to your exchange : using transient queues would allow OOPSes to
68 be lost if the amqp2disk process were to be shutdown for a non-trivial
69 duration. The --bind-to option will cause the queue to be created and
70 bound to the given exchange. This is only needed the first time as it
71 is created persistently.
72 """)
73 description = "Load OOPS reports into oops-tools from AMQP."
74 parser = optparse.OptionParser(
75 description=description, usage=usage)
76 parser.add_option('--output', help="Root directory to store OOPSes in.")
77 parser.add_option('--host', help="AQMP host / host:port.")
78 parser.add_option('--username', help="AQMP username.")
79 parser.add_option('--password', help="AQMP password.")
80 parser.add_option('--vhost', help="AMQP vhost.")
81 parser.add_option('--queue', help="AMQP queue name.")
82 parser.add_option(
83 '--bind-to', help="AMQP exchange to bind to (only needed once).")
84 options, args = parser.parse_args(argv[1:])
85 def needed(optname):
86 if getattr(options, optname, None) is None:
87 raise ValueError('option "%s" must be supplied' % optname)
88 needed('host')
89 needed('output')
90 needed('username')
91 needed('password')
92 needed('vhost')
93 needed('queue')
94 connection = amqp.Connection(host=options.host, userid=options.username,
95 password=options.password, virtual_host=options.vhost)
96 channel = connection.channel()
97 if options.bind_to:
98 channel.queue_declare(options.queue, durable=True, auto_delete=False)
99 channel.queue_bind(options.queue, options.bind_to)
100 config = make_amqp_config(options.output)
101 receiver = oops_amqp.Receiver(config, channel, options.queue)
102 try:
103 receiver.run_forever()
104 except KeyboardInterrupt:
105 pass
106
107
108def db_publisher(report):
109 """Publish OOPS reports to the oops-tools django store."""
110 # the first publisher will either inherit or assign, so this should be
111 # impossible.
112 assert report['id'] is not None
113 # Some fallback methods could lead to duplicate paths into the DB: exit
114 # early if the OOPS is already loaded.
115 try:
116 res = Oops.objects.get(oopsid__exact=report['id'])
117 except Oops.DoesNotExist:
118 res = parsed_oops_to_model_oops(
119 report, report['datedir_repo_filepath'])
120 return res.oopsid
121 return None
122
123
124def make_amqp_config(output_dir):
125 """Create an OOPS Config for republishing amqp OOPSes.
126
127 An OOPS published to this config will be written to disk and then loaded
128 into the database.
129
130 :param output_dir: The directory to write OOPSes too.
131 """
132 config = Config()
133 disk_publisher = oops_datedir_repo.DateDirRepo(
134 output_dir, inherit_id=True, stash_path=True)
135 config.publishers.append(disk_publisher.publish)
136 config.publishers.append(db_publisher)
137 return config
0138
=== modified file 'versions.cfg'
--- versions.cfg 2011-10-13 20:18:51 +0000
+++ versions.cfg 2011-10-16 23:09:17 +0000
@@ -19,8 +19,9 @@
19launchpadlib = 1.6.019launchpadlib = 1.6.0
20lazr.config = 1.1.320lazr.config = 1.1.3
21mechanize = 0.1.1121mechanize = 0.1.11
22oops = 0.0.722oops = 0.0.9
23oops-datedir-repo = 0.0.723oops-amqp = 0.0.1
24oops-datedir-repo = 0.0.9
24setuptools = 0.6c1125setuptools = 0.6c11
25z3c.recipe.filetemplate = 2.0.326z3c.recipe.filetemplate = 2.0.3
26z3c.recipe.sphinxdoc = 0.0.827z3c.recipe.sphinxdoc = 0.0.8

Subscribers

People subscribed via source and target branches

to all changes: