diff --git a/instana/__init__.py b/instana/__init__.py index 335b345c..bc5e34bc 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -61,6 +61,7 @@ def load_instrumentation(): # Import & initialize instrumentation from .instrumentation import asynqp # noqa from .instrumentation import mysqlpython # noqa + from .instrumentation import redis # noqa from .instrumentation import sqlalchemy # noqa from .instrumentation import sudsjurko # noqa from .instrumentation import urllib3 # noqa diff --git a/instana/instrumentation/redis.py b/instana/instrumentation/redis.py new file mode 100644 index 00000000..65c297c4 --- /dev/null +++ b/instana/instrumentation/redis.py @@ -0,0 +1,79 @@ +from __future__ import absolute_import + +import opentracing +import opentracing.ext.tags as ext +import wrapt + +from ..log import logger +from ..singletons import tracer + +try: + import redis + + @wrapt.patch_function_wrapper('redis.client','StrictRedis.execute_command') + def execute_command_with_instana(wrapped, instance, args, kwargs): + parent_span = tracer.active_span + + # If we're not tracing, just return + if parent_span is None: + return wrapped(*args, **kwargs) + + with tracer.start_active_span("redis", child_of=parent_span) as scope: + + try: + ckw = instance.connection_pool.connection_kwargs + url = "redis://%s:%d/%d" % (ckw['host'], ckw['port'], ckw['db']) + scope.span.set_tag("connection", url) + scope.span.set_tag("driver", "redis-py") + scope.span.set_tag("command", args[0]) + + rv = wrapped(*args, **kwargs) + except Exception as e: + scope.span.set_tag("redis.error", str(e)) + scope.span.set_tag("error", True) + ec = scope.span.tags.get('ec', 0) + scope.span.set_tag("ec", ec+1) + raise + else: + return rv + + @wrapt.patch_function_wrapper('redis.client','BasePipeline.execute') + def execute_with_instana(wrapped, instance, args, kwargs): + parent_span = tracer.active_span + + # If we're not tracing, just return + if parent_span is None: + return wrapped(*args, **kwargs) + + with tracer.start_active_span("redis", child_of=parent_span) as scope: + + try: + ckw = instance.connection_pool.connection_kwargs + url = "redis://%s:%d/%d" % (ckw['host'], ckw['port'], ckw['db']) + scope.span.set_tag("connection", url) + scope.span.set_tag("driver", "redis-py") + scope.span.set_tag("command", 'PIPELINE') + + try: + pipe_cmds = [] + for e in instance.command_stack: + pipe_cmds.append(e[0][0]) + scope.span.set_tag("subCommands", pipe_cmds) + except Exception as e: + # If anything breaks during cmd collection, just log a + # debug message + logger.debug("Error collecting pipeline commands") + + rv = wrapped(*args, **kwargs) + except Exception as e: + scope.span.set_tag("redis.error", str(e)) + scope.span.set_tag("error", True) + ec = scope.span.tags.get('ec', 0) + scope.span.set_tag("ec", ec+1) + raise + else: + return rv + + logger.debug("Instrumenting redis") +except ImportError: + pass diff --git a/instana/json_span.py b/instana/json_span.py index 74a6dc68..522b1248 100644 --- a/instana/json_span.py +++ b/instana/json_span.py @@ -30,6 +30,7 @@ class Data(object): custom = None http = None rabbitmq = None + redis = None sdk = None service = None sqlalchemy = None @@ -72,6 +73,18 @@ class RabbitmqData(object): def __init__(self, **kwds): self.__dict__.update(kwds) + +class RedisData(object): + connection = None + driver = None + command = None + error = None + subCommands = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) + + class SQLAlchemyData(object): sql = None url = None diff --git a/instana/recorder.py b/instana/recorder.py index bb00062a..bc8b56e5 100644 --- a/instana/recorder.py +++ b/instana/recorder.py @@ -12,7 +12,8 @@ import instana.singletons from .json_span import (CustomData, Data, HttpData, JsonSpan, MySQLData, - RabbitmqData, SDKData, SoapData, SQLAlchemyData) + RabbitmqData, RedisData, SDKData, SoapData, + SQLAlchemyData) from .log import logger if sys.version_info.major is 2: @@ -22,12 +23,12 @@ class InstanaRecorder(SpanRecorder): - registered_spans = ("django", "memcache", "mysql", "rabbitmq", "rpc-client", - "rpc-server", "sqlalchemy", "soap", "urllib3", "wsgi") + registered_spans = ("django", "memcache", "mysql", "rabbitmq", "redis", + "rpc-client", "rpc-server", "sqlalchemy", "soap", "urllib3", "wsgi") http_spans = ("django", "wsgi", "urllib3", "soap") - exit_spans = ("memcache", "mysql", "rabbitmq", "rpc-client", "sqlalchemy", - "soap", "urllib3") + exit_spans = ("memcache", "mysql", "rabbitmq", "redis", "rpc-client", + "sqlalchemy", "soap", "urllib3") entry_spans = ("django", "wsgi", "rabbitmq", "rpc-server") entry_kind = ["entry", "server", "consumer"] @@ -115,6 +116,13 @@ def build_registered_span(self, span): address=span.tags.pop('address', None), key=span.tags.pop('key', None)) + if span.operation_name == "redis": + data.redis = RedisData(connection=span.tags.pop('connection', None), + driver=span.tags.pop('driver', None), + command=span.tags.pop('command', None), + error=span.tags.pop('redis.error', None), + subCommands=span.tags.pop('subCommands', None)) + if span.operation_name == "sqlalchemy": data.sqlalchemy = SQLAlchemyData(sql=span.tags.pop('sqlalchemy.sql', None), eng=span.tags.pop('sqlalchemy.eng', None), diff --git a/setup.py b/setup.py index efaeab90..824850d0 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ def check_setuptools(): 'psycopg2>=2.7.1', 'pyOpenSSL>=16.1.0;python_version<="2.7"', 'pytest>=3.0.1', + 'redis>=2.10.6', 'requests>=2.17.1', 'sqlalchemy>=1.1.15', 'spyne>=2.9', diff --git a/tests/helpers.py b/tests/helpers.py index 59aa7cb7..15713791 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -43,3 +43,11 @@ testenv['postgresql_pw'] = os.environ['TRAVIS_POSTGRESQL_PASS'] else: testenv['postgresql_pw'] = '' + +""" +Redis Environment +""" +if 'REDIS' in os.environ: + testenv['redis_url']= os.environ['REDIS'] +else: + testenv['redis_url'] = '127.0.0.1:6379' diff --git a/tests/test_redis.py b/tests/test_redis.py new file mode 100644 index 00000000..442f70eb --- /dev/null +++ b/tests/test_redis.py @@ -0,0 +1,311 @@ +from __future__ import absolute_import + +import os +import sys +import unittest + +import redis + +from .helpers import testenv +from instana.singletons import tracer + + +class TestRedis(unittest.TestCase): + def setUp(self): + """ Clear all spans before a test run """ + self.recorder = tracer.recorder + self.recorder.clear_spans() + self.strict_redis = redis.StrictRedis.from_url("redis://%s/0" % testenv['redis_url']) + self.redis = redis.Redis.from_url("redis://%s/0" % testenv['redis_url']) + + def tearDown(self): + pass + + def test_set_get(self): + result = None + with tracer.start_active_span('test'): + self.strict_redis.set('foox', 'barX') + self.strict_redis.set('fooy', 'barY') + result = self.strict_redis.get('foox') + + spans = self.recorder.queued_spans() + self.assertEqual(4, len(spans)) + + self.assertEqual(b'barX', result) + + rs1_span = spans[0] + rs2_span = spans[1] + rs3_span = spans[2] + test_span = spans[3] + + self.assertIsNone(tracer.active_span) + + # Same traceId + self.assertEqual(test_span.t, rs1_span.t) + self.assertEqual(test_span.t, rs2_span.t) + self.assertEqual(test_span.t, rs3_span.t) + + # Parent relationships + self.assertEqual(rs1_span.p, test_span.s) + self.assertEqual(rs2_span.p, test_span.s) + self.assertEqual(rs3_span.p, test_span.s) + + # Error logging + self.assertFalse(test_span.error) + self.assertIsNone(test_span.ec) + self.assertFalse(rs1_span.error) + self.assertIsNone(rs1_span.ec) + self.assertFalse(rs2_span.error) + self.assertIsNone(rs2_span.ec) + self.assertFalse(rs3_span.error) + self.assertIsNone(rs3_span.ec) + + # Redis span 1 + self.assertEqual('redis', rs1_span.n) + self.assertFalse('custom' in rs1_span.data.__dict__) + self.assertTrue('redis' in rs1_span.data.__dict__) + + self.assertEqual('redis-py', rs1_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs1_span.data.redis.connection) + self.assertEqual("SET", rs1_span.data.redis.command) + self.assertIsNone(rs1_span.data.redis.error) + + self.assertIsNotNone(rs1_span.stack) + self.assertTrue(type(rs1_span.stack) is list) + self.assertGreater(len(rs1_span.stack), 0) + + # Redis span 2 + self.assertEqual('redis', rs2_span.n) + self.assertFalse('custom' in rs2_span.data.__dict__) + self.assertTrue('redis' in rs2_span.data.__dict__) + + self.assertEqual('redis-py', rs2_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs2_span.data.redis.connection) + self.assertEqual("SET", rs2_span.data.redis.command) + self.assertIsNone(rs2_span.data.redis.error) + + self.assertIsNotNone(rs2_span.stack) + self.assertTrue(type(rs2_span.stack) is list) + self.assertGreater(len(rs2_span.stack), 0) + + # Redis span 3 + self.assertEqual('redis', rs3_span.n) + self.assertFalse('custom' in rs3_span.data.__dict__) + self.assertTrue('redis' in rs3_span.data.__dict__) + + self.assertEqual('redis-py', rs3_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs3_span.data.redis.connection) + self.assertEqual("GET", rs3_span.data.redis.command) + self.assertIsNone(rs3_span.data.redis.error) + + self.assertIsNotNone(rs3_span.stack) + self.assertTrue(type(rs3_span.stack) is list) + self.assertGreater(len(rs3_span.stack), 0) + + def test_set_incr_get(self): + result = None + with tracer.start_active_span('test'): + self.strict_redis.set('counter', '10') + self.strict_redis.incr('counter') + result = self.strict_redis.get('counter') + + spans = self.recorder.queued_spans() + self.assertEqual(4, len(spans)) + + self.assertEqual(b'11', result) + + rs1_span = spans[0] + rs2_span = spans[1] + rs3_span = spans[2] + test_span = spans[3] + + self.assertIsNone(tracer.active_span) + + # Same traceId + self.assertEqual(test_span.t, rs1_span.t) + self.assertEqual(test_span.t, rs2_span.t) + self.assertEqual(test_span.t, rs3_span.t) + + # Parent relationships + self.assertEqual(rs1_span.p, test_span.s) + self.assertEqual(rs2_span.p, test_span.s) + self.assertEqual(rs3_span.p, test_span.s) + + # Error logging + self.assertFalse(test_span.error) + self.assertIsNone(test_span.ec) + self.assertFalse(rs1_span.error) + self.assertIsNone(rs1_span.ec) + self.assertFalse(rs2_span.error) + self.assertIsNone(rs2_span.ec) + self.assertFalse(rs3_span.error) + self.assertIsNone(rs3_span.ec) + + # Redis span 1 + self.assertEqual('redis', rs1_span.n) + self.assertFalse('custom' in rs1_span.data.__dict__) + self.assertTrue('redis' in rs1_span.data.__dict__) + + self.assertEqual('redis-py', rs1_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs1_span.data.redis.connection) + self.assertEqual("SET", rs1_span.data.redis.command) + self.assertIsNone(rs1_span.data.redis.error) + + self.assertIsNotNone(rs1_span.stack) + self.assertTrue(type(rs1_span.stack) is list) + self.assertGreater(len(rs1_span.stack), 0) + + # Redis span 2 + self.assertEqual('redis', rs2_span.n) + self.assertFalse('custom' in rs2_span.data.__dict__) + self.assertTrue('redis' in rs2_span.data.__dict__) + + self.assertEqual('redis-py', rs2_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs2_span.data.redis.connection) + self.assertEqual("INCRBY", rs2_span.data.redis.command) + self.assertIsNone(rs2_span.data.redis.error) + + self.assertIsNotNone(rs2_span.stack) + self.assertTrue(type(rs2_span.stack) is list) + self.assertGreater(len(rs2_span.stack), 0) + + # Redis span 3 + self.assertEqual('redis', rs3_span.n) + self.assertFalse('custom' in rs3_span.data.__dict__) + self.assertTrue('redis' in rs3_span.data.__dict__) + + self.assertEqual('redis-py', rs3_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs3_span.data.redis.connection) + self.assertEqual("GET", rs3_span.data.redis.command) + self.assertIsNone(rs3_span.data.redis.error) + + self.assertIsNotNone(rs3_span.stack) + self.assertTrue(type(rs3_span.stack) is list) + self.assertGreater(len(rs3_span.stack), 0) + + def test_old_redis_client(self): + result = None + with tracer.start_active_span('test'): + self.redis.set('foox', 'barX') + self.redis.set('fooy', 'barY') + result = self.redis.get('foox') + + spans = self.recorder.queued_spans() + self.assertEqual(4, len(spans)) + + self.assertEqual(b'barX', result) + + rs1_span = spans[0] + rs2_span = spans[1] + rs3_span = spans[2] + test_span = spans[3] + + self.assertIsNone(tracer.active_span) + + # Same traceId + self.assertEqual(test_span.t, rs1_span.t) + self.assertEqual(test_span.t, rs2_span.t) + self.assertEqual(test_span.t, rs3_span.t) + + # Parent relationships + self.assertEqual(rs1_span.p, test_span.s) + self.assertEqual(rs2_span.p, test_span.s) + self.assertEqual(rs3_span.p, test_span.s) + + # Error logging + self.assertFalse(test_span.error) + self.assertIsNone(test_span.ec) + self.assertFalse(rs1_span.error) + self.assertIsNone(rs1_span.ec) + self.assertFalse(rs2_span.error) + self.assertIsNone(rs2_span.ec) + self.assertFalse(rs3_span.error) + self.assertIsNone(rs3_span.ec) + + # Redis span 1 + self.assertEqual('redis', rs1_span.n) + self.assertFalse('custom' in rs1_span.data.__dict__) + self.assertTrue('redis' in rs1_span.data.__dict__) + + self.assertEqual('redis-py', rs1_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs1_span.data.redis.connection) + self.assertEqual("SET", rs1_span.data.redis.command) + self.assertIsNone(rs1_span.data.redis.error) + + self.assertIsNotNone(rs1_span.stack) + self.assertTrue(type(rs1_span.stack) is list) + self.assertGreater(len(rs1_span.stack), 0) + + # Redis span 2 + self.assertEqual('redis', rs2_span.n) + self.assertFalse('custom' in rs2_span.data.__dict__) + self.assertTrue('redis' in rs2_span.data.__dict__) + + self.assertEqual('redis-py', rs2_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs2_span.data.redis.connection) + self.assertEqual("SET", rs2_span.data.redis.command) + self.assertIsNone(rs2_span.data.redis.error) + + self.assertIsNotNone(rs2_span.stack) + self.assertTrue(type(rs2_span.stack) is list) + self.assertGreater(len(rs2_span.stack), 0) + + # Redis span 3 + self.assertEqual('redis', rs3_span.n) + self.assertFalse('custom' in rs3_span.data.__dict__) + self.assertTrue('redis' in rs3_span.data.__dict__) + + self.assertEqual('redis-py', rs3_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs3_span.data.redis.connection) + self.assertEqual("GET", rs3_span.data.redis.command) + self.assertIsNone(rs3_span.data.redis.error) + + self.assertIsNotNone(rs3_span.stack) + self.assertTrue(type(rs3_span.stack) is list) + self.assertGreater(len(rs3_span.stack), 0) + + def test_pipelined_requests(self): + result = None + with tracer.start_active_span('test'): + pipe = self.strict_redis.pipeline() + pipe.set('foox', 'barX') + pipe.set('fooy', 'barY') + pipe.get('foox') + result = pipe.execute() + + spans = self.recorder.queued_spans() + self.assertEqual(2, len(spans)) + + self.assertEqual([True, True, b'barX'], result) + + rs1_span = spans[0] + test_span = spans[1] + + self.assertIsNone(tracer.active_span) + + # Same traceId + self.assertEqual(test_span.t, rs1_span.t) + + # Parent relationships + self.assertEqual(rs1_span.p, test_span.s) + + # Error logging + self.assertFalse(test_span.error) + self.assertIsNone(test_span.ec) + self.assertFalse(rs1_span.error) + self.assertIsNone(rs1_span.ec) + + # Redis span 1 + self.assertEqual('redis', rs1_span.n) + self.assertFalse('custom' in rs1_span.data.__dict__) + self.assertTrue('redis' in rs1_span.data.__dict__) + + self.assertEqual('redis-py', rs1_span.data.redis.driver) + self.assertEqual("redis://%s/0" % testenv['redis_url'], rs1_span.data.redis.connection) + self.assertEqual("PIPELINE", rs1_span.data.redis.command) + self.assertEqual(['SET', 'SET', 'GET'], rs1_span.data.redis.subCommands) + self.assertIsNone(rs1_span.data.redis.error) + + self.assertIsNotNone(rs1_span.stack) + self.assertTrue(type(rs1_span.stack) is list) + self.assertGreater(len(rs1_span.stack), 0)