Skip to content

Commit b6413ea

Browse files
authored
New Cassandra Instrumentation & Tests (#213)
* Cassandra Instrumentation & Tests * Add cassandra docker image to CircleCI * Make cassandra first to load * New KVs, tests, cleanup and optimizations * Attempted dedicated Cassandra run * CircleCI: Manually set Cassandra HEAP * CircleCI official cassandra (albeit outdated) * CircleCI: Increase the memory! * Add a dedicated cassandra job * Add Py27 dedicated cassandra run * Remove unnecessary couchbase steps from cassandra jobs * Cleanup and fix py27 install * Add deps for reqs.txt * Remove unused store_artifacts
1 parent 958a151 commit b6413ea

File tree

9 files changed

+477
-28
lines changed

9 files changed

+477
-28
lines changed

.circleci/config.yml

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ jobs:
5858
. venv/bin/activate
5959
python runtests.py
6060
61-
- store_artifacts:
62-
path: test-reports
63-
destination: test-reports
64-
6561
python35:
6662
docker:
6763
- image: circleci/python:3.5.6
@@ -114,10 +110,6 @@ jobs:
114110
. venv/bin/activate
115111
python runtests.py
116112
117-
- store_artifacts:
118-
path: test-reports
119-
destination: test-reports
120-
121113
python36:
122114
docker:
123115
- image: circleci/python:3.6.8
@@ -170,13 +162,104 @@ jobs:
170162
. venv/bin/activate
171163
python runtests.py
172164
173-
- store_artifacts:
174-
path: test-reports
175-
destination: test-reports
165+
py27cassandra:
166+
docker:
167+
- image: circleci/python:2.7.15
168+
- image: circleci/cassandra:3.10
169+
environment:
170+
MAX_HEAP_SIZE: 2048m
171+
HEAP_NEWSIZE: 512m
172+
173+
working_directory: ~/repo
174+
175+
steps:
176+
- checkout
177+
178+
# Download and cache dependencies
179+
- restore_cache:
180+
keys:
181+
- v1-dependencies-{{ checksum "requirements.txt" }}
182+
# fallback to using the latest cache if no exact match is found
183+
- v1-dependencies-
184+
185+
- run:
186+
name: install dependencies
187+
command: |
188+
sudo apt-get update
189+
sudo apt install lsb-release -y
190+
curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb
191+
sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb
192+
sudo apt-get update
193+
sudo apt install libcouchbase-dev -y
194+
rm -rf venv
195+
export PATH=/home/circleci/.local/bin:$PATH
196+
pip install --user -U pip setuptools virtualenv
197+
virtualenv --python=python2.7 --always-copy venv
198+
. venv/bin/activate
199+
pip install -U pip
200+
python setup.py install_egg_info
201+
pip install -r requirements-test.txt
202+
203+
- save_cache:
204+
paths:
205+
- ./venv
206+
key: v1-dependencies-{{ checksum "requirements.txt" }}
207+
208+
- run:
209+
name: run tests
210+
command: |
211+
. venv/bin/activate
212+
nosetests -v tests/test_cassandra-driver.py:TestCassandra
213+
214+
py36cassandra:
215+
docker:
216+
- image: circleci/python:3.6.8
217+
- image: circleci/cassandra:3.10
218+
environment:
219+
MAX_HEAP_SIZE: 2048m
220+
HEAP_NEWSIZE: 512m
221+
222+
working_directory: ~/repo
223+
224+
steps:
225+
- checkout
226+
227+
# Download and cache dependencies
228+
- restore_cache:
229+
keys:
230+
- v1-dependencies-{{ checksum "requirements.txt" }}
231+
# fallback to using the latest cache if no exact match is found
232+
- v1-dependencies-
233+
234+
- run:
235+
name: install dependencies
236+
command: |
237+
sudo apt-get update
238+
sudo apt install lsb-release -y
239+
python -m venv venv
240+
. venv/bin/activate
241+
pip install -U pip
242+
python setup.py install_egg_info
243+
pip install -r requirements.txt
244+
pip install -r requirements-test.txt
245+
246+
- save_cache:
247+
paths:
248+
- ./venv
249+
key: v1-dependencies-{{ checksum "requirements.txt" }}
250+
251+
- run:
252+
name: run tests
253+
command: |
254+
. venv/bin/activate
255+
nosetests -v tests/test_cassandra-driver.py:TestCassandra
256+
176257
workflows:
177258
version: 2
178259
build:
179260
jobs:
180261
- python27
181262
- python35
182263
- python36
264+
- py27cassandra
265+
- py36cassandra

instana/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def boot_agent():
6868
else:
6969
from .instrumentation import mysqlclient
7070

71+
from .instrumentation import cassandra_inst
7172
from .instrumentation import couchbase_inst
7273
from .instrumentation import flask
7374
from .instrumentation import grpcio
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""
2+
cassandra instrumentation
3+
https://docs.datastax.com/en/developer/python-driver/3.20/
4+
https://github.com/datastax/python-driver
5+
"""
6+
from __future__ import absolute_import
7+
8+
from distutils.version import LooseVersion
9+
import wrapt
10+
11+
from ..log import logger
12+
from ..singletons import tracer
13+
14+
try:
15+
import cassandra
16+
17+
consistency_levels = dict({0: "ANY",
18+
1: "ONE",
19+
2: "TWO",
20+
3: "THREE",
21+
4: "QUORUM",
22+
5: "ALL",
23+
6: "LOCAL_QUORUM",
24+
7: "EACH_QUORUM",
25+
8: "SERIAL",
26+
9: "LOCAL_SERIAL",
27+
10: "LOCAL_ONE"})
28+
29+
def collect_response(span, fn):
30+
tried_hosts = list()
31+
for host in fn.attempted_hosts:
32+
tried_hosts.append("%s:%d" % (host.endpoint.address, host.endpoint.port))
33+
34+
span.set_tag("cassandra.triedHosts", tried_hosts)
35+
span.set_tag("cassandra.coordHost", fn.coordinator_host)
36+
37+
cl = fn.query.consistency_level
38+
if cl and cl in consistency_levels:
39+
span.set_tag("cassandra.achievedConsistency", consistency_levels[cl])
40+
41+
42+
def cb_request_finish(results, span, fn):
43+
collect_response(span, fn)
44+
span.finish()
45+
46+
def cb_request_error(results, span, fn):
47+
collect_response(span, fn)
48+
49+
span.set_tag("error", True)
50+
ec = span.tags.get('ec', 0)
51+
span.set_tag("ec", ec + 1)
52+
span.set_tag("cassandra.error", results.message)
53+
span.finish()
54+
55+
def request_init_with_instana(fn):
56+
parent_span = tracer.active_span
57+
58+
if parent_span is not None:
59+
ctags = dict()
60+
if isinstance(fn.query, cassandra.query.SimpleStatement):
61+
ctags["cassandra.query"] = fn.query.query_string
62+
elif isinstance(fn.query, cassandra.query.BoundStatement):
63+
ctags["cassandra.query"] = fn.query.prepared_statement.query_string
64+
65+
ctags["cassandra.keyspace"] = fn.session.keyspace
66+
ctags["cassandra.cluster"] = fn.session.cluster.metadata.cluster_name
67+
68+
span = tracer.start_span(
69+
operation_name="cassandra",
70+
child_of=parent_span,
71+
tags=ctags)
72+
73+
fn.add_callback(cb_request_finish, span, fn)
74+
fn.add_errback(cb_request_error, span, fn)
75+
76+
@wrapt.patch_function_wrapper('cassandra.cluster', 'Session.__init__')
77+
def init_with_instana(wrapped, instance, args, kwargs):
78+
session = wrapped(*args, **kwargs)
79+
instance.add_request_init_listener(request_init_with_instana)
80+
return session
81+
82+
logger.debug("Instrumenting cassandra")
83+
84+
except ImportError:
85+
pass

instana/json_span.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,26 @@ class JsonSpan(BaseSpan):
2626
stack = None
2727

2828

29+
class CassandraData(BaseSpan):
30+
cluster = None
31+
query = None
32+
keyspace = None
33+
fetchSize = None
34+
achievedConsistency = None
35+
triedHosts = None
36+
fullyFetched = None
37+
error = None
38+
39+
2940
class CustomData(BaseSpan):
3041
tags = None
3142
logs = None
3243

3344

3445
class Data(BaseSpan):
3546
baggage = None
47+
cassandra = None
48+
couchbase = None
3649
custom = None
3750
http = None
3851
log = None

instana/recorder.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99

1010
import instana.singletons
1111

12-
from .json_span import (CouchbaseData, CustomData, Data, HttpData, JsonSpan, LogData, MySQLData, PostgresData,
13-
RabbitmqData, RedisData, RenderData, RPCData, SDKData, SoapData,
14-
SQLAlchemyData)
12+
from .json_span import (CassandraData, CouchbaseData, CustomData, Data, HttpData, JsonSpan, LogData,
13+
MySQLData, PostgresData, RabbitmqData, RedisData, RenderData,
14+
RPCData, SDKData, SoapData, SQLAlchemyData)
15+
1516
from .log import logger
1617
from .util import every
1718

@@ -23,15 +24,16 @@
2324

2425
class InstanaRecorder(SpanRecorder):
2526
THREAD_NAME = "Instana Span Reporting"
26-
registered_spans = ("aiohttp-client", "aiohttp-server", "couchbase", "django", "log", "memcache", "mysql",
27-
"postgres", "rabbitmq", "redis", "render", "rpc-client", "rpc-server", "sqlalchemy", "soap",
28-
"tornado-client", "tornado-server", "urllib3", "wsgi")
27+
registered_spans = ("aiohttp-client", "aiohttp-server", "cassandra", "couchbase", "django", "log",
28+
"memcache", "mysql", "postgres", "rabbitmq", "redis", "render", "rpc-client",
29+
"rpc-server", "sqlalchemy", "soap", "tornado-client", "tornado-server",
30+
"urllib3", "wsgi")
2931

3032
http_spans = ("aiohttp-client", "aiohttp-server", "django", "http", "soap", "tornado-client",
3133
"tornado-server", "urllib3", "wsgi")
3234

33-
exit_spans = ("aiohttp-client", "couchbase", "log", "memcache", "mysql", "postgres", "rabbitmq", "redis", "rpc-client",
34-
"sqlalchemy", "soap", "tornado-client", "urllib3")
35+
exit_spans = ("aiohttp-client", "cassandra", "couchbase", "log", "memcache", "mysql", "postgres",
36+
"rabbitmq", "redis", "rpc-client", "sqlalchemy", "soap", "tornado-client", "urllib3")
3537

3638
entry_spans = ("aiohttp-server", "django", "wsgi", "rabbitmq", "rpc-server", "tornado-server")
3739

@@ -164,22 +166,32 @@ def build_registered_span(self, span):
164166
if data.rabbitmq.sort == 'consume':
165167
kind = 1 # entry
166168

167-
if span.operation_name == "couchbase":
169+
elif span.operation_name == "cassandra":
170+
data.cassandra = CassandraData(cluster=span.tags.pop('cassandra.cluster', None),
171+
query=span.tags.pop('cassandra.query', None),
172+
keyspace=span.tags.pop('cassandra.keyspace', None),
173+
fetchSize=span.tags.pop('cassandra.fetchSize', None),
174+
achievedConsistency=span.tags.pop('cassandra.achievedConsistency', None),
175+
triedHosts=span.tags.pop('cassandra.triedHosts', None),
176+
fullyFetched=span.tags.pop('cassandra.fullyFetched', None),
177+
error=span.tags.pop('cassandra.error', None))
178+
179+
elif span.operation_name == "couchbase":
168180
data.couchbase = CouchbaseData(hostname=span.tags.pop('couchbase.hostname', None),
169181
bucket=span.tags.pop('couchbase.bucket', None),
170182
type=span.tags.pop('couchbase.type', None),
171183
error=span.tags.pop('couchbase.error', None),
172184
error_type=span.tags.pop('couchbase.error_type', None),
173185
sql=span.tags.pop('couchbase.sql', None))
174186

175-
if span.operation_name == "redis":
187+
elif span.operation_name == "redis":
176188
data.redis = RedisData(connection=span.tags.pop('connection', None),
177189
driver=span.tags.pop('driver', None),
178190
command=span.tags.pop('command', None),
179191
error=span.tags.pop('redis.error', None),
180192
subCommands=span.tags.pop('subCommands', None))
181193

182-
if span.operation_name == "rpc-client" or span.operation_name == "rpc-server":
194+
elif span.operation_name == "rpc-client" or span.operation_name == "rpc-server":
183195
data.rpc = RPCData(flavor=span.tags.pop('rpc.flavor', None),
184196
host=span.tags.pop('rpc.host', None),
185197
port=span.tags.pop('rpc.port', None),
@@ -189,22 +201,22 @@ def build_registered_span(self, span):
189201
baggage=span.tags.pop('rpc.baggage', None),
190202
error=span.tags.pop('rpc.error', None))
191203

192-
if span.operation_name == "render":
204+
elif span.operation_name == "render":
193205
data.render = RenderData(name=span.tags.pop('name', None),
194206
type=span.tags.pop('type', None))
195207
data.log = LogData(message=span.tags.pop('message', None),
196208
parameters=span.tags.pop('parameters', None))
197209

198-
if span.operation_name == "sqlalchemy":
210+
elif span.operation_name == "sqlalchemy":
199211
data.sqlalchemy = SQLAlchemyData(sql=span.tags.pop('sqlalchemy.sql', None),
200212
eng=span.tags.pop('sqlalchemy.eng', None),
201213
url=span.tags.pop('sqlalchemy.url', None),
202214
err=span.tags.pop('sqlalchemy.err', None))
203215

204-
if span.operation_name == "soap":
216+
elif span.operation_name == "soap":
205217
data.soap = SoapData(action=span.tags.pop('soap.action', None))
206218

207-
if span.operation_name == "mysql":
219+
elif span.operation_name == "mysql":
208220
data.mysql = MySQLData(host=span.tags.pop('host', None),
209221
db=span.tags.pop(ext.DATABASE_INSTANCE, None),
210222
user=span.tags.pop(ext.DATABASE_USER, None),
@@ -213,7 +225,7 @@ def build_registered_span(self, span):
213225
tskey = list(data.custom.logs.keys())[0]
214226
data.mysql.error = data.custom.logs[tskey]['message']
215227

216-
if span.operation_name == "postgres":
228+
elif span.operation_name == "postgres":
217229
data.pg = PostgresData(host=span.tags.pop('host', None),
218230
db=span.tags.pop(ext.DATABASE_INSTANCE, None),
219231
user=span.tags.pop(ext.DATABASE_USER, None),
@@ -223,7 +235,7 @@ def build_registered_span(self, span):
223235
tskey = list(data.custom.logs.keys())[0]
224236
data.pg.error = data.custom.logs[tskey]['message']
225237

226-
if span.operation_name == "log":
238+
elif span.operation_name == "log":
227239
data.log = {}
228240
# use last special key values
229241
# TODO - logic might need a tweak here

runtests.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
command_line = [__file__, '--verbose']
66

7+
# Cassandra tests are run in dedicated jobs on CircleCI and will
8+
# be run explicitly. (So always exclude them here)
9+
command_line.extend(['-e', 'cassandra'])
10+
711
if LooseVersion(sys.version) < LooseVersion('3.5.3'):
812
command_line.extend(['-e', 'asynqp', '-e', 'aiohttp',
913
'-e', 'async', '-e', 'tornado',

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def check_setuptools():
7070
'aiohttp>=3.5.4;python_version>="3.5"',
7171
'asynqp>=0.4;python_version>="3.5"',
7272
'couchbase==2.5.9',
73+
'cassandra-driver==3.20.2',
7374
'django>=1.11,<2.2',
7475
'nose>=1.0',
7576
'flask>=0.12.2',

0 commit comments

Comments
 (0)