Merge lp:~lifeless/storm/timelinetracer into lp:storm

Proposed by Robert Collins
Status: Merged
Merged at revision: 402
Proposed branch: lp:~lifeless/storm/timelinetracer
Merge into: lp:storm
Diff against target: 214 lines (+77/-40)
2 files modified
storm/tracer.py (+29/-13)
tests/tracer.py (+48/-27)
To merge this branch: bzr merge lp:~lifeless/storm/timelinetracer
Reviewer Review Type Date Requested Status
James Henstridge Approve
Review via email: mp+75109@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
James Henstridge (jamesh) wrote :

Looks good. I noticed a few small issues, but it looks pretty much ready to merge:

[1]
+ # be preserved as that is where we are substiting values in.

Should be "substituting"

[2]
+ #param timeline_factory: A factory function to produce the timeline to

Should be "@param"

[3]
It looks like all the tests are running with conn.param_mark == '?'. It'd be good to have at least one that runs with a StubConnection with param_mark set to '%s' to test the other branch in connection_raw_execute.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

all actioned, pushing it up now. Thanks!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storm/tracer.py'
--- storm/tracer.py 2011-09-12 06:43:04 +0000
+++ storm/tracer.py 2011-09-13 04:08:34 +0000
@@ -118,12 +118,20 @@
118 # There are some bind parameters so we want to insert them into118 # There are some bind parameters so we want to insert them into
119 # the sql statement so we can log the statement.119 # the sql statement so we can log the statement.
120 query_params = list(connection.to_database(params))120 query_params = list(connection.to_database(params))
121 # We need to ensure % symbols used for LIKE statements etc are121 if connection.param_mark == '%s':
122 # properly quoted or else the string format operation will fail.122 # Double the %'s in the string so that python string formatting
123 quoted_statement = re.sub(123 # can restore them to the correct number. Note that %s needs to
124 # be preserved as that is where we are substiting values in.
125 quoted_statement = re.sub(
124 "%%%", "%%%%", re.sub("%([^s])", r"%%\1", statement))126 "%%%", "%%%%", re.sub("%([^s])", r"%%\1", statement))
125 quoted_statement = convert_param_marks(127 else:
126 quoted_statement, connection.param_mark, "%s")128 # Double all the %'s in the statement so that python string
129 # formatting can restore them to the correct number. Any %s in
130 # the string should be preserved because the param_mark is not
131 # %s.
132 quoted_statement = re.sub("%", "%%", statement)
133 quoted_statement = convert_param_marks(
134 quoted_statement, connection.param_mark, "%s")
127 # We need to massage the query parameters a little to deal with135 # We need to massage the query parameters a little to deal with
128 # string parameters which represent encoded binary data.136 # string parameters which represent encoded binary data.
129 render_params = []137 render_params = []
@@ -132,7 +140,12 @@
132 render_params.append(repr(param.encode('utf8')))140 render_params.append(repr(param.encode('utf8')))
133 else:141 else:
134 render_params.append(repr(param))142 render_params.append(repr(param))
135 statement_to_log = quoted_statement % tuple(render_params)143 try:
144 statement_to_log = quoted_statement % tuple(render_params)
145 except TypeError:
146 statement_to_log = \
147 "Unformattable query: %r with params %r." % (
148 statement, query_params)
136 self._expanded_raw_execute(connection, raw_cursor, statement_to_log)149 self._expanded_raw_execute(connection, raw_cursor, statement_to_log)
137150
138 def _expanded_raw_execute(self, connection, raw_cursor, statement):151 def _expanded_raw_execute(self, connection, raw_cursor, statement):
@@ -146,26 +159,29 @@
146 For more information on timelines see the module at159 For more information on timelines see the module at
147 http://pypi.python.org/pypi/timeline.160 http://pypi.python.org/pypi/timeline.
148 161
149 The timeline to use is obtained via a threading.local lookup -162 The timeline to use is obtained by calling the timeline_factory supplied to
150 self.threadinfo.timeline. If TimelineTracer is being used in a thread163 the constructor. This simple function takes no parameters and returns a
151 pooling environment, you should set this to the timeline you wish to164 timeline to use. If it returns None, the tracer is bypassed.
152 accumulate actions against before making queries.
153 """165 """
154166
155 def __init__(self, prefix='SQL-'):167 def __init__(self, timeline_factory, prefix='SQL-'):
156 """Create a TimelineTracer.168 """Create a TimelineTracer.
157169
170 #param timeline_factory: A factory function to produce the timeline to
171 record a query against.
158 @param prefix: A prefix to give the connection name when starting an172 @param prefix: A prefix to give the connection name when starting an
159 action. Connection names are found by trying a getattr for 'name'173 action. Connection names are found by trying a getattr for 'name'
160 on the connection object. If no name has been assigned, '<unknown>'174 on the connection object. If no name has been assigned, '<unknown>'
161 is used instead.175 is used instead.
162 """176 """
163 super(TimelineTracer, self).__init__()177 super(TimelineTracer, self).__init__()
178 self.timeline_factory = timeline_factory
179 self.prefix = prefix
180 # Stores the action in progress in a given thread.
164 self.threadinfo = threading.local()181 self.threadinfo = threading.local()
165 self.prefix = prefix
166182
167 def _expanded_raw_execute(self, connection, raw_cursor, statement):183 def _expanded_raw_execute(self, connection, raw_cursor, statement):
168 timeline = getattr(self.threadinfo, 'timeline', None)184 timeline = self.timeline_factory()
169 if timeline is None:185 if timeline is None:
170 return186 return
171 connection_name = getattr(connection, 'name', '<unknown>')187 connection_name = getattr(connection, 'name', '<unknown>')
172188
=== modified file 'tests/tracer.py'
--- tests/tracer.py 2011-09-12 10:24:04 +0000
+++ tests/tracer.py 2011-09-13 04:08:34 +0000
@@ -361,6 +361,19 @@
361 [(conn, 'cursor', "SELECT * FROM person where name = 'VAR1'")],361 [(conn, 'cursor', "SELECT * FROM person where name = 'VAR1'")],
362 tracer.calls)362 tracer.calls)
363363
364 def test_qmark_percent_s_literal_preserved(self):
365 """With ? parameters %s in the statement can be kept intact."""
366 tracer = self.LoggingBaseStatementTracer()
367 conn = self.StubConnection()
368 var1 = MockVariable(1)
369 tracer.connection_raw_execute(
370 conn, 'cursor',
371 "SELECT * FROM person where id > ? AND name LIKE '%s'", [var1])
372 self.assertEqual(
373 [(conn, 'cursor',
374 "SELECT * FROM person where id > 1 AND name LIKE '%s'")],
375 tracer.calls)
376
364 def test_int_variable_as_int(self):377 def test_int_variable_as_int(self):
365 """Int parameters are formatted as an int literal."""378 """Int parameters are formatted as an int literal."""
366 tracer = self.LoggingBaseStatementTracer()379 tracer = self.LoggingBaseStatementTracer()
@@ -386,21 +399,37 @@
386 "LIKE '%%' || 'substring' || '-suffix%%'")],399 "LIKE '%%' || 'substring' || '-suffix%%'")],
387 tracer.calls)400 tracer.calls)
388401
402 def test_unformattable_statements_are_handled(self):
403 tracer = self.LoggingBaseStatementTracer()
404 conn = self.StubConnection()
405 var1 = MockVariable(u'substring')
406 tracer.connection_raw_execute(
407 conn, 'cursor', "%s %s",
408 [var1])
409 self.assertEqual(
410 [(conn, 'cursor',
411 "Unformattable query: '%s %s' with params [u'substring'].")],
412 tracer.calls)
413
389414
390class TimelineTracerTest(TestHelper):415class TimelineTracerTest(TestHelper):
391416
392 def is_supported(self):417 def is_supported(self):
393 return timeline is not None418 return timeline is not None
394419
420 def factory(self):
421 self.timeline = timeline.Timeline()
422 return self.timeline
423
395 def test_separate_tracers_own_state(self):424 def test_separate_tracers_own_state(self):
396 """"Check that multiple TimelineTracer's could be used at once."""425 """"Check that multiple TimelineTracer's could be used at once."""
397 tracer1 = TimelineTracer()426 tracer1 = TimelineTracer(self.factory)
398 tracer2 = TimelineTracer()427 tracer2 = TimelineTracer(self.factory)
399 tracer1.threadinfo.timeline = 'foo'428 tracer1.threadinfo.action = 'foo'
400 self.assertEqual(None, getattr(tracer2.threadinfo, 'timeline', None))429 self.assertEqual(None, getattr(tracer2.threadinfo, 'action', None))
401430
402 def test_error_finishes_action(self):431 def test_error_finishes_action(self):
403 tracer = TimelineTracer()432 tracer = TimelineTracer(self.factory)
404 action = timeline.Timeline().start('foo', 'bar')433 action = timeline.Timeline().start('foo', 'bar')
405 tracer.threadinfo.action = action434 tracer.threadinfo.action = action
406 tracer.connection_raw_execute_error(435 tracer.connection_raw_execute_error(
@@ -408,27 +437,25 @@
408 self.assertNotEqual(None, action.duration)437 self.assertNotEqual(None, action.duration)
409438
410 def test_success_finishes_action(self):439 def test_success_finishes_action(self):
411 tracer = TimelineTracer()440 tracer = TimelineTracer(self.factory)
412 action = timeline.Timeline().start('foo', 'bar')441 action = timeline.Timeline().start('foo', 'bar')
413 tracer.threadinfo.action = action442 tracer.threadinfo.action = action
414 tracer.connection_raw_execute_success(443 tracer.connection_raw_execute_success(
415 'conn', 'cursor', 'statement', 'params')444 'conn', 'cursor', 'statement', 'params')
416 self.assertNotEqual(None, action.duration)445 self.assertNotEqual(None, action.duration)
417446
418 def test_finds_timeline_from_threadinfo(self):447 def test_finds_timeline_from_factory(self):
419 """tracer.threadinfo.timeline is writable as part of the API."""448 factory_result = timeline.Timeline()
420 tracer = TimelineTracer()449 factory = lambda:factory_result
421 tracer.threadinfo.timeline = timeline.Timeline()450 tracer = TimelineTracer(lambda:factory_result)
422 tracer._expanded_raw_execute('conn', 'cursor', 'statement')451 tracer._expanded_raw_execute('conn', 'cursor', 'statement')
423 self.assertEqual(1, len(tracer.threadinfo.timeline.actions))452 self.assertEqual(1, len(factory_result.actions))
424453
425 def test_action_details_are_statement(self):454 def test_action_details_are_statement(self):
426 """The detail in the timeline action is the formatted SQL statement."""455 """The detail in the timeline action is the formatted SQL statement."""
427 tracer = TimelineTracer()456 tracer = TimelineTracer(self.factory)
428 tracer.threadinfo.timeline = timeline.Timeline()
429 tracer._expanded_raw_execute('conn', 'cursor', 'statement')457 tracer._expanded_raw_execute('conn', 'cursor', 'statement')
430 self.assertEqual(458 self.assertEqual('statement', self.timeline.actions[-1].detail)
431 'statement', tracer.threadinfo.timeline.actions[-1].detail)
432459
433 def test_category_from_prefix_and_connection_name(self):460 def test_category_from_prefix_and_connection_name(self):
434 class StubConnection(Connection):461 class StubConnection(Connection):
@@ -438,24 +465,18 @@
438 self._event = None465 self._event = None
439 self._raw_connection = None466 self._raw_connection = None
440 self.name = 'Foo'467 self.name = 'Foo'
441 tracer = TimelineTracer(prefix='bar-')468 tracer = TimelineTracer(self.factory, prefix='bar-')
442 tracer.threadinfo.timeline = timeline.Timeline()
443 tracer._expanded_raw_execute(StubConnection(), 'cursor', 'statement')469 tracer._expanded_raw_execute(StubConnection(), 'cursor', 'statement')
444 self.assertEqual(470 self.assertEqual('bar-Foo', self.timeline.actions[-1].category)
445 'bar-Foo', tracer.threadinfo.timeline.actions[-1].category)
446471
447 def test_unnamed_connection(self):472 def test_unnamed_connection(self):
448 """A connection with no name has <unknown> put in as a placeholder."""473 """A connection with no name has <unknown> put in as a placeholder."""
449 tracer = TimelineTracer(prefix='bar-')474 tracer = TimelineTracer(self.factory, prefix='bar-')
450 tracer.threadinfo.timeline = timeline.Timeline()
451 tracer._expanded_raw_execute('conn', 'cursor', 'statement')475 tracer._expanded_raw_execute('conn', 'cursor', 'statement')
452 self.assertEqual(476 self.assertEqual('bar-<unknown>', self.timeline.actions[-1].category)
453 'bar-<unknown>', tracer.threadinfo.timeline.actions[-1].category)
454477
455 def test_default_prefix(self):478 def test_default_prefix(self):
456 """By default the prefix "SQL-" is added to the action's category."""479 """By default the prefix "SQL-" is added to the action's category."""
457 tracer = TimelineTracer()480 tracer = TimelineTracer(self.factory)
458 tracer.threadinfo.timeline = timeline.Timeline()
459 tracer._expanded_raw_execute('conn', 'cursor', 'statement')481 tracer._expanded_raw_execute('conn', 'cursor', 'statement')
460 self.assertEqual(482 self.assertEqual('SQL-<unknown>', self.timeline.actions[-1].category)
461 'SQL-<unknown>', tracer.threadinfo.timeline.actions[-1].category)

Subscribers

People subscribed via source and target branches

to status/vote changes: