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
1=== added file 'canonical/oops/reporter.py'
2--- canonical/oops/reporter.py 1970-01-01 00:00:00 +0000
3+++ canonical/oops/reporter.py 2011-05-06 23:23:22 +0000
4@@ -0,0 +1,97 @@
5+# Copyright 2009-2011 Canonical Ltd.
6+#
7+# This file is part of wsgi-oops.
8+#
9+# wsgi-oops is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU Lesser General Public License version 3
11+# as published by the Free Software Foundation.
12+#
13+# wsgi-oops is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU Lesser General Public License for more details.
17+#
18+# You should have received a copy of the GNU Lesser General Public License
19+# along with wsgi-oops. If not, see <http://www.gnu.org/licenses/>.
20+
21+""" General oops request lifetime stuff, abstracted from WSGI things
22+
23+"""
24+
25+__all__ = ["OOPSReporter", "DEFAULT_SERIALIZER_FACTORY"]
26+
27+import sys
28+import logging
29+
30+from storm.tracer import install_tracer, remove_tracer_type
31+
32+from canonical.oops import serializer
33+from canonical.oops.oops import OOPS, OOPSLog, OOPSLoggingHandler
34+from canonical.oops.stormtracer import OOPSStorm, OOPSStormTracer
35+
36+DEFAULT_SERIALIZER_FACTORY = 'canonical.oops.serializer.OOPSBz2Serializer'
37+
38+
39+class OOPSReporter(object):
40+ """An object encapsulating OOPS reporting and oops allocation."""
41+
42+ def __init__(self, oops_dir,
43+ serializer_factory_name=DEFAULT_SERIALIZER_FACTORY,
44+ debug=False, key="appserver"):
45+ """Install tracers and set up the oops serializer."""
46+ # Add the oops to the storm tracing
47+ remove_tracer_type(OOPSStormTracer)
48+ install_tracer(OOPSStormTracer())
49+ # Add the oops to the standard logging
50+ root_logger = logging.getLogger()
51+ for handler in root_logger.handlers:
52+ if isinstance(handler, OOPSLoggingHandler):
53+ break
54+ else:
55+ root_logger.addHandler(OOPSLoggingHandler())
56+ fh = debug and sys.stderr or None
57+ self.serializer = serializer.create(serializer_factory_name,
58+ key, oops_dir, fh=fh)
59+
60+ def open_request(self):
61+ """Create a new OOPS context manager for a single request."""
62+ return OOPSRequest(self.serializer)
63+
64+
65+class OOPSRequest(object):
66+ """A context manager embodying the life cycle of a request."""
67+
68+ def __init__(self, serializer):
69+ """Create and retain an OOPS instance."""
70+ self.serializer = serializer
71+ self.oops = OOPS(self.serializer.get_next_oops_id)
72+ self.oops.add(OOPSStorm(thread_local=True))
73+ self.oops.add(OOPSLog(thread_local=True))
74+
75+ def __enter__(self):
76+ """Enter the context, returning the context's OOPS instance."""
77+ return self.oops
78+
79+ def __exit__(self, ex_type, ex_value, ex_tb):
80+ """Leave the context, reporting the OOPS if appropriate."""
81+ root_logger = logging.getLogger()
82+
83+ if ex_type is not None:
84+ self.oops.keep()
85+ message = "Unhandled application exception " \
86+ "(OOPS %s)" % (self.oops.id,)
87+ exc_info = (ex_type, ex_value, ex_tb)
88+ root_logger.error(message, exc_info=exc_info)
89+
90+ if self.oops.wanted:
91+ try:
92+ self.serializer.dump(self.oops)
93+ except:
94+ exc_info = sys.exc_info()
95+ try:
96+ message = "Error when dumping OOPS %s" % self.oops.id
97+ root_logger.error(message, exc_info=exc_info)
98+ finally:
99+ exc_info = None
100+
101+ return False
102
103=== added file 'canonical/oops/tests/test_reporter.py'
104--- canonical/oops/tests/test_reporter.py 1970-01-01 00:00:00 +0000
105+++ canonical/oops/tests/test_reporter.py 2011-05-06 23:23:22 +0000
106@@ -0,0 +1,71 @@
107+# Copyright 2009-2011 Canonical Ltd.
108+#
109+# This file is part of wsgi-oops.
110+#
111+# wsgi-oops is free software: you can redistribute it and/or modify
112+# it under the terms of the GNU Lesser General Public License version 3
113+# as published by the Free Software Foundation.
114+#
115+# wsgi-oops is distributed in the hope that it will be useful,
116+# but WITHOUT ANY WARRANTY; without even the implied warranty of
117+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
118+# GNU Lesser General Public License for more details.
119+#
120+# You should have received a copy of the GNU Lesser General Public License
121+# along with wsgi-oops. If not, see <http://www.gnu.org/licenses/>.
122+
123+"""Tests for canoncal.oops.reporter."""
124+import os
125+import unittest
126+
127+from canonical.oops.tests import testcase
128+from canonical.oops.oops import OOPS
129+from canonical.oops.reporter import OOPSReporter
130+
131+
132+class ReporterTests(testcase.OopsTestCase):
133+ """ simple test of wsgi oops just test for all ok and error """
134+
135+ def setUp(self):
136+ """Set up an OOPSReporter instance."""
137+ super(ReporterTests, self).setUp()
138+ self.reporter = OOPSReporter(self.tmp_dir)
139+
140+ def test_reporter_context(self):
141+ """Test that we can open a reporter context."""
142+ with self.reporter.open_request():
143+ pass
144+
145+ def test_reporter_context_oops(self):
146+ """Test that we can access the oops in the context."""
147+ with self.reporter.open_request() as oops:
148+ self.assertEqual(type(oops), OOPS)
149+
150+ def test_oops_no_error(self):
151+ """Test that we don't want the oops if no error in the context."""
152+
153+ def run_with_context():
154+ """Run in the context and return the oops."""
155+ with self.reporter.open_request() as oops:
156+ return oops
157+
158+ oops = run_with_context()
159+ self.assert_(not oops.wanted)
160+
161+ def test_oops_with_error(self):
162+ """Test what happens when we raise an error within a context."""
163+
164+
165+ class UniqueException(Exception):
166+ """A unique exception class for this test."""
167+
168+ def __init__(self, oops):
169+ """Save the oops."""
170+ self.oops = oops
171+
172+
173+ try:
174+ with self.reporter.open_request() as oops:
175+ raise UniqueException(oops)
176+ except UniqueException, e:
177+ self.assert_(e.oops.wanted)
178
179=== modified file 'canonical/oops/tests/testcase.py'
180--- canonical/oops/tests/testcase.py 2011-01-18 09:50:00 +0000
181+++ canonical/oops/tests/testcase.py 2011-05-06 23:23:22 +0000
182@@ -1,4 +1,4 @@
183-# Copyright 2009 Canonical Ltd.
184+# Copyright 2009-2011 Canonical Ltd.
185 #
186 # This file is part of wsgi-oops.
187 #
188@@ -42,7 +42,34 @@
189 return self.app(environ, start_response)
190
191
192-class WSGIOopsTestCase(unittest.TestCase):
193+class OopsTestCase(unittest.TestCase):
194+ """Test case that creates a temp directory for oops serialization."""
195+
196+ def setUp(self):
197+ """Set up temp directory."""
198+ super(OopsTestCase, self).setUp()
199+ self.test_root_dir = None
200+ self.tmp_dir = self._make_tmp_dir()
201+
202+ def _make_tmp_dir(self):
203+ """Create and return a temp dir for the current test method."""
204+ if getattr(self, 'test_root_dir', None) is None:
205+ self.test_root_dir = tempfile.mkdtemp('', 'wsgi_oops')
206+ return os.path.join(self.test_root_dir, self._testMethodName)
207+
208+ def tearDown(self):
209+ """Clean up the temporary directory."""
210+ super(OopsTestCase, self).tearDown()
211+ self.rm_tmp_dir()
212+
213+ def rm_tmp_dir(self):
214+ """Do the actual cleanup of the temporary directory."""
215+ root_dir = getattr(self, 'test_root_dir', None)
216+ if root_dir and os.path.exists(root_dir):
217+ shutil.rmtree(root_dir)
218+
219+
220+class WSGIOopsTestCase(OopsTestCase):
221 """ base test case for WSGI oops related tests
222
223 Provides some utilities to ease the writting of tests.
224@@ -56,28 +83,10 @@
225
226 def setUp(self):
227 super(WSGIOopsTestCase, self).setUp()
228- self.test_root_dir = None
229- self.tmp_dir = self._make_tmp_dir()
230 self.oops_ware_interceptor = OopsWareInterceptor(self.create_test_app())
231 self.app = wsgi.OopsWare(self.oops_ware_interceptor, self.tmp_dir)
232 self.testapp = TestApp(self.app)
233
234- def _make_tmp_dir(self):
235- """ creates and returns safe temp dir for the current test method """
236- if getattr(self, 'test_root_dir', None) is None:
237- self.test_root_dir = tempfile.mkdtemp('', 'wsgi_oops')
238- return os.path.join(self.test_root_dir, self._testMethodName)
239-
240- def tearDown(self):
241- super(WSGIOopsTestCase, self).tearDown()
242- self.rm_tmp_dir()
243-
244- def rm_tmp_dir(self):
245- """ clean up the temp dir """
246- root_dir = getattr(self, 'test_root_dir', None)
247- if root_dir and os.path.exists(root_dir):
248- shutil.rmtree(root_dir)
249-
250 def create_test_app(self):
251 """ return a simple wsgi app """
252 return SimpleApplication()
253
254=== modified file 'canonical/oops/wsgi.py'
255--- canonical/oops/wsgi.py 2011-03-05 00:24:31 +0000
256+++ canonical/oops/wsgi.py 2011-05-06 23:23:22 +0000
257@@ -31,15 +31,10 @@
258 import time
259 import logging
260
261-from storm.tracer import install_tracer, remove_tracer_type
262-
263-from canonical.oops import serializer
264-from canonical.oops.oops import (
265- OOPS, OOPSLog, OOPSMetaData, OOPSLoggingHandler )
266+from canonical.oops.oops import OOPSMetaData
267 from canonical.oops.utils import resolve_name
268-from canonical.oops.stormtracer import OOPSStorm, OOPSStormTracer
269+from canonical.oops.reporter import OOPSReporter, DEFAULT_SERIALIZER_FACTORY
270
271-DEFAULT_SERIALIZER_FACTORY = 'canonical.oops.serializer.OOPSBz2Serializer'
272 DEFAULT_COMPLETION_TRACKER = 'canonical.oops.wsgi.track_iterator_completion'
273
274 def track_iterator_completion(body, on_completion):
275@@ -81,19 +76,10 @@
276 self.app = app
277 self.hide_meta = hide_meta
278 self.oops_on_404 = oops_on_404
279- # Add the oops to the storm tracing
280- remove_tracer_type(OOPSStormTracer)
281- install_tracer(OOPSStormTracer())
282- # Add the oops to the standard logging
283- root_logger = logging.getLogger()
284- for handler in root_logger.handlers:
285- if isinstance(handler, OOPSLoggingHandler):
286- break
287- else:
288- root_logger.addHandler(OOPSLoggingHandler())
289- fh = debug and sys.stderr or None
290- self.serial = serializer.create(serializer_factory_name,
291- key, oops_dir, fh=fh)
292+ self.reporter = OOPSReporter(oops_dir=oops_dir,
293+ serializer_factory_name=
294+ serializer_factory_name,
295+ debug=debug, key=key)
296 self.track_completion = resolve_name(completion_tracker_name)
297
298 def __call__(self, environ, start_response):
299@@ -104,9 +90,8 @@
300 root_logger = logging.getLogger()
301
302 # Request start, record the time and create oops tracking object
303- oops = OOPS(self.serial.get_next_oops_id)
304- oops.add(OOPSStorm(thread_local=True))
305- oops.add(OOPSLog(thread_local=True))
306+ context = self.reporter.open_request()
307+ oops = context.__enter__()
308
309 metadata = OOPSMetaData(environ)
310 if not self.hide_meta:
311@@ -151,26 +136,10 @@
312
313 def on_completion(ex_type, ex_value, ex_tb):
314 """Handle request completion."""
315- exc_info = None
316- if ex_type is not None:
317- oops.keep()
318- message = "Unhandled WSGI application exception " \
319- "(OOPS %s)" % (oops.id,)
320- exc_info = (ex_type, ex_value, ex_tb)
321- root_logger.error(message, exc_info=exc_info)
322-
323 metadata["request-end"] = time.time()
324-
325- if environ.has_key('dump-oops') or oops.wanted:
326- try:
327- self.serial.dump(oops)
328- except:
329- exc_info = sys.exc_info()
330- try:
331- message = "Error when dumping OOPS %s" % oops.id
332- root_logger.error(message, exc_info=exc_info)
333- finally:
334- exc_info = None
335+ if environ.has_key('dump-oops'):
336+ oops.keep()
337+ context.__exit__(ex_type, ex_value, ex_tb)
338
339 try:
340 body = self.app(environ, trap_response)

Subscribers

People subscribed via source and target branches