From cd519af93fd2393a58eb6922310c2ca1e513d24c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Mon, 27 Jun 2016 15:03:37 +0000 Subject: [PATCH 1/6] new redis contrib integration --- circle.yml | 1 + ddtrace/contrib/redis/__init__.py | 9 +++ ddtrace/contrib/redis/tracers.py | 106 ++++++++++++++++++++++++++++++ ddtrace/contrib/redis/util.py | 51 ++++++++++++++ ddtrace/ext/redis.py | 12 ++++ tests/contrib/redis/__init__.py | 0 tests/contrib/redis/test.py | 100 ++++++++++++++++++++++++++++ 7 files changed, 279 insertions(+) create mode 100644 ddtrace/contrib/redis/__init__.py create mode 100644 ddtrace/contrib/redis/tracers.py create mode 100644 ddtrace/contrib/redis/util.py create mode 100644 ddtrace/ext/redis.py create mode 100644 tests/contrib/redis/__init__.py create mode 100644 tests/contrib/redis/test.py diff --git a/circle.yml b/circle.yml index 0379c2d6573..359b8028c09 100644 --- a/circle.yml +++ b/circle.yml @@ -10,5 +10,6 @@ test: override: - docker run -d -p 9200:9200 elasticsearch:2.3 - docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=test -e POSTGRES_USER=test -e POSTGRES_DB=test postgres:9.5 + - docker run -d -p 6379:6379 redis:3.2 - python2.7 setup.py test - python3.4 setup.py test diff --git a/ddtrace/contrib/redis/__init__.py b/ddtrace/contrib/redis/__init__.py new file mode 100644 index 00000000000..bbe08099e58 --- /dev/null +++ b/ddtrace/contrib/redis/__init__.py @@ -0,0 +1,9 @@ +from ..util import require_modules + +required_modules = ['redis'] + +with require_modules(required_modules) as missing_modules: + if not missing_modules: + from .tracers import get_traced_redis, get_traced_redis_from + + __all__ = ['get_traced_redis', 'get_traced_redis_from'] diff --git a/ddtrace/contrib/redis/tracers.py b/ddtrace/contrib/redis/tracers.py new file mode 100644 index 00000000000..43358bf3362 --- /dev/null +++ b/ddtrace/contrib/redis/tracers.py @@ -0,0 +1,106 @@ +""" +tracers exposed publicly +""" +# stdlib +import time + +try: + from redis import Redis + from redis.client import StrictPipeline +except ImportError: + Redis, StrictPipeline = object, object + +# dogtrace +from .util import format_command_args, _extract_conn_tags +from ...ext import redis as redisx + + +DEFAULT_SERVICE = 'redis' + + +def get_traced_redis(ddtracer, service=DEFAULT_SERVICE): + return _get_traced_redis(ddtracer, Redis, service) + + +def get_traced_redis_from(ddtracer, baseclass, service=DEFAULT_SERVICE): + return _get_traced_redis(ddtracer, baseclass, service) + +# pylint: disable=protected-access +def _get_traced_redis(ddtracer, baseclass, service): + class TracedPipeline(StrictPipeline): + _datadog_tracer = ddtracer + _datadog_service = service + + def __init__(self, *args, **kwargs): + self._datadog_pipeline_creation = time.time() + super(TracedPipeline, self).__init__(*args, **kwargs) + + def execute(self, *args, **kwargs): + commands, queries = [], [] + with self._datadog_tracer.trace('redis.pipeline') as s: + s.service = self._datadog_service + s.span_type = redisx.TYPE + + for cargs, _ in self.command_stack: + commands.append(cargs[0]) + queries.append(format_command_args(cargs)) + + s.set_tag(redisx.CMD, ', '.join(commands)) + query = '\n'.join(queries) + s.resource = query + + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.PIPELINE_LEN, len(self.command_stack)) + s.set_tag(redisx.PIPELINE_AGE, time.time()-self._datadog_pipeline_creation) + + result = super(TracedPipeline, self).execute(self, *args, **kwargs) + return result + + def immediate_execute_command(self, *args, **kwargs): + command_name = args[0] + + with self._datadog_tracer.trace('redis.command') as s: + s.service = self._datadog_service + s.span_type = redisx.TYPE + # currently no quantization on the client side + s.resource = format_command_args(args) + s.set_tag(redisx.CMD, (args or [None])[0]) + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.ARGS_LEN, len(args)) + + s.set_tag(redisx.IMMEDIATE_PIPELINE, True) + + result = super(TracedPipeline, self).immediate_execute_command(*args, **options) + return result + + class TracedRedis(baseclass): + _datadog_tracer = ddtracer + _datadog_service = service + + def execute_command(self, *args, **options): + command_name = args[0] + + with self._datadog_tracer.trace('redis.command') as s: + s.service = self._datadog_service + s.span_type = redisx.TYPE + # currently no quantization on the client side + s.resource = format_command_args(args) + s.set_tag(redisx.CMD, (args or [None])[0]) + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.ARGS_LEN, len(args)) + + result = super(TracedRedis, self).execute_command(*args, **options) + return result + + def pipeline(self, transaction=True, shard_hint=None): + return TracedPipeline( + self.connection_pool, + self.response_callbacks, + transaction, + shard_hint + ) + + return TracedRedis diff --git a/ddtrace/contrib/redis/util.py b/ddtrace/contrib/redis/util.py new file mode 100644 index 00000000000..01875b55e65 --- /dev/null +++ b/ddtrace/contrib/redis/util.py @@ -0,0 +1,51 @@ +""" +Some utils used by the dogtrace redis integration +""" +from ...ext import redis as redisx, net + +VALUE_PLACEHOLDER = "?" +VALUE_MAX_LENGTH = 100 +VALUE_TOO_LONG_MARK = "..." +COMMAND_MAX_LENGTH = 1000 + + +def _extract_conn_tags(conn_kwargs): + """ Transform redis conn info into dogtrace metas """ + try: + return { + net.TARGET_HOST: conn_kwargs['host'], + net.TARGET_PORT: conn_kwargs['port'], + redisx.DB: conn_kwargs['db'] or 0, + } + except Exception: + return {} + + +def format_command_args(args): + """Format a command by removing unwanted values + + Restrict what we keep from the values sent (with a SET, HGET, LPUSH, ...): + - Skip binary content + - Truncate + """ + formatted_length = 0 + formatted_args = [] + for arg in args: + try: + command = unicode(arg) + if len(command) > VALUE_MAX_LENGTH: + command = command[:VALUE_MAX_LENGTH] + VALUE_TOO_LONG_MARK + if formatted_length + len(command) > COMMAND_MAX_LENGTH: + formatted_args.append( + command[:COMMAND_MAX_LENGTH-formatted_length] + + VALUE_TOO_LONG_MARK + ) + break + + formatted_args.append(command) + formatted_length += len(command) + except Exception: + formatted_args.append(VALUE_PLACEHOLDER) + break + + return " ".join(formatted_args) diff --git a/ddtrace/ext/redis.py b/ddtrace/ext/redis.py new file mode 100644 index 00000000000..b637ed1ea20 --- /dev/null +++ b/ddtrace/ext/redis.py @@ -0,0 +1,12 @@ +# type of the spans +TYPE = 'redis' + +# net extension +DB = 'out.redis_db' + +# standard tags +CMD = 'redis.command' +ARGS_LEN = 'redis.args_length' +PIPELINE_LEN = 'redis.pipeline_length' +PIPELINE_AGE = 'redis.pipeline_age' +IMMEDIATE_PIPELINE = 'redis.pipeline_immediate_command' diff --git a/tests/contrib/redis/__init__.py b/tests/contrib/redis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/contrib/redis/test.py b/tests/contrib/redis/test.py new file mode 100644 index 00000000000..179894d950a --- /dev/null +++ b/tests/contrib/redis/test.py @@ -0,0 +1,100 @@ +import unittest + +from ddtrace.contrib.redis import missing_modules + +if missing_modules: + raise unittest.SkipTest("Missing dependencies %s" % missing_modules) + +import redis +from nose.tools import eq_, ok_ + +from ddtrace.tracer import Tracer +from ddtrace.contrib.redis import get_traced_redis, get_traced_redis_from + +from ...test_tracer import DummyWriter + + +class RedisTest(unittest.TestCase): + SERVICE = 'test-cache' + + def setUp(self): + """ purge redis """ + r = redis.Redis() + r.flushall() + + def tearDown(self): + r = redis.Redis() + r.flushall() + + def test_basic_class(self): + writer = DummyWriter() + tracer = Tracer(writer=writer) + + TracedRedisCache = get_traced_redis(tracer, service=self.SERVICE) + r = TracedRedisCache() + + us = r.get('cheese') + eq_(us, None) + spans = writer.pop() + eq_(len(spans), 1) + span = spans[0] + eq_(span.service, self.SERVICE) + eq_(span.name, 'redis.command') + eq_(span.span_type, 'redis') + eq_(span.error, 0) + eq_(span.meta, {'out.host': u'localhost', 'redis.command': u'GET', 'out.port': u'6379', 'redis.args_length': u'2', 'out.redis_db': u'0'}) + eq_(span.resource, 'GET cheese') + + + def test_basic_class_pipeline(self): + writer = DummyWriter() + tracer = Tracer(writer=writer) + + TracedRedisCache = get_traced_redis(tracer, service=self.SERVICE) + r = TracedRedisCache() + + with r.pipeline() as p: + p.set('blah', 32) + p.rpush('foo', 'soethus') + p.hgetall('xxx') + + p.execute() + + spans = writer.pop() + eq_(len(spans), 1) + span = spans[0] + eq_(span.service, self.SERVICE) + eq_(span.name, 'redis.pipeline') + eq_(span.span_type, 'redis') + eq_(span.error, 0) + eq_(span.get_tag('out.redis_db'), '0') + eq_(span.get_tag('out.host'), 'localhost') + ok_(float(span.get_tag('redis.pipeline_age')) > 0) + eq_(span.get_tag('redis.pipeline_length'), '3') + eq_(span.get_tag('redis.command'), 'SET, RPUSH, HGETALL') + eq_(span.get_tag('out.port'), '6379') + eq_(span.resource, 'SET blah 32\nRPUSH foo soethus\nHGETALL xxx') + + def test_custom_class(self): + class MyCustomRedis(redis.Redis): + def execute_command(self, *args, **kwargs): + response = super(MyCustomRedis, self).execute_command(*args, **kwargs) + return 'YO%sYO' % response + + + writer = DummyWriter() + tracer = Tracer(writer=writer) + + TracedRedisCache = get_traced_redis_from(tracer, MyCustomRedis, service=self.SERVICE) + r = TracedRedisCache() + + r.set('foo', 42) + resp = r.get('foo') + eq_(resp, 'YO42YO') + + spans = writer.pop() + eq_(len(spans), 2) + eq_(spans[0].name, 'redis.command') + eq_(spans[0].resource, 'SET foo 42') + eq_(spans[1].name, 'redis.command') + eq_(spans[1].resource, 'GET foo') From a69d4d2cc06cc1b32c51d4d3efee88c428507671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Thu, 30 Jun 2016 14:03:44 +0000 Subject: [PATCH 2/6] stop default circle ci redis --- circle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/circle.yml b/circle.yml index 359b8028c09..9970170d00a 100644 --- a/circle.yml +++ b/circle.yml @@ -6,6 +6,7 @@ machine: dependencies: pre: - sudo service postgresql stop + - sudo service redis-server stop test: override: - docker run -d -p 9200:9200 elasticsearch:2.3 From 4576d1faeb0db31d154be078720c2a9aa506470e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Thu, 30 Jun 2016 14:23:08 +0000 Subject: [PATCH 3/6] [redis] import properly module and base classes --- ddtrace/contrib/redis/__init__.py | 2 +- ddtrace/contrib/redis/tracers.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/ddtrace/contrib/redis/__init__.py b/ddtrace/contrib/redis/__init__.py index bbe08099e58..2c557ee6091 100644 --- a/ddtrace/contrib/redis/__init__.py +++ b/ddtrace/contrib/redis/__init__.py @@ -1,6 +1,6 @@ from ..util import require_modules -required_modules = ['redis'] +required_modules = ['redis', 'redis.client'] with require_modules(required_modules) as missing_modules: if not missing_modules: diff --git a/ddtrace/contrib/redis/tracers.py b/ddtrace/contrib/redis/tracers.py index 43358bf3362..5a76932f313 100644 --- a/ddtrace/contrib/redis/tracers.py +++ b/ddtrace/contrib/redis/tracers.py @@ -4,11 +4,8 @@ # stdlib import time -try: - from redis import Redis - from redis.client import StrictPipeline -except ImportError: - Redis, StrictPipeline = object, object +from redis import StrictRedis +from redis.client import StrictPipeline # dogtrace from .util import format_command_args, _extract_conn_tags @@ -19,7 +16,7 @@ def get_traced_redis(ddtracer, service=DEFAULT_SERVICE): - return _get_traced_redis(ddtracer, Redis, service) + return _get_traced_redis(ddtracer, StrictRedis, service) def get_traced_redis_from(ddtracer, baseclass, service=DEFAULT_SERVICE): From fb1ed5ec94e6aabba901169f0da962b249e438c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Thu, 30 Jun 2016 14:28:55 +0000 Subject: [PATCH 4/6] [redis] add sampler support --- ddtrace/contrib/redis/tracers.py | 75 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/ddtrace/contrib/redis/tracers.py b/ddtrace/contrib/redis/tracers.py index 5a76932f313..beb5d36ceef 100644 --- a/ddtrace/contrib/redis/tracers.py +++ b/ddtrace/contrib/redis/tracers.py @@ -35,62 +35,61 @@ def __init__(self, *args, **kwargs): def execute(self, *args, **kwargs): commands, queries = [], [] with self._datadog_tracer.trace('redis.pipeline') as s: - s.service = self._datadog_service - s.span_type = redisx.TYPE + if s.sampled: + s.service = self._datadog_service + s.span_type = redisx.TYPE - for cargs, _ in self.command_stack: - commands.append(cargs[0]) - queries.append(format_command_args(cargs)) + for cargs, _ in self.command_stack: + commands.append(cargs[0]) + queries.append(format_command_args(cargs)) - s.set_tag(redisx.CMD, ', '.join(commands)) - query = '\n'.join(queries) - s.resource = query + s.set_tag(redisx.CMD, ', '.join(commands)) + query = '\n'.join(queries) + s.resource = query - s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) - # FIXME[leo]: convert to metric? - s.set_tag(redisx.PIPELINE_LEN, len(self.command_stack)) - s.set_tag(redisx.PIPELINE_AGE, time.time()-self._datadog_pipeline_creation) + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.PIPELINE_LEN, len(self.command_stack)) + s.set_tag(redisx.PIPELINE_AGE, time.time()-self._datadog_pipeline_creation) - result = super(TracedPipeline, self).execute(self, *args, **kwargs) - return result + return super(TracedPipeline, self).execute(self, *args, **kwargs) def immediate_execute_command(self, *args, **kwargs): command_name = args[0] with self._datadog_tracer.trace('redis.command') as s: - s.service = self._datadog_service - s.span_type = redisx.TYPE - # currently no quantization on the client side - s.resource = format_command_args(args) - s.set_tag(redisx.CMD, (args or [None])[0]) - s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) - # FIXME[leo]: convert to metric? - s.set_tag(redisx.ARGS_LEN, len(args)) + if s.sampled: + s.service = self._datadog_service + s.span_type = redisx.TYPE + # currently no quantization on the client side + s.resource = format_command_args(args) + s.set_tag(redisx.CMD, (args or [None])[0]) + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.ARGS_LEN, len(args)) - s.set_tag(redisx.IMMEDIATE_PIPELINE, True) + s.set_tag(redisx.IMMEDIATE_PIPELINE, True) - result = super(TracedPipeline, self).immediate_execute_command(*args, **options) - return result + return super(TracedPipeline, self).immediate_execute_command(*args, **options) class TracedRedis(baseclass): _datadog_tracer = ddtracer _datadog_service = service def execute_command(self, *args, **options): - command_name = args[0] - with self._datadog_tracer.trace('redis.command') as s: - s.service = self._datadog_service - s.span_type = redisx.TYPE - # currently no quantization on the client side - s.resource = format_command_args(args) - s.set_tag(redisx.CMD, (args or [None])[0]) - s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) - # FIXME[leo]: convert to metric? - s.set_tag(redisx.ARGS_LEN, len(args)) - - result = super(TracedRedis, self).execute_command(*args, **options) - return result + if s.sampled: + command_name = args[0] + s.service = self._datadog_service + s.span_type = redisx.TYPE + # currently no quantization on the client side + s.resource = format_command_args(args) + s.set_tag(redisx.CMD, (args or [None])[0]) + s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + # FIXME[leo]: convert to metric? + s.set_tag(redisx.ARGS_LEN, len(args)) + + return super(TracedRedis, self).execute_command(*args, **options) def pipeline(self, transaction=True, shard_hint=None): return TracedPipeline( From 317195694483e53a59b86e5670820952d789992f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Thu, 30 Jun 2016 15:02:02 +0000 Subject: [PATCH 5/6] [redis] unicode compatible resources --- tests/contrib/redis/test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/contrib/redis/test.py b/tests/contrib/redis/test.py index 179894d950a..326bbd933f9 100644 --- a/tests/contrib/redis/test.py +++ b/tests/contrib/redis/test.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import unittest from ddtrace.contrib.redis import missing_modules @@ -55,7 +58,7 @@ def test_basic_class_pipeline(self): with r.pipeline() as p: p.set('blah', 32) - p.rpush('foo', 'soethus') + p.rpush('foo', u'éé') p.hgetall('xxx') p.execute() @@ -73,7 +76,7 @@ def test_basic_class_pipeline(self): eq_(span.get_tag('redis.pipeline_length'), '3') eq_(span.get_tag('redis.command'), 'SET, RPUSH, HGETALL') eq_(span.get_tag('out.port'), '6379') - eq_(span.resource, 'SET blah 32\nRPUSH foo soethus\nHGETALL xxx') + eq_(span.resource, u'SET blah 32\nRPUSH foo éé\nHGETALL xxx') def test_custom_class(self): class MyCustomRedis(redis.Redis): From 143ad448ea945794de14744b298bad70e760ea2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o=20Cavaill=C3=A9?= Date: Fri, 1 Jul 2016 11:43:28 +0000 Subject: [PATCH 6/6] [trace/redis] add meta --- ddtrace/contrib/redis/tracers.py | 23 +++++++++++++++++------ tests/contrib/redis/test.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ddtrace/contrib/redis/tracers.py b/ddtrace/contrib/redis/tracers.py index beb5d36ceef..a3450690aa0 100644 --- a/ddtrace/contrib/redis/tracers.py +++ b/ddtrace/contrib/redis/tracers.py @@ -15,18 +15,19 @@ DEFAULT_SERVICE = 'redis' -def get_traced_redis(ddtracer, service=DEFAULT_SERVICE): - return _get_traced_redis(ddtracer, StrictRedis, service) +def get_traced_redis(ddtracer, service=DEFAULT_SERVICE, meta=None): + return _get_traced_redis(ddtracer, StrictRedis, service, meta) -def get_traced_redis_from(ddtracer, baseclass, service=DEFAULT_SERVICE): - return _get_traced_redis(ddtracer, baseclass, service) +def get_traced_redis_from(ddtracer, baseclass, service=DEFAULT_SERVICE, meta=None): + return _get_traced_redis(ddtracer, baseclass, service, meta) # pylint: disable=protected-access -def _get_traced_redis(ddtracer, baseclass, service): +def _get_traced_redis(ddtracer, baseclass, service, meta): class TracedPipeline(StrictPipeline): _datadog_tracer = ddtracer _datadog_service = service + _datadog_meta = meta def __init__(self, *args, **kwargs): self._datadog_pipeline_creation = time.time() @@ -48,6 +49,7 @@ def execute(self, *args, **kwargs): s.resource = query s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + s.set_tags(self._datadog_meta) # FIXME[leo]: convert to metric? s.set_tag(redisx.PIPELINE_LEN, len(self.command_stack)) s.set_tag(redisx.PIPELINE_AGE, time.time()-self._datadog_pipeline_creation) @@ -65,6 +67,7 @@ def immediate_execute_command(self, *args, **kwargs): s.resource = format_command_args(args) s.set_tag(redisx.CMD, (args or [None])[0]) s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + s.set_tags(self._datadog_meta) # FIXME[leo]: convert to metric? s.set_tag(redisx.ARGS_LEN, len(args)) @@ -75,6 +78,11 @@ def immediate_execute_command(self, *args, **kwargs): class TracedRedis(baseclass): _datadog_tracer = ddtracer _datadog_service = service + _datadog_meta = meta + + @classmethod + def set_datadog_meta(cls, meta): + cls._datadog_meta = meta def execute_command(self, *args, **options): with self._datadog_tracer.trace('redis.command') as s: @@ -86,17 +94,20 @@ def execute_command(self, *args, **options): s.resource = format_command_args(args) s.set_tag(redisx.CMD, (args or [None])[0]) s.set_tags(_extract_conn_tags(self.connection_pool.connection_kwargs)) + s.set_tags(self._datadog_meta) # FIXME[leo]: convert to metric? s.set_tag(redisx.ARGS_LEN, len(args)) return super(TracedRedis, self).execute_command(*args, **options) def pipeline(self, transaction=True, shard_hint=None): - return TracedPipeline( + tp = TracedPipeline( self.connection_pool, self.response_callbacks, transaction, shard_hint ) + tp._datadog_meta = meta + return tp return TracedRedis diff --git a/tests/contrib/redis/test.py b/tests/contrib/redis/test.py index 326bbd933f9..b7911c06635 100644 --- a/tests/contrib/redis/test.py +++ b/tests/contrib/redis/test.py @@ -48,6 +48,19 @@ def test_basic_class(self): eq_(span.meta, {'out.host': u'localhost', 'redis.command': u'GET', 'out.port': u'6379', 'redis.args_length': u'2', 'out.redis_db': u'0'}) eq_(span.resource, 'GET cheese') + def test_meta_override(self): + writer = DummyWriter() + tracer = Tracer(writer=writer) + + TracedRedisCache = get_traced_redis(tracer, service=self.SERVICE, meta={'cheese': 'camembert'}) + r = TracedRedisCache() + + r.get('cheese') + spans = writer.pop() + eq_(len(spans), 1) + span = spans[0] + eq_(span.service, self.SERVICE) + ok_('cheese' in span.meta and span.meta['cheese'] == 'camembert') def test_basic_class_pipeline(self): writer = DummyWriter()