Skip to content

Commit f55f1e5

Browse files
committed
First pass.
1 parent 86417dd commit f55f1e5

File tree

6 files changed

+275
-3
lines changed

6 files changed

+275
-3
lines changed

.travis.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
language: python
2+
python:
3+
- 2.7
4+
- 3.3
5+
- 3.4
6+
7+
script: python setup.py test

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2014 bret barker
3+
Copyright (c) 2014 Bret Barker
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,53 @@
1-
flask-queryinspect
2-
==================
1+
# Flask QueryInspect Extension
2+
3+
Flask-QueryInspect is a Flask extension that provides metrics on SQL queries executed for each request.
4+
5+
It assumes use of and relies upon SQLAlchemy as the underlying ORM.
6+
7+
I've used this method for several Flask projects and was finally inspired to turn it into an extension after seeing
8+
https://github.com/dobarkod/django-queryinspect.
9+
10+
Flask-QueryInspect tracks the count of SQL reads and writes, new DB connections, and total request and total query time for each request. It does not yet handle reporting of duplicate queries like Django-QueryInspect.
11+
12+
# Installation #
13+
14+
```
15+
pip install Flask-QueryInspect
16+
```
17+
18+
## Usage ##
19+
20+
```python
21+
app = Flask(__name__)
22+
db = SQLAlchemy(cls.app)
23+
app.config['QUERYINSPECT_ENABLED'] = True
24+
qi = QueryInspect(cls.app)
25+
```
26+
27+
## Configuration ##
28+
29+
QueryInspect has the following optional config vars that can be set in
30+
Flask's app.config:
31+
32+
Variable | Default | Description
33+
------------- | ------------- | -------------
34+
QUERYINSPECT_ENABLED | True | False to completely disable QueryInspect
35+
QUERYINSPECT_HEADERS | True | Enable response header injection
36+
QUERYINSPECT_HEADERS_COMBINED | True | Enable combined single header, else use [Django QueryInspect's](https://github.com/dobarkod/django-queryinspect) style
37+
QUERYINSPECT_LOG | True | Enable logging, in Librato style
38+
39+
## Combined Header Format ##
40+
41+
```
42+
X-QueryInspect-Combined: reads=1,writes=1,conns=0,q_time=0.2ms,r_time=2.9ms
43+
```
44+
45+
## Log Format ##
46+
47+
Logging is output using the standard Python logging module (logger name is 'flask_queryinspect') and formatted so it can be easily read by Librato, see: https://devcenter.heroku.com/articles/librato#custom-log-based-metrics
48+
49+
E.g.:
50+
```
51+
INFO measure#qi.r_time=2.9ms, measure#qi.q_time=0.2ms,count#qi.reads=1, count#qi.writes=1, count#qi.conns=0
52+
```
53+

flask_queryinspect.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
import time
3+
from flask import current_app
4+
from sqlalchemy.engine import Engine
5+
from sqlalchemy.event import listen
6+
7+
log = logging.getLogger(__name__)
8+
9+
# Find the stack on which we want to store metrics
10+
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
11+
# before that we need to use the _request_ctx_stack.
12+
try:
13+
from flask import _app_ctx_stack as stack
14+
except ImportError:
15+
from flask import _request_ctx_stack as stack
16+
17+
18+
class QueryInspect(object):
19+
20+
def __init__(self, app=None):
21+
self.app = app
22+
if app is not None:
23+
self.init_app(app)
24+
25+
def init_app(self, app):
26+
app.config.setdefault('QUERYINSPECT_ENABLED', False)
27+
app.config.setdefault('QUERYINSPECT_HEADERS', True)
28+
app.config.setdefault('QUERYINSPECT_HEADERS_COMBINED', True)
29+
app.config.setdefault('QUERYINSPECT_LOG', True)
30+
31+
app.before_request(self.before_request)
32+
app.after_request(self.after_request)
33+
34+
listen(Engine, 'connect', self.connect)
35+
listen(Engine, 'before_cursor_execute', self.before_cursor_execute)
36+
listen(Engine, 'after_cursor_execute', self.after_cursor_execute)
37+
38+
def connect(self, dbapi_connection, connection_record):
39+
if not current_app or not current_app.config.get('QUERYINSPECT_ENABLED'):
40+
return
41+
stack.queryinspect['conns'] += 1
42+
43+
def before_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
44+
if not current_app or not current_app.config.get('QUERYINSPECT_ENABLED'):
45+
return
46+
stack.queryinspect['q_start'] = time.time()
47+
48+
def after_cursor_execute(self, conn, cursor, statement, parameters, context, executemany):
49+
if not current_app or not current_app.config.get('QUERYINSPECT_ENABLED'):
50+
return
51+
stack.queryinspect['q_time'] += time.time() - stack.queryinspect['q_start']
52+
if statement.lower().startswith('select'):
53+
stack.queryinspect['reads'] += 1
54+
else:
55+
stack.queryinspect['writes'] += 1
56+
57+
def before_request(self, *kw1, **kw2):
58+
if not current_app or not current_app.config.get('QUERYINSPECT_ENABLED'):
59+
return
60+
stack.queryinspect = {
61+
'r_start': time.time(),
62+
'q_start': 0,
63+
'r_time': 0,
64+
'q_time': 0,
65+
'reads': 0,
66+
'writes': 0,
67+
'conns': 0
68+
}
69+
70+
def after_request(self, response, *kw1, **kw2):
71+
if not current_app or not current_app.config.get('QUERYINSPECT_ENABLED'):
72+
return
73+
74+
qi = stack.queryinspect
75+
qi['r_time'] = time.time() - qi['r_start']
76+
qi['q_time_ms'] = round(qi['q_time'] * 1000, 1)
77+
qi['r_time_ms'] = round(qi['r_time'] * 1000, 1)
78+
79+
if current_app.config.get('QUERYINSPECT_LOG'):
80+
log.info('measure#qi.r_time=%(r_time_ms).1fms, measure#qi.q_time=%(q_time_ms).1fms,' +
81+
'count#qi.reads=%(reads)d, count#qi.writes=%(writes)d, count#qi.conns=%(conns)d', qi)
82+
83+
if current_app.config.get('QUERYINSPECT_HEADERS'):
84+
if current_app.config.get('QUERYINSPECT_HEADERS_COMBINED'):
85+
combo = ('reads=%(reads)d,writes=%(writes)d,conns=%(conns)d,q_time=%(q_time_ms).1fms,' +
86+
'r_time=%(r_time_ms).1fms') % qi
87+
response.headers['X-QueryInspect-Combined'] = combo
88+
else:
89+
response.headers['X-QueryInspect-Num-SQL-Queries'] = qi['reads'] + qi['writes']
90+
#response.headers['X-QueryInspect-Duplicate-SQL-Queries'] = qi['dupes']
91+
response.headers['X-QueryInspect-Total-SQL-Time'] = qi['q_time']
92+
response.headers['X-QueryInspect-Total-Request-Time'] = qi['r_time']
93+
return response

setup.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Flask-QueryInspect
3+
-------------
4+
5+
Flask-QueryInspect is a Flask extension that provides metrics on SQL queries executed for each request.
6+
7+
It assumes use of and relies upon SQLAlchemy as the underlying ORM.
8+
9+
This extension was inspired by internal tooling of this sort, and
10+
then directly by https://github.com/dobarkod/django-queryinspect
11+
12+
"""
13+
from setuptools import setup
14+
15+
16+
setup(
17+
name='Flask-QueryInspect',
18+
version='0.1',
19+
url='https://github.com/noise/flask-queryinspect',
20+
license='MIT',
21+
author='Bret Barker',
22+
author_email='bret@abitrandom.net',
23+
description='Flask extension to provide metrics on SQL queries per request.',
24+
long_description=__doc__,
25+
py_modules=['flask_queryinspect'],
26+
zip_safe=False,
27+
include_package_data=True,
28+
platforms='any',
29+
install_requires=[
30+
'Flask',
31+
'SQLAlchemy'
32+
],
33+
tests_require=[
34+
'Flask-SQLAlchemy'
35+
],
36+
test_suite='test_queryinspect',
37+
classifiers=[
38+
'Environment :: Web Environment',
39+
'Intended Audience :: Developers',
40+
'License :: OSI Approved :: MIT License',
41+
'Operating System :: OS Independent',
42+
'Programming Language :: Python',
43+
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
44+
'Topic :: Software Development :: Libraries :: Python Modules'
45+
],
46+
)

test_queryinspect.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
import time
3+
import unittest
4+
from flask import Flask
5+
from flask.ext.sqlalchemy import SQLAlchemy
6+
from flask.ext.queryinspect import QueryInspect
7+
8+
log = logging.getLogger(__name__)
9+
10+
11+
class TestQueryInspect(unittest.TestCase):
12+
@classmethod
13+
def setUpClass(cls):
14+
logging.basicConfig(format='%(asctime)-15s %(levelname)s %(message)s')
15+
logging.getLogger('flask_queryinspect').setLevel(logging.DEBUG)
16+
logging.getLogger('test_queryinspect').setLevel(logging.DEBUG)
17+
18+
cls.app = Flask(__name__)
19+
cls.app.config['TESTING'] = True
20+
21+
cls.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
22+
#cls.app.config['SQLALCHEMY_ECHO'] = True
23+
db = SQLAlchemy(cls.app)
24+
cls.db = db
25+
26+
cls.app.config['QUERYINSPECT_ENABLED'] = True
27+
cls.qi = QueryInspect(cls.app)
28+
29+
class TestModel(db.Model):
30+
id = db.Column(db.Integer, primary_key=True)
31+
foo = db.Column(db.Unicode(10))
32+
33+
def __init__(self, foo):
34+
self.foo = foo
35+
36+
db.create_all(app=cls.app)
37+
for i in xrange(10):
38+
db.session.add(TestModel('test_%d' % i))
39+
db.session.commit()
40+
41+
@cls.app.route('/')
42+
def index():
43+
return u'No Queries'
44+
45+
@cls.app.route('/mix')
46+
def mix():
47+
t = TestModel.query.first()
48+
t.foo = 'bar'
49+
db.session.commit()
50+
return u'Reads and writes'
51+
52+
@cls.app.route('/slow')
53+
def slow():
54+
time.sleep(.1)
55+
return u'Slow request'
56+
57+
def test_noqueries(self):
58+
with self.app.test_client() as c:
59+
res = c.get('/')
60+
log.debug(res.headers)
61+
self.assertTrue(res.headers['X-QueryInspect-Combined'].startswith('reads=0,writes=0,conns=0'))
62+
63+
def test_mix(self):
64+
with self.app.test_client() as c:
65+
res = c.get('/mix')
66+
log.debug(res.headers)
67+
self.assertTrue(res.headers['X-QueryInspect-Combined'].startswith('reads=1,writes=1'))
68+
69+
def test_rtime(self):
70+
self.app.config['QUERYINSPECT_HEADERS_COMBINED'] = False
71+
with self.app.test_client() as c:
72+
res = c.get('/slow')
73+
r_time = res.headers['X-QueryInspect-Total-Request-Time']
74+
self.assertTrue(90 < r_time > 110)
75+
self.app.config['QUERYINSPECT_HEADERS_COMBINED'] = True

0 commit comments

Comments
 (0)