Merge ~cjwatson/launchpad:better-oops-traceback-annotations into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: d00b1f5c789e5fa4a19c2c4b13cbcf00d4ac6c04
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:better-oops-traceback-annotations
Merge into: launchpad:master
Diff against target: 225 lines (+94/-37)
3 files modified
lib/lp/services/timeline/requesttimeline.py (+57/-3)
lib/lp/services/webapp/adapter.py (+8/-34)
lib/lp/services/webapp/tests/test_statementtracer.py (+29/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+438607@code.launchpad.net

Commit message

Include traceback supplements and info in timelines

Description of the change

Some code adds extra information to tracebacks (documented in https://zopeexceptions.readthedocs.io/en/latest/narr.html): in particular, the page templating engine does this, and it makes it much easier to work out what tracebacks in the middle of page rendering mean. Unfortunately, this information wasn't recorded in the individual entries in an OOPS timeline, which in some cases made OOPSes very hard to decipher (LP: #2009357 is a case in point).

Adjust the way we construct `Timeline` objects so that they format tracebacks using the machinery in `zope.exceptions`, which knows how to deal with this extra information.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

Thanks! That will be a big help!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/services/timeline/requesttimeline.py b/lib/lp/services/timeline/requesttimeline.py
2index 7fb0332..c529ace 100644
3--- a/lib/lp/services/timeline/requesttimeline.py
4+++ b/lib/lp/services/timeline/requesttimeline.py
5@@ -5,13 +5,19 @@
6
7 __all__ = [
8 "get_request_timeline",
9+ "make_timeline",
10 "set_request_timeline",
11 "temporary_request_timeline",
12 ]
13
14+import sys
15 from contextlib import contextmanager
16+from functools import partial
17+from typing import Callable, MutableSequence, Optional
18
19 from timeline import Timeline
20+from timeline.timedaction import TimedAction
21+from zope.exceptions.exceptionformatter import extract_stack
22
23 # XXX RobertCollins 2010-09-01 bug=623199: Undesirable but pragmatic.
24 # Because of this bug, rather than using request.annotations we have
25@@ -20,6 +26,54 @@ from timeline import Timeline
26 from lp.services import webapp
27
28
29+class FilteredTimeline(Timeline):
30+ """A timeline that filters its actions.
31+
32+ This is useful for requests that are expected to log actions with very
33+ large details (for example, large bulk SQL INSERT statements), where we
34+ don't want the overhead of storing those in memory.
35+ """
36+
37+ def __init__(self, actions=None, detail_filter=None, **kwargs):
38+ super().__init__(actions=actions, **kwargs)
39+ self.detail_filter = detail_filter
40+
41+ def start(self, category, detail, allow_nested=False):
42+ """See `Timeline`."""
43+ if self.detail_filter is not None:
44+ detail = self.detail_filter(category, detail)
45+ return super().start(category, detail)
46+
47+
48+def format_stack():
49+ """Format a stack like traceback.format_stack, but skip 2 frames.
50+
51+ This means the stack formatting frame isn't in the backtrace itself.
52+
53+ Also add supplemental information to the traceback using
54+ `zope.exceptions.exceptionformatter`.
55+ """
56+ return extract_stack(f=sys._getframe(2))
57+
58+
59+def make_timeline(
60+ actions: Optional[MutableSequence[TimedAction]] = None,
61+ detail_filter: Optional[Callable[[str, str], str]] = None,
62+) -> Timeline:
63+ """Make a new `Timeline`, configured appropriately for Launchpad.
64+
65+ :param actions: The sequence used to store the logged SQL statements.
66+ :param detail_filter: An optional (category, detail) -> detail callable
67+ that filters action details. This may be used when some details are
68+ expected to be very large.
69+ """
70+ if detail_filter is not None:
71+ factory = partial(FilteredTimeline, detail_filter=detail_filter)
72+ else:
73+ factory = Timeline
74+ return factory(actions=actions, format_stack=format_stack)
75+
76+
77 def get_request_timeline(request):
78 """Get a `Timeline` for request.
79
80@@ -34,9 +88,9 @@ def get_request_timeline(request):
81 try:
82 return webapp.adapter._local.request_timeline
83 except AttributeError:
84- return set_request_timeline(request, Timeline())
85+ return set_request_timeline(request, make_timeline())
86 # Disabled code path: bug 623199, ideally we would use this code path.
87- return request.annotations.setdefault("timeline", Timeline())
88+ return request.annotations.setdefault("timeline", make_timeline())
89
90
91 def set_request_timeline(request, timeline):
92@@ -64,7 +118,7 @@ def temporary_request_timeline(request):
93 """
94 old_timeline = get_request_timeline(request)
95 try:
96- set_request_timeline(request, Timeline())
97+ set_request_timeline(request, make_timeline())
98 yield
99 finally:
100 set_request_timeline(request, old_timeline)
101diff --git a/lib/lp/services/webapp/adapter.py b/lib/lp/services/webapp/adapter.py
102index dc57f0d..2e16397 100644
103--- a/lib/lp/services/webapp/adapter.py
104+++ b/lib/lp/services/webapp/adapter.py
105@@ -29,7 +29,6 @@ from storm.exceptions import TimeoutError
106 from storm.store import Store
107 from storm.tracer import install_tracer
108 from talisker.logs import logging_context
109-from timeline.timeline import Timeline
110 from zope.component import getUtility
111 from zope.interface import alsoProvides, classImplements, implementer, provider
112 from zope.security.proxy import removeSecurityProxy
113@@ -52,6 +51,7 @@ from lp.services.log.loglevels import DEBUG2
114 from lp.services.stacktrace import extract_stack, extract_tb, print_list
115 from lp.services.timeline.requesttimeline import (
116 get_request_timeline,
117+ make_timeline,
118 set_request_timeline,
119 )
120 from lp.services.timeout import set_default_timeout_function
121@@ -134,25 +134,6 @@ class CommitLogger:
122 action.finish()
123
124
125-class FilteredTimeline(Timeline):
126- """A timeline that filters its actions.
127-
128- This is useful for requests that are expected to log actions with very
129- large details (for example, large bulk SQL INSERT statements), where we
130- don't want the overhead of storing those in memory.
131- """
132-
133- def __init__(self, actions=None, detail_filter=None, **kwargs):
134- super().__init__(actions=actions, **kwargs)
135- self.detail_filter = detail_filter
136-
137- def start(self, category, detail, allow_nested=False):
138- """See `Timeline`."""
139- if self.detail_filter is not None:
140- detail = self.detail_filter(category, detail)
141- return super().start(category, detail)
142-
143-
144 def set_request_started(
145 starttime=None,
146 request_statements=None,
147@@ -187,19 +168,12 @@ def set_request_started(
148 starttime = time()
149 _local.request_start_time = starttime
150 request = get_current_browser_request()
151- if detail_filter is not None:
152- timeline_factory = partial(
153- FilteredTimeline, detail_filter=detail_filter
154- )
155- else:
156- timeline_factory = Timeline
157- if request_statements is not None:
158- # Specify a specific sequence object for the timeline.
159- set_request_timeline(request, timeline_factory(request_statements))
160- else:
161- # Ensure a timeline is created, so that time offset for actions is
162- # reasonable.
163- set_request_timeline(request, timeline_factory())
164+ # Ensure a timeline is created, so that time offset for actions is
165+ # reasonable.
166+ set_request_timeline(
167+ request,
168+ make_timeline(actions=request_statements, detail_filter=detail_filter),
169+ )
170 _local.current_statement_timeout = None
171 _local.enable_timeout = enable_timeout
172 _local.commit_logger = CommitLogger(transaction)
173@@ -219,7 +193,7 @@ def clear_request_started():
174 _local.sql_logging_start = None
175 _local.sql_logging_tracebacks_if = None
176 request = get_current_browser_request()
177- set_request_timeline(request, Timeline())
178+ set_request_timeline(request, make_timeline())
179 if getattr(_local, "commit_logger", None) is not None:
180 transaction.manager.unregisterSynch(_local.commit_logger)
181 del _local.commit_logger
182diff --git a/lib/lp/services/webapp/tests/test_statementtracer.py b/lib/lp/services/webapp/tests/test_statementtracer.py
183index 383865d..92351e0 100644
184--- a/lib/lp/services/webapp/tests/test_statementtracer.py
185+++ b/lib/lp/services/webapp/tests/test_statementtracer.py
186@@ -10,6 +10,7 @@ from contextlib import contextmanager
187 from lazr.restful.utils import get_current_browser_request
188
189 from lp.services.osutils import override_environ
190+from lp.services.tests.test_stacktrace import Supplement
191 from lp.services.timeline.requesttimeline import get_request_timeline
192 from lp.services.webapp import adapter as da
193 from lp.testing import (
194@@ -461,3 +462,31 @@ class TestLoggingWithinRequest(TestCaseWithFactory):
195 self.connection, None, "SELECT * FROM one", (), Exception()
196 )
197 self.assertIsNone(self.connection._lp_statement_action)
198+
199+ def test_includes_traceback_supplement_and_info(self):
200+ # The timeline records information from `__traceback_supplement__`
201+ # and `__traceback_info__` in tracebacks.
202+ tracer = da.LaunchpadStatementTracer()
203+
204+ def call():
205+ __traceback_supplement__ = (
206+ Supplement,
207+ {"expression": "something"},
208+ )
209+ __traceback_info__ = "Extra information"
210+ tracer.connection_raw_execute(
211+ self.connection, None, "SELECT * FROM one", ()
212+ )
213+
214+ with person_logged_in(self.person):
215+ with StormStatementRecorder(tracebacks_if=True):
216+ call()
217+ timeline = get_request_timeline(get_current_browser_request())
218+ self.assertRegex(
219+ timeline.actions[-1].backtrace,
220+ "\n"
221+ " File .*, in call\n"
222+ " .*\n"
223+ " - Expression: something\n"
224+ " - __traceback_info__: Extra information\n",
225+ )

Subscribers

People subscribed via source and target branches

to status/vote changes: