Skip to content

Commit 8823655

Browse files
Instrument RedisCluster clients (#1177)
* Instrument RedisCluster clients * reformat files * update changelogs * refactor _traced_execute_pipeline * handle AttributeError * handle IndexError * refactor _traced_execute_pipeline * move hasattr check to _set_connection_attributes function Co-authored-by: Srikanth Chekuri <[email protected]>
1 parent ee40839 commit 8823655

File tree

5 files changed

+208
-6
lines changed

5 files changed

+208
-6
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.12.0rc2-0.32b0...HEAD)
99

10+
### Added
11+
- `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients
12+
([#1177](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1177))
13+
1014
## [1.12.0rc2-0.32b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0rc2-0.32b0) - 2022-07-01
1115

1216

Diff for: instrumentation/opentelemetry-instrumentation-redis/src/opentelemetry/instrumentation/redis/__init__.py

+55-5
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,12 @@ def response_hook(span, instance, response):
122122
if redis.VERSION >= _REDIS_ASYNCIO_VERSION:
123123
import redis.asyncio
124124

125+
_REDIS_CLUSTER_VERSION = (4, 1, 0)
126+
_REDIS_ASYNCIO_CLUSTER_VERSION = (4, 3, 0)
127+
125128

126129
def _set_connection_attributes(span, conn):
127-
if not span.is_recording():
130+
if not span.is_recording() or not hasattr(conn, "connection_pool"):
128131
return
129132
for key, value in _extract_conn_attributes(
130133
conn.connection_pool.connection_kwargs
@@ -159,10 +162,29 @@ def _traced_execute_command(func, instance, args, kwargs):
159162
return response
160163

161164
def _traced_execute_pipeline(func, instance, args, kwargs):
162-
cmds = [_format_command_args(c) for c, _ in instance.command_stack]
163-
resource = "\n".join(cmds)
165+
try:
166+
command_stack = (
167+
instance.command_stack
168+
if hasattr(instance, "command_stack")
169+
else instance._command_stack
170+
)
164171

165-
span_name = " ".join([args[0] for args, _ in instance.command_stack])
172+
cmds = [
173+
_format_command_args(c.args if hasattr(c, "args") else c[0])
174+
for c in command_stack
175+
]
176+
resource = "\n".join(cmds)
177+
178+
span_name = " ".join(
179+
[
180+
(c.args[0] if hasattr(c, "args") else c[0][0])
181+
for c in command_stack
182+
]
183+
)
184+
except (AttributeError, IndexError):
185+
command_stack = []
186+
resource = ""
187+
span_name = ""
166188

167189
with tracer.start_as_current_span(
168190
span_name, kind=trace.SpanKind.CLIENT
@@ -171,7 +193,7 @@ def _traced_execute_pipeline(func, instance, args, kwargs):
171193
span.set_attribute(SpanAttributes.DB_STATEMENT, resource)
172194
_set_connection_attributes(span, instance)
173195
span.set_attribute(
174-
"db.redis.pipeline_length", len(instance.command_stack)
196+
"db.redis.pipeline_length", len(command_stack)
175197
)
176198
response = func(*args, **kwargs)
177199
if callable(response_hook):
@@ -196,6 +218,17 @@ def _traced_execute_pipeline(func, instance, args, kwargs):
196218
f"{pipeline_class}.immediate_execute_command",
197219
_traced_execute_command,
198220
)
221+
if redis.VERSION >= _REDIS_CLUSTER_VERSION:
222+
wrap_function_wrapper(
223+
"redis.cluster",
224+
"RedisCluster.execute_command",
225+
_traced_execute_command,
226+
)
227+
wrap_function_wrapper(
228+
"redis.cluster",
229+
"ClusterPipeline.execute",
230+
_traced_execute_pipeline,
231+
)
199232
if redis.VERSION >= _REDIS_ASYNCIO_VERSION:
200233
wrap_function_wrapper(
201234
"redis.asyncio",
@@ -212,6 +245,17 @@ def _traced_execute_pipeline(func, instance, args, kwargs):
212245
f"{pipeline_class}.immediate_execute_command",
213246
_traced_execute_command,
214247
)
248+
if redis.VERSION >= _REDIS_ASYNCIO_CLUSTER_VERSION:
249+
wrap_function_wrapper(
250+
"redis.asyncio.cluster",
251+
"RedisCluster.execute_command",
252+
_traced_execute_command,
253+
)
254+
wrap_function_wrapper(
255+
"redis.asyncio.cluster",
256+
"ClusterPipeline.execute",
257+
_traced_execute_pipeline,
258+
)
215259

216260

217261
class RedisInstrumentor(BaseInstrumentor):
@@ -258,8 +302,14 @@ def _uninstrument(self, **kwargs):
258302
unwrap(redis.Redis, "pipeline")
259303
unwrap(redis.client.Pipeline, "execute")
260304
unwrap(redis.client.Pipeline, "immediate_execute_command")
305+
if redis.VERSION >= _REDIS_CLUSTER_VERSION:
306+
unwrap(redis.cluster.RedisCluster, "execute_command")
307+
unwrap(redis.cluster.ClusterPipeline, "execute")
261308
if redis.VERSION >= _REDIS_ASYNCIO_VERSION:
262309
unwrap(redis.asyncio.Redis, "execute_command")
263310
unwrap(redis.asyncio.Redis, "pipeline")
264311
unwrap(redis.asyncio.client.Pipeline, "execute")
265312
unwrap(redis.asyncio.client.Pipeline, "immediate_execute_command")
313+
if redis.VERSION >= _REDIS_ASYNCIO_CLUSTER_VERSION:
314+
unwrap(redis.asyncio.cluster.RedisCluster, "execute_command")
315+
unwrap(redis.asyncio.cluster.ClusterPipeline, "execute")

Diff for: tests/opentelemetry-docker-tests/tests/docker-compose.yml

+11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@ services:
2727
image: redis:4.0-alpine
2828
ports:
2929
- "127.0.0.1:6379:6379"
30+
otrediscluster:
31+
image: grokzen/redis-cluster:6.2.0
32+
environment:
33+
- IP=0.0.0.0
34+
ports:
35+
- "127.0.0.1:7000:7000"
36+
- "127.0.0.1:7001:7001"
37+
- "127.0.0.1:7002:7002"
38+
- "127.0.0.1:7003:7003"
39+
- "127.0.0.1:7004:7004"
40+
- "127.0.0.1:7005:7005"
3041
otjaeger:
3142
image: jaegertracing/all-in-one:1.8
3243
environment:

Diff for: tests/opentelemetry-docker-tests/tests/redis/test_redis_functional.py

+137
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,72 @@ def test_parent(self):
124124
self.assertEqual(child_span.name, "GET")
125125

126126

127+
class TestRedisClusterInstrument(TestBase):
128+
def setUp(self):
129+
super().setUp()
130+
self.redis_client = redis.cluster.RedisCluster(
131+
host="localhost", port=7000
132+
)
133+
self.redis_client.flushall()
134+
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
135+
136+
def tearDown(self):
137+
super().tearDown()
138+
RedisInstrumentor().uninstrument()
139+
140+
def _check_span(self, span, name):
141+
self.assertEqual(span.name, name)
142+
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
143+
144+
def test_basics(self):
145+
self.assertIsNone(self.redis_client.get("cheese"))
146+
spans = self.memory_exporter.get_finished_spans()
147+
self.assertEqual(len(spans), 1)
148+
span = spans[0]
149+
self._check_span(span, "GET")
150+
self.assertEqual(
151+
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
152+
)
153+
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
154+
155+
def test_pipeline_traced(self):
156+
with self.redis_client.pipeline(transaction=False) as pipeline:
157+
pipeline.set("blah", 32)
158+
pipeline.rpush("foo", "éé")
159+
pipeline.hgetall("xxx")
160+
pipeline.execute()
161+
162+
spans = self.memory_exporter.get_finished_spans()
163+
self.assertEqual(len(spans), 1)
164+
span = spans[0]
165+
self._check_span(span, "SET RPUSH HGETALL")
166+
self.assertEqual(
167+
span.attributes.get(SpanAttributes.DB_STATEMENT),
168+
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
169+
)
170+
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
171+
172+
def test_parent(self):
173+
"""Ensure OpenTelemetry works with redis."""
174+
ot_tracer = trace.get_tracer("redis_svc")
175+
176+
with ot_tracer.start_as_current_span("redis_get"):
177+
self.assertIsNone(self.redis_client.get("cheese"))
178+
179+
spans = self.memory_exporter.get_finished_spans()
180+
self.assertEqual(len(spans), 2)
181+
child_span, parent_span = spans[0], spans[1]
182+
183+
# confirm the parenting
184+
self.assertIsNone(parent_span.parent)
185+
self.assertIs(child_span.parent, parent_span.get_span_context())
186+
187+
self.assertEqual(parent_span.name, "redis_get")
188+
self.assertEqual(parent_span.instrumentation_info.name, "redis_svc")
189+
190+
self.assertEqual(child_span.name, "GET")
191+
192+
127193
def async_call(coro):
128194
loop = asyncio.get_event_loop()
129195
return loop.run_until_complete(coro)
@@ -238,6 +304,77 @@ def test_parent(self):
238304
self.assertEqual(child_span.name, "GET")
239305

240306

307+
class TestAsyncRedisClusterInstrument(TestBase):
308+
def setUp(self):
309+
super().setUp()
310+
self.redis_client = redis.asyncio.cluster.RedisCluster(
311+
host="localhost", port=7000
312+
)
313+
async_call(self.redis_client.flushall())
314+
RedisInstrumentor().instrument(tracer_provider=self.tracer_provider)
315+
316+
def tearDown(self):
317+
super().tearDown()
318+
RedisInstrumentor().uninstrument()
319+
320+
def _check_span(self, span, name):
321+
self.assertEqual(span.name, name)
322+
self.assertIs(span.status.status_code, trace.StatusCode.UNSET)
323+
324+
def test_basics(self):
325+
self.assertIsNone(async_call(self.redis_client.get("cheese")))
326+
spans = self.memory_exporter.get_finished_spans()
327+
self.assertEqual(len(spans), 1)
328+
span = spans[0]
329+
self._check_span(span, "GET")
330+
self.assertEqual(
331+
span.attributes.get(SpanAttributes.DB_STATEMENT), "GET cheese"
332+
)
333+
self.assertEqual(span.attributes.get("db.redis.args_length"), 2)
334+
335+
def test_pipeline_traced(self):
336+
async def pipeline_simple():
337+
async with self.redis_client.pipeline(
338+
transaction=False
339+
) as pipeline:
340+
pipeline.set("blah", 32)
341+
pipeline.rpush("foo", "éé")
342+
pipeline.hgetall("xxx")
343+
await pipeline.execute()
344+
345+
async_call(pipeline_simple())
346+
347+
spans = self.memory_exporter.get_finished_spans()
348+
self.assertEqual(len(spans), 1)
349+
span = spans[0]
350+
self._check_span(span, "SET RPUSH HGETALL")
351+
self.assertEqual(
352+
span.attributes.get(SpanAttributes.DB_STATEMENT),
353+
"SET blah 32\nRPUSH foo éé\nHGETALL xxx",
354+
)
355+
self.assertEqual(span.attributes.get("db.redis.pipeline_length"), 3)
356+
357+
def test_parent(self):
358+
"""Ensure OpenTelemetry works with redis."""
359+
ot_tracer = trace.get_tracer("redis_svc")
360+
361+
with ot_tracer.start_as_current_span("redis_get"):
362+
self.assertIsNone(async_call(self.redis_client.get("cheese")))
363+
364+
spans = self.memory_exporter.get_finished_spans()
365+
self.assertEqual(len(spans), 2)
366+
child_span, parent_span = spans[0], spans[1]
367+
368+
# confirm the parenting
369+
self.assertIsNone(parent_span.parent)
370+
self.assertIs(child_span.parent, parent_span.get_span_context())
371+
372+
self.assertEqual(parent_span.name, "redis_get")
373+
self.assertEqual(parent_span.instrumentation_info.name, "redis_svc")
374+
375+
self.assertEqual(child_span.name, "GET")
376+
377+
241378
class TestRedisDBIndexInstrument(TestBase):
242379
def setUp(self):
243380
super().setUp()

Diff for: tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ deps =
501501
psycopg2 ~= 2.8.4
502502
aiopg >= 0.13.0, < 1.3.0
503503
sqlalchemy ~= 1.4
504-
redis ~= 4.2
504+
redis ~= 4.3
505505
celery[pytest] >= 4.0, < 6.0
506506
protobuf~=3.13
507507
requests==2.25.0

0 commit comments

Comments
 (0)