PageRenderTime 573ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/debug_toolbar/panels/sql.py

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