Merge lp:~lifeless/python-oops-tools/polish into lp:python-oops-tools

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: 19
Merged at revision: 18
Proposed branch: lp:~lifeless/python-oops-tools/polish
Merge into: lp:python-oops-tools
Diff against target: 183 lines (+29/-20)
6 files modified
src/oopstools/NEWS.txt (+5/-0)
src/oopstools/oops/models.py (+10/-10)
src/oopstools/oops/templates/oops.html (+3/-1)
src/oopstools/oops/test/oops.txt (+1/-1)
src/oopstools/oops/test/pagetest.txt (+2/-1)
src/oopstools/oops/test/test_dboopsloader.py (+8/-7)
To merge this branch: bzr merge lp:~lifeless/python-oops-tools/polish
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+82365@code.launchpad.net

Commit message

Show backtraces in OOPS report timelines.

Description of the change

Update the oops-tools model to show the new backtrace column in OOPS report timelines.

This is a bit crude - I'm making a somewhat horrid table more horrid (by making it wider). It also makes simple queries have a big gap between them.

OTOH there is a bit of a steep curve to get a js library in here due to our preferred tech not being the default in Django. I think we'll have a net benefit from this change without nice JS hiding, and I'm going to investigate JS hiding separately - e.g. Talk to francis about ROI, preferred choice etc etc.

This change can be easily backed out (its about 5 minutes to go from intent to deployed on our oops-tools instance a the moment) if we decide its a problem, so I'd like to get it up there and see.

Most of the changes have test coverage (in that tests broke when the change was made) except the actual template change itself. I've added a smoke test there (that the heading exists, which implies something about the body of the table), but its not truely satisfactory. OTOH I don't know what would be really satisfactory, given we're dealing with a pagetest here.

Thanks for your consideration :)

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Is there no HTML test that would covers this at all? I do have a few concerns:

 * Out of interest, any idea how costly it is to record, and later, render tracebacks for every event?

 * What about HTML-escaping? What if the traceback contained a line like:

        123, "</pre>", (a < b))

—wouldn't that upset the rendered HTML? Not likely to be a big problem for us, I suppose, but if we want this code to be reusable…

review: Needs Information
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

I keep forgetting that <pre> doesn't change tag parsing. Which means that the traceback and all other text items going out are almost certainly escaped just fine.

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

Thanks for the review. I started a thread about the overheads on launchpad-dev - I measured 0.2seconds for recording, stringifying, and shoving in a list, 1000 tracebacks in a 60 deep stack frame.For a well behaved page with < 100 callouts (SQL, memcache etc) thats an overhead of 20ms, which isn't free, but isn't huge either. For a great page doing 20-30 callouts, thats 4-7ms, totally tolerable. So bad pages may be noticable, on good pages will be fine. This app doesn't do the recording of course; thats done by the oops producer, which doesn't do template rendering - it just puts the capture strings into a bson document if an OOPS is recorded.

On the html/pre side, I'm not a django expert. I'll do a quick manual test to make sure that the django template engine does handle a malicious backtrace.

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

Django escapes the content in the {{}} so </pre> in a traceback becomes &lt;/pre&gt; etc. All is good.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/oopstools/NEWS.txt'
--- src/oopstools/NEWS.txt 2011-11-16 02:22:14 +0000
+++ src/oopstools/NEWS.txt 2011-11-16 08:16:24 +0000
@@ -22,6 +22,11 @@
22 a bit better.22 a bit better.
23 (Robert Collins, William Grant, Roman Yepishev, #885416, #884265)23 (Robert Collins, William Grant, Roman Yepishev, #885416, #884265)
2424
25* Report timelines now include a backtrace for each event, making it easier to
26 determine the origin of the event (e.g. why something is looked up).
27 Timelines without this information will show 'unknown' in the relevant
28 column on the OOPS page. (Robert Collins, #890976)
29
25* The req_vars variable in OOPS reports may now be a dict.30* The req_vars variable in OOPS reports may now be a dict.
26 (Robert Collins, #888866)31 (Robert Collins, #888866)
2732
2833
=== modified file 'src/oopstools/oops/models.py'
--- src/oopstools/oops/models.py 2011-11-16 07:25:56 +0000
+++ src/oopstools/oops/models.py 2011-11-16 08:16:24 +0000
@@ -350,11 +350,11 @@
350 # Sanity check/coerce the statements into what we expect.350 # Sanity check/coerce the statements into what we expect.
351 # Must be a list:351 # Must be a list:
352 if not isinstance(statements, list):352 if not isinstance(statements, list):
353 statements = [(0, 0, 'badtimeline', str(statements))]353 statements = [(0, 0, 'badtimeline', str(statements), 'unknown')]
354 else:354 else:
355 filler = (0, 0, 'unknown', 'unknown')355 filler = (0, 0, 'unknown', 'unknown', 'unknown')
356 for row, action in enumerate(statements):356 for row, action in enumerate(statements):
357 statements[row] = tuple(action[:4]) + filler[len(action):]357 statements[row] = tuple(action[:5]) + filler[len(action):]
358 duration = oops.get('duration')358 duration = oops.get('duration')
359 if duration is not None:359 if duration is not None:
360 total_time = int(duration * 1000)360 total_time = int(duration * 1000)
@@ -388,7 +388,7 @@
388 most_expensive_statement = None388 most_expensive_statement = None
389 else:389 else:
390 costs = {}390 costs = {}
391 for (start, end, db_id, statement) in statements:391 for (start, end, db_id, statement, tb) in statements:
392 statement = replace_variables(statement)392 statement = replace_variables(statement)
393 cost = end - start393 cost = end - start
394 if statement in costs:394 if statement in costs:
@@ -566,8 +566,8 @@
566 #XXX need docstring and test566 #XXX need docstring and test
567 if not self.statements:567 if not self.statements:
568 return []568 return []
569 return [(count+1, start, stop, db_id, stop - start, stmt) for569 return [(count+1, start, stop, db_id, stop - start, stmt, tb) for
570 (count, (start, stop, db_id, stmt)) in570 (count, (start, stop, db_id, stmt, tb)) in
571 enumerate(self.statements)]571 enumerate(self.statements)]
572572
573 def repeated_statements(self):573 def repeated_statements(self):
@@ -623,7 +623,7 @@
623 if not self.statements:623 if not self.statements:
624 return 0624 return 0
625 accumulated_time = 0625 accumulated_time = 0
626 for (start, stop, db_id, stmt) in self.statements:626 for (start, stop, db_id, stmt, trace) in self.statements:
627 length = stop - start627 length = stop - start
628 accumulated_time += length628 accumulated_time += length
629 return accumulated_time629 return accumulated_time
@@ -636,13 +636,13 @@
636 def _group_queries(self, unsorted_statements):636 def _group_queries(self, unsorted_statements):
637 """Group SQL log queries.637 """Group SQL log queries.
638638
639 unsorted_statements is a list of (start, stop, db_id, statement).639 unsorted_statements is a list of (start, stop, db_id, statement, tb).
640 Return a list of (total_time_ms, count, mean_length, saving, db_id,640 Return a list of (total_time_ms, count, mean_length, saving, db_id,
641 statement).641 statement).
642 """642 """
643 sorted_statements = sorted(643 sorted_statements = sorted(
644 unsorted_statements,644 unsorted_statements,
645 key=lambda (start, stop, db_id, statement): statement645 key=lambda row: row[3]
646 )646 )
647 groups = []647 groups = []
648 def append_to_group(gtime, gcount, db_id, statement):648 def append_to_group(gtime, gcount, db_id, statement):
@@ -653,7 +653,7 @@
653653
654 group_statement = None654 group_statement = None
655 for row in sorted_statements:655 for row in sorted_statements:
656 start, stop, db_id, statement = row656 start, stop, db_id, statement, tb = row
657 statement = replace_variables(statement)657 statement = replace_variables(statement)
658 length = stop - start658 length = stop - start
659 if statement != group_statement:659 if statement != group_statement:
660660
=== modified file 'src/oopstools/oops/templates/oops.html'
--- src/oopstools/oops/templates/oops.html 2011-10-13 20:18:51 +0000
+++ src/oopstools/oops/templates/oops.html 2011-11-16 08:16:24 +0000
@@ -168,16 +168,18 @@
168 <th>Length</th>168 <th>Length</th>
169 <th>Database id</th>169 <th>Database id</th>
170 <th>Statement</th>170 <th>Statement</th>
171 <th>Traceback</th>
171 </tr>172 </tr>
172 </thead>173 </thead>
173 <tbody>174 <tbody>
174 {% for num, start, stop, db_id, length, statement in oops.formatted_statements %}175 {% for num, start, stop, db_id, length, statement, tb in oops.formatted_statements %}
175 <tr>176 <tr>
176 <td>{{num}}.</td>177 <td>{{num}}.</td>
177 <td>{{start}}</td>178 <td>{{start}}</td>
178 <td>{{length}}ms</td>179 <td>{{length}}ms</td>
179 <td>{{db_id}}</td>180 <td>{{db_id}}</td>
180 <td><pre>{{statement|format_sql}}</pre></td>181 <td><pre>{{statement|format_sql}}</pre></td>
182 <td><pre>{{tb}}</pre></td>
181 </tr>183 </tr>
182 {% endfor %}184 {% endfor %}
183 </tbody>185 </tbody>
184186
=== modified file 'src/oopstools/oops/test/oops.txt'
--- src/oopstools/oops/test/oops.txt 2011-11-02 21:29:36 +0000
+++ src/oopstools/oops/test/oops.txt 2011-11-16 08:16:24 +0000
@@ -263,7 +263,7 @@
263recorded together with the SQL statement.263recorded together with the SQL statement.
264264
265 >>> oops_with_db_id = Oops.objects.get(oopsid__exact="OOPS-1308x1")265 >>> oops_with_db_id = Oops.objects.get(oopsid__exact="OOPS-1308x1")
266 >>> for start, stop, db_id, statement in oops_with_db_id.statements:266 >>> for start, stop, db_id, statement, tb in oops_with_db_id.statements:
267 ... print start, stop, db_id, statement267 ... print start, stop, db_id, statement
268 4 5 session UPDATE SessionData SET last_accessed ...268 4 5 session UPDATE SessionData SET last_accessed ...
269 6 7 session SELECT key, pickle FROM SessionPkgData WHERE ...269 6 7 session SELECT key, pickle FROM SessionPkgData WHERE ...
270270
=== modified file 'src/oopstools/oops/test/pagetest.txt'
--- src/oopstools/oops/test/pagetest.txt 2011-11-02 23:26:40 +0000
+++ src/oopstools/oops/test/pagetest.txt 2011-11-16 08:16:24 +0000
@@ -38,7 +38,8 @@
38 ...<div id="request_variables">...38 ...<div id="request_variables">...
39 ...39 ...
40 ...<li>CHANNEL_CREATION_TIME: 1...</li>...40 ...<li>CHANNEL_CREATION_TIME: 1...</li>...
41 ...41 ...SQL Statement Log...
42 ...Start...Length...Database id...Statement...Traceback...
4243
43It also accepts the OOPS id without the "OOPS-" part.44It also accepts the OOPS id without the "OOPS-" part.
4445
4546
=== modified file 'src/oopstools/oops/test/test_dboopsloader.py'
--- src/oopstools/oops/test/test_dboopsloader.py 2011-11-16 02:22:14 +0000
+++ src/oopstools/oops/test/test_dboopsloader.py 2011-11-16 08:16:24 +0000
@@ -196,7 +196,8 @@
196 'timeline': 'a'196 'timeline': 'a'
197 }197 }
198 oops = parsed_oops_to_model_oops(report, 'bug-890001-1')198 oops = parsed_oops_to_model_oops(report, 'bug-890001-1')
199 self.assertEqual([(0, 0, 'badtimeline', 'a')], oops.statements)199 self.assertEqual(
200 [(0, 0, 'badtimeline', 'a', 'unknown')], oops.statements)
200 201
201 def test_short_timeline_row(self):202 def test_short_timeline_row(self):
202 # A timeline row that is short is padded with 'unknown' or 0's.203 # A timeline row that is short is padded with 'unknown' or 0's.
@@ -211,10 +212,10 @@
211 }212 }
212 oops = parsed_oops_to_model_oops(report, 'bug-890001-2')213 oops = parsed_oops_to_model_oops(report, 'bug-890001-2')
213 self.assertEqual([214 self.assertEqual([
214 (0, 0, 'unknown', 'unknown'),215 (0, 0, 'unknown', 'unknown', 'unknown'),
215 (1, 0, 'unknown', 'unknown'),216 (1, 0, 'unknown', 'unknown', 'unknown'),
216 (1, 2, 'unknown', 'unknown'),217 (1, 2, 'unknown', 'unknown', 'unknown'),
217 (1, 2, 'foo', 'unknown'),218 (1, 2, 'foo', 'unknown', 'unknown'),
218 ], oops.statements)219 ], oops.statements)
219220
220 def test_long_timeline_row(self):221 def test_long_timeline_row(self):
@@ -222,12 +223,12 @@
222 report = {223 report = {
223 'id': 'bug-890001-3',224 'id': 'bug-890001-3',
224 'timeline': [225 'timeline': [
225 (1, 2, 'foo', 'bar', 'baz'),226 (1, 2, 'foo', 'bar', 'baz', 'quux'),
226 ],227 ],
227 }228 }
228 oops = parsed_oops_to_model_oops(report, 'bug-890001-3')229 oops = parsed_oops_to_model_oops(report, 'bug-890001-3')
229 self.assertEqual([230 self.assertEqual([
230 (1, 2, 'foo', 'bar'),231 (1, 2, 'foo', 'bar', 'baz'),
231 ], oops.statements)232 ], oops.statements)
232233
233 def test_empty_report_long_id_uses_unknown_bug_889982(self):234 def test_empty_report_long_id_uses_unknown_bug_889982(self):

Subscribers

People subscribed via source and target branches

to all changes: