Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

panel.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. from __future__ import absolute_import, unicode_literals
  2. import uuid
  3. from collections import defaultdict
  4. from copy import copy
  5. from pprint import saferepr
  6. from django.conf.urls import url
  7. from django.db import connections
  8. from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __
  9. from debug_toolbar.panels import Panel
  10. from debug_toolbar.panels.sql import views
  11. from debug_toolbar.panels.sql.forms import SQLSelectForm
  12. from debug_toolbar.panels.sql.tracking import unwrap_cursor, wrap_cursor
  13. from debug_toolbar.panels.sql.utils import (
  14. contrasting_color_generator, reformat_sql,
  15. )
  16. from debug_toolbar.utils import render_stacktrace
  17. def get_isolation_level_display(vendor, level):
  18. if vendor == 'postgresql':
  19. import psycopg2.extensions
  20. choices = {
  21. psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _("Autocommit"),
  22. psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _("Read uncommitted"),
  23. psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _("Read committed"),
  24. psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _("Repeatable read"),
  25. psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _("Serializable"),
  26. }
  27. else:
  28. raise ValueError(vendor)
  29. return choices.get(level)
  30. def get_transaction_status_display(vendor, level):
  31. if vendor == 'postgresql':
  32. import psycopg2.extensions
  33. choices = {
  34. psycopg2.extensions.TRANSACTION_STATUS_IDLE: _("Idle"),
  35. psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _("Active"),
  36. psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _("In transaction"),
  37. psycopg2.extensions.TRANSACTION_STATUS_INERROR: _("In error"),
  38. psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _("Unknown"),
  39. }
  40. else:
  41. raise ValueError(vendor)
  42. return choices.get(level)
  43. class SQLPanel(Panel):
  44. """
  45. Panel that displays information about the SQL queries run while processing
  46. the request.
  47. """
  48. def __init__(self, *args, **kwargs):
  49. super(SQLPanel, self).__init__(*args, **kwargs)
  50. self._offset = {k: len(connections[k].queries) for k in connections}
  51. self._sql_time = 0
  52. self._num_queries = 0
  53. self._queries = []
  54. self._databases = {}
  55. self._transaction_status = {}
  56. self._transaction_ids = {}
  57. def get_transaction_id(self, alias):
  58. if alias not in connections:
  59. return
  60. conn = connections[alias].connection
  61. if not conn:
  62. return
  63. if conn.vendor == 'postgresql':
  64. cur_status = conn.get_transaction_status()
  65. else:
  66. raise ValueError(conn.vendor)
  67. last_status = self._transaction_status.get(alias)
  68. self._transaction_status[alias] = cur_status
  69. if not cur_status:
  70. # No available state
  71. return None
  72. if cur_status != last_status:
  73. if cur_status:
  74. self._transaction_ids[alias] = uuid.uuid4().hex
  75. else:
  76. self._transaction_ids[alias] = None
  77. return self._transaction_ids[alias]
  78. def record(self, alias, **kwargs):
  79. self._queries.append((alias, kwargs))
  80. if alias not in self._databases:
  81. self._databases[alias] = {
  82. 'time_spent': kwargs['duration'],
  83. 'num_queries': 1,
  84. }
  85. else:
  86. self._databases[alias]['time_spent'] += kwargs['duration']
  87. self._databases[alias]['num_queries'] += 1
  88. self._sql_time += kwargs['duration']
  89. self._num_queries += 1
  90. # Implement the Panel API
  91. nav_title = _("SQL")
  92. @property
  93. def nav_subtitle(self):
  94. return __("%d query in %.2fms", "%d queries in %.2fms",
  95. self._num_queries) % (self._num_queries, self._sql_time)
  96. @property
  97. def title(self):
  98. count = len(self._databases)
  99. return __('SQL queries from %(count)d connection',
  100. 'SQL queries from %(count)d connections',
  101. count) % {'count': count}
  102. template = 'debug_toolbar/panels/sql.html'
  103. @classmethod
  104. def get_urls(cls):
  105. return [
  106. url(r'^sql_select/$', views.sql_select, name='sql_select'),
  107. url(r'^sql_explain/$', views.sql_explain, name='sql_explain'),
  108. url(r'^sql_profile/$', views.sql_profile, name='sql_profile'),
  109. ]
  110. def enable_instrumentation(self):
  111. # This is thread-safe because database connections are thread-local.
  112. for connection in connections.all():
  113. wrap_cursor(connection, self)
  114. def disable_instrumentation(self):
  115. for connection in connections.all():
  116. unwrap_cursor(connection)
  117. def generate_stats(self, request, response):
  118. colors = contrasting_color_generator()
  119. trace_colors = defaultdict(lambda: next(colors))
  120. query_similar = defaultdict(lambda: defaultdict(int))
  121. query_duplicates = defaultdict(lambda: defaultdict(int))
  122. # The keys used to determine similar and duplicate queries.
  123. def similar_key(query):
  124. return query['raw_sql']
  125. def duplicate_key(query):
  126. raw_params = () if query['raw_params'] is None else tuple(query['raw_params'])
  127. # saferepr() avoids problems because of unhashable types
  128. # (e.g. lists) when used as dictionary keys.
  129. # https://github.com/jazzband/django-debug-toolbar/issues/1091
  130. return (query['raw_sql'], saferepr(raw_params))
  131. if self._queries:
  132. width_ratio_tally = 0
  133. factor = int(256.0 / (len(self._databases) * 2.5))
  134. for n, db in enumerate(self._databases.values()):
  135. rgb = [0, 0, 0]
  136. color = n % 3
  137. rgb[color] = 256 - n // 3 * factor
  138. nn = color
  139. # XXX: pretty sure this is horrible after so many aliases
  140. while rgb[color] < factor:
  141. nc = min(256 - rgb[color], 256)
  142. rgb[color] += nc
  143. nn += 1
  144. if nn > 2:
  145. nn = 0
  146. rgb[nn] = nc
  147. db['rgb_color'] = rgb
  148. trans_ids = {}
  149. trans_id = None
  150. i = 0
  151. for alias, query in self._queries:
  152. query_similar[alias][similar_key(query)] += 1
  153. query_duplicates[alias][duplicate_key(query)] += 1
  154. trans_id = query.get('trans_id')
  155. last_trans_id = trans_ids.get(alias)
  156. if trans_id != last_trans_id:
  157. if last_trans_id:
  158. self._queries[(i - 1)][1]['ends_trans'] = True
  159. trans_ids[alias] = trans_id
  160. if trans_id:
  161. query['starts_trans'] = True
  162. if trans_id:
  163. query['in_trans'] = True
  164. query['alias'] = alias
  165. if 'iso_level' in query:
  166. query['iso_level'] = get_isolation_level_display(query['vendor'],
  167. query['iso_level'])
  168. if 'trans_status' in query:
  169. query['trans_status'] = get_transaction_status_display(query['vendor'],
  170. query['trans_status'])
  171. query['form'] = SQLSelectForm(auto_id=None, initial=copy(query))
  172. if query['sql']:
  173. query['sql'] = reformat_sql(query['sql'])
  174. query['rgb_color'] = self._databases[alias]['rgb_color']
  175. try:
  176. query['width_ratio'] = (query['duration'] / self._sql_time) * 100
  177. query['width_ratio_relative'] = (
  178. 100.0 * query['width_ratio'] / (100.0 - width_ratio_tally))
  179. except ZeroDivisionError:
  180. query['width_ratio'] = 0
  181. query['width_ratio_relative'] = 0
  182. query['start_offset'] = width_ratio_tally
  183. query['end_offset'] = query['width_ratio'] + query['start_offset']
  184. width_ratio_tally += query['width_ratio']
  185. query['stacktrace'] = render_stacktrace(query['stacktrace'])
  186. i += 1
  187. query['trace_color'] = trace_colors[query['stacktrace']]
  188. if trans_id:
  189. self._queries[(i - 1)][1]['ends_trans'] = True
  190. # Queries are similar / duplicates only if there's as least 2 of them.
  191. # Also, to hide queries, we need to give all the duplicate groups an id
  192. query_colors = contrasting_color_generator()
  193. query_similar_colors = {
  194. alias: {
  195. query: (similar_count, next(query_colors))
  196. for query, similar_count in queries.items()
  197. if similar_count >= 2
  198. }
  199. for alias, queries in query_similar.items()
  200. }
  201. query_duplicates_colors = {
  202. alias: {
  203. query: (duplicate_count, next(query_colors))
  204. for query, duplicate_count in queries.items()
  205. if duplicate_count >= 2
  206. }
  207. for alias, queries in query_duplicates.items()
  208. }
  209. for alias, query in self._queries:
  210. try:
  211. (query["similar_count"], query["similar_color"]) = (
  212. query_similar_colors[alias][similar_key(query)]
  213. )
  214. (query["duplicate_count"], query["duplicate_color"]) = (
  215. query_duplicates_colors[alias][duplicate_key(query)]
  216. )
  217. except KeyError:
  218. pass
  219. for alias, alias_info in self._databases.items():
  220. try:
  221. alias_info["similar_count"] = sum(
  222. e[0] for e in query_similar_colors[alias].values()
  223. )
  224. alias_info["duplicate_count"] = sum(
  225. e[0] for e in query_duplicates_colors[alias].values()
  226. )
  227. except KeyError:
  228. pass
  229. self.record_stats({
  230. 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']),
  231. 'queries': [q for a, q in self._queries],
  232. 'sql_time': self._sql_time,
  233. })
  234. def generate_server_timing(self, request, response):
  235. stats = self.get_stats()
  236. title = 'SQL {} queries'.format(len(stats.get('queries', [])))
  237. value = stats.get('sql_time', 0)
  238. self.record_server_timing('sql_time', title, value)