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