Skip to content

Commit 04e8e6a

Browse files
authored
New couchbase instrumentation (#201)
* Couchbase support systems. * Initial couchbase instrumentation * Instrument all the ops & many more tests. * Update sql field name * Generalize test env for CircleCI * Install couchbase dev package for py package * Switch to couchbase server sandbox for tests * Use testenv vars for value comparisons * Install couchbase deps for each pipeline * Run Couchbase server sandbox in each pipeline * Refactor out data collection; Tranlate N1QLQuery arg type * Supported versions lockdown
1 parent 3a7ff60 commit 04e8e6a

File tree

8 files changed

+1413
-3
lines changed

8 files changed

+1413
-3
lines changed

.circleci/config.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
- image: circleci/mariadb:10.1-ram
1616
- image: circleci/redis:5.0.4
1717
- image: rabbitmq:3.5.4
18+
- image: couchbase/server-sandbox:5.5.0
1819

1920
working_directory: ~/repo
2021

@@ -31,6 +32,12 @@ jobs:
3132
- run:
3233
name: install dependencies
3334
command: |
35+
sudo apt-get update
36+
sudo apt install lsb-release -y
37+
curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb
38+
sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb
39+
sudo apt-get update
40+
sudo apt install libcouchbase-dev -y
3441
rm -rf venv
3542
export PATH=/home/circleci/.local/bin:$PATH
3643
pip install --user -U pip setuptools virtualenv
@@ -66,6 +73,7 @@ jobs:
6673
- image: circleci/mariadb:10-ram
6774
- image: circleci/redis:5.0.4
6875
- image: rabbitmq:3.5.4
76+
- image: couchbase/server-sandbox:5.5.0
6977

7078
working_directory: ~/repo
7179

@@ -82,6 +90,12 @@ jobs:
8290
- run:
8391
name: install dependencies
8492
command: |
93+
sudo apt-get update
94+
sudo apt install lsb-release -y
95+
curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb
96+
sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb
97+
sudo apt-get update
98+
sudo apt install libcouchbase-dev -y
8599
python -m venv venv
86100
. venv/bin/activate
87101
pip install -U pip
@@ -115,6 +129,7 @@ jobs:
115129
- image: circleci/mariadb:10-ram
116130
- image: circleci/redis:5.0.4
117131
- image: rabbitmq:3.5.4
132+
- image: couchbase/server-sandbox:5.5.0
118133

119134
working_directory: ~/repo
120135

@@ -131,6 +146,12 @@ jobs:
131146
- run:
132147
name: install dependencies
133148
command: |
149+
sudo apt-get update
150+
sudo apt install lsb-release -y
151+
curl -O https://packages.couchbase.com/releases/couchbase-release/couchbase-release-1.0-6-amd64.deb
152+
sudo dpkg -i ./couchbase-release-1.0-6-amd64.deb
153+
sudo apt-get update
154+
sudo apt install libcouchbase-dev -y
134155
python -m venv venv
135156
. venv/bin/activate
136157
pip install -U pip

instana/__init__.py

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

70+
from .instrumentation import couchbase_inst
7071
from .instrumentation import flask
7172
from .instrumentation import grpcio
7273
from .instrumentation.tornado import client
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""
2+
couchbase instrumentation - This instrumentation supports the Python CouchBase 2.3.4 --> 2.5.x SDK currently:
3+
https://docs.couchbase.com/python-sdk/2.5/start-using-sdk.html
4+
"""
5+
from __future__ import absolute_import
6+
7+
from distutils.version import LooseVersion
8+
import wrapt
9+
10+
from ..log import logger
11+
from ..singletons import tracer
12+
13+
try:
14+
import couchbase
15+
from couchbase.n1ql import N1QLQuery
16+
17+
# List of operations to instrument
18+
# incr, incr_multi, decr, decr_multi, retrieve_in are wrappers around operations above
19+
operations = ['upsert', 'insert', 'replace', 'append', 'prepend', 'get', 'rget',
20+
'touch', 'lock', 'unlock', 'remove', 'counter', 'mutate_in', 'lookup_in',
21+
'stats', 'ping', 'diagnostics', 'observe',
22+
23+
'upsert_multi', 'insert_multi', 'replace_multi', 'append_multi',
24+
'prepend_multi', 'get_multi', 'touch_multi', 'lock_multi', 'unlock_multi',
25+
'observe_multi', 'endure_multi', 'remove_multi', 'counter_multi']
26+
27+
def capture_kvs(scope, instance, query_arg, op):
28+
try:
29+
scope.span.set_tag('couchbase.hostname', instance.server_nodes[0])
30+
scope.span.set_tag('couchbase.bucket', instance.bucket)
31+
scope.span.set_tag('couchbase.type', op)
32+
33+
if query_arg is not None:
34+
query = None
35+
if type(query_arg) is N1QLQuery:
36+
query = query_arg.statement
37+
else:
38+
query = query_arg
39+
40+
scope.span.set_tag('couchbase.sql', query)
41+
except:
42+
# No fail on key capture - best effort
43+
pass
44+
45+
def make_wrapper(op):
46+
def wrapper(wrapped, instance, args, kwargs):
47+
parent_span = tracer.active_span
48+
49+
# If we're not tracing, just return
50+
if parent_span is None:
51+
return wrapped(*args, **kwargs)
52+
53+
with tracer.start_active_span("couchbase", child_of=parent_span) as scope:
54+
capture_kvs(scope, instance, None, op)
55+
try:
56+
return wrapped(*args, **kwargs)
57+
except Exception as e:
58+
scope.span.log_exception(e)
59+
scope.span.set_tag('couchbase.error', repr(e))
60+
raise
61+
return wrapper
62+
63+
def query_with_instana(wrapped, instance, args, kwargs):
64+
parent_span = tracer.active_span
65+
66+
# If we're not tracing, just return
67+
if parent_span is None:
68+
return wrapped(*args, **kwargs)
69+
70+
with tracer.start_active_span("couchbase", child_of=parent_span) as scope:
71+
capture_kvs(scope, instance, args[0], 'n1ql_query')
72+
try:
73+
return wrapped(*args, **kwargs)
74+
except Exception as e:
75+
scope.span.log_exception(e)
76+
scope.span.set_tag('couchbase.error', repr(e))
77+
raise
78+
79+
if hasattr(couchbase, '__version__') \
80+
and (LooseVersion(couchbase.__version__) >= LooseVersion('2.3.4')) \
81+
and (LooseVersion(couchbase.__version__) < LooseVersion('3.0.0')):
82+
logger.debug("Instrumenting couchbase")
83+
wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.n1ql_query', query_with_instana)
84+
for op in operations:
85+
f = make_wrapper(op)
86+
wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.%s' % op, f)
87+
88+
except ImportError:
89+
pass

instana/json_span.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class Data(BaseSpan):
4848
log = None
4949

5050

51+
class CouchbaseData(BaseSpan):
52+
hostname = None
53+
bucket = None
54+
type = None
55+
error = None
56+
error_code = None
57+
sql = None
58+
59+
5160
class HttpData(BaseSpan):
5261
host = None
5362
url = None

instana/recorder.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import instana.singletons
1111

12-
from .json_span import (CustomData, Data, HttpData, JsonSpan, LogData, MySQLData, PostgresData,
12+
from .json_span import (CouchbaseData, CustomData, Data, HttpData, JsonSpan, LogData, MySQLData, PostgresData,
1313
RabbitmqData, RedisData, RenderData, RPCData, SDKData, SoapData,
1414
SQLAlchemyData)
1515
from .log import logger
@@ -23,15 +23,18 @@
2323

2424
class InstanaRecorder(SpanRecorder):
2525
THREAD_NAME = "Instana Span Reporting"
26-
registered_spans = ("aiohttp-client", "aiohttp-server", "django", "log", "memcache", "mysql",
26+
registered_spans = ("aiohttp-client", "aiohttp-server", "couchbase", "django", "log", "memcache", "mysql",
2727
"postgres", "rabbitmq", "redis", "render", "rpc-client", "rpc-server", "sqlalchemy", "soap",
2828
"tornado-client", "tornado-server", "urllib3", "wsgi")
29+
2930
http_spans = ("aiohttp-client", "aiohttp-server", "django", "http", "soap", "tornado-client",
3031
"tornado-server", "urllib3", "wsgi")
3132

32-
exit_spans = ("aiohttp-client", "log", "memcache", "mysql", "postgres", "rabbitmq", "redis", "rpc-client",
33+
exit_spans = ("aiohttp-client", "couchbase", "log", "memcache", "mysql", "postgres", "rabbitmq", "redis", "rpc-client",
3334
"sqlalchemy", "soap", "tornado-client", "urllib3")
35+
3436
entry_spans = ("aiohttp-server", "django", "wsgi", "rabbitmq", "rpc-server", "tornado-server")
37+
3538
local_spans = ("log", "render")
3639

3740
entry_kind = ["entry", "server", "consumer"]
@@ -161,6 +164,14 @@ def build_registered_span(self, span):
161164
if data.rabbitmq.sort == 'consume':
162165
kind = 1 # entry
163166

167+
if span.operation_name == "couchbase":
168+
data.couchbase = CouchbaseData(hostname=span.tags.pop('couchbase.hostname', None),
169+
bucket=span.tags.pop('couchbase.bucket', None),
170+
type=span.tags.pop('couchbase.type', None),
171+
error=span.tags.pop('couchbase.error', None),
172+
error_type=span.tags.pop('couchbase.error_type', None),
173+
sql=span.tags.pop('couchbase.sql', None))
174+
164175
if span.operation_name == "redis":
165176
data.redis = RedisData(connection=span.tags.pop('connection', None),
166177
driver=span.tags.pop('driver', None),

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def check_setuptools():
6969
'test': [
7070
'aiohttp>=3.5.4;python_version>="3.5"',
7171
'asynqp>=0.4;python_version>="3.5"',
72+
'couchbase==2.5.9',
7273
'django>=1.11,<2.2',
7374
'nose>=1.0',
7475
'flask>=0.12.2',

tests/helpers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
testenv = {}
44

5+
"""
6+
CouchDB Environment
7+
"""
8+
testenv['couchdb_host'] = os.environ.get('COUCHDB_HOST', '127.0.0.1')
9+
testenv['couchdb_username'] = os.environ.get('COUCHDB_USERNAME', 'Administrator')
10+
testenv['couchdb_password'] = os.environ.get('COUCHDB_PASSWORD', 'password')
11+
512
"""
613
MySQL Environment
714
"""

0 commit comments

Comments
 (0)