Skip to content

Commit

Permalink
Add /sql_profiler to enable SQL profiling
Browse files Browse the repository at this point in the history
Disabled by default. You must enable it by setting the SQL_PROFILER_SECRET env
var and using the view (/sql_profiler) to switch it ON and OFF. Warning, it
will slow down everything.
  • Loading branch information
Patrick Valsecchi committed Mar 29, 2017
1 parent 1de5fdd commit d174b30
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ applications:
* Logging handler for CEE/UDP logs
* An optional (enabled by setting the LOG_VIEW_SECRET env var) view (/logging/level)
to change runtime the log levels
* SQL profiler to debug DB performance problems, disabled by default. You must enable it by setting the
SQL_PROFILER_SECRET env var and using the view (/sql_profiler) to switch it ON and OFF. Warning,
it will slow down everything.
* Error handlers to send JSON messages to the client in case of error
* A cornice service drop in replacement for setting up CORS

Expand Down
1 change: 1 addition & 0 deletions acceptance_tests/tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ services:
STATSD_ADDRESS: 127.0.0.1:8125
STATSD_PREFIX: acceptance
LOG_VIEW_SECRET: changeme
SQL_PROFILER_SECRET: changeme
SQL_LOG_LEVEL: DEBUG
OTHER_LOG_LEVEL: DEBUG
DEVELOPMENT: 1
Expand Down
21 changes: 21 additions & 0 deletions acceptance_tests/tests/tests/test_sql_profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
def _switch(app_connection, enable=None):
params = {'secret': 'changeme'}
if enable is not None:
params['enable'] = "1" if enable else "0"
answer = app_connection.get_json("sql_profiler", params=params)
assert answer['status'] == 200
return answer['enabled']


def test_ok(app_connection, slave_db_connection):
assert _switch(app_connection) is False
assert _switch(app_connection, enable=True) is True
try:
assert _switch(app_connection) is True
app_connection.get_json("hello")
finally:
_switch(app_connection, enable=False)


def test_no_secret(app_connection):
app_connection.get_json("sql_profiler", params={'enable': '1'}, expected_status=403)
3 changes: 2 additions & 1 deletion c2cwsgiutils/pyramid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cornice
import pyramid_tm

from c2cwsgiutils import stats, pyramid_logging
from c2cwsgiutils import stats, pyramid_logging, sql_profiler


def includeme(config):
Expand All @@ -14,5 +14,6 @@ def includeme(config):
config.include(cornice.includeme)
stats.init(config)
pyramid_logging.install_subscriber(config)
sql_profiler.init(config)
config.scan("c2cwsgiutils.services")
config.scan("c2cwsgiutils.errors")
69 changes: 69 additions & 0 deletions c2cwsgiutils/sql_profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
A view (URL=/sql_provider) allowing to enabled/disable a SQL spy that runs an "EXPLAIN ANALYZE" on
every SELECT query going through SQLAlchemy.
"""
import logging
import os
from pyramid.httpexceptions import HTTPForbidden
import re
import sqlalchemy.event
import sqlalchemy.engine

ENV_KEY = 'SQL_PROFILER_SECRET'
LOG = logging.getLogger(__name__)
enabled = False


def _sql_profiler_view(request):
global enabled
if request.params.get('secret') != os.environ[ENV_KEY]:
raise HTTPForbidden('Missing or invalid secret parameter')
if 'enable' in request.params:
if request.params['enable'] == '1':
if not enabled:
LOG.warning("Enabling the SQL profiler")
sqlalchemy.event.listen(sqlalchemy.engine.Engine, "before_cursor_execute",
_before_cursor_execute)
enabled = True
return {'status': 200, 'enabled': True}
else:
if enabled:
LOG.warning("Disabling the SQL profiler")
sqlalchemy.event.remove(sqlalchemy.engine.Engine, "before_cursor_execute",
_before_cursor_execute)
enabled = False
return {'status': 200, 'enabled': False}
else:
return {'status': 200, 'enabled': enabled}


def _beautify_sql(statement):
statement = re.sub(r'SELECT [^\n]*\n', 'SELECT ...\n', statement)
statement = re.sub(r' ((?:LEFT )?(?:OUTER )?JOIN )', r'\n\1', statement)
statement = re.sub(r' ON ', r'\n ON ', statement)
statement = re.sub(r' GROUP BY ', r'\nGROUP BY ', statement)
statement = re.sub(r' ORDER BY ', r'\nORDER BY ', statement)
return statement


def _indent(statement, indent=' '):
return indent + ("\n" + indent).join(statement.split('\n'))


def _before_cursor_execute(conn, _cursor, statement, parameters, _context, _executemany):
if statement.startswith("SELECT ") and LOG.isEnabledFor(logging.INFO):
output = "statement:\n%s\nparameters: %s\nplan:\n " % (_indent(_beautify_sql(statement)),
repr(parameters))
output += '\n '.join([row[0] for row in conn.engine.execute("EXPLAIN ANALYZE " + statement,
parameters)])
LOG.info(output)


def init(config):
"""
Install a pyramid event handler that adds the request information
"""
if 'SQL_PROFILER_SECRET' in os.environ:
config.add_route("sql_profiler", r"/sql_profiler", request_method="GET")
config.add_view(_sql_profiler_view, route_name="sql_profiler", renderer="json", http_cache=0)
LOG.info("Enabled the /sql_profiler API")
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from setuptools import setup, find_packages


VERSION = '0.6.0'
VERSION = '0.7.0'
HERE = os.path.abspath(os.path.dirname(__file__))
INSTALL_REQUIRES = open(os.path.join(HERE, 'rel_requirements.txt')).read().splitlines()

Expand Down

0 comments on commit d174b30

Please sign in to comment.