Skip to content

Commit 240077a

Browse files
authored
MySQLdb Instrumentation (mysql-python) (#87)
* 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 * Fix .travis.yml syntax error * linter fixups; test touchups * Work-around for "command out of sync on Travis" (but not locally) * 2nd Work-around for "command out of sync on Travis" (but not locally) * 3rd Work-around for "command out of sync on Travis" (but not locally) * 4th Work-around for "command out of sync on Travis" (but not locally) * Break out test proc removal * Use env specific vars depending on where tests run * Use env var db name * Linter fixups; fix wrapped calls * Linter fixups; fix error count check * Update to use mysql span KVs
1 parent a55897c commit 240077a

File tree

13 files changed

+489
-81
lines changed

13 files changed

+489
-81
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='mysql')
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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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('host', "%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+
def execute(self, sql, params=None):
35+
try:
36+
span = None
37+
context = internal_tracer.current_context()
38+
39+
# If we're not tracing, just return
40+
if context is None:
41+
return self.__wrapped__.execute(sql, params)
42+
43+
span = internal_tracer.start_span(self._module_name, child_of=context)
44+
span = self._collect_kvs(span, sql)
45+
46+
result = self.__wrapped__.execute(sql, params)
47+
except Exception as e:
48+
if span:
49+
span.log_exception(e)
50+
raise
51+
else:
52+
return result
53+
finally:
54+
if span:
55+
span.finish()
56+
57+
def executemany(self, sql, seq_of_parameters):
58+
try:
59+
span = None
60+
context = internal_tracer.current_context()
61+
62+
# If we're not tracing, just return
63+
if context is None:
64+
return self.__wrapped__.executemany(sql, seq_of_parameters)
65+
66+
span = internal_tracer.start_span(self._module_name, child_of=context)
67+
span = self._collect_kvs(span, sql)
68+
69+
result = self.__wrapped__.executemany(sql, seq_of_parameters)
70+
except Exception as e:
71+
if span:
72+
span.log_exception(e)
73+
raise
74+
else:
75+
return result
76+
finally:
77+
if span:
78+
span.finish()
79+
80+
def callproc(self, proc_name, params):
81+
try:
82+
span = None
83+
context = internal_tracer.current_context()
84+
85+
# If we're not tracing, just return
86+
if context is None:
87+
return self.__wrapped__.execute(proc_name, params)
88+
89+
span = internal_tracer.start_span(self._module_name, child_of=context)
90+
span = self._collect_kvs(span, proc_name)
91+
92+
result = self.__wrapped__.callproc(proc_name, params)
93+
except Exception as e:
94+
if span:
95+
span.log_exception(e)
96+
raise
97+
else:
98+
return result
99+
finally:
100+
if span:
101+
span.finish()
102+
103+
104+
class ConnectionWrapper(wrapt.ObjectProxy):
105+
__slots__ = ('_module_name', '_connect_params')
106+
107+
def __init__(self, connection, module_name, connect_params):
108+
super(ConnectionWrapper, self).__init__(wrapped=connection)
109+
self._module_name = module_name
110+
self._connect_params = connect_params
111+
112+
def cursor(self, *args, **kwargs):
113+
return CursorWrapper(
114+
cursor=self.__wrapped__.cursor(*args, **kwargs),
115+
module_name=self._module_name,
116+
connect_params=self._connect_params,
117+
cursor_params=(args, kwargs) if args or kwargs else None)
118+
119+
def begin(self):
120+
return self.__wrapped__.begin()
121+
122+
def commit(self):
123+
return self.__wrapped__.commit()
124+
125+
def rollback(self):
126+
return self.__wrapped__.rollback()
127+
128+
129+
class ConnectionFactory(object):
130+
def __init__(self, connect_func, module_name):
131+
self._connect_func = connect_func
132+
self._module_name = module_name
133+
self._wrapper_ctor = ConnectionWrapper
134+
135+
def __call__(self, *args, **kwargs):
136+
connect_params = (args, kwargs) if args or kwargs else None
137+
138+
return self._wrapper_ctor(
139+
connection=self._connect_func(*args, **kwargs),
140+
module_name=self._module_name,
141+
connect_params=connect_params)

instana/json_span.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ def __init__(self, **kwds):
2828
self.__dict__.update(kwds)
2929

3030

31+
class MySQLData(object):
32+
db = None
33+
host = None
34+
user = None
35+
stmt = None
36+
error = None
37+
38+
def __init__(self, **kwds):
39+
self.__dict__.update(kwds)
40+
41+
3142
class HttpData(object):
3243
host = None
3344
url = None
@@ -37,6 +48,7 @@ class HttpData(object):
3748
def __init__(self, **kwds):
3849
self.__dict__.update(kwds)
3950

51+
4052
class SoapData(object):
4153
action = None
4254

instana/recorder.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import instana
1111

1212
from .agent_const import AGENT_TRACES_URL
13-
from .json_span import CustomData, Data, HttpData, JsonSpan, SDKData, SoapData
13+
from .json_span import CustomData, Data, HttpData, JsonSpan, MySQLData, SDKData, SoapData
1414

1515
if sys.version_info.major is 2:
1616
import Queue as queue
@@ -20,8 +20,9 @@
2020

2121
class InstanaRecorder(SpanRecorder):
2222
sensor = None
23-
registered_spans = ("django", "memcache", "rpc-client",
23+
registered_spans = ("django", "memcache", "mysql", "rpc-client",
2424
"rpc-server", "soap", "urllib3", "wsgi")
25+
http_spans = ("django", "wsgi", "urllib3", "soap")
2526
entry_kind = ["entry", "server", "consumer"]
2627
exit_kind = ["exit", "client", "producer"]
2728
queue = queue.Queue()
@@ -83,14 +84,27 @@ def record_span(self, span):
8384

8485
def build_registered_span(self, span):
8586
""" Takes a BasicSpan and converts it into a registered JsonSpan """
86-
data = Data(http=HttpData(host=self.get_host_name(span),
87-
url=self.get_string_tag(span, ext.HTTP_URL),
88-
method=self.get_string_tag(span, ext.HTTP_METHOD),
89-
status=self.get_tag(span, ext.HTTP_STATUS_CODE)),
90-
soap=SoapData(action=self.get_tag(span, 'soap.action')),
91-
baggage=span.context.baggage,
87+
data = Data(baggage=span.context.baggage,
9288
custom=CustomData(tags=span.tags,
93-
logs=self.collect_logs(span)))
89+
logs=self.collect_logs(span)))
90+
91+
if span.operation_name in self.http_spans:
92+
data.http = HttpData(host=self.get_host_name(span),
93+
url=self.get_string_tag(span, ext.HTTP_URL),
94+
method=self.get_string_tag(span, ext.HTTP_METHOD),
95+
status=self.get_tag(span, ext.HTTP_STATUS_CODE))
96+
97+
if span.operation_name == "soap":
98+
data.soap = SoapData(action=self.get_tag(span, 'soap.action'))
99+
100+
if span.operation_name == "mysql":
101+
data.mysql = MySQLData(host=self.get_tag(span, 'host'),
102+
db=self.get_tag(span, ext.DATABASE_INSTANCE),
103+
user=self.get_tag(span, ext.DATABASE_USER),
104+
stmt=self.get_tag(span, ext.DATABASE_STATEMENT))
105+
if len(data.custom.logs.keys()):
106+
tskey = list(data.custom.logs.keys())[0]
107+
data.mysql.error = data.custom.logs[tskey]['message']
94108

95109
entityFrom = {'e': self.sensor.agent.from_.pid,
96110
'h': self.sensor.agent.from_.agentUuid}
@@ -103,7 +117,7 @@ def build_registered_span(self, span):
103117
ts=int(round(span.start_time * 1000)),
104118
d=int(round(span.duration * 1000)),
105119
f=entityFrom,
106-
ec=self.get_tag(span, "ec"),
120+
ec=self.get_tag(span, "ec", 0),
107121
error=self.get_tag(span, "error"),
108122
data=data)
109123

@@ -129,15 +143,15 @@ def build_sdk_span(self, span):
129143
d=int(round(span.duration * 1000)),
130144
n="sdk",
131145
f=entityFrom,
132-
# ec=self.get_tag(span, "ec"),
133-
# error=self.get_tag(span, "error"),
146+
ec=self.get_tag(span, "ec"),
147+
error=self.get_tag(span, "error"),
134148
data=data)
135149

136-
def get_tag(self, span, tag):
150+
def get_tag(self, span, tag, default=None):
137151
if tag in span.tags:
138152
return span.tags[tag]
139153

140-
return None
154+
return default
141155

142156
def get_string_tag(self, span, tag):
143157
ret = self.get_tag(span, tag)

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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@
1818
entry_points={
1919
'instana': ['string = instana:load'],
2020
'flask': ['flask = instana.flaskana:hook'],
21-
'runtime': ['string = instana:load'], # deprecated: use same as 'instana'
22-
'django': ['string = instana:load'], # deprecated: use same as 'instana'
23-
'django19': ['string = instana:load'], # deprecated: use same as 'instana'
21+
'runtime': ['string = instana:load'], # deprecated: use same as 'instana'
22+
'django': ['string = instana:load'], # deprecated: use same as 'instana'
23+
'django19': ['string = instana:load'], # deprecated: use same as 'instana'
2424
},
2525
extras_require={
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)