Skip to content

Commit a1b631c

Browse files
authored
SQLAlchemy Instrumentation (#102)
* Initial instrumentation, tests and infra. * Centralize test env configuration * Fix whitespace * SQLAlchemy KVs, span conversion * Moar tests; Add error logging * Add test for transactions * Remove auth from connect strings
1 parent 16f1437 commit a1b631c

File tree

8 files changed

+368
-68
lines changed

8 files changed

+368
-68
lines changed

instana/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ def load_instrumentation():
6060
if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ:
6161
# Import & initialize instrumentation
6262
from .instrumentation import asynqp # noqa
63-
from .instrumentation import urllib3 # noqa
64-
from .instrumentation import sudsjurko # noqa
6563
from .instrumentation import mysqlpython # noqa
64+
from .instrumentation import sqlalchemy # noqa
65+
from .instrumentation import sudsjurko # noqa
66+
from .instrumentation import urllib3 # noqa
6667
from .instrumentation.django import middleware # noqa
6768

6869

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import absolute_import
2+
3+
import opentracing
4+
import opentracing.ext.tags as ext
5+
import wrapt
6+
import re
7+
8+
from ..log import logger
9+
from ..singletons import tracer
10+
11+
try:
12+
import sqlalchemy
13+
from sqlalchemy import event
14+
from sqlalchemy.engine import Engine
15+
16+
url_regexp = re.compile('\/\/(\S+@)')
17+
18+
@event.listens_for(Engine, 'before_cursor_execute', named=True)
19+
def receive_before_cursor_execute(**kw):
20+
try:
21+
parent_span = tracer.active_span
22+
23+
# If we're not tracing, just return
24+
if parent_span is None:
25+
return
26+
27+
scope = tracer.start_active_span("sqlalchemy", child_of=parent_span)
28+
context = kw['context']
29+
context._stan_scope = scope
30+
31+
conn = kw['conn']
32+
url = str(conn.engine.url)
33+
scope.span.set_tag('sqlalchemy.sql', kw['statement'])
34+
scope.span.set_tag('sqlalchemy.eng', conn.engine.name)
35+
scope.span.set_tag('sqlalchemy.url', url_regexp.sub('//', url))
36+
except Exception as e:
37+
logger.debug(e)
38+
finally:
39+
return
40+
41+
@event.listens_for(Engine, 'after_cursor_execute', named=True)
42+
def receive_after_cursor_execute(**kw):
43+
context = kw['context']
44+
45+
if context is not None and hasattr(context, '_stan_scope'):
46+
this_scope = context._stan_scope
47+
if this_scope is not None:
48+
this_scope.close()
49+
50+
@event.listens_for(Engine, 'dbapi_error', named=True)
51+
def receive_dbapi_error(**kw):
52+
context = kw['context']
53+
54+
if context is not None and hasattr(context, '_stan_scope'):
55+
this_scope = context._stan_scope
56+
if this_scope is not None:
57+
this_scope.span.set_tag("error", True)
58+
ec = this_scope.span.tags.get('ec', 0)
59+
this_scope.span.set_tag("ec", ec+1)
60+
61+
if 'exception' in kw:
62+
e = kw['exception']
63+
this_scope.span.set_tag('sqlalchemy.err', str(e))
64+
else:
65+
this_scope.span.set_tag('sqlalchemy.err', "No dbapi error specified.")
66+
this_scope.close()
67+
68+
69+
logger.debug("Instrumenting sqlalchemy")
70+
except ImportError:
71+
pass

instana/json_span.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ def __init__(self, **kwds):
2626

2727

2828
class Data(object):
29-
service = None
30-
http = None
3129
baggage = None
3230
custom = None
31+
http = None
32+
rabbitmq = None
3333
sdk = None
34+
service = None
35+
sqlalchemy = None
3436
soap = None
35-
rabbitmq = None
3637

3738
def __init__(self, **kwds):
3839
self.__dict__.update(kwds)
@@ -70,6 +71,15 @@ class RabbitmqData(object):
7071
def __init__(self, **kwds):
7172
self.__dict__.update(kwds)
7273

74+
class SQLAlchemyData(object):
75+
sql = None
76+
url = None
77+
eng = None
78+
error = None
79+
80+
def __init__(self, **kwds):
81+
self.__dict__.update(kwds)
82+
7383

7484
class SoapData(object):
7585
action = None

instana/recorder.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import instana.singletons
1313

1414
from .json_span import (CustomData, Data, HttpData, JsonSpan, MySQLData,
15-
RabbitmqData, SDKData, SoapData)
15+
RabbitmqData, SDKData, SoapData, SQLAlchemyData)
1616
from .log import logger
1717

1818
if sys.version_info.major is 2:
@@ -23,10 +23,11 @@
2323

2424
class InstanaRecorder(SpanRecorder):
2525
registered_spans = ("django", "memcache", "mysql", "rabbitmq", "rpc-client",
26-
"rpc-server", "soap", "urllib3", "wsgi")
26+
"rpc-server", "sqlalchemy", "soap", "urllib3", "wsgi")
2727
http_spans = ("django", "wsgi", "urllib3", "soap")
2828

29-
exit_spans = ("memcache", "mysql", "rabbitmq", "rpc-client", "soap", "urllib3")
29+
exit_spans = ("memcache", "mysql", "rabbitmq", "rpc-client", "sqlalchemy",
30+
"soap", "urllib3")
3031
entry_spans = ("django", "wsgi", "rabbitmq", "rpc-server")
3132

3233
entry_kind = ["entry", "server", "consumer"]
@@ -113,6 +114,13 @@ def build_registered_span(self, span):
113114
address=span.tags.pop('address', None),
114115
key=span.tags.pop('key', None))
115116

117+
if span.operation_name == "sqlalchemy":
118+
data.sqlalchemy = SQLAlchemyData(sql=span.tags.pop('sqlalchemy.sql', None),
119+
eng=span.tags.pop('sqlalchemy.eng', None),
120+
url=span.tags.pop('sqlalchemy.url', None),
121+
err=span.tags.pop('sqlalchemy.err', None))
122+
123+
116124
if span.operation_name == "soap":
117125
data.soap = SoapData(action=span.tags.pop('soap.action', None))
118126

@@ -125,11 +133,6 @@ def build_registered_span(self, span):
125133
tskey = list(data.custom.logs.keys())[0]
126134
data.mysql.error = data.custom.logs[tskey]['message']
127135

128-
if len(span.tags) > 0:
129-
if data.custom is None:
130-
data.custom = CustomData()
131-
data.custom.tags = span.tags
132-
133136
entityFrom = {'e': instana.singletons.agent.from_.pid,
134137
'h': instana.singletons.agent.from_.agentUuid}
135138

@@ -152,6 +155,11 @@ def build_registered_span(self, span):
152155
json_span.error = error
153156
json_span.ec = ec
154157

158+
if len(span.tags) > 0:
159+
if data.custom is None:
160+
data.custom = CustomData()
161+
data.custom.tags = span.tags
162+
155163
return json_span
156164

157165
def build_sdk_span(self, span):

setup.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@ def check_setuptools():
5454
'lxml>=3.4',
5555
'mock>=2.0.0',
5656
'MySQL-python>=1.2.5;python_version<="2.7"',
57+
'psycopg2>=2.7.1',
5758
'pyOpenSSL>=16.1.0;python_version<="2.7"',
5859
'pytest>=3.0.1',
5960
'requests>=2.17.1',
60-
'urllib3[secure]>=1.15',
61+
'sqlalchemy>=1.1.15',
6162
'spyne>=2.9',
62-
'suds-jurko>=0.6'
63+
'suds-jurko>=0.6',
64+
'urllib3[secure]>=1.15'
6365
],
6466
},
6567
test_suite='nose.collector',

tests/helpers.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
3+
testenv = {}
4+
5+
"""
6+
MySQL Environment
7+
"""
8+
if 'MYSQL_HOST' in os.environ:
9+
testenv['mysql_host']= os.environ['MYSQL_HOST']
10+
elif 'TRAVIS_MYSQL_HOST' in os.environ:
11+
testenv['mysql_host'] = os.environ['TRAVIS_MYSQL_HOST']
12+
else:
13+
testenv['mysql_host'] = '127.0.0.1'
14+
15+
testenv['mysql_port'] = int(os.environ.get('MYSQL_PORT', '3306'))
16+
testenv['mysql_db'] = os.environ.get('MYSQL_DB', 'travis_ci_test')
17+
testenv['mysql_user'] = os.environ.get('MYSQL_USER', 'root')
18+
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'] = ''
25+
26+
"""
27+
PostgreSQL Environment
28+
"""
29+
if 'POSTGRESQL_HOST' in os.environ:
30+
testenv['postgresql_host']= os.environ['POSTGRESQL_HOST']
31+
elif 'TRAVIS_POSTGRESQL_HOST' in os.environ:
32+
testenv['postgresql_host'] = os.environ['TRAVIS_POSTGRESQL_HOST']
33+
else:
34+
testenv['postgresql_host'] = '127.0.0.1'
35+
36+
testenv['postgresql_port'] = int(os.environ.get('POSTGRESQL_PORT', '3306'))
37+
testenv['postgresql_db'] = os.environ.get('POSTGRESQL_DB', 'travis_ci_test')
38+
testenv['postgresql_user'] = os.environ.get('POSTGRESQL_USER', 'root')
39+
40+
if 'POSTGRESQL_PW' in os.environ:
41+
testenv['postgresql_pw'] = os.environ['POSTGRESQL_PW']
42+
elif 'TRAVIS_POSTGRESQL_PASS' in os.environ:
43+
testenv['postgresql_pw'] = os.environ['TRAVIS_POSTGRESQL_PASS']
44+
else:
45+
testenv['postgresql_pw'] = ''

tests/test_mysql-python.py

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from instana.singletons import tracer
1111

12+
from .helpers import testenv
13+
1214
if sys.version_info < (3, 0):
1315
import MySQLdb
1416
else:
@@ -17,36 +19,6 @@
1719

1820
logger = logging.getLogger(__name__)
1921

20-
21-
if 'MYSQL_HOST' in os.environ:
22-
mysql_host = os.environ['MYSQL_HOST']
23-
elif 'TRAVIS_MYSQL_HOST' in os.environ:
24-
mysql_host = os.environ['TRAVIS_MYSQL_HOST']
25-
else:
26-
mysql_host = '127.0.0.1'
27-
28-
if 'MYSQL_PORT' in os.environ:
29-
mysql_port = int(os.environ['MYSQL_PORT'])
30-
else:
31-
mysql_port = 3306
32-
33-
if 'MYSQL_DB' in os.environ:
34-
mysql_db = os.environ['MYSQL_DB']
35-
else:
36-
mysql_db = "travis_ci_test"
37-
38-
if 'MYSQL_USER' in os.environ:
39-
mysql_user = os.environ['MYSQL_USER']
40-
else:
41-
mysql_user = "root"
42-
43-
if 'MYSQL_PW' in os.environ:
44-
mysql_pw = os.environ['MYSQL_PW']
45-
elif 'TRAVIS_MYSQL_PASS' in os.environ:
46-
mysql_pw = os.environ['TRAVIS_MYSQL_PASS']
47-
else:
48-
mysql_pw = ''
49-
5022
create_table_query = 'CREATE TABLE IF NOT EXISTS users(id serial primary key, \
5123
name varchar(40) NOT NULL, email varchar(40) NOT NULL)'
5224

@@ -57,9 +29,9 @@
5729
END
5830
"""
5931

60-
db = MySQLdb.connect(host=mysql_host, port=mysql_port,
61-
user=mysql_user, passwd=mysql_pw,
62-
db=mysql_db)
32+
db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'],
33+
user=testenv['mysql_user'], passwd=testenv['mysql_pw'],
34+
db=testenv['mysql_db'])
6335

6436
cursor = db.cursor()
6537
cursor.execute(create_table_query)
@@ -83,10 +55,10 @@
8355

8456
class TestMySQLPython:
8557
def setUp(self):
86-
logger.warn("MySQL connecting: %s:<pass>@%s:3306/%s", mysql_user, mysql_host, mysql_db)
87-
self.db = MySQLdb.connect(host=mysql_host, port=mysql_port,
88-
user=mysql_user, passwd=mysql_pw,
89-
db=mysql_db)
58+
logger.warn("MySQL connecting: %s:<pass>@%s:3306/%s", testenv['mysql_user'], testenv['mysql_host'], testenv['mysql_db'])
59+
self.db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'],
60+
user=testenv['mysql_user'], passwd=testenv['mysql_pw'],
61+
db=testenv['mysql_db'])
9062
self.cursor = self.db.cursor()
9163
self.recorder = tracer.recorder
9264
self.recorder.clear_spans()
@@ -126,10 +98,10 @@ def test_basic_query(self):
12698
assert_equals(None, db_span.ec)
12799

128100
assert_equals(db_span.n, "mysql")
129-
assert_equals(db_span.data.mysql.db, mysql_db)
130-
assert_equals(db_span.data.mysql.user, mysql_user)
101+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
102+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
131103
assert_equals(db_span.data.mysql.stmt, 'SELECT * from users')
132-
assert_equals(db_span.data.mysql.host, "%s:3306" % mysql_host)
104+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
133105

134106
def test_basic_insert(self):
135107
result = None
@@ -154,10 +126,10 @@ def test_basic_insert(self):
154126
assert_equals(None, db_span.ec)
155127

156128
assert_equals(db_span.n, "mysql")
157-
assert_equals(db_span.data.mysql.db, mysql_db)
158-
assert_equals(db_span.data.mysql.user, mysql_user)
129+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
130+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
159131
assert_equals(db_span.data.mysql.stmt, 'INSERT INTO users(name, email) VALUES(%s, %s)')
160-
assert_equals(db_span.data.mysql.host, "%s:3306" % mysql_host)
132+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
161133

162134
def test_executemany(self):
163135
result = None
@@ -182,10 +154,10 @@ def test_executemany(self):
182154
assert_equals(None, db_span.ec)
183155

184156
assert_equals(db_span.n, "mysql")
185-
assert_equals(db_span.data.mysql.db, mysql_db)
186-
assert_equals(db_span.data.mysql.user, mysql_user)
157+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
158+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
187159
assert_equals(db_span.data.mysql.stmt, 'INSERT INTO users(name, email) VALUES(%s, %s)')
188-
assert_equals(db_span.data.mysql.host, "%s:3306" % mysql_host)
160+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
189161

190162
def test_call_proc(self):
191163
result = None
@@ -208,10 +180,10 @@ def test_call_proc(self):
208180
assert_equals(None, db_span.ec)
209181

210182
assert_equals(db_span.n, "mysql")
211-
assert_equals(db_span.data.mysql.db, mysql_db)
212-
assert_equals(db_span.data.mysql.user, mysql_user)
183+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
184+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
213185
assert_equals(db_span.data.mysql.stmt, 'test_proc')
214-
assert_equals(db_span.data.mysql.host, "%s:3306" % mysql_host)
186+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])
215187

216188
def test_error_capture(self):
217189
result = None
@@ -240,10 +212,10 @@ def test_error_capture(self):
240212

241213
assert_equals(True, db_span.error)
242214
assert_equals(1, db_span.ec)
243-
assert_equals(db_span.data.mysql.error, '(1146, "Table \'%s.blah\' doesn\'t exist")' % mysql_db)
215+
assert_equals(db_span.data.mysql.error, '(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db'])
244216

245217
assert_equals(db_span.n, "mysql")
246-
assert_equals(db_span.data.mysql.db, mysql_db)
247-
assert_equals(db_span.data.mysql.user, mysql_user)
218+
assert_equals(db_span.data.mysql.db, testenv['mysql_db'])
219+
assert_equals(db_span.data.mysql.user, testenv['mysql_user'])
248220
assert_equals(db_span.data.mysql.stmt, 'SELECT * from blah')
249-
assert_equals(db_span.data.mysql.host, "%s:3306" % mysql_host)
221+
assert_equals(db_span.data.mysql.host, "%s:3306" % testenv['mysql_host'])

0 commit comments

Comments
 (0)