PageRenderTime 270ms CodeModel.GetById 30ms RepoModel.GetById 2ms app.codeStats 0ms

/debug_toolbar/panels/sql.py

https://github.com/hakanw/django-debug-toolbar
Python | 233 lines | 203 code | 23 blank | 7 comment | 17 complexity | cb07415186ba90bb4224b716eb165aa6 MD5 | raw file
  1. import re
  2. import uuid
  3. from django.db.backends import BaseDatabaseWrapper
  4. from django.template.loader import render_to_string
  5. from django.utils.html import escape
  6. from django.utils.safestring import mark_safe
  7. from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __
  8. from debug_toolbar.utils.compat.db import connections
  9. from debug_toolbar.middleware import DebugToolbarMiddleware
  10. from debug_toolbar.panels import DebugPanel
  11. from debug_toolbar.utils import sqlparse
  12. from debug_toolbar.utils.tracking.db import CursorWrapper
  13. from debug_toolbar.utils.tracking import replace_call
  14. # Inject our tracking cursor
  15. @replace_call(BaseDatabaseWrapper.cursor)
  16. def cursor(func, self):
  17. result = func(self)
  18. djdt = DebugToolbarMiddleware.get_current()
  19. if not djdt:
  20. return result
  21. logger = djdt.get_panel(SQLDebugPanel)
  22. return CursorWrapper(result, self, logger=logger)
  23. def get_isolation_level_display(engine, level):
  24. if engine == 'psycopg2':
  25. import psycopg2.extensions
  26. choices = {
  27. psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: 'Autocommit',
  28. psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: 'Read uncommitted',
  29. psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: 'Read committed',
  30. psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: 'Repeatable read',
  31. psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: 'Serializable',
  32. }
  33. else:
  34. raise ValueError(engine)
  35. return choices.get(level)
  36. def get_transaction_status_display(engine, level):
  37. if engine == 'psycopg2':
  38. import psycopg2.extensions
  39. choices = {
  40. psycopg2.extensions.TRANSACTION_STATUS_IDLE: 'Idle',
  41. psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: 'Active',
  42. psycopg2.extensions.TRANSACTION_STATUS_INTRANS: 'In transaction',
  43. psycopg2.extensions.TRANSACTION_STATUS_INERROR: 'In error',
  44. psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: 'Unknown',
  45. }
  46. else:
  47. raise ValueError(engine)
  48. return choices.get(level)
  49. class SQLDebugPanel(DebugPanel):
  50. """
  51. Panel that displays information about the SQL queries run while processing
  52. the request.
  53. """
  54. name = 'SQL'
  55. has_content = True
  56. def __init__(self, *args, **kwargs):
  57. super(self.__class__, self).__init__(*args, **kwargs)
  58. self._offset = dict((k, len(connections[k].queries)) for k in connections)
  59. self._sql_time = 0
  60. self._num_queries = 0
  61. self._queries = []
  62. self._databases = {}
  63. self._transaction_status = {}
  64. self._transaction_ids = {}
  65. def get_transaction_id(self, alias):
  66. conn = connections[alias].connection
  67. if not conn:
  68. return None
  69. engine = conn.__class__.__module__.split('.', 1)[0]
  70. if engine == 'psycopg2':
  71. cur_status = conn.get_transaction_status()
  72. else:
  73. raise ValueError(engine)
  74. last_status = self._transaction_status.get(alias)
  75. self._transaction_status[alias] = cur_status
  76. if not cur_status:
  77. # No available state
  78. return None
  79. if cur_status != last_status:
  80. if cur_status:
  81. self._transaction_ids[alias] = uuid.uuid4().hex
  82. else:
  83. self._transaction_ids[alias] = None
  84. return self._transaction_ids[alias]
  85. def record(self, alias, **kwargs):
  86. self._queries.append((alias, kwargs))
  87. if alias not in self._databases:
  88. self._databases[alias] = {
  89. 'time_spent': kwargs['duration'],
  90. 'num_queries': 1,
  91. }
  92. else:
  93. self._databases[alias]['time_spent'] += kwargs['duration']
  94. self._databases[alias]['num_queries'] += 1
  95. self._sql_time += kwargs['duration']
  96. self._num_queries += 1
  97. def nav_title(self):
  98. return _('SQL')
  99. def nav_subtitle(self):
  100. # TODO l10n: use ngettext
  101. return "%d %s in %.2fms" % (
  102. self._num_queries,
  103. (self._num_queries == 1) and 'query' or 'queries',
  104. self._sql_time
  105. )
  106. def title(self):
  107. count = len(self._databases)
  108. return __('SQL Queries from %(count)d connection', 'SQL Queries from %(count)d connections', count) % dict(
  109. count=count,
  110. )
  111. def url(self):
  112. return ''
  113. def content(self):
  114. if self._queries:
  115. width_ratio_tally = 0
  116. colors = [
  117. (256, 0, 0), # red
  118. (0, 256, 0), # blue
  119. (0, 0, 256), # green
  120. ]
  121. factor = int(256.0/(len(self._databases)*2.5))
  122. for n, db in enumerate(self._databases.itervalues()):
  123. rgb = [0, 0, 0]
  124. color = n % 3
  125. rgb[color] = 256 - n/3*factor
  126. nn = color
  127. # XXX: pretty sure this is horrible after so many aliases
  128. while rgb[color] < factor:
  129. nc = min(256 - rgb[color], 256)
  130. rgb[color] += nc
  131. nn += 1
  132. if nn > 2:
  133. nn = 0
  134. rgb[nn] = nc
  135. db['rgb_color'] = rgb
  136. trans_ids = {}
  137. trans_id = None
  138. i = 0
  139. for alias, query in self._queries:
  140. trans_id = query.get('trans_id')
  141. last_trans_id = trans_ids.get(alias)
  142. if trans_id != last_trans_id:
  143. if last_trans_id:
  144. self._queries[i-1][1]['ends_trans'] = True
  145. trans_ids[alias] = trans_id
  146. if trans_id:
  147. query['starts_trans'] = True
  148. if trans_id:
  149. query['in_trans'] = True
  150. query['alias'] = alias
  151. if 'iso_level' in query:
  152. query['iso_level'] = get_isolation_level_display(query['engine'], query['iso_level'])
  153. if 'trans_status' in query:
  154. query['trans_status'] = get_transaction_status_display(query['engine'], query['trans_status'])
  155. query['sql'] = reformat_sql(query['sql'])
  156. query['rgb_color'] = self._databases[alias]['rgb_color']
  157. try:
  158. query['width_ratio'] = (query['duration'] / self._sql_time) * 100
  159. query['width_ratio_relative'] = 100.0 * query['width_ratio'] / (100.0 - width_ratio_tally)
  160. except ZeroDivisionError:
  161. query['width_ratio'] = 0
  162. query['width_ratio_relative'] = 0
  163. query['start_offset'] = width_ratio_tally
  164. query['end_offset'] = query['width_ratio'] + query['start_offset']
  165. width_ratio_tally += query['width_ratio']
  166. stacktrace = []
  167. for frame in query['stacktrace']:
  168. params = map(escape, frame[0].rsplit('/', 1) + list(frame[1:]))
  169. stacktrace.append(u'<span class="path">{0}/</span><span class="file">{1}</span> in <span class="func">{3}</span>(<span class="lineno">{2}</span>)\n <span class="code">{4}</span>'.format(*params))
  170. query['stacktrace'] = mark_safe('\n'.join(stacktrace))
  171. i += 1
  172. if trans_id:
  173. self._queries[i-1][1]['ends_trans'] = True
  174. context = self.context.copy()
  175. context.update({
  176. 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']),
  177. 'queries': [q for a, q in self._queries],
  178. 'sql_time': self._sql_time,
  179. })
  180. return render_to_string('debug_toolbar/panels/sql.html', context)
  181. class BoldKeywordFilter(sqlparse.filters.Filter):
  182. """sqlparse filter to bold SQL keywords"""
  183. def process(self, stack, stream):
  184. """Process the token stream"""
  185. for token_type, value in stream:
  186. is_keyword = token_type in sqlparse.tokens.Keyword
  187. if is_keyword:
  188. yield sqlparse.tokens.Text, '<strong>'
  189. yield token_type, escape(value)
  190. if is_keyword:
  191. yield sqlparse.tokens.Text, '</strong>'
  192. def swap_fields(sql):
  193. return re.sub('SELECT</strong> (.*) <strong>FROM', 'SELECT</strong> <a class="djDebugUncollapsed djDebugToggle" href="#">&bull;&bull;&bull;</a> ' +
  194. '<a class="djDebugCollapsed djDebugToggle" href="#">\g<1></a> <strong>FROM', sql)
  195. def reformat_sql(sql):
  196. stack = sqlparse.engine.FilterStack()
  197. stack.preprocess.append(BoldKeywordFilter()) # add our custom filter
  198. stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings
  199. return swap_fields(''.join(stack.run(sql)))