Skip to content

Commit 5a8c11d

Browse files
committed
Add new stack trace functionality
* Add a private _StackTraceRecorder class to debug_toolbar.utils which implements caching to reduce the overhead of expensive file system operations. This class has a get_stack_trace() method which combines the functionality of the get_stack() and tidy_stacktrace() functions. * Add a new debug_toolbar.utils function, get_stack_trace() which gets or instantiates a thread/async task context-local _StackTraceRecorder instance and returns a stack trace using its get_stack_trace() method. * Add a new debug_toolbar.utils function, clear_stack_trace_caches(), which removes any thread/async task context-local _StackTraceRecorder instance. * Update the DebugToolbarMiddleware to call the clear_stack_trace_caches() function after processing a request to ensure that each subsequent request gets a clean cache.
1 parent 66a767d commit 5a8c11d

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

debug_toolbar/middleware.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from debug_toolbar import settings as dt_settings
1212
from debug_toolbar.toolbar import DebugToolbar
13+
from debug_toolbar.utils import clear_stack_trace_caches
1314

1415
_HTML_TYPES = ("text/html", "application/xhtml+xml")
1516

@@ -56,6 +57,7 @@ def __call__(self, request):
5657
# Run panels like Django middleware.
5758
response = toolbar.process_request(request)
5859
finally:
60+
clear_stack_trace_caches()
5961
# Deactivate instrumentation ie. monkey-unpatch. This must run
6062
# regardless of the response. Keep 'return' clauses below.
6163
for panel in reversed(toolbar.enabled_panels):

debug_toolbar/utils.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import inspect
2+
import linecache
23
import os.path
34
import sys
45
from importlib import import_module
56
from pprint import pformat
67

78
import django
9+
from asgiref.local import Local
810
from django.core.exceptions import ImproperlyConfigured
911
from django.template import Node
1012
from django.utils.html import format_html
@@ -18,6 +20,9 @@
1820
threading = None
1921

2022

23+
_local_data = Local()
24+
25+
2126
# Figure out some paths
2227
django_path = os.path.realpath(os.path.dirname(django.__file__))
2328

@@ -242,6 +247,99 @@ def get_stack(context=1):
242247
return framelist
243248

244249

250+
def _stack_frames(depth=1):
251+
frame = inspect.currentframe()
252+
while frame is not None:
253+
if depth > 0:
254+
depth -= 1
255+
else:
256+
yield frame
257+
frame = frame.f_back
258+
259+
260+
class _StackTraceRecorder:
261+
def __init__(self, excluded_paths):
262+
self.excluded_paths = excluded_paths
263+
self.filename_cache = {}
264+
self.is_excluded_cache = {}
265+
266+
def get_source_file(self, frame):
267+
frame_filename = frame.f_code.co_filename
268+
269+
value = self.filename_cache.get(frame_filename)
270+
if value is None:
271+
filename = inspect.getsourcefile(frame)
272+
if filename is None:
273+
is_source = False
274+
filename = frame_filename
275+
else:
276+
is_source = True
277+
# Ensure linecache validity the first time this recorder
278+
# encounters the filename in this frame.
279+
linecache.checkcache(filename)
280+
value = (filename, is_source)
281+
self.filename_cache[frame_filename] = value
282+
283+
return value
284+
285+
def is_excluded_path(self, path):
286+
excluded = self.is_excluded_cache.get(path)
287+
if excluded is None:
288+
resolved_path = os.path.realpath(path)
289+
excluded = any(
290+
resolved_path.startswith(excluded_path)
291+
for excluded_path in self.excluded_paths
292+
)
293+
self.is_excluded_cache[path] = excluded
294+
return excluded
295+
296+
def get_stack_trace(self, include_locals=False, depth=1):
297+
trace = []
298+
for frame in _stack_frames(depth=depth + 1):
299+
filename, is_source = self.get_source_file(frame)
300+
301+
if self.is_excluded_path(filename):
302+
continue
303+
304+
line_no = frame.f_lineno
305+
func_name = frame.f_code.co_name
306+
307+
if is_source:
308+
module = inspect.getmodule(frame, filename)
309+
module_globals = module.__dict__ if module is not None else None
310+
source_line = linecache.getline(
311+
filename, line_no, module_globals
312+
).strip()
313+
else:
314+
source_line = ""
315+
316+
frame_locals = frame.f_locals if include_locals else None
317+
318+
trace.append((filename, line_no, func_name, source_line, frame_locals))
319+
trace.reverse()
320+
return trace
321+
322+
323+
def get_stack_trace(depth=1):
324+
config = dt_settings.get_config()
325+
if config["ENABLE_STACKTRACES"]:
326+
stack_trace_recorder = getattr(_local_data, "stack_trace_recorder", None)
327+
if stack_trace_recorder is None:
328+
stack_trace_recorder = _StackTraceRecorder(hidden_paths)
329+
_local_data.stack_trace_recorder = stack_trace_recorder
330+
return stack_trace_recorder.get_stack_trace(
331+
include_locals=config["ENABLE_STACKTRACES_LOCALS"],
332+
depth=depth,
333+
)
334+
else:
335+
return []
336+
337+
338+
def clear_stack_trace_caches():
339+
if hasattr(_local_data, "stack_trace_recorder"):
340+
del _local_data.stack_trace_recorder
341+
342+
245343
class ThreadCollector:
246344
def __init__(self):
247345
if threading is None:

0 commit comments

Comments
 (0)