Skip to content

Commit 9424b6a

Browse files
authored
MySQLdb Instrumentation (mysql-python) (#84)
* Fix background server names. * More attempts to quiet spyne background server * Initial MySQLdb instrumentation and basic tests. * Add Mysql-python to test bundle; fix python 3 test runs * Fix Python version test * Update import to use new locations * Make sure pip is updated before package installation * Instrumentation safeties; executemany impl * Centralize KV collection; callproc support * Upgrade setuptools for python 3.3 issue * For Travis, use a blank password * Create mysql db before test run * formatting * formatting * Trigger Travis build * Trigger Travis build * Trigger Travis build * Make sure message has a message * Add test to validate error capture/logging
1 parent 84a94d4 commit 9424b6a

File tree

9 files changed

+450
-16
lines changed

9 files changed

+450
-16
lines changed

.travis.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
language: python
2+
23
python:
34
- "2.7"
45
- "3.3"
56
- "3.4"
67
- "3.5"
78
- "3.6"
9+
10+
before_install:
11+
- "pip install --upgrade pip"
12+
- "pip install --upgrade setuptools"
13+
- 'mysql -e 'CREATE DATABASE travis_ci_test;'
14+
815
install: "pip install -r requirements-test.txt"
16+
17+
918
script: nosetests -v

instana/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ def load(module):
5656
# noqa: ignore=W0611
5757
from .instrumentation import urllib3 # noqa
5858
from .instrumentation import sudsjurko # noqa
59+
from .instrumentation import mysqlpython # noqa
5960
from .instrumentation.django import middleware # noqa
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 MySQLdb # noqa
8+
9+
cf = ConnectionFactory(connect_func=MySQLdb.connect, module_name='MySQLdb')
10+
11+
setattr(MySQLdb, 'connect', cf)
12+
if hasattr(MySQLdb, 'Connect'):
13+
setattr(MySQLdb, 'Connect', cf)
14+
15+
logger.debug("Instrumenting mysql-python")
16+
except ImportError:
17+
pass

instana/instrumentation/pep0249.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# This is a wrapper for PEP-0249: Python Database API Specification v2.0
2+
import opentracing.ext.tags as ext
3+
import wrapt
4+
5+
from ..tracer import internal_tracer
6+
from ..log import logger
7+
8+
9+
class CursorWrapper(wrapt.ObjectProxy):
10+
__slots__ = ('_module_name', '_connect_params', '_cursor_params')
11+
12+
def __init__(self, cursor, module_name,
13+
connect_params=None, cursor_params=None):
14+
super(CursorWrapper, self).__init__(wrapped=cursor)
15+
self._module_name = module_name
16+
self._connect_params = connect_params
17+
self._cursor_params = cursor_params
18+
19+
def _collect_kvs(self, span, sql):
20+
try:
21+
span.set_tag(ext.SPAN_KIND, 'exit')
22+
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_TYPE, 'mysql')
25+
span.set_tag(ext.DATABASE_USER, self._connect_params[1]['user'])
26+
span.set_tag(ext.PEER_ADDRESS, "mysql://%s:%s" %
27+
(self._connect_params[1]['host'],
28+
self._connect_params[1]['port']))
29+
except Exception as e:
30+
logger.debug(e)
31+
finally:
32+
return span
33+
34+
35+
def execute(self, sql, params=None):
36+
try:
37+
span = None
38+
context = internal_tracer.current_context()
39+
40+
# If we're not tracing, just return
41+
if context is None:
42+
return self.__wrapped__.execute(sql, params)
43+
44+
span = internal_tracer.start_span(self._module_name, child_of=context)
45+
span = self._collect_kvs(span, sql)
46+
span.set_tag('op', 'execute')
47+
48+
result = self.__wrapped__.execute(sql, params)
49+
except Exception as e:
50+
if span:
51+
span.log_exception(e)
52+
raise
53+
else:
54+
return result
55+
finally:
56+
if span:
57+
span.finish()
58+
59+
def executemany(self, sql, seq_of_parameters):
60+
try:
61+
span = None
62+
context = internal_tracer.current_context()
63+
64+
# If we're not tracing, just return
65+
if context is None:
66+
return self.__wrapped__.execute(sql, params)
67+
68+
span = internal_tracer.start_span(self._module_name, child_of=context)
69+
span = self._collect_kvs(span, sql)
70+
span.set_tag('op', 'executemany')
71+
span.set_tag('count', len(seq_of_parameters))
72+
73+
result = self.__wrapped__.executemany(sql, seq_of_parameters)
74+
except Exception as e:
75+
if span:
76+
span.log_exception(e)
77+
raise
78+
else:
79+
return result
80+
finally:
81+
if span:
82+
span.finish()
83+
84+
85+
def callproc(self, proc_name, params):
86+
try:
87+
span = None
88+
context = internal_tracer.current_context()
89+
90+
# If we're not tracing, just return
91+
if context is None:
92+
return self.__wrapped__.execute(sql, params)
93+
94+
span = internal_tracer.start_span(self._module_name, child_of=context)
95+
span = self._collect_kvs(span, proc_name)
96+
span.set_tag('op', 'callproc')
97+
98+
result = self.__wrapped__.callproc(proc_name, params)
99+
except Exception as e:
100+
if span:
101+
span.log_exception(e)
102+
raise
103+
else:
104+
return result
105+
finally:
106+
if span:
107+
span.finish()
108+
109+
110+
class ConnectionWrapper(wrapt.ObjectProxy):
111+
__slots__ = ('_module_name', '_connect_params')
112+
113+
def __init__(self, connection, module_name, connect_params):
114+
super(ConnectionWrapper, self).__init__(wrapped=connection)
115+
self._module_name = module_name
116+
self._connect_params = connect_params
117+
118+
def cursor(self, *args, **kwargs):
119+
return CursorWrapper(
120+
cursor=self.__wrapped__.cursor(*args, **kwargs),
121+
module_name=self._module_name,
122+
connect_params=self._connect_params,
123+
cursor_params=(args, kwargs) if args or kwargs else None)
124+
125+
def begin(self):
126+
return self.__wrapped__.begin()
127+
128+
def commit(self):
129+
return self.__wrapped__.commit()
130+
131+
def rollback(self):
132+
return self.__wrapped__.rollback()
133+
134+
135+
class ConnectionFactory(object):
136+
def __init__(self, connect_func, module_name):
137+
self._connect_func = connect_func
138+
self._module_name = module_name
139+
self._wrapper_ctor = ConnectionWrapper
140+
141+
def __call__(self, *args, **kwargs):
142+
connect_params = (args, kwargs) if args or kwargs else None
143+
144+
return self._wrapper_ctor(
145+
connection=self._connect_func(*args, **kwargs),
146+
module_name=self._module_name,
147+
connect_params=connect_params)

instana/span.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def finish(self, finish_time=None):
1616
super(InstanaSpan, self).finish(finish_time)
1717

1818
def log_exception(self, e):
19-
if hasattr(e, 'message'):
19+
if hasattr(e, 'message') and len(e.message):
2020
self.log_kv({'message': e.message})
2121
elif hasattr(e, '__str__'):
2222
self.log_kv({'message': e.__str__()})

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@
2626
'test': [
2727
'nose>=1.0',
2828
'flask>=0.12.2',
29+
'lxml>=3.4',
30+
'MySQL-python>=1.2.5;python_version<="2.7"',
2931
'requests>=2.17.1',
3032
'urllib3[secure]>=1.15',
3133
'spyne>=2.9',
32-
'lxml>=3.4',
3334
'suds-jurko>=0.6'
3435
],
3536
},

tests/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,23 @@
1313
# Spawn our background Flask app that the tests will throw
1414
# requests at. Don't continue until the test app is fully
1515
# up and running.
16-
timer = threading.Thread(target=flaskalino.run)
17-
timer.daemon = True
18-
timer.name = "Background Flask app"
16+
flask = threading.Thread(target=flaskalino.run)
17+
flask.daemon = True
18+
flask.name = "Background Flask app"
1919
print("Starting background Flask app...")
20-
timer.start()
20+
flask.start()
2121

2222

2323
# Background Soap Server
2424
#
2525
# Spawn our background Flask app that the tests will throw
2626
# requests at. Don't continue until the test app is fully
2727
# up and running.
28-
timer = threading.Thread(target=soapserver.serve_forever)
29-
timer.daemon = True
30-
timer.name = "Background Soap server"
28+
soap = threading.Thread(target=soapserver.serve_forever)
29+
soap.daemon = True
30+
soap.name = "Background Soap server"
3131
print("Starting background Soap server...")
32-
timer.start()
32+
soap.start()
3333

3434

3535
time.sleep(1)

tests/apps/soapserver4132.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,21 @@ def client_fault(ctx):
4141

4242

4343

44+
# logging.basicConfig(level=logging.WARN)
45+
logging.getLogger('suds').setLevel(logging.WARN)
46+
logging.getLogger('suds.resolver').setLevel(logging.WARN)
47+
logging.getLogger('spyne.protocol.xml').setLevel(logging.WARN)
48+
logging.getLogger('spyne.model.complex').setLevel(logging.WARN)
49+
logging.getLogger('spyne.interface._base').setLevel(logging.WARN)
50+
logging.getLogger('spyne.interface.xml').setLevel(logging.WARN)
51+
logging.getLogger('spyne.util.appreg').setLevel(logging.WARN)
52+
4453
app = Application([StanSoapService], 'instana.tests.app.ask_question',
4554
in_protocol=Soap11(validator='lxml'), out_protocol=Soap11())
4655

4756
# Use Instana middleware so we can test context passing and Soap server traces.
4857
wsgi_app = iWSGIMiddleware(WsgiApplication(app))
4958
soapserver = make_server('127.0.0.1', 4132, wsgi_app)
5059

51-
logging.basicConfig(level=logging.WARN)
52-
logging.getLogger('suds').setLevel(logging.WARN)
53-
logging.getLogger('suds.resolver').setLevel(logging.WARN)
54-
logging.getLogger('spyne.protocol.xml').setLevel(logging.WARN)
55-
logging.getLogger('spyne.model.complex').setLevel(logging.WARN)
56-
5760
if __name__ == '__main__':
5861
soapserver.serve_forever()

0 commit comments

Comments
 (0)