From 6334983458abd4380c21275d1229527778cf93a6 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 15 Nov 2013 22:43:59 +0100 Subject: Continue moving panel-specific code within panels. Structure the SQL and template panels as packages as they're growing. --- debug_toolbar/forms.py | 90 --------- debug_toolbar/panels/sql.py | 319 ------------------------------ debug_toolbar/panels/sql/__init__.py | 1 + debug_toolbar/panels/sql/forms.py | 90 +++++++++ debug_toolbar/panels/sql/panel.py | 210 ++++++++++++++++++++ debug_toolbar/panels/sql/tracking.py | 168 ++++++++++++++++ debug_toolbar/panels/sql/utils.py | 37 ++++ debug_toolbar/panels/sql/views.py | 113 +++++++++++ debug_toolbar/panels/template.py | 202 ------------------- debug_toolbar/panels/template/__init__.py | 1 + debug_toolbar/panels/template/panel.py | 159 +++++++++++++++ debug_toolbar/panels/template/views.py | 46 +++++ debug_toolbar/utils/sql.py | 37 ---- debug_toolbar/utils/tracking/__init__.py | 0 debug_toolbar/utils/tracking/db.py | 168 ---------------- 15 files changed, 825 insertions(+), 816 deletions(-) delete mode 100644 debug_toolbar/forms.py delete mode 100644 debug_toolbar/panels/sql.py create mode 100644 debug_toolbar/panels/sql/__init__.py create mode 100644 debug_toolbar/panels/sql/forms.py create mode 100644 debug_toolbar/panels/sql/panel.py create mode 100644 debug_toolbar/panels/sql/tracking.py create mode 100644 debug_toolbar/panels/sql/utils.py create mode 100644 debug_toolbar/panels/sql/views.py delete mode 100644 debug_toolbar/panels/template.py create mode 100644 debug_toolbar/panels/template/__init__.py create mode 100644 debug_toolbar/panels/template/panel.py create mode 100644 debug_toolbar/panels/template/views.py delete mode 100644 debug_toolbar/utils/sql.py delete mode 100644 debug_toolbar/utils/tracking/__init__.py delete mode 100644 debug_toolbar/utils/tracking/db.py (limited to 'debug_toolbar') diff --git a/debug_toolbar/forms.py b/debug_toolbar/forms.py deleted file mode 100644 index 4f60c11..0000000 --- a/debug_toolbar/forms.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import unicode_literals - -import json -import hashlib - -from django import forms -from django.conf import settings -from django.db import connections -from django.utils.encoding import force_text -from django.utils.functional import cached_property -from django.core.exceptions import ValidationError - -from debug_toolbar.utils.sql import reformat_sql - - -class SQLSelectForm(forms.Form): - """ - Validate params - - sql: The sql statement with interpolated params - raw_sql: The sql statement with placeholders - params: JSON encoded parameter values - duration: time for SQL to execute passed in from toolbar just for redisplay - hash: the hash of (secret + sql + params) for tamper checking - """ - sql = forms.CharField() - raw_sql = forms.CharField() - params = forms.CharField() - alias = forms.CharField(required=False, initial='default') - duration = forms.FloatField() - hash = forms.CharField() - - def __init__(self, *args, **kwargs): - initial = kwargs.get('initial', None) - - if initial is not None: - initial['hash'] = self.make_hash(initial) - - super(SQLSelectForm, self).__init__(*args, **kwargs) - - for name in self.fields: - self.fields[name].widget = forms.HiddenInput() - - def clean_raw_sql(self): - value = self.cleaned_data['raw_sql'] - - if not value.lower().strip().startswith('select'): - raise ValidationError("Only 'select' queries are allowed.") - - return value - - def clean_params(self): - value = self.cleaned_data['params'] - - try: - return json.loads(value) - except ValueError: - raise ValidationError('Is not valid JSON') - - def clean_alias(self): - value = self.cleaned_data['alias'] - - if value not in connections: - raise ValidationError("Database alias '%s' not found" % value) - - return value - - def clean_hash(self): - hash = self.cleaned_data['hash'] - - if hash != self.make_hash(self.data): - raise ValidationError('Tamper alert') - - return hash - - def reformat_sql(self): - return reformat_sql(self.cleaned_data['sql']) - - def make_hash(self, data): - params = (force_text(settings.SECRET_KEY) + - force_text(data['sql']) + force_text(data['params'])) - return hashlib.sha1(params.encode('utf-8')).hexdigest() - - @property - def connection(self): - return connections[self.cleaned_data['alias']] - - @cached_property - def cursor(self): - return self.connection.cursor() diff --git a/debug_toolbar/panels/sql.py b/debug_toolbar/panels/sql.py deleted file mode 100644 index 226778b..0000000 --- a/debug_toolbar/panels/sql.py +++ /dev/null @@ -1,319 +0,0 @@ -from __future__ import unicode_literals - -import uuid -from copy import copy - -from django.conf.urls import patterns, url -from django.db import connections -from django.http import HttpResponseBadRequest -from django.shortcuts import render -from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ -from django.views.decorators.csrf import csrf_exempt - -from debug_toolbar.forms import SQLSelectForm -from debug_toolbar.panels import DebugPanel -from debug_toolbar.utils import render_stacktrace -from debug_toolbar.utils.sql import reformat_sql -from debug_toolbar.utils.tracking.db import CursorWrapper - - -def get_isolation_level_display(engine, level): - if engine == 'psycopg2': - import psycopg2.extensions - choices = { - psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _('Autocommit'), - psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _('Read uncommitted'), - psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _('Read committed'), - psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _('Repeatable read'), - psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _('Serializable'), - } - else: - raise ValueError(engine) - return choices.get(level) - - -def get_transaction_status_display(engine, level): - if engine == 'psycopg2': - import psycopg2.extensions - choices = { - psycopg2.extensions.TRANSACTION_STATUS_IDLE: _('Idle'), - psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _('Active'), - psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _('In transaction'), - psycopg2.extensions.TRANSACTION_STATUS_INERROR: _('In error'), - psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _('Unknown'), - } - else: - raise ValueError(engine) - return choices.get(level) - - -class SQLDebugPanel(DebugPanel): - """ - Panel that displays information about the SQL queries run while processing - the request. - """ - name = 'SQL' - template = 'debug_toolbar/panels/sql.html' - has_content = True - - def __init__(self, *args, **kwargs): - super(SQLDebugPanel, self).__init__(*args, **kwargs) - self._offset = dict((k, len(connections[k].queries)) for k in connections) - self._sql_time = 0 - self._num_queries = 0 - self._queries = [] - self._databases = {} - self._transaction_status = {} - self._transaction_ids = {} - - def get_transaction_id(self, alias): - if alias not in connections: - return - conn = connections[alias].connection - if not conn: - return - - engine = conn.__class__.__module__.split('.', 1)[0] - if engine == 'psycopg2': - cur_status = conn.get_transaction_status() - else: - raise ValueError(engine) - - last_status = self._transaction_status.get(alias) - self._transaction_status[alias] = cur_status - - if not cur_status: - # No available state - return None - - if cur_status != last_status: - if cur_status: - self._transaction_ids[alias] = uuid.uuid4().hex - else: - self._transaction_ids[alias] = None - - return self._transaction_ids[alias] - - def record(self, alias, **kwargs): - self._queries.append((alias, kwargs)) - if alias not in self._databases: - self._databases[alias] = { - 'time_spent': kwargs['duration'], - 'num_queries': 1, - } - else: - self._databases[alias]['time_spent'] += kwargs['duration'] - self._databases[alias]['num_queries'] += 1 - self._sql_time += kwargs['duration'] - self._num_queries += 1 - - @classmethod - def get_urls(cls): - return patterns('debug_toolbar.panels.sql', # noqa - url(r'^sql_select/$', 'sql_select', name='sql_select'), - url(r'^sql_explain/$', 'sql_explain', name='sql_explain'), - url(r'^sql_profile/$', 'sql_profile', name='sql_profile'), - ) - - def nav_title(self): - return _('SQL') - - def nav_subtitle(self): - return __("%d query in %.2fms", "%d queries in %.2fms", - self._num_queries) % (self._num_queries, self._sql_time) - - def title(self): - count = len(self._databases) - return __('SQL Queries from %(count)d connection', - 'SQL Queries from %(count)d connections', - count) % dict(count=count) - - def enable_instrumentation(self): - # This is thread-safe because database connections are thread-local. - for connection in connections.all(): - if not hasattr(connection, '_djdt_cursor'): - connection._djdt_cursor = connection.cursor - connection.cursor = lambda: CursorWrapper( - connection._djdt_cursor(), connection, self) - - def disable_instrumentation(self): - for connection in connections.all(): - if hasattr(connection, '_djdt_cursor'): - del connection._djdt_cursor - del connection.cursor - - def process_response(self, request, response): - if self._queries: - width_ratio_tally = 0 - factor = int(256.0 / (len(self._databases) * 2.5)) - for n, db in enumerate(self._databases.values()): - rgb = [0, 0, 0] - color = n % 3 - rgb[color] = 256 - n / 3 * factor - nn = color - # XXX: pretty sure this is horrible after so many aliases - while rgb[color] < factor: - nc = min(256 - rgb[color], 256) - rgb[color] += nc - nn += 1 - if nn > 2: - nn = 0 - rgb[nn] = nc - db['rgb_color'] = rgb - - trans_ids = {} - trans_id = None - i = 0 - for alias, query in self._queries: - trans_id = query.get('trans_id') - last_trans_id = trans_ids.get(alias) - - if trans_id != last_trans_id: - if last_trans_id: - self._queries[(i - 1)][1]['ends_trans'] = True - trans_ids[alias] = trans_id - if trans_id: - query['starts_trans'] = True - if trans_id: - query['in_trans'] = True - - query['alias'] = alias - if 'iso_level' in query: - query['iso_level'] = get_isolation_level_display(query['engine'], - query['iso_level']) - if 'trans_status' in query: - query['trans_status'] = get_transaction_status_display(query['engine'], - query['trans_status']) - - query['form'] = SQLSelectForm(auto_id=None, initial=copy(query)) - - if query['sql']: - query['sql'] = reformat_sql(query['sql']) - query['rgb_color'] = self._databases[alias]['rgb_color'] - try: - query['width_ratio'] = (query['duration'] / self._sql_time) * 100 - query['width_ratio_relative'] = ( - 100.0 * query['width_ratio'] / (100.0 - width_ratio_tally)) - except ZeroDivisionError: - query['width_ratio'] = 0 - query['width_ratio_relative'] = 0 - query['start_offset'] = width_ratio_tally - query['end_offset'] = query['width_ratio'] + query['start_offset'] - width_ratio_tally += query['width_ratio'] - query['stacktrace'] = render_stacktrace(query['stacktrace']) - i += 1 - - if trans_id: - self._queries[(i - 1)][1]['ends_trans'] = True - - self.record_stats({ - 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']), - 'queries': [q for a, q in self._queries], - 'sql_time': self._sql_time, - }) - - -@csrf_exempt -def sql_select(request): - """Returns the output of the SQL SELECT statement""" - form = SQLSelectForm(request.POST or None) - - if form.is_valid(): - sql = form.cleaned_data['raw_sql'] - params = form.cleaned_data['params'] - cursor = form.cursor - cursor.execute(sql, params) - headers = [d[0] for d in cursor.description] - result = cursor.fetchall() - cursor.close() - context = { - 'result': result, - 'sql': form.reformat_sql(), - 'duration': form.cleaned_data['duration'], - 'headers': headers, - 'alias': form.cleaned_data['alias'], - } - return render(request, 'debug_toolbar/panels/sql_select.html', context) - return HttpResponseBadRequest('Form errors') - - -@csrf_exempt -def sql_explain(request): - """Returns the output of the SQL EXPLAIN on the given query""" - form = SQLSelectForm(request.POST or None) - - if form.is_valid(): - sql = form.cleaned_data['raw_sql'] - params = form.cleaned_data['params'] - cursor = form.cursor - - conn = form.connection - engine = conn.__class__.__module__.split('.', 1)[0] - - if engine == "sqlite3": - # SQLite's EXPLAIN dumps the low-level opcodes generated for a query; - # EXPLAIN QUERY PLAN dumps a more human-readable summary - # See http://www.sqlite.org/lang_explain.html for details - cursor.execute("EXPLAIN QUERY PLAN %s" % (sql,), params) - elif engine == "psycopg2": - cursor.execute("EXPLAIN ANALYZE %s" % (sql,), params) - else: - cursor.execute("EXPLAIN %s" % (sql,), params) - - headers = [d[0] for d in cursor.description] - result = cursor.fetchall() - cursor.close() - context = { - 'result': result, - 'sql': form.reformat_sql(), - 'duration': form.cleaned_data['duration'], - 'headers': headers, - 'alias': form.cleaned_data['alias'], - } - return render(request, 'debug_toolbar/panels/sql_explain.html', context) - return HttpResponseBadRequest('Form errors') - - -@csrf_exempt -def sql_profile(request): - """Returns the output of running the SQL and getting the profiling statistics""" - form = SQLSelectForm(request.POST or None) - - if form.is_valid(): - sql = form.cleaned_data['raw_sql'] - params = form.cleaned_data['params'] - cursor = form.cursor - result = None - headers = None - result_error = None - try: - cursor.execute("SET PROFILING=1") # Enable profiling - cursor.execute(sql, params) # Execute SELECT - cursor.execute("SET PROFILING=0") # Disable profiling - # The Query ID should always be 1 here but I'll subselect to get - # the last one just in case... - cursor.execute(""" - SELECT * - FROM information_schema.profiling - WHERE query_id = ( - SELECT query_id - FROM information_schema.profiling - ORDER BY query_id DESC - LIMIT 1 - ) -""") - headers = [d[0] for d in cursor.description] - result = cursor.fetchall() - except Exception: - result_error = "Profiling is either not available or not supported by your database." - cursor.close() - context = { - 'result': result, - 'result_error': result_error, - 'sql': form.reformat_sql(), - 'duration': form.cleaned_data['duration'], - 'headers': headers, - 'alias': form.cleaned_data['alias'], - } - return render(request, 'debug_toolbar/panels/sql_profile.html', context) - return HttpResponseBadRequest('Form errors') diff --git a/debug_toolbar/panels/sql/__init__.py b/debug_toolbar/panels/sql/__init__.py new file mode 100644 index 0000000..90e05cb --- /dev/null +++ b/debug_toolbar/panels/sql/__init__.py @@ -0,0 +1 @@ +from debug_toolbar.panels.sql.panel import SQLDebugPanel # noqa diff --git a/debug_toolbar/panels/sql/forms.py b/debug_toolbar/panels/sql/forms.py new file mode 100644 index 0000000..c18be0c --- /dev/null +++ b/debug_toolbar/panels/sql/forms.py @@ -0,0 +1,90 @@ +from __future__ import unicode_literals + +import json +import hashlib + +from django import forms +from django.conf import settings +from django.db import connections +from django.utils.encoding import force_text +from django.utils.functional import cached_property +from django.core.exceptions import ValidationError + +from debug_toolbar.panels.sql.utils import reformat_sql + + +class SQLSelectForm(forms.Form): + """ + Validate params + + sql: The sql statement with interpolated params + raw_sql: The sql statement with placeholders + params: JSON encoded parameter values + duration: time for SQL to execute passed in from toolbar just for redisplay + hash: the hash of (secret + sql + params) for tamper checking + """ + sql = forms.CharField() + raw_sql = forms.CharField() + params = forms.CharField() + alias = forms.CharField(required=False, initial='default') + duration = forms.FloatField() + hash = forms.CharField() + + def __init__(self, *args, **kwargs): + initial = kwargs.get('initial', None) + + if initial is not None: + initial['hash'] = self.make_hash(initial) + + super(SQLSelectForm, self).__init__(*args, **kwargs) + + for name in self.fields: + self.fields[name].widget = forms.HiddenInput() + + def clean_raw_sql(self): + value = self.cleaned_data['raw_sql'] + + if not value.lower().strip().startswith('select'): + raise ValidationError("Only 'select' queries are allowed.") + + return value + + def clean_params(self): + value = self.cleaned_data['params'] + + try: + return json.loads(value) + except ValueError: + raise ValidationError('Is not valid JSON') + + def clean_alias(self): + value = self.cleaned_data['alias'] + + if value not in connections: + raise ValidationError("Database alias '%s' not found" % value) + + return value + + def clean_hash(self): + hash = self.cleaned_data['hash'] + + if hash != self.make_hash(self.data): + raise ValidationError('Tamper alert') + + return hash + + def reformat_sql(self): + return reformat_sql(self.cleaned_data['sql']) + + def make_hash(self, data): + params = (force_text(settings.SECRET_KEY) + + force_text(data['sql']) + force_text(data['params'])) + return hashlib.sha1(params.encode('utf-8')).hexdigest() + + @property + def connection(self): + return connections[self.cleaned_data['alias']] + + @cached_property + def cursor(self): + return self.connection.cursor() diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py new file mode 100644 index 0000000..cb80901 --- /dev/null +++ b/debug_toolbar/panels/sql/panel.py @@ -0,0 +1,210 @@ +from __future__ import unicode_literals + +import uuid +from copy import copy + +from django.conf.urls import patterns, url +from django.db import connections +from django.utils.translation import ugettext_lazy as _, ungettext_lazy as __ + +from debug_toolbar.panels import DebugPanel +from debug_toolbar.panels.sql.forms import SQLSelectForm +from debug_toolbar.utils import render_stacktrace +from debug_toolbar.panels.sql.utils import reformat_sql +from debug_toolbar.panels.sql.tracking import CursorWrapper + + +def get_isolation_level_display(engine, level): + if engine == 'psycopg2': + import psycopg2.extensions + choices = { + psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT: _('Autocommit'), + psycopg2.extensions.ISOLATION_LEVEL_READ_UNCOMMITTED: _('Read uncommitted'), + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED: _('Read committed'), + psycopg2.extensions.ISOLATION_LEVEL_REPEATABLE_READ: _('Repeatable read'), + psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE: _('Serializable'), + } + else: + raise ValueError(engine) + return choices.get(level) + + +def get_transaction_status_display(engine, level): + if engine == 'psycopg2': + import psycopg2.extensions + choices = { + psycopg2.extensions.TRANSACTION_STATUS_IDLE: _('Idle'), + psycopg2.extensions.TRANSACTION_STATUS_ACTIVE: _('Active'), + psycopg2.extensions.TRANSACTION_STATUS_INTRANS: _('In transaction'), + psycopg2.extensions.TRANSACTION_STATUS_INERROR: _('In error'), + psycopg2.extensions.TRANSACTION_STATUS_UNKNOWN: _('Unknown'), + } + else: + raise ValueError(engine) + return choices.get(level) + + +class SQLDebugPanel(DebugPanel): + """ + Panel that displays information about the SQL queries run while processing + the request. + """ + name = 'SQL' + template = 'debug_toolbar/panels/sql.html' + has_content = True + + def __init__(self, *args, **kwargs): + super(SQLDebugPanel, self).__init__(*args, **kwargs) + self._offset = dict((k, len(connections[k].queries)) for k in connections) + self._sql_time = 0 + self._num_queries = 0 + self._queries = [] + self._databases = {} + self._transaction_status = {} + self._transaction_ids = {} + + def get_transaction_id(self, alias): + if alias not in connections: + return + conn = connections[alias].connection + if not conn: + return + + engine = conn.__class__.__module__.split('.', 1)[0] + if engine == 'psycopg2': + cur_status = conn.get_transaction_status() + else: + raise ValueError(engine) + + last_status = self._transaction_status.get(alias) + self._transaction_status[alias] = cur_status + + if not cur_status: + # No available state + return None + + if cur_status != last_status: + if cur_status: + self._transaction_ids[alias] = uuid.uuid4().hex + else: + self._transaction_ids[alias] = None + + return self._transaction_ids[alias] + + def record(self, alias, **kwargs): + self._queries.append((alias, kwargs)) + if alias not in self._databases: + self._databases[alias] = { + 'time_spent': kwargs['duration'], + 'num_queries': 1, + } + else: + self._databases[alias]['time_spent'] += kwargs['duration'] + self._databases[alias]['num_queries'] += 1 + self._sql_time += kwargs['duration'] + self._num_queries += 1 + + @classmethod + def get_urls(cls): + return patterns('debug_toolbar.panels.sql.views', # noqa + url(r'^sql_select/$', 'sql_select', name='sql_select'), + url(r'^sql_explain/$', 'sql_explain', name='sql_explain'), + url(r'^sql_profile/$', 'sql_profile', name='sql_profile'), + ) + + def nav_title(self): + return _('SQL') + + def nav_subtitle(self): + return __("%d query in %.2fms", "%d queries in %.2fms", + self._num_queries) % (self._num_queries, self._sql_time) + + def title(self): + count = len(self._databases) + return __('SQL Queries from %(count)d connection', + 'SQL Queries from %(count)d connections', + count) % dict(count=count) + + def enable_instrumentation(self): + # This is thread-safe because database connections are thread-local. + for connection in connections.all(): + if not hasattr(connection, '_djdt_cursor'): + connection._djdt_cursor = connection.cursor + connection.cursor = lambda: CursorWrapper( + connection._djdt_cursor(), connection, self) + + def disable_instrumentation(self): + for connection in connections.all(): + if hasattr(connection, '_djdt_cursor'): + del connection._djdt_cursor + del connection.cursor + + def process_response(self, request, response): + if self._queries: + width_ratio_tally = 0 + factor = int(256.0 / (len(self._databases) * 2.5)) + for n, db in enumerate(self._databases.values()): + rgb = [0, 0, 0] + color = n % 3 + rgb[color] = 256 - n / 3 * factor + nn = color + # XXX: pretty sure this is horrible after so many aliases + while rgb[color] < factor: + nc = min(256 - rgb[color], 256) + rgb[color] += nc + nn += 1 + if nn > 2: + nn = 0 + rgb[nn] = nc + db['rgb_color'] = rgb + + trans_ids = {} + trans_id = None + i = 0 + for alias, query in self._queries: + trans_id = query.get('trans_id') + last_trans_id = trans_ids.get(alias) + + if trans_id != last_trans_id: + if last_trans_id: + self._queries[(i - 1)][1]['ends_trans'] = True + trans_ids[alias] = trans_id + if trans_id: + query['starts_trans'] = True + if trans_id: + query['in_trans'] = True + + query['alias'] = alias + if 'iso_level' in query: + query['iso_level'] = get_isolation_level_display(query['engine'], + query['iso_level']) + if 'trans_status' in query: + query['trans_status'] = get_transaction_status_display(query['engine'], + query['trans_status']) + + query['form'] = SQLSelectForm(auto_id=None, initial=copy(query)) + + if query['sql']: + query['sql'] = reformat_sql(query['sql']) + query['rgb_color'] = self._databases[alias]['rgb_color'] + try: + query['width_ratio'] = (query['duration'] / self._sql_time) * 100 + query['width_ratio_relative'] = ( + 100.0 * query['width_ratio'] / (100.0 - width_ratio_tally)) + except ZeroDivisionError: + query['width_ratio'] = 0 + query['width_ratio_relative'] = 0 + query['start_offset'] = width_ratio_tally + query['end_offset'] = query['width_ratio'] + query['start_offset'] + width_ratio_tally += query['width_ratio'] + query['stacktrace'] = render_stacktrace(query['stacktrace']) + i += 1 + + if trans_id: + self._queries[(i - 1)][1]['ends_trans'] = True + + self.record_stats({ + 'databases': sorted(self._databases.items(), key=lambda x: -x[1]['time_spent']), + 'queries': [q for a, q in self._queries], + 'sql_time': self._sql_time, + }) diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py new file mode 100644 index 0000000..fd56ff9 --- /dev/null +++ b/debug_toolbar/panels/sql/tracking.py @@ -0,0 +1,168 @@ +from __future__ import unicode_literals + +import sys + +import json +from threading import local +from time import time + +from django.template import Node +from django.utils.encoding import force_text +from django.utils import six + +from debug_toolbar.utils import tidy_stacktrace, get_template_info, get_stack +from debug_toolbar.utils import settings as dt_settings + + +class SQLQueryTriggered(Exception): + """Thrown when template panel triggers a query""" + pass + + +class ThreadLocalState(local): + def __init__(self): + self.enabled = True + + @property + def Wrapper(self): + if self.enabled: + return NormalCursorWrapper + return ExceptionCursorWrapper + + def recording(self, v): + self.enabled = v + + +state = ThreadLocalState() +recording = state.recording # export function + + +def CursorWrapper(*args, **kwds): # behave like a class + return state.Wrapper(*args, **kwds) + + +class ExceptionCursorWrapper(object): + """ + Wraps a cursor and raises an exception on any operation. + Used in Templates panel. + """ + def __init__(self, cursor, db, logger): + pass + + def __getattr__(self, attr): + raise SQLQueryTriggered() + + +class NormalCursorWrapper(object): + """ + Wraps a cursor and logs queries. + """ + + def __init__(self, cursor, db, logger): + self.cursor = cursor + # Instance of a BaseDatabaseWrapper subclass + self.db = db + # logger must implement a ``record`` method + self.logger = logger + + def _quote_expr(self, element): + if isinstance(element, six.string_types): + return "'%s'" % force_text(element).replace("'", "''") + else: + return repr(element) + + def _quote_params(self, params): + if not params: + return params + if isinstance(params, dict): + return dict((key, self._quote_expr(value)) + for key, value in params.items()) + return list(map(self._quote_expr, params)) + + def _decode(self, param): + try: + return force_text(param, strings_only=True) + except UnicodeDecodeError: + return '(encoded string)' + + def execute(self, sql, params=()): + start_time = time() + try: + return self.cursor.execute(sql, params) + finally: + stop_time = time() + duration = (stop_time - start_time) * 1000 + if dt_settings.CONFIG['ENABLE_STACKTRACES']: + stacktrace = tidy_stacktrace(reversed(get_stack())) + else: + stacktrace = [] + _params = '' + try: + _params = json.dumps(list(map(self._decode, params))) + except Exception: + pass # object not JSON serializable + + template_info = None + cur_frame = sys._getframe().f_back + try: + while cur_frame is not None: + if cur_frame.f_code.co_name == 'render': + node = cur_frame.f_locals['self'] + if isinstance(node, Node): + template_info = get_template_info(node.source) + break + cur_frame = cur_frame.f_back + except Exception: + pass + del cur_frame + + alias = getattr(self.db, 'alias', 'default') + conn = self.db.connection + # HACK: avoid imports + if conn: + engine = conn.__class__.__module__.split('.', 1)[0] + else: + engine = 'unknown' + + params = { + 'engine': engine, + 'alias': alias, + 'sql': self.db.ops.last_executed_query( + self.cursor, sql, self._quote_params(params)), + 'duration': duration, + 'raw_sql': sql, + 'params': _params, + 'stacktrace': stacktrace, + 'start_time': start_time, + 'stop_time': stop_time, + 'is_slow': duration > dt_settings.CONFIG['SQL_WARNING_THRESHOLD'], + 'is_select': sql.lower().strip().startswith('select'), + 'template_info': template_info, + } + + if engine == 'psycopg2': + # If an erroneous query was ran on the connection, it might + # be in a state where checking isolation_level raises an + # exception. + try: + iso_level = conn.isolation_level + except conn.InternalError: + iso_level = 'unknown' + params.update({ + 'trans_id': self.logger.get_transaction_id(alias), + 'trans_status': conn.get_transaction_status(), + 'iso_level': iso_level, + 'encoding': conn.encoding, + }) + + # We keep `sql` to maintain backwards compatibility + self.logger.record(**params) + + def executemany(self, sql, param_list): + return self.cursor.executemany(sql, param_list) + + def __getattr__(self, attr): + return getattr(self.cursor, attr) + + def __iter__(self): + return iter(self.cursor) diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py new file mode 100644 index 0000000..00728a3 --- /dev/null +++ b/debug_toolbar/panels/sql/utils.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + +import re + +from django.utils.html import escape + +import sqlparse +from sqlparse import tokens as T + + +class BoldKeywordFilter: + """sqlparse filter to bold SQL keywords""" + def process(self, stack, stream): + """Process the token stream""" + for token_type, value in stream: + is_keyword = token_type in T.Keyword + if is_keyword: + yield T.Text, '' + yield token_type, escape(value) + if is_keyword: + yield T.Text, '' + + +def reformat_sql(sql): + stack = sqlparse.engine.FilterStack() + stack.preprocess.append(BoldKeywordFilter()) # add our custom filter + stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings + return swap_fields(''.join(stack.run(sql))) + + +def swap_fields(sql): + expr = r'SELECT (...........*?) FROM' + subs = (r'SELECT ' + r'••• ' + r'\1 ' + r'FROM') + return re.sub(expr, subs, sql) diff --git a/debug_toolbar/panels/sql/views.py b/debug_toolbar/panels/sql/views.py new file mode 100644 index 0000000..346cf6e --- /dev/null +++ b/debug_toolbar/panels/sql/views.py @@ -0,0 +1,113 @@ +from __future__ import unicode_literals + +from django.http import HttpResponseBadRequest +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt + +from debug_toolbar.panels.sql.forms import SQLSelectForm + + +@csrf_exempt +def sql_select(request): + """Returns the output of the SQL SELECT statement""" + form = SQLSelectForm(request.POST or None) + + if form.is_valid(): + sql = form.cleaned_data['raw_sql'] + params = form.cleaned_data['params'] + cursor = form.cursor + cursor.execute(sql, params) + headers = [d[0] for d in cursor.description] + result = cursor.fetchall() + cursor.close() + context = { + 'result': result, + 'sql': form.reformat_sql(), + 'duration': form.cleaned_data['duration'], + 'headers': headers, + 'alias': form.cleaned_data['alias'], + } + return render(request, 'debug_toolbar/panels/sql_select.html', context) + return HttpResponseBadRequest('Form errors') + + +@csrf_exempt +def sql_explain(request): + """Returns the output of the SQL EXPLAIN on the given query""" + form = SQLSelectForm(request.POST or None) + + if form.is_valid(): + sql = form.cleaned_data['raw_sql'] + params = form.cleaned_data['params'] + cursor = form.cursor + + conn = form.connection + engine = conn.__class__.__module__.split('.', 1)[0] + + if engine == "sqlite3": + # SQLite's EXPLAIN dumps the low-level opcodes generated for a query; + # EXPLAIN QUERY PLAN dumps a more human-readable summary + # See http://www.sqlite.org/lang_explain.html for details + cursor.execute("EXPLAIN QUERY PLAN %s" % (sql,), params) + elif engine == "psycopg2": + cursor.execute("EXPLAIN ANALYZE %s" % (sql,), params) + else: + cursor.execute("EXPLAIN %s" % (sql,), params) + + headers = [d[0] for d in cursor.description] + result = cursor.fetchall() + cursor.close() + context = { + 'result': result, + 'sql': form.reformat_sql(), + 'duration': form.cleaned_data['duration'], + 'headers': headers, + 'alias': form.cleaned_data['alias'], + } + return render(request, 'debug_toolbar/panels/sql_explain.html', context) + return HttpResponseBadRequest('Form errors') + + +@csrf_exempt +def sql_profile(request): + """Returns the output of running the SQL and getting the profiling statistics""" + form = SQLSelectForm(request.POST or None) + + if form.is_valid(): + sql = form.cleaned_data['raw_sql'] + params = form.cleaned_data['params'] + cursor = form.cursor + result = None + headers = None + result_error = None + try: + cursor.execute("SET PROFILING=1") # Enable profiling + cursor.execute(sql, params) # Execute SELECT + cursor.execute("SET PROFILING=0") # Disable profiling + # The Query ID should always be 1 here but I'll subselect to get + # the last one just in case... + cursor.execute(""" + SELECT * + FROM information_schema.profiling + WHERE query_id = ( + SELECT query_id + FROM information_schema.profiling + ORDER BY query_id DESC + LIMIT 1 + ) +""") + headers = [d[0] for d in cursor.description] + result = cursor.fetchall() + except Exception: + result_error = "Profiling is either not available or not supported by your database." + cursor.close() + context = { + 'result': result, + 'result_error': result_error, + 'sql': form.reformat_sql(), + 'duration': form.cleaned_data['duration'], + 'headers': headers, + 'alias': form.cleaned_data['alias'], + } + return render(request, 'debug_toolbar/panels/sql_profile.html', context) + return HttpResponseBadRequest('Form errors') diff --git a/debug_toolbar/panels/template.py b/debug_toolbar/panels/template.py deleted file mode 100644 index e21cc9e..0000000 --- a/debug_toolbar/panels/template.py +++ /dev/null @@ -1,202 +0,0 @@ -from __future__ import unicode_literals - -from os.path import normpath -from pprint import pformat - -import django -from django import http -from django.http import HttpResponseBadRequest -from django.conf import settings -from django.conf.urls import patterns, url -from django.db.models.query import QuerySet, RawQuerySet -from django.shortcuts import render -from django.template import TemplateDoesNotExist -from django.template.context import get_standard_processors -from django.test.signals import template_rendered -from django.utils.encoding import force_text -from django.utils.safestring import mark_safe -from django.utils import six -from django.utils.translation import ugettext_lazy as _ - -from debug_toolbar.panels import DebugPanel -from debug_toolbar.utils.tracking.db import recording, SQLQueryTriggered -from debug_toolbar.utils import settings as dt_settings - -# Code taken and adapted from Simon Willison and Django Snippets: -# http://www.djangosnippets.org/snippets/766/ - -# Monkey-patch to enable the template_rendered signal. The receiver returns -# immediately when the panel is disabled to keep the overhead small. - -from django.test.utils import instrumented_test_render -from django.template import Template - -if Template._render != instrumented_test_render: - Template.original_render = Template._render - Template._render = instrumented_test_render - - -if django.VERSION[:2] < (1, 7): - # Monkey-patch versions of Django where Template doesn't store origin. - # See https://code.djangoproject.com/ticket/16096. - - old_template_init = Template.__init__ - - def new_template_init(self, template_string, origin=None, name=''): - old_template_init(self, template_string, origin, name) - self.origin = origin - - Template.__init__ = new_template_init - - -class TemplateDebugPanel(DebugPanel): - """ - A panel that lists all templates used during processing of a response. - """ - name = 'Template' - template = 'debug_toolbar/panels/templates.html' - has_content = True - - def __init__(self, *args, **kwargs): - super(TemplateDebugPanel, self).__init__(*args, **kwargs) - self.templates = [] - template_rendered.connect(self._store_template_info) - - def _store_template_info(self, sender, **kwargs): - if not self.enabled: - return - - template, context = kwargs['template'], kwargs['context'] - - # Skip templates that we are generating through the debug toolbar. - if (isinstance(template.name, six.string_types) and - template.name.startswith('debug_toolbar/')): - return - - context_list = [] - for context_layer in context.dicts: - temp_layer = {} - if hasattr(context_layer, 'items'): - for key, value in context_layer.items(): - # Replace any request elements - they have a large - # unicode representation and the request data is - # already made available from the Request Vars panel. - if isinstance(value, http.HttpRequest): - temp_layer[key] = '<>' - # Replace the debugging sql_queries element. The SQL - # data is already made available from the SQL panel. - elif key == 'sql_queries' and isinstance(value, list): - temp_layer[key] = '<>' - # Replace LANGUAGES, which is available in i18n context processor - elif key == 'LANGUAGES' and isinstance(value, tuple): - temp_layer[key] = '<>' - # QuerySet would trigger the database: user can run the query from SQL Panel - elif isinstance(value, (QuerySet, RawQuerySet)): - model_name = "%s.%s" % ( - value.model._meta.app_label, value.model.__name__) - temp_layer[key] = '<<%s of %s>>' % ( - value.__class__.__name__.lower(), model_name) - else: - try: - recording(False) - pformat(value) # this MAY trigger a db query - except SQLQueryTriggered: - temp_layer[key] = '<>' - except UnicodeEncodeError: - temp_layer[key] = '<>' - except Exception: - temp_layer[key] = '<>' - else: - temp_layer[key] = value - finally: - recording(True) - try: - context_list.append(pformat(temp_layer)) - except UnicodeEncodeError: - pass - - kwargs['context'] = [force_text(item) for item in context_list] - self.templates.append(kwargs) - - @classmethod - def get_urls(cls): - return patterns('debug_toolbar.panels.template', # noqa - url(r'^template_source/$', template_source, name='template_source'), - ) - - def nav_title(self): - return _('Templates') - - def title(self): - num_templates = len(self.templates) - return _('Templates (%(num_templates)s rendered)') % {'num_templates': num_templates} - - def process_response(self, request, response): - context_processors = dict( - [ - ("%s.%s" % (k.__module__, k.__name__), - pformat(k(request))) for k in get_standard_processors() - ] - ) - template_context = [] - for template_data in self.templates: - info = {} - # Clean up some info about templates - template = template_data.get('template', None) - if not hasattr(template, 'origin'): - continue - if template.origin and template.origin.name: - template.origin_name = template.origin.name - else: - template.origin_name = 'No origin' - info['template'] = template - # Clean up context for better readability - if dt_settings.CONFIG['SHOW_TEMPLATE_CONTEXT']: - context_list = template_data.get('context', []) - info['context'] = '\n'.join(context_list) - template_context.append(info) - - self.record_stats({ - 'templates': template_context, - 'template_dirs': [normpath(x) for x in settings.TEMPLATE_DIRS], - 'context_processors': context_processors, - }) - - -def template_source(request): - """ - Return the source of a template, syntax-highlighted by Pygments if - it's available. - """ - template_name = request.GET.get('template', None) - if template_name is None: - return HttpResponseBadRequest('"template" key is required') - - from django.template.loader import find_template_loader - loaders = [] - for loader_name in settings.TEMPLATE_LOADERS: - loader = find_template_loader(loader_name) - if loader is not None: - loaders.append(loader) - for loader in loaders: - try: - source, display_name = loader.load_template_source(template_name) - break - except TemplateDoesNotExist: - source = "Template Does Not Exist: %s" % (template_name,) - - try: - from pygments import highlight - from pygments.lexers import HtmlDjangoLexer - from pygments.formatters import HtmlFormatter - - source = highlight(source, HtmlDjangoLexer(), HtmlFormatter()) - source = mark_safe(source) - source.pygmentized = True - except ImportError: - pass - - return render(request, 'debug_toolbar/panels/template_source.html', { - 'source': source, - 'template_name': template_name - }) diff --git a/debug_toolbar/panels/template/__init__.py b/debug_toolbar/panels/template/__init__.py new file mode 100644 index 0000000..d2f595d --- /dev/null +++ b/debug_toolbar/panels/template/__init__.py @@ -0,0 +1 @@ +from debug_toolbar.panels.template.panel import TemplateDebugPanel # noqa diff --git a/debug_toolbar/panels/template/panel.py b/debug_toolbar/panels/template/panel.py new file mode 100644 index 0000000..7c4b06a --- /dev/null +++ b/debug_toolbar/panels/template/panel.py @@ -0,0 +1,159 @@ +from __future__ import unicode_literals + +from os.path import normpath +from pprint import pformat + +import django +from django import http +from django.conf import settings +from django.conf.urls import patterns, url +from django.db.models.query import QuerySet, RawQuerySet +from django.template.context import get_standard_processors +from django.test.signals import template_rendered +from django.utils.encoding import force_text +from django.utils import six +from django.utils.translation import ugettext_lazy as _ + +from debug_toolbar.panels import DebugPanel +from debug_toolbar.panels.sql.tracking import recording, SQLQueryTriggered +from debug_toolbar.utils import settings as dt_settings + +# Code taken and adapted from Simon Willison and Django Snippets: +# http://www.djangosnippets.org/snippets/766/ + +# Monkey-patch to enable the template_rendered signal. The receiver returns +# immediately when the panel is disabled to keep the overhead small. + +from django.test.utils import instrumented_test_render +from django.template import Template + +if Template._render != instrumented_test_render: + Template.original_render = Template._render + Template._render = instrumented_test_render + + +if django.VERSION[:2] < (1, 7): + # Monkey-patch versions of Django where Template doesn't store origin. + # See https://code.djangoproject.com/ticket/16096. + + old_template_init = Template.__init__ + + def new_template_init(self, template_string, origin=None, name=''): + old_template_init(self, template_string, origin, name) + self.origin = origin + + Template.__init__ = new_template_init + + +class TemplateDebugPanel(DebugPanel): + """ + A panel that lists all templates used during processing of a response. + """ + name = 'Template' + template = 'debug_toolbar/panels/templates.html' + has_content = True + + def __init__(self, *args, **kwargs): + super(TemplateDebugPanel, self).__init__(*args, **kwargs) + self.templates = [] + template_rendered.connect(self._store_template_info) + + def _store_template_info(self, sender, **kwargs): + if not self.enabled: + return + + template, context = kwargs['template'], kwargs['context'] + + # Skip templates that we are generating through the debug toolbar. + if (isinstance(template.name, six.string_types) and + template.name.startswith('debug_toolbar/')): + return + + context_list = [] + for context_layer in context.dicts: + temp_layer = {} + if hasattr(context_layer, 'items'): + for key, value in context_layer.items(): + # Replace any request elements - they have a large + # unicode representation and the request data is + # already made available from the Request Vars panel. + if isinstance(value, http.HttpRequest): + temp_layer[key] = '<>' + # Replace the debugging sql_queries element. The SQL + # data is already made available from the SQL panel. + elif key == 'sql_queries' and isinstance(value, list): + temp_layer[key] = '<>' + # Replace LANGUAGES, which is available in i18n context processor + elif key == 'LANGUAGES' and isinstance(value, tuple): + temp_layer[key] = '<>' + # QuerySet would trigger the database: user can run the query from SQL Panel + elif isinstance(value, (QuerySet, RawQuerySet)): + model_name = "%s.%s" % ( + value.model._meta.app_label, value.model.__name__) + temp_layer[key] = '<<%s of %s>>' % ( + value.__class__.__name__.lower(), model_name) + else: + try: + recording(False) + pformat(value) # this MAY trigger a db query + except SQLQueryTriggered: + temp_layer[key] = '<>' + except UnicodeEncodeError: + temp_layer[key] = '<>' + except Exception: + temp_layer[key] = '<>' + else: + temp_layer[key] = value + finally: + recording(True) + try: + context_list.append(pformat(temp_layer)) + except UnicodeEncodeError: + pass + + kwargs['context'] = [force_text(item) for item in context_list] + self.templates.append(kwargs) + + @classmethod + def get_urls(cls): + return patterns('debug_toolbar.panels.template.views', # noqa + url(r'^template_source/$', 'template_source', name='template_source'), + ) + + def nav_title(self): + return _('Templates') + + def title(self): + num_templates = len(self.templates) + return _('Templates (%(num_templates)s rendered)') % {'num_templates': num_templates} + + def process_response(self, request, response): + context_processors = dict( + [ + ("%s.%s" % (k.__module__, k.__name__), + pformat(k(request))) for k in get_standard_processors() + ] + ) + template_context = [] + for template_data in self.templates: + info = {} + # Clean up some info about templates + template = template_data.get('template', None) + if not hasattr(template, 'origin'): + continue + if template.origin and template.origin.name: + template.origin_name = template.origin.name + else: + template.origin_name = 'No origin' + info['template'] = template + # Clean up context for better readability + if dt_settings.CONFIG['SHOW_TEMPLATE_CONTEXT']: + context_list = template_data.get('context', []) + info['context'] = '\n'.join(context_list) + template_context.append(info) + + self.record_stats({ + 'templates': template_context, + 'template_dirs': [normpath(x) for x in settings.TEMPLATE_DIRS], + 'context_processors': context_processors, + }) diff --git a/debug_toolbar/panels/template/views.py b/debug_toolbar/panels/template/views.py new file mode 100644 index 0000000..30bd167 --- /dev/null +++ b/debug_toolbar/panels/template/views.py @@ -0,0 +1,46 @@ +from __future__ import unicode_literals + +from django.http import HttpResponseBadRequest +from django.conf import settings +from django.shortcuts import render +from django.template import TemplateDoesNotExist +from django.template.loader import find_template_loader +from django.utils.safestring import mark_safe + + +def template_source(request): + """ + Return the source of a template, syntax-highlighted by Pygments if + it's available. + """ + template_name = request.GET.get('template', None) + if template_name is None: + return HttpResponseBadRequest('"template" key is required') + + loaders = [] + for loader_name in settings.TEMPLATE_LOADERS: + loader = find_template_loader(loader_name) + if loader is not None: + loaders.append(loader) + for loader in loaders: + try: + source, display_name = loader.load_template_source(template_name) + break + except TemplateDoesNotExist: + source = "Template Does Not Exist: %s" % (template_name,) + + try: + from pygments import highlight + from pygments.lexers import HtmlDjangoLexer + from pygments.formatters import HtmlFormatter + + source = highlight(source, HtmlDjangoLexer(), HtmlFormatter()) + source = mark_safe(source) + source.pygmentized = True + except ImportError: + pass + + return render(request, 'debug_toolbar/panels/template_source.html', { + 'source': source, + 'template_name': template_name + }) diff --git a/debug_toolbar/utils/sql.py b/debug_toolbar/utils/sql.py deleted file mode 100644 index 00728a3..0000000 --- a/debug_toolbar/utils/sql.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import unicode_literals - -import re - -from django.utils.html import escape - -import sqlparse -from sqlparse import tokens as T - - -class BoldKeywordFilter: - """sqlparse filter to bold SQL keywords""" - def process(self, stack, stream): - """Process the token stream""" - for token_type, value in stream: - is_keyword = token_type in T.Keyword - if is_keyword: - yield T.Text, '' - yield token_type, escape(value) - if is_keyword: - yield T.Text, '' - - -def reformat_sql(sql): - stack = sqlparse.engine.FilterStack() - stack.preprocess.append(BoldKeywordFilter()) # add our custom filter - stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings - return swap_fields(''.join(stack.run(sql))) - - -def swap_fields(sql): - expr = r'SELECT (...........*?) FROM' - subs = (r'SELECT ' - r'••• ' - r'\1 ' - r'FROM') - return re.sub(expr, subs, sql) diff --git a/debug_toolbar/utils/tracking/__init__.py b/debug_toolbar/utils/tracking/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/debug_toolbar/utils/tracking/db.py b/debug_toolbar/utils/tracking/db.py deleted file mode 100644 index fd56ff9..0000000 --- a/debug_toolbar/utils/tracking/db.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import unicode_literals - -import sys - -import json -from threading import local -from time import time - -from django.template import Node -from django.utils.encoding import force_text -from django.utils import six - -from debug_toolbar.utils import tidy_stacktrace, get_template_info, get_stack -from debug_toolbar.utils import settings as dt_settings - - -class SQLQueryTriggered(Exception): - """Thrown when template panel triggers a query""" - pass - - -class ThreadLocalState(local): - def __init__(self): - self.enabled = True - - @property - def Wrapper(self): - if self.enabled: - return NormalCursorWrapper - return ExceptionCursorWrapper - - def recording(self, v): - self.enabled = v - - -state = ThreadLocalState() -recording = state.recording # export function - - -def CursorWrapper(*args, **kwds): # behave like a class - return state.Wrapper(*args, **kwds) - - -class ExceptionCursorWrapper(object): - """ - Wraps a cursor and raises an exception on any operation. - Used in Templates panel. - """ - def __init__(self, cursor, db, logger): - pass - - def __getattr__(self, attr): - raise SQLQueryTriggered() - - -class NormalCursorWrapper(object): - """ - Wraps a cursor and logs queries. - """ - - def __init__(self, cursor, db, logger): - self.cursor = cursor - # Instance of a BaseDatabaseWrapper subclass - self.db = db - # logger must implement a ``record`` method - self.logger = logger - - def _quote_expr(self, element): - if isinstance(element, six.string_types): - return "'%s'" % force_text(element).replace("'", "''") - else: - return repr(element) - - def _quote_params(self, params): - if not params: - return params - if isinstance(params, dict): - return dict((key, self._quote_expr(value)) - for key, value in params.items()) - return list(map(self._quote_expr, params)) - - def _decode(self, param): - try: - return force_text(param, strings_only=True) - except UnicodeDecodeError: - return '(encoded string)' - - def execute(self, sql, params=()): - start_time = time() - try: - return self.cursor.execute(sql, params) - finally: - stop_time = time() - duration = (stop_time - start_time) * 1000 - if dt_settings.CONFIG['ENABLE_STACKTRACES']: - stacktrace = tidy_stacktrace(reversed(get_stack())) - else: - stacktrace = [] - _params = '' - try: - _params = json.dumps(list(map(self._decode, params))) - except Exception: - pass # object not JSON serializable - - template_info = None - cur_frame = sys._getframe().f_back - try: - while cur_frame is not None: - if cur_frame.f_code.co_name == 'render': - node = cur_frame.f_locals['self'] - if isinstance(node, Node): - template_info = get_template_info(node.source) - break - cur_frame = cur_frame.f_back - except Exception: - pass - del cur_frame - - alias = getattr(self.db, 'alias', 'default') - conn = self.db.connection - # HACK: avoid imports - if conn: - engine = conn.__class__.__module__.split('.', 1)[0] - else: - engine = 'unknown' - - params = { - 'engine': engine, - 'alias': alias, - 'sql': self.db.ops.last_executed_query( - self.cursor, sql, self._quote_params(params)), - 'duration': duration, - 'raw_sql': sql, - 'params': _params, - 'stacktrace': stacktrace, - 'start_time': start_time, - 'stop_time': stop_time, - 'is_slow': duration > dt_settings.CONFIG['SQL_WARNING_THRESHOLD'], - 'is_select': sql.lower().strip().startswith('select'), - 'template_info': template_info, - } - - if engine == 'psycopg2': - # If an erroneous query was ran on the connection, it might - # be in a state where checking isolation_level raises an - # exception. - try: - iso_level = conn.isolation_level - except conn.InternalError: - iso_level = 'unknown' - params.update({ - 'trans_id': self.logger.get_transaction_id(alias), - 'trans_status': conn.get_transaction_status(), - 'iso_level': iso_level, - 'encoding': conn.encoding, - }) - - # We keep `sql` to maintain backwards compatibility - self.logger.record(**params) - - def executemany(self, sql, param_list): - return self.cursor.executemany(sql, param_list) - - def __getattr__(self, attr): - return getattr(self.cursor, attr) - - def __iter__(self): - return iter(self.cursor) -- cgit v1.2.3