Skip to content

Commit d3ad149

Browse files
authored
PyMySQL Instrumentation, support and tests (#166)
* PyMySQL Instrumentation, support and tests * Add py2 special case * Add sql_sanitizer and sanitize queries
1 parent b9bd828 commit d3ad149

File tree

8 files changed

+295
-13
lines changed

8 files changed

+295
-13
lines changed

instana/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ def boot_agent():
6565
from .instrumentation.tornado import server
6666
from .instrumentation import logging
6767
from .instrumentation import mysqlpython
68+
from .instrumentation import pymysql
6869
from .instrumentation import redis
6970
from .instrumentation import sqlalchemy
7071
from .instrumentation import sudsjurko

instana/instrumentation/pep0249.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from ..log import logger
66
from ..singletons import tracer
7+
from ..util import sql_sanitizer
78

89

910
class CursorWrapper(wrapt.ObjectProxy):
@@ -20,7 +21,7 @@ def _collect_kvs(self, span, sql):
2021
try:
2122
span.set_tag(ext.SPAN_KIND, 'exit')
2223
span.set_tag(ext.DATABASE_INSTANCE, self._connect_params[1]['db'])
23-
span.set_tag(ext.DATABASE_STATEMENT, sql)
24+
span.set_tag(ext.DATABASE_STATEMENT, sql_sanitizer(sql))
2425
span.set_tag(ext.DATABASE_TYPE, 'mysql')
2526
span.set_tag(ext.DATABASE_USER, self._connect_params[1]['user'])
2627
span.set_tag('host', "%s:%s" %

instana/instrumentation/pymysql.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import absolute_import
2+
3+
from ..log import logger
4+
from .pep0249 import ConnectionFactory
5+
6+
try:
7+
import pymysql #
8+
9+
cf = ConnectionFactory(connect_func=pymysql.connect, module_name='mysql')
10+
11+
setattr(pymysql, 'connect', cf)
12+
if hasattr(pymysql, 'Connect'):
13+
setattr(pymysql, 'Connect', cf)
14+
15+
logger.debug("Instrumenting pymysql")
16+
except ImportError:
17+
pass

instana/util.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ def strip_secrets(qp, matcher, kwlist):
182182
logger.debug("strip_secrets", exc_info=True)
183183

184184

185+
def sql_sanitizer(sql):
186+
"""
187+
Removes values from valid SQL statements and returns a stripped version.
188+
189+
:param sql: The SQL statement to be sanitized
190+
:return: String - A sanitized SQL statement without values.
191+
"""
192+
return regexp_sql_values.sub('?', sql)
193+
194+
195+
# Used by sql_sanitizer
196+
regexp_sql_values = re.compile('(\'[\s\S][^\']*\'|\d*\.\d+|\d+|NULL)')
197+
198+
185199
def get_default_gateway():
186200
"""
187201
Attempts to read /proc/self/net/route to determine the default gateway in use.
@@ -230,6 +244,9 @@ def get_py_source(file):
230244
finally:
231245
return response
232246

247+
# Used by get_py_source
248+
regexp_py = re.compile('\.py$')
249+
233250

234251
def every(delay, task, name):
235252
"""
@@ -253,5 +270,5 @@ def every(delay, task, name):
253270
next_time += (time.time() - next_time) // delay * delay + delay
254271

255272

256-
# Used by get_py_source
257-
regexp_py = re.compile('\.py$')
273+
274+

setup.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# coding: utf-8
2-
from distutils.version import LooseVersion
3-
from setuptools import find_packages, setup
42
import sys
53
from os import path
4+
from distutils.version import LooseVersion
5+
from setuptools import find_packages, setup
66

77
# Import README.md into long_description
88
pwd = path.abspath(path.dirname(__file__))
@@ -16,6 +16,7 @@
1616

1717

1818
def check_setuptools():
19+
""" Validate that we have min version required of setuptools """
1920
import pkg_resources
2021
st_version = pkg_resources.get_distribution('setuptools').version
2122
if LooseVersion(st_version) < LooseVersion('20.2.2'):
@@ -73,6 +74,7 @@ def check_setuptools():
7374
'mock>=2.0.0',
7475
'MySQL-python>=1.2.5;python_version<="2.7"',
7576
'psycopg2>=2.7.1',
77+
'PyMySQL[rsa]>=0.9.1',
7678
'pyOpenSSL>=16.1.0;python_version<="2.7"',
7779
'pytest>=3.0.1',
7880
'redis<3.0.0',
@@ -85,7 +87,8 @@ def check_setuptools():
8587
],
8688
},
8789
test_suite='nose.collector',
88-
keywords=['performance', 'opentracing', 'metrics', 'monitoring', 'tracing', 'distributed-tracing'],
90+
keywords=['performance', 'opentracing', 'metrics', 'monitoring',
91+
'tracing', 'distributed-tracing'],
8992
classifiers=[
9093
'Development Status :: 5 - Production/Stable',
9194
'Framework :: Django',

tests/helpers.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@
1515
testenv['mysql_port'] = int(os.environ.get('MYSQL_PORT', '3306'))
1616
testenv['mysql_db'] = os.environ.get('MYSQL_DB', 'circle_test')
1717
testenv['mysql_user'] = os.environ.get('MYSQL_USER', 'root')
18+
testenv['mysql_pw'] = os.environ.get('MYSQL_PW', '')
1819

19-
if 'MYSQL_PW' in os.environ:
20-
testenv['mysql_pw'] = os.environ['MYSQL_PW']
21-
elif 'TRAVIS_MYSQL_PASS' in os.environ:
22-
testenv['mysql_pw'] = os.environ['TRAVIS_MYSQL_PASS']
23-
else:
24-
testenv['mysql_pw'] = ''
2520

2621
"""
2722
PostgreSQL Environment

tests/test_mysql-python.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import absolute_import
22

33
import logging
4-
import os
54
import sys
65
from unittest import SkipTest
76

tests/test_pymysql.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
from __future__ import absolute_import
2+
3+
import logging
4+
import sys
5+
from unittest import SkipTest
6+
7+
from nose.tools import assert_equals
8+
9+
from instana.singletons import tracer
10+
11+
from .helpers import testenv
12+
13+
import pymysql
14+
15+
logger = logging.getLogger(__name__)
16+
17+
create_table_query = 'CREATE TABLE IF NOT EXISTS users(id serial primary key, \
18+
name varchar(40) NOT NULL, email varchar(40) NOT NULL)'
19+
20+
create_proc_query = """
21+
CREATE PROCEDURE test_proc(IN t VARCHAR(255))
22+
BEGIN
23+
SELECT name FROM users WHERE name = t;
24+
END
25+
"""
26+
27+
db = pymysql.connect(host=testenv['mysql_host'], port=testenv['mysql_port'],
28+
user=testenv['mysql_user'], passwd=testenv['mysql_pw'],
29+
db=testenv['mysql_db'])
30+
31+
cursor = db.cursor()
32+
cursor.execute(create_table_query)
33+
34+
while cursor.nextset() is not None:
35+
pass
36+
37+
cursor.execute('DROP PROCEDURE IF EXISTS test_proc')
38+
39+
while cursor.nextset() is not None:
40+
pass
41+
42+
cursor.execute(create_proc_query)
43+
44+
while cursor.nextset() is not None:
45+
pass
46+
47+
cursor.close()
48+
db.close()
49+
50+
51+
class TestPyMySQL:
52+
def setUp(self):
53+
logger.warn("MySQL connecting: %s:<pass>@%s:3306/%s", testenv['mysql_user'], testenv['mysql_host'], testenv['mysql_db'])
54+
self.db = pymysql.connect(host=testenv['mysql_host'], port=testenv['mysql_port'],
55+
user=testenv['mysql_user'], passwd=testenv['mysql_pw'],
56+
db=testenv['mysql_db'])
57+
self.cursor = self.db.cursor()
58+
self.recorder = tracer.recorder
59+
self.recorder.clear_spans()
60+
tracer.cur_ctx = None
61+
62+
def tearDown(self):
63+
""" Do nothing for now """
64+
return None
65+
66+
def test_vanilla_query(self):
67+
self.cursor.execute("""SELECT * from users""")
68+
result = self.cursor.fetchone()
69+
assert_equals(3, len(result))
70+
71+
spans = self.recorder.queued_spans()
72+
assert_equals(0, len(spans))
73+
74+
def test_basic_query(self):
75+
result = None
76+
with tracer.start_active_span('test'):
77+
result = self.cursor.execute("""SELECT * from users""")
78+
self.cursor.fetchone()
79+
80+
assert(result >= 0)
81+
82+
spans = self.recorder.queued_spans()
83+
assert_equals(2, len(spans))
84+
85+
db_span = spans[0]
86+
test_span = spans[1]
87+
88+
assert_equals("test", test_span.data.sdk.name)
89+
assert_equals(test_span.t, db_span.t)
90+
assert_equals(db_span.p, test_span.s)
91+
92+
assert_equals(None, db_span.error)
93+
assert_equals(None, db_span.ec)
94+
95+
assert_equals(db_span.n, "mysql")
96+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
97+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
98+
assert_equals(db_span.data.mysql.stmt, 'SELECT * from users')
99+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
100+
101+
def test_query_with_params(self):
102+
result = None
103+
with tracer.start_active_span('test'):
104+
result = self.cursor.execute("""SELECT * from users where id=1""")
105+
self.cursor.fetchone()
106+
107+
assert(result >= 0)
108+
109+
spans = self.recorder.queued_spans()
110+
assert_equals(2, len(spans))
111+
112+
db_span = spans[0]
113+
test_span = spans[1]
114+
115+
assert_equals("test", test_span.data.sdk.name)
116+
assert_equals(test_span.t, db_span.t)
117+
assert_equals(db_span.p, test_span.s)
118+
119+
assert_equals(None, db_span.error)
120+
assert_equals(None, db_span.ec)
121+
122+
assert_equals(db_span.n, "mysql")
123+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
124+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
125+
assert_equals(db_span.data.mysql.stmt, 'SELECT * from users where id=?')
126+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
127+
128+
def test_basic_insert(self):
129+
result = None
130+
with tracer.start_active_span('test'):
131+
result = self.cursor.execute(
132+
"""INSERT INTO users(name, email) VALUES(%s, %s)""",
133+
('beaker', '[email protected]'))
134+
135+
assert_equals(1, result)
136+
137+
spans = self.recorder.queued_spans()
138+
assert_equals(2, len(spans))
139+
140+
db_span = spans[0]
141+
test_span = spans[1]
142+
143+
assert_equals("test", test_span.data.sdk.name)
144+
assert_equals(test_span.t, db_span.t)
145+
assert_equals(db_span.p, test_span.s)
146+
147+
assert_equals(None, db_span.error)
148+
assert_equals(None, db_span.ec)
149+
150+
assert_equals(db_span.n, "mysql")
151+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
152+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
153+
assert_equals(db_span.data.mysql.stmt, 'INSERT INTO users(name, email) VALUES(%s, %s)')
154+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
155+
156+
def test_executemany(self):
157+
result = None
158+
with tracer.start_active_span('test'):
159+
result = self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)",
160+
[('beaker', '[email protected]'), ('beaker', '[email protected]')])
161+
self.db.commit()
162+
163+
assert_equals(2, result)
164+
165+
spans = self.recorder.queued_spans()
166+
assert_equals(2, len(spans))
167+
168+
db_span = spans[0]
169+
test_span = spans[1]
170+
171+
assert_equals("test", test_span.data.sdk.name)
172+
assert_equals(test_span.t, db_span.t)
173+
assert_equals(db_span.p, test_span.s)
174+
175+
assert_equals(None, db_span.error)
176+
assert_equals(None, db_span.ec)
177+
178+
assert_equals(db_span.n, "mysql")
179+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
180+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
181+
assert_equals(db_span.data.mysql.stmt, 'INSERT INTO users(name, email) VALUES(%s, %s)')
182+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
183+
184+
def test_call_proc(self):
185+
result = None
186+
with tracer.start_active_span('test'):
187+
result = self.cursor.callproc('test_proc', ('beaker',))
188+
189+
assert(result)
190+
191+
spans = self.recorder.queued_spans()
192+
assert_equals(2, len(spans))
193+
194+
db_span = spans[0]
195+
test_span = spans[1]
196+
197+
assert_equals("test", test_span.data.sdk.name)
198+
assert_equals(test_span.t, db_span.t)
199+
assert_equals(db_span.p, test_span.s)
200+
201+
assert_equals(None, db_span.error)
202+
assert_equals(None, db_span.ec)
203+
204+
assert_equals(db_span.n, "mysql")
205+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
206+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
207+
assert_equals(db_span.data.mysql.stmt, 'test_proc')
208+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
209+
210+
def test_error_capture(self):
211+
result = None
212+
span = None
213+
try:
214+
with tracer.start_active_span('test'):
215+
result = self.cursor.execute("""SELECT * from blah""")
216+
self.cursor.fetchone()
217+
except Exception:
218+
pass
219+
finally:
220+
if span:
221+
span.finish()
222+
223+
assert(result is None)
224+
225+
spans = self.recorder.queued_spans()
226+
assert_equals(2, len(spans))
227+
228+
db_span = spans[0]
229+
test_span = spans[1]
230+
231+
assert_equals("test", test_span.data.sdk.name)
232+
assert_equals(test_span.t, db_span.t)
233+
assert_equals(db_span.p, test_span.s)
234+
235+
assert_equals(True, db_span.error)
236+
assert_equals(1, db_span.ec)
237+
238+
if sys.version_info[0] >= 3:
239+
# Python 3
240+
assert_equals(db_span.data.mysql.error, u'(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db'])
241+
else:
242+
# Python 2
243+
assert_equals(db_span.data.mysql.error, u'(1146, u"Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db'])
244+
245+
assert_equals(db_span.n, "mysql")
246+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
247+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
248+
assert_equals(db_span.data.mysql.stmt, 'SELECT * from blah')
249+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])

0 commit comments

Comments
 (0)