PageRenderTime 84ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/debug_toolbar/panels/sql.py

https://github.com/stevejalim/django-debug-toolbar
Python | 218 lines | 207 code | 4 blank | 7 comment | 0 complexity | 601c931add40aaf160cd18daf159e5b0 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. from datetime import datetime
  2. import os
  3. import sys
  4. import SocketServer
  5. import traceback
  6. import django
  7. from django.conf import settings
  8. from django.db import connection
  9. from django.db.backends import util
  10. from django.views.debug import linebreak_iter
  11. from django.template import Node
  12. from django.template.loader import render_to_string
  13. from django.utils import simplejson
  14. from django.utils.encoding import force_unicode
  15. from django.utils.hashcompat import sha_constructor
  16. from django.utils.translation import ugettext_lazy as _
  17. from debug_toolbar.panels import DebugPanel
  18. from debug_toolbar.utils import sqlparse
  19. # Figure out some paths
  20. django_path = os.path.realpath(os.path.dirname(django.__file__))
  21. socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__))
  22. # TODO:This should be set in the toolbar loader as a default and panels should
  23. # get a copy of the toolbar object with access to its config dictionary
  24. toolbar_settings = getattr(settings, 'DEBUG_TOOLBAR_CONFIG', {})
  25. SQL_WARNING_THRESHOLD = toolbar_settings.get('SQL_WARNING_THRESHOLD', 500)
  26. HIDE_DJANGO_SQL = toolbar_settings.get('HIDE_DJANGO_SQL', True)
  27. STACKTRACE_ROOT = toolbar_settings.get('STACKTRACE_ROOT', '')
  28. def tidy_stacktrace(strace):
  29. """
  30. Clean up stacktrace and remove all entries that:
  31. 1. Are part of Django (except contrib apps)
  32. 2. Are part of SocketServer (used by Django's dev server)
  33. 3. Are the last entry (which is part of our stacktracing code)
  34. """
  35. trace = []
  36. for s in strace[:-1]:
  37. s_path = os.path.realpath(s[0])
  38. if HIDE_DJANGO_SQL and django_path in s_path and not 'django/contrib' in s_path:
  39. continue
  40. if socketserver_path in s_path:
  41. continue
  42. trace.append((s[0].replace(STACKTRACE_ROOT, ''), s[1], s[2], s[3]))
  43. return trace
  44. def get_template_info(source, context_lines=3):
  45. line = 0
  46. upto = 0
  47. source_lines = []
  48. before = during = after = ""
  49. origin, (start, end) = source
  50. template_source = origin.reload()
  51. for num, next in enumerate(linebreak_iter(template_source)):
  52. if start >= upto and end <= next:
  53. line = num
  54. before = template_source[upto:start]
  55. during = template_source[start:end]
  56. after = template_source[end:next]
  57. source_lines.append((num, template_source[upto:next]))
  58. upto = next
  59. top = max(1, line - context_lines)
  60. bottom = min(len(source_lines), line + 1 + context_lines)
  61. context = []
  62. for num, content in source_lines[top:bottom]:
  63. context.append({
  64. 'num': num,
  65. 'content': content,
  66. 'highlight': (num == line),
  67. })
  68. return {
  69. 'name': origin.name,
  70. 'context': context,
  71. }
  72. class DatabaseStatTracker(util.CursorDebugWrapper):
  73. """
  74. Replacement for CursorDebugWrapper which stores additional information
  75. in `connection.queries`.
  76. """
  77. def execute(self, sql, params=()):
  78. start = datetime.now()
  79. try:
  80. return self.cursor.execute(sql, params)
  81. finally:
  82. stop = datetime.now()
  83. duration = ms_from_timedelta(stop - start)
  84. stacktrace = tidy_stacktrace(traceback.extract_stack())
  85. _params = ''
  86. try:
  87. _params = simplejson.dumps([force_unicode(x, strings_only=True) for x in params])
  88. except TypeError:
  89. pass # object not JSON serializable
  90. template_info = None
  91. cur_frame = sys._getframe().f_back
  92. try:
  93. while cur_frame is not None:
  94. if cur_frame.f_code.co_name == 'render':
  95. node = cur_frame.f_locals['self']
  96. if isinstance(node, Node):
  97. template_info = get_template_info(node.source)
  98. break
  99. cur_frame = cur_frame.f_back
  100. except:
  101. pass
  102. del cur_frame
  103. # We keep `sql` to maintain backwards compatibility
  104. self.db.queries.append({
  105. 'sql': self.db.ops.last_executed_query(self.cursor, sql, params),
  106. 'duration': duration,
  107. 'raw_sql': sql,
  108. 'params': _params,
  109. 'hash': sha_constructor(settings.SECRET_KEY + sql + _params).hexdigest(),
  110. 'stacktrace': stacktrace,
  111. 'start_time': start,
  112. 'stop_time': stop,
  113. 'is_slow': (duration > SQL_WARNING_THRESHOLD),
  114. 'is_select': sql.lower().strip().startswith('select'),
  115. 'template_info': template_info,
  116. })
  117. util.CursorDebugWrapper = DatabaseStatTracker
  118. class SQLDebugPanel(DebugPanel):
  119. """
  120. Panel that displays information about the SQL queries run while processing
  121. the request.
  122. """
  123. name = 'SQL'
  124. has_content = True
  125. def __init__(self, *args, **kwargs):
  126. super(self.__class__, self).__init__(*args, **kwargs)
  127. self._offset = len(connection.queries)
  128. self._sql_time = 0
  129. self._queries = []
  130. def nav_title(self):
  131. return _('SQL')
  132. def nav_subtitle(self):
  133. self._queries = connection.queries[self._offset:]
  134. self._sql_time = sum([q['duration'] for q in self._queries])
  135. num_queries = len(self._queries)
  136. # TODO l10n: use ngettext
  137. return "%d %s in %.2fms" % (
  138. num_queries,
  139. (num_queries == 1) and 'query' or 'queries',
  140. self._sql_time
  141. )
  142. def title(self):
  143. return _('SQL Queries')
  144. def url(self):
  145. return ''
  146. def content(self):
  147. width_ratio_tally = 0
  148. most_executed = {}
  149. for query in self._queries:
  150. query['sql'] = reformat_sql(query['sql'])
  151. query['last_stacktrace'] = query['stacktrace'][-1]
  152. raw_query = reformat_sql(query['raw_sql'])
  153. most_executed.setdefault(raw_query, []).append(query)
  154. try:
  155. query['width_ratio'] = (query['duration'] / self._sql_time) * 100
  156. except ZeroDivisionError:
  157. query['width_ratio'] = 0
  158. query['start_offset'] = width_ratio_tally
  159. width_ratio_tally += query['width_ratio']
  160. most_executed = most_executed.items()
  161. most_executed.sort(key = lambda v: len(v[1]), reverse=True)
  162. most_executed = most_executed[:10]
  163. context = self.context.copy()
  164. context = {
  165. 'queries': self._queries,
  166. 'sql_time': self._sql_time,
  167. 'is_mysql': settings.DATABASE_ENGINE == 'mysql',
  168. 'most_executed': most_executed,
  169. }
  170. return render_to_string('debug_toolbar/panels/sql.html', context)
  171. def ms_from_timedelta(td):
  172. """
  173. Given a timedelta object, returns a float representing milliseconds
  174. """
  175. return (td.seconds * 1000) + (td.microseconds / 1000.0)
  176. class BoldKeywordFilter(sqlparse.filters.Filter):
  177. """sqlparse filter to bold SQL keywords"""
  178. def process(self, stack, stream):
  179. """Process the token stream"""
  180. for token_type, value in stream:
  181. is_keyword = token_type in sqlparse.tokens.Keyword
  182. if is_keyword:
  183. yield sqlparse.tokens.Text, '<strong>'
  184. yield token_type, django.utils.html.escape(value)
  185. if is_keyword:
  186. yield sqlparse.tokens.Text, '</strong>'
  187. def reformat_sql(sql):
  188. stack = sqlparse.engine.FilterStack()
  189. stack.preprocess.append(BoldKeywordFilter()) # add our custom filter
  190. stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings
  191. return ''.join(stack.run(sql))