Merge lp:~tcole/wsgi-oops/generic-oops-api into lp:wsgi-oops

Proposed by Tim Cole
Status: Merged
Approved by: Philip Fibiger
Approved revision: 60
Merged at revision: 58
Proposed branch: lp:~tcole/wsgi-oops/generic-oops-api
Merge into: lp:wsgi-oops
Diff against target: 340 lines (+208/-62)
4 files modified
canonical/oops/reporter.py (+97/-0)
canonical/oops/tests/test_reporter.py (+71/-0)
canonical/oops/tests/testcase.py (+29/-20)
canonical/oops/wsgi.py (+11/-42)
To merge this branch: bzr merge lp:~tcole/wsgi-oops/generic-oops-api
Reviewer Review Type Date Requested Status
Philip Fibiger (community) Approve
Martin Albisetti (community) Approve
Review via email: mp+60258@code.launchpad.net

Commit message

Introduce a generic OOPS API not tied to WSGI.

Description of the change

Introduce a generic OOPS API not tied to WSGI.

To post a comment you must log in.
Revision history for this message
Martin Albisetti (beuno) :
review: Approve
Revision history for this message
Philip Fibiger (pfibiger) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'canonical/oops/reporter.py'
--- canonical/oops/reporter.py 1970-01-01 00:00:00 +0000
+++ canonical/oops/reporter.py 2011-05-06 23:23:22 +0000
@@ -0,0 +1,97 @@
1# Copyright 2009-2011 Canonical Ltd.
2#
3# This file is part of wsgi-oops.
4#
5# wsgi-oops is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# wsgi-oops is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with wsgi-oops. If not, see <http://www.gnu.org/licenses/>.
16
17""" General oops request lifetime stuff, abstracted from WSGI things
18
19"""
20
21__all__ = ["OOPSReporter", "DEFAULT_SERIALIZER_FACTORY"]
22
23import sys
24import logging
25
26from storm.tracer import install_tracer, remove_tracer_type
27
28from canonical.oops import serializer
29from canonical.oops.oops import OOPS, OOPSLog, OOPSLoggingHandler
30from canonical.oops.stormtracer import OOPSStorm, OOPSStormTracer
31
32DEFAULT_SERIALIZER_FACTORY = 'canonical.oops.serializer.OOPSBz2Serializer'
33
34
35class OOPSReporter(object):
36 """An object encapsulating OOPS reporting and oops allocation."""
37
38 def __init__(self, oops_dir,
39 serializer_factory_name=DEFAULT_SERIALIZER_FACTORY,
40 debug=False, key="appserver"):
41 """Install tracers and set up the oops serializer."""
42 # Add the oops to the storm tracing
43 remove_tracer_type(OOPSStormTracer)
44 install_tracer(OOPSStormTracer())
45 # Add the oops to the standard logging
46 root_logger = logging.getLogger()
47 for handler in root_logger.handlers:
48 if isinstance(handler, OOPSLoggingHandler):
49 break
50 else:
51 root_logger.addHandler(OOPSLoggingHandler())
52 fh = debug and sys.stderr or None
53 self.serializer = serializer.create(serializer_factory_name,
54 key, oops_dir, fh=fh)
55
56 def open_request(self):
57 """Create a new OOPS context manager for a single request."""
58 return OOPSRequest(self.serializer)
59
60
61class OOPSRequest(object):
62 """A context manager embodying the life cycle of a request."""
63
64 def __init__(self, serializer):
65 """Create and retain an OOPS instance."""
66 self.serializer = serializer
67 self.oops = OOPS(self.serializer.get_next_oops_id)
68 self.oops.add(OOPSStorm(thread_local=True))
69 self.oops.add(OOPSLog(thread_local=True))
70
71 def __enter__(self):
72 """Enter the context, returning the context's OOPS instance."""
73 return self.oops
74
75 def __exit__(self, ex_type, ex_value, ex_tb):
76 """Leave the context, reporting the OOPS if appropriate."""
77 root_logger = logging.getLogger()
78
79 if ex_type is not None:
80 self.oops.keep()
81 message = "Unhandled application exception " \
82 "(OOPS %s)" % (self.oops.id,)
83 exc_info = (ex_type, ex_value, ex_tb)
84 root_logger.error(message, exc_info=exc_info)
85
86 if self.oops.wanted:
87 try:
88 self.serializer.dump(self.oops)
89 except:
90 exc_info = sys.exc_info()
91 try:
92 message = "Error when dumping OOPS %s" % self.oops.id
93 root_logger.error(message, exc_info=exc_info)
94 finally:
95 exc_info = None
96
97 return False
098
=== added file 'canonical/oops/tests/test_reporter.py'
--- canonical/oops/tests/test_reporter.py 1970-01-01 00:00:00 +0000
+++ canonical/oops/tests/test_reporter.py 2011-05-06 23:23:22 +0000
@@ -0,0 +1,71 @@
1# Copyright 2009-2011 Canonical Ltd.
2#
3# This file is part of wsgi-oops.
4#
5# wsgi-oops is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License version 3
7# as published by the Free Software Foundation.
8#
9# wsgi-oops is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with wsgi-oops. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for canoncal.oops.reporter."""
18import os
19import unittest
20
21from canonical.oops.tests import testcase
22from canonical.oops.oops import OOPS
23from canonical.oops.reporter import OOPSReporter
24
25
26class ReporterTests(testcase.OopsTestCase):
27 """ simple test of wsgi oops just test for all ok and error """
28
29 def setUp(self):
30 """Set up an OOPSReporter instance."""
31 super(ReporterTests, self).setUp()
32 self.reporter = OOPSReporter(self.tmp_dir)
33
34 def test_reporter_context(self):
35 """Test that we can open a reporter context."""
36 with self.reporter.open_request():
37 pass
38
39 def test_reporter_context_oops(self):
40 """Test that we can access the oops in the context."""
41 with self.reporter.open_request() as oops:
42 self.assertEqual(type(oops), OOPS)
43
44 def test_oops_no_error(self):
45 """Test that we don't want the oops if no error in the context."""
46
47 def run_with_context():
48 """Run in the context and return the oops."""
49 with self.reporter.open_request() as oops:
50 return oops
51
52 oops = run_with_context()
53 self.assert_(not oops.wanted)
54
55 def test_oops_with_error(self):
56 """Test what happens when we raise an error within a context."""
57
58
59 class UniqueException(Exception):
60 """A unique exception class for this test."""
61
62 def __init__(self, oops):
63 """Save the oops."""
64 self.oops = oops
65
66
67 try:
68 with self.reporter.open_request() as oops:
69 raise UniqueException(oops)
70 except UniqueException, e:
71 self.assert_(e.oops.wanted)
072
=== modified file 'canonical/oops/tests/testcase.py'
--- canonical/oops/tests/testcase.py 2011-01-18 09:50:00 +0000
+++ canonical/oops/tests/testcase.py 2011-05-06 23:23:22 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd.1# Copyright 2009-2011 Canonical Ltd.
2#2#
3# This file is part of wsgi-oops.3# This file is part of wsgi-oops.
4#4#
@@ -42,7 +42,34 @@
42 return self.app(environ, start_response)42 return self.app(environ, start_response)
4343
4444
45class WSGIOopsTestCase(unittest.TestCase):45class OopsTestCase(unittest.TestCase):
46 """Test case that creates a temp directory for oops serialization."""
47
48 def setUp(self):
49 """Set up temp directory."""
50 super(OopsTestCase, self).setUp()
51 self.test_root_dir = None
52 self.tmp_dir = self._make_tmp_dir()
53
54 def _make_tmp_dir(self):
55 """Create and return a temp dir for the current test method."""
56 if getattr(self, 'test_root_dir', None) is None:
57 self.test_root_dir = tempfile.mkdtemp('', 'wsgi_oops')
58 return os.path.join(self.test_root_dir, self._testMethodName)
59
60 def tearDown(self):
61 """Clean up the temporary directory."""
62 super(OopsTestCase, self).tearDown()
63 self.rm_tmp_dir()
64
65 def rm_tmp_dir(self):
66 """Do the actual cleanup of the temporary directory."""
67 root_dir = getattr(self, 'test_root_dir', None)
68 if root_dir and os.path.exists(root_dir):
69 shutil.rmtree(root_dir)
70
71
72class WSGIOopsTestCase(OopsTestCase):
46 """ base test case for WSGI oops related tests73 """ base test case for WSGI oops related tests
4774
48 Provides some utilities to ease the writting of tests.75 Provides some utilities to ease the writting of tests.
@@ -56,28 +83,10 @@
5683
57 def setUp(self):84 def setUp(self):
58 super(WSGIOopsTestCase, self).setUp()85 super(WSGIOopsTestCase, self).setUp()
59 self.test_root_dir = None
60 self.tmp_dir = self._make_tmp_dir()
61 self.oops_ware_interceptor = OopsWareInterceptor(self.create_test_app())86 self.oops_ware_interceptor = OopsWareInterceptor(self.create_test_app())
62 self.app = wsgi.OopsWare(self.oops_ware_interceptor, self.tmp_dir)87 self.app = wsgi.OopsWare(self.oops_ware_interceptor, self.tmp_dir)
63 self.testapp = TestApp(self.app)88 self.testapp = TestApp(self.app)
6489
65 def _make_tmp_dir(self):
66 """ creates and returns safe temp dir for the current test method """
67 if getattr(self, 'test_root_dir', None) is None:
68 self.test_root_dir = tempfile.mkdtemp('', 'wsgi_oops')
69 return os.path.join(self.test_root_dir, self._testMethodName)
70
71 def tearDown(self):
72 super(WSGIOopsTestCase, self).tearDown()
73 self.rm_tmp_dir()
74
75 def rm_tmp_dir(self):
76 """ clean up the temp dir """
77 root_dir = getattr(self, 'test_root_dir', None)
78 if root_dir and os.path.exists(root_dir):
79 shutil.rmtree(root_dir)
80
81 def create_test_app(self):90 def create_test_app(self):
82 """ return a simple wsgi app """91 """ return a simple wsgi app """
83 return SimpleApplication()92 return SimpleApplication()
8493
=== modified file 'canonical/oops/wsgi.py'
--- canonical/oops/wsgi.py 2011-03-05 00:24:31 +0000
+++ canonical/oops/wsgi.py 2011-05-06 23:23:22 +0000
@@ -31,15 +31,10 @@
31import time31import time
32import logging32import logging
3333
34from storm.tracer import install_tracer, remove_tracer_type34from canonical.oops.oops import OOPSMetaData
35
36from canonical.oops import serializer
37from canonical.oops.oops import (
38 OOPS, OOPSLog, OOPSMetaData, OOPSLoggingHandler )
39from canonical.oops.utils import resolve_name35from canonical.oops.utils import resolve_name
40from canonical.oops.stormtracer import OOPSStorm, OOPSStormTracer36from canonical.oops.reporter import OOPSReporter, DEFAULT_SERIALIZER_FACTORY
4137
42DEFAULT_SERIALIZER_FACTORY = 'canonical.oops.serializer.OOPSBz2Serializer'
43DEFAULT_COMPLETION_TRACKER = 'canonical.oops.wsgi.track_iterator_completion'38DEFAULT_COMPLETION_TRACKER = 'canonical.oops.wsgi.track_iterator_completion'
4439
45def track_iterator_completion(body, on_completion):40def track_iterator_completion(body, on_completion):
@@ -81,19 +76,10 @@
81 self.app = app76 self.app = app
82 self.hide_meta = hide_meta77 self.hide_meta = hide_meta
83 self.oops_on_404 = oops_on_40478 self.oops_on_404 = oops_on_404
84 # Add the oops to the storm tracing79 self.reporter = OOPSReporter(oops_dir=oops_dir,
85 remove_tracer_type(OOPSStormTracer)80 serializer_factory_name=
86 install_tracer(OOPSStormTracer())81 serializer_factory_name,
87 # Add the oops to the standard logging82 debug=debug, key=key)
88 root_logger = logging.getLogger()
89 for handler in root_logger.handlers:
90 if isinstance(handler, OOPSLoggingHandler):
91 break
92 else:
93 root_logger.addHandler(OOPSLoggingHandler())
94 fh = debug and sys.stderr or None
95 self.serial = serializer.create(serializer_factory_name,
96 key, oops_dir, fh=fh)
97 self.track_completion = resolve_name(completion_tracker_name)83 self.track_completion = resolve_name(completion_tracker_name)
9884
99 def __call__(self, environ, start_response):85 def __call__(self, environ, start_response):
@@ -104,9 +90,8 @@
104 root_logger = logging.getLogger()90 root_logger = logging.getLogger()
10591
106 # Request start, record the time and create oops tracking object92 # Request start, record the time and create oops tracking object
107 oops = OOPS(self.serial.get_next_oops_id)93 context = self.reporter.open_request()
108 oops.add(OOPSStorm(thread_local=True))94 oops = context.__enter__()
109 oops.add(OOPSLog(thread_local=True))
11095
111 metadata = OOPSMetaData(environ)96 metadata = OOPSMetaData(environ)
112 if not self.hide_meta:97 if not self.hide_meta:
@@ -151,26 +136,10 @@
151136
152 def on_completion(ex_type, ex_value, ex_tb):137 def on_completion(ex_type, ex_value, ex_tb):
153 """Handle request completion."""138 """Handle request completion."""
154 exc_info = None
155 if ex_type is not None:
156 oops.keep()
157 message = "Unhandled WSGI application exception " \
158 "(OOPS %s)" % (oops.id,)
159 exc_info = (ex_type, ex_value, ex_tb)
160 root_logger.error(message, exc_info=exc_info)
161
162 metadata["request-end"] = time.time()139 metadata["request-end"] = time.time()
163140 if environ.has_key('dump-oops'):
164 if environ.has_key('dump-oops') or oops.wanted:141 oops.keep()
165 try:142 context.__exit__(ex_type, ex_value, ex_tb)
166 self.serial.dump(oops)
167 except:
168 exc_info = sys.exc_info()
169 try:
170 message = "Error when dumping OOPS %s" % oops.id
171 root_logger.error(message, exc_info=exc_info)
172 finally:
173 exc_info = None
174143
175 try:144 try:
176 body = self.app(environ, trap_response)145 body = self.app(environ, trap_response)

Subscribers

People subscribed via source and target branches