Merge lp:~canonical-isd-hackers/canonical-identity-provider/767454-lp-time into lp:canonical-identity-provider/release

Proposed by David Owen
Status: Merged
Approved by: Simon Davy
Approved revision: no longer in the source branch.
Merged at revision: 248
Proposed branch: lp:~canonical-isd-hackers/canonical-identity-provider/767454-lp-time
Merge into: lp:canonical-identity-provider/release
Prerequisite: lp:~canonical-isd-hackers/canonical-identity-provider/log-request
Diff against target: 317 lines (+122/-48)
9 files modified
django_project/config_dev/config/devel.cfg (+1/-0)
django_project/config_dev/config/main.cfg (+1/-0)
identityprovider/middleware/exception.py (+3/-1)
identityprovider/middleware/time.py (+25/-0)
identityprovider/middleware/util.py (+5/-3)
identityprovider/schema.py (+1/-0)
identityprovider/tests/test_middleware.py (+75/-44)
identityprovider/urls.py (+3/-0)
identityprovider/views/testing.py (+8/-0)
To merge this branch: bzr merge lp:~canonical-isd-hackers/canonical-identity-provider/767454-lp-time
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+86062@code.launchpad.net

Commit message

Log requests that take too long to complete.

To post a comment you must log in.
Revision history for this message
Simon Davy (bloodearnest) wrote :

LGTM

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/config_dev/config/devel.cfg'
2--- django_project/config_dev/config/devel.cfg 2011-12-07 18:53:10 +0000
3+++ django_project/config_dev/config/devel.cfg 2011-12-16 16:07:26 +0000
4@@ -27,6 +27,7 @@
5 saml2idp
6 devserver
7 middleware_classes =
8+ identityprovider.middleware.time.TimeMiddleware
9 identityprovider.middleware.dbfailover.DBFailoverMiddleware
10 django.middleware.common.CommonMiddleware
11 django.contrib.sessions.middleware.SessionMiddleware
12
13=== modified file 'django_project/config_dev/config/main.cfg'
14--- django_project/config_dev/config/main.cfg 2011-12-16 15:46:14 +0000
15+++ django_project/config_dev/config/main.cfg 2011-12-16 16:07:26 +0000
16@@ -55,6 +55,7 @@
17 login_url = /+login
18 managers = %(admins)s
19 middleware_classes =
20+ identityprovider.middleware.time.TimeMiddleware
21 # identityprovider.middleware.dbfailover.DBFailoverMiddleware
22 django.middleware.common.CommonMiddleware
23 django.contrib.sessions.middleware.SessionMiddleware
24
25=== modified file 'identityprovider/middleware/exception.py'
26--- identityprovider/middleware/exception.py 2011-12-15 22:07:59 +0000
27+++ identityprovider/middleware/exception.py 2011-12-16 16:07:26 +0000
28@@ -1,6 +1,7 @@
29 # Copyright 2011 Canonical Ltd. This software is licensed under the
30 # GNU Affero General Public License version 3 (see the file LICENSE).
31
32+import logging
33 import sys
34 import traceback
35
36@@ -15,4 +16,5 @@
37 class LogExceptionMiddleware(object):
38
39 def process_exception(self, request, exception):
40- log_request(request)
41+ logging.exception('Unhandled exception in application')
42+ log_request(request, log_exception_trace=True)
43
44=== added file 'identityprovider/middleware/time.py'
45--- identityprovider/middleware/time.py 1970-01-01 00:00:00 +0000
46+++ identityprovider/middleware/time.py 2011-12-16 16:07:26 +0000
47@@ -0,0 +1,25 @@
48+# Copyright 2011 Canonical Ltd. This software is licensed under the
49+# GNU Affero General Public License version 3 (see the file LICENSE).
50+
51+import datetime
52+import logging as log
53+
54+from django.conf import settings
55+
56+from .util import log_request
57+
58+
59+class TimeMiddleware(object):
60+
61+ def process_request(self, request):
62+ request._time_start = datetime.datetime.now()
63+ return None
64+
65+ def process_response(self, request, response):
66+ time_taken = datetime.datetime.now() - request._time_start
67+ limit = datetime.timedelta(milliseconds=settings.HANDLER_TIMEOUT_MILLIS)
68+ if time_taken > limit:
69+ log.warning('Took too long: %s' % time_taken)
70+ log_request(request)
71+
72+ return response
73
74=== modified file 'identityprovider/middleware/util.py'
75--- identityprovider/middleware/util.py 2011-12-15 22:07:59 +0000
76+++ identityprovider/middleware/util.py 2011-12-16 16:07:26 +0000
77@@ -10,6 +10,7 @@
78 from django.db import connection
79 from django.views import debug
80
81+
82 debug.HIDDEN_SETTINGS = re.compile(
83 'SECRET|PASSWORD|PROFANITIES_LIST|PRIVATE|secret|password|private')
84
85@@ -85,16 +86,17 @@
86 return frames
87
88
89-def log_request(request):
90+def log_request(request, log_exception_trace=False):
91 """Log the request, so that it can be kept in the oops file"""
92 request = _sanitize_request(request)
93 reporter = _ExceptionReporter(request, *sys.exc_info())
94+
95 template_debug = settings.TEMPLATE_DEBUG
96 settings.TEMPLATE_DEBUG = True
97 try:
98- logging.exception('Unhandled exception in application')
99 logging.warn(reporter.get_traceback_html())
100- logging.warn(traceback.format_exc())
101+ if log_exception_trace:
102+ logging.warn(traceback.format_exc())
103 for query in connection.queries:
104 logging.warn("time: %(time)s sql: %(sql)s" % query)
105 finally:
106
107=== modified file 'identityprovider/schema.py'
108--- identityprovider/schema.py 2011-11-21 19:38:07 +0000
109+++ identityprovider/schema.py 2011-12-16 16:07:26 +0000
110@@ -40,6 +40,7 @@
111 db_statement_timeout_millis = IntConfigOption(
112 default=0, help='0 disables timeout')
113 extra_pythonpath = LinesConfigOption(item=StringConfigOption())
114+ handler_timeout_millis = IntConfigOption(default=10000)
115 oops_dir = StringConfigOption(help='Absolute path to the directory oops'
116 ' reports will be stored in')
117 pgconnect_timeout = IntConfigOption(default=10)
118
119=== modified file 'identityprovider/tests/test_middleware.py'
120--- identityprovider/tests/test_middleware.py 2011-12-15 22:07:59 +0000
121+++ identityprovider/tests/test_middleware.py 2011-12-16 16:07:26 +0000
122@@ -1,23 +1,28 @@
123-# Copyright 2010 Canonical Ltd. This software is licensed under the
124+# Copyright 2011 Canonical Ltd. This software is licensed under the
125 # GNU Affero General Public License version 3 (see the file LICENSE).
126
127-import logging
128 import re
129 import sys
130+from time import sleep
131
132 from django.conf import settings
133+from django.core.urlresolvers import reverse
134+from django.http import HttpResponse
135 from django.test import TestCase
136 from django.contrib.auth.models import User, AnonymousUser
137
138+from mock import patch
139+
140 from openid.message import IDENTIFIER_SELECT, OPENID1_URL_LIMIT, OPENID2_NS
141
142-from identityprovider.tests.utils import MockRequest
143-from identityprovider.models import Account, EmailAddress, OpenIDRPConfig
144 from identityprovider.middleware import exception, util as middleware_util
145 from identityprovider.middleware.useraccount import (
146 UserAccountConversionMiddleware)
147+from identityprovider.models import Account, EmailAddress, OpenIDRPConfig
148+from identityprovider.tests.utils import MockRequest
149+from identityprovider.views import testing as test_views
150
151-from .utils import SSOBaseTestCase
152+from .utils import SSOBaseTestCase, patch_settings
153
154
155 def _extract_csrf_token(response):
156@@ -72,7 +77,7 @@
157 self.assertTrue(isinstance(request.user, AnonymousUser))
158
159
160-class ExceptionMiddlewareTestCase(TestCase):
161+class UtilTestCase(TestCase):
162
163 POST_PASSWORD = 'P1!2@3#'
164 POST_SECRET = 'P2@3#4$'
165@@ -83,16 +88,8 @@
166 POST_PUBLIC = 'Canonical Rocks!'
167 GET_PUBLIC = 'Canonical Rocks!'
168 asterisks = '********'
169- PRIVATE_SETTING = '7&8*9('
170- PUBLIC_SETTING = 'Something public'
171- FRAME_VAR = '*8*8*8*8'
172-
173- LOG_FILENAME = '/tmp/logging_example.out'
174- logging.basicConfig(filename=LOG_FILENAME, level=logging.DEBUG)
175-
176- logging.debug('This message should go to the log file')
177-
178- def setUp(self):
179+
180+ def test_request_sanitization(self):
181 request = MockRequest('/login')
182 request.POST = {}
183 request.GET = {}
184@@ -106,34 +103,9 @@
185 request.GET['get_secret'] = self.GET_SECRET
186 request.GET['get_private'] = self.GET_PRIVATE
187 request.GET['get_public'] = self.GET_PUBLIC
188- self.request = request
189- self.middleware = exception.LogExceptionMiddleware()
190- settings.PRIVATE_SETTING = self.PRIVATE_SETTING
191- settings.PUBLIC_SETTING = self.PUBLIC_SETTING
192-
193- def test_settings_sanitization(self):
194- try:
195- x = 2 / 0
196- except:
197- reporter = middleware_util._ExceptionReporter(self.request,
198- *sys.exc_info())
199- html = reporter.get_traceback_html()
200- self.assertFalse(self.PRIVATE_SETTING in html)
201- self.assertTrue(self.PUBLIC_SETTING in html)
202-
203- def test_frames_sanitization(self):
204- try:
205- FRAME_PASSWORD = self.FRAME_VAR
206- public_variable = 2 / 0
207- except:
208- reporter = middleware_util._ExceptionReporter(self.request,
209- *sys.exc_info())
210- html = reporter.get_traceback_html()
211- self.assertFalse(self.FRAME_VAR in html)
212- self.assertTrue('public_variable' in html)
213-
214- def test_request_sanitization(self):
215- req = middleware_util._sanitize_request(self.request)
216+
217+ req = middleware_util._sanitize_request(request)
218+
219 # Make sure sensitive data is no longer in the request
220 self.assertFalse(self.POST_PASSWORD in req.POST.values())
221 self.assertFalse(self.POST_SECRET in req.POST.values())
222@@ -153,6 +125,65 @@
223 self.assertEquals(req.GET['get_public'], self.GET_PUBLIC)
224
225
226+class ExceptionMiddlewareTestCase(TestCase):
227+
228+ PRIVATE_SETTING = '7&8*9('
229+ PUBLIC_SETTING = 'Something public'
230+ FRAME_VAR = '*8*8*8*8'
231+
232+ def setUp(self):
233+ self.request = MockRequest('/login')
234+ self.request.META = {'SERVER_NAME': 'localhost', 'SERVER_PORT': 80}
235+ self.middleware = exception.LogExceptionMiddleware()
236+
237+ def _get_traceback_html(self):
238+ reporter = middleware_util._ExceptionReporter(
239+ self.request, *sys.exc_info())
240+ return reporter.get_traceback_html()
241+
242+ def test_settings_sanitization(self):
243+ settings.PRIVATE_SETTING = self.PRIVATE_SETTING
244+ settings.PUBLIC_SETTING = self.PUBLIC_SETTING
245+
246+ try:
247+ x = 2 / 0
248+ except:
249+ html = self._get_traceback_html()
250+
251+ self.assertFalse(self.PRIVATE_SETTING in html)
252+ self.assertTrue(self.PUBLIC_SETTING in html)
253+
254+ def test_frames_sanitization(self):
255+ try:
256+ FRAME_PASSWORD = self.FRAME_VAR
257+ public_variable = 2 / 0
258+ except:
259+ html = self._get_traceback_html()
260+
261+ self.assertFalse(self.FRAME_VAR in html)
262+ self.assertTrue('public_variable' in html)
263+
264+
265+class TimeMiddlewareTestCase(TestCase):
266+
267+ @patch('identityprovider.middleware.time.log_request')
268+ @patch.object(test_views, 'dummy_hook')
269+ def _check(self, timeout_millis, sleep_secs, calls, hook, log):
270+ hook.return_value = HttpResponse('DONE')
271+ hook.side_effect = sleep(sleep_secs)
272+ path = reverse(test_views.dummy)
273+ with patch_settings(HANDLER_TIMEOUT_MILLIS=timeout_millis):
274+ self.client.get(path)
275+ self.assertEquals(hook.call_count, 1)
276+ self.assertEquals(log.call_count, calls)
277+
278+ def test_under_time(self):
279+ self._check(5000, 0, 0)
280+
281+ def test_over_time(self):
282+ self._check(1, 0.1, 1)
283+
284+
285 # The CSRF middleware requires a session cookie in order to activate.
286 # The tests perform a login in order to acquire this session cookie.
287 class CSRFMiddlewareTestCase(SSOBaseTestCase):
288
289=== modified file 'identityprovider/urls.py'
290--- identityprovider/urls.py 2011-12-15 18:35:03 +0000
291+++ identityprovider/urls.py 2011-12-16 16:07:26 +0000
292@@ -122,6 +122,9 @@
293 urlpatterns += patterns('identityprovider.views.account',
294 (r'^%(optional_token)s\+deactivate$' % repls, 'account_deactivate'),
295 )
296+ urlpatterns += patterns('identityprovider.views.testing',
297+ (r'^dummy$', 'dummy'),
298+ )
299
300 # Keep this at the end, so no important URL will be overridden
301 if settings.BRAND.lower() == 'ubuntu':
302
303=== modified file 'identityprovider/views/testing.py'
304--- identityprovider/views/testing.py 2010-11-02 19:45:33 +0000
305+++ identityprovider/views/testing.py 2011-12-16 16:07:26 +0000
306@@ -75,3 +75,11 @@
307 <input type="submit" value="Lock">
308 </form>
309 ''')
310+
311+# This gives mock & patch a nice easy location to patch a wait into
312+# the handler.
313+def dummy_hook():
314+ return HttpResponse('DONE')
315+
316+def dummy(request):
317+ return dummy_hook()