Skip to content

Commit ae5bca6

Browse files
committed
feat(integrations): add support for cluster clients from redis sdk
1 parent fee865c commit ae5bca6

File tree

7 files changed

+407
-37
lines changed

7 files changed

+407
-37
lines changed

sentry_sdk/integrations/redis/__init__.py

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414

1515
if TYPE_CHECKING:
16-
from typing import Any, Dict, Sequence
16+
from typing import Any, Dict, Sequence, Callable
1717
from sentry_sdk.tracing import Span
1818

1919
_SINGLE_KEY_COMMANDS = frozenset(
@@ -83,8 +83,7 @@ def _set_pipeline_data(
8383
):
8484
# type: (Span, bool, Any, bool, Sequence[Any]) -> None
8585
span.set_tag("redis.is_cluster", is_cluster)
86-
transaction = is_transaction if not is_cluster else False
87-
span.set_tag("redis.transaction", transaction)
86+
span.set_tag("redis.transaction", is_transaction)
8887

8988
commands = []
9089
for i, arg in enumerate(command_stack):
@@ -118,7 +117,7 @@ def _set_client_data(span, is_cluster, name, *args):
118117
span.set_tag("redis.key", args[0])
119118

120119

121-
def _set_db_data(span, connection_params):
120+
def _set_db_data_on_span(span, connection_params):
122121
# type: (Span, Dict[str, Any]) -> None
123122
span.set_data(SPANDATA.DB_SYSTEM, "redis")
124123

@@ -135,8 +134,34 @@ def _set_db_data(span, connection_params):
135134
span.set_data(SPANDATA.SERVER_PORT, port)
136135

137136

138-
def patch_redis_pipeline(pipeline_cls, is_cluster, get_command_args_fn):
139-
# type: (Any, bool, Any) -> None
137+
def _set_db_data(span, redis_instance):
138+
# type: (Span, Any) -> None
139+
_set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs)
140+
141+
142+
def _set_cluster_db_data(span, redis_cluster_instance):
143+
# type: (Span, Any) -> None
144+
default_node = redis_cluster_instance.get_default_node()
145+
if default_node:
146+
_set_db_data_on_span(
147+
span, {"host": default_node.host, "port": default_node.port}
148+
)
149+
150+
151+
def _set_async_cluster_db_data(span, async_redis_cluster_instance):
152+
# type: (Span, Any) -> None
153+
default_node = async_redis_cluster_instance.get_default_node()
154+
if default_node and default_node.connection_kwargs:
155+
_set_db_data_on_span(span, default_node.connection_kwargs)
156+
157+
158+
def _set_async_cluster_pipeline_db_data(span, async_redis_cluster_pipeline_instance):
159+
# type: (Span, Any) -> None
160+
_set_async_cluster_db_data(span, async_redis_cluster_pipeline_instance._client)
161+
162+
163+
def patch_redis_pipeline(pipeline_cls, is_cluster, get_command_args_fn, set_db_data_fn):
164+
# type: (Any, bool, Any, Callable[[Span, Any], None]) -> None
140165
old_execute = pipeline_cls.execute
141166

142167
def sentry_patched_execute(self, *args, **kwargs):
@@ -150,12 +175,12 @@ def sentry_patched_execute(self, *args, **kwargs):
150175
op=OP.DB_REDIS, description="redis.pipeline.execute"
151176
) as span:
152177
with capture_internal_exceptions():
153-
_set_db_data(span, self.connection_pool.connection_kwargs)
178+
set_db_data_fn(span, self)
154179
_set_pipeline_data(
155180
span,
156181
is_cluster,
157182
get_command_args_fn,
158-
self.transaction,
183+
False if is_cluster else self.transaction,
159184
self.command_stack,
160185
)
161186

@@ -164,8 +189,8 @@ def sentry_patched_execute(self, *args, **kwargs):
164189
pipeline_cls.execute = sentry_patched_execute
165190

166191

167-
def patch_redis_client(cls, is_cluster):
168-
# type: (Any, bool) -> None
192+
def patch_redis_client(cls, is_cluster, set_db_data_fn):
193+
# type: (Any, bool, Callable[[Span, Any], None]) -> None
169194
"""
170195
This function can be used to instrument custom redis client classes or
171196
subclasses.
@@ -189,7 +214,7 @@ def sentry_patched_execute_command(self, name, *args, **kwargs):
189214
description = description[: integration.max_data_size - len("...")] + "..."
190215

191216
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
192-
_set_db_data(span, self.connection_pool.connection_kwargs)
217+
set_db_data_fn(span, self)
193218
_set_client_data(span, is_cluster, name, *args)
194219

195220
return old_execute_command(self, name, *args, **kwargs)
@@ -199,14 +224,16 @@ def sentry_patched_execute_command(self, name, *args, **kwargs):
199224

200225
def _patch_redis(StrictRedis, client): # noqa: N803
201226
# type: (Any, Any) -> None
202-
patch_redis_client(StrictRedis, is_cluster=False)
203-
patch_redis_pipeline(client.Pipeline, False, _get_redis_command_args)
227+
patch_redis_client(StrictRedis, is_cluster=False, set_db_data_fn=_set_db_data)
228+
patch_redis_pipeline(client.Pipeline, False, _get_redis_command_args, _set_db_data)
204229
try:
205230
strict_pipeline = client.StrictPipeline
206231
except AttributeError:
207232
pass
208233
else:
209-
patch_redis_pipeline(strict_pipeline, False, _get_redis_command_args)
234+
patch_redis_pipeline(
235+
strict_pipeline, False, _get_redis_command_args, _set_db_data
236+
)
210237

211238
try:
212239
import redis.asyncio
@@ -218,8 +245,56 @@ def _patch_redis(StrictRedis, client): # noqa: N803
218245
patch_redis_async_pipeline,
219246
)
220247

221-
patch_redis_async_client(redis.asyncio.client.StrictRedis)
222-
patch_redis_async_pipeline(redis.asyncio.client.Pipeline)
248+
patch_redis_async_client(
249+
redis.asyncio.client.StrictRedis,
250+
is_cluster=False,
251+
set_db_data_fn=_set_db_data,
252+
)
253+
patch_redis_async_pipeline(
254+
redis.asyncio.client.Pipeline,
255+
False,
256+
_get_redis_command_args,
257+
set_db_data_fn=_set_db_data,
258+
)
259+
260+
261+
def _patch_redis_cluster():
262+
# type: () -> None
263+
"""Patches the cluster module on redis SDK (as opposed to rediscluster library)"""
264+
try:
265+
from redis import RedisCluster, cluster
266+
except ImportError:
267+
pass
268+
else:
269+
patch_redis_client(RedisCluster, True, _set_cluster_db_data)
270+
patch_redis_pipeline(
271+
cluster.ClusterPipeline,
272+
True,
273+
_parse_rediscluster_command,
274+
_set_cluster_db_data,
275+
)
276+
277+
try:
278+
from redis.asyncio import cluster as async_cluster
279+
except ImportError:
280+
pass
281+
else:
282+
from sentry_sdk.integrations.redis.asyncio import (
283+
patch_redis_async_client,
284+
patch_redis_async_pipeline,
285+
)
286+
287+
patch_redis_async_client(
288+
async_cluster.RedisCluster,
289+
is_cluster=True,
290+
set_db_data_fn=_set_async_cluster_db_data,
291+
)
292+
patch_redis_async_pipeline(
293+
async_cluster.ClusterPipeline,
294+
True,
295+
_parse_rediscluster_command,
296+
set_db_data_fn=_set_async_cluster_pipeline_db_data,
297+
)
223298

224299

225300
def _patch_rb():
@@ -229,9 +304,15 @@ def _patch_rb():
229304
except ImportError:
230305
pass
231306
else:
232-
patch_redis_client(rb.clients.FanoutClient, is_cluster=False)
233-
patch_redis_client(rb.clients.MappingClient, is_cluster=False)
234-
patch_redis_client(rb.clients.RoutingClient, is_cluster=False)
307+
patch_redis_client(
308+
rb.clients.FanoutClient, is_cluster=False, set_db_data_fn=_set_db_data
309+
)
310+
patch_redis_client(
311+
rb.clients.MappingClient, is_cluster=False, set_db_data_fn=_set_db_data
312+
)
313+
patch_redis_client(
314+
rb.clients.RoutingClient, is_cluster=False, set_db_data_fn=_set_db_data
315+
)
235316

236317

237318
def _patch_rediscluster():
@@ -241,7 +322,9 @@ def _patch_rediscluster():
241322
except ImportError:
242323
return
243324

244-
patch_redis_client(rediscluster.RedisCluster, is_cluster=True)
325+
patch_redis_client(
326+
rediscluster.RedisCluster, is_cluster=True, set_db_data_fn=_set_db_data
327+
)
245328

246329
# up to v1.3.6, __version__ attribute is a tuple
247330
# from v2.0.0, __version__ is a string and VERSION a tuple
@@ -251,11 +334,17 @@ def _patch_rediscluster():
251334
# https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst
252335
if (0, 2, 0) < version < (2, 0, 0):
253336
pipeline_cls = rediscluster.pipeline.StrictClusterPipeline
254-
patch_redis_client(rediscluster.StrictRedisCluster, is_cluster=True)
337+
patch_redis_client(
338+
rediscluster.StrictRedisCluster,
339+
is_cluster=True,
340+
set_db_data_fn=_set_db_data,
341+
)
255342
else:
256343
pipeline_cls = rediscluster.pipeline.ClusterPipeline
257344

258-
patch_redis_pipeline(pipeline_cls, True, _parse_rediscluster_command)
345+
patch_redis_pipeline(
346+
pipeline_cls, True, _parse_rediscluster_command, set_db_data_fn=_set_db_data
347+
)
259348

260349

261350
class RedisIntegration(Integration):
@@ -274,6 +363,7 @@ def setup_once():
274363
raise DidNotEnable("Redis client not installed")
275364

276365
_patch_redis(StrictRedis, client)
366+
_patch_redis_cluster()
277367
_patch_rb()
278368

279369
try:

sentry_sdk/integrations/redis/asyncio.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@
44
from sentry_sdk.consts import OP
55
from sentry_sdk.integrations.redis import (
66
RedisIntegration,
7-
_get_redis_command_args,
87
_get_span_description,
98
_set_client_data,
10-
_set_db_data,
119
_set_pipeline_data,
1210
)
1311
from sentry_sdk._types import TYPE_CHECKING
12+
from sentry_sdk.tracing import Span
1413
from sentry_sdk.utils import capture_internal_exceptions
1514

1615
if TYPE_CHECKING:
17-
from typing import Any
16+
from typing import Any, Callable
1817

1918

20-
def patch_redis_async_pipeline(pipeline_cls):
21-
# type: (Any) -> None
19+
def patch_redis_async_pipeline(
20+
pipeline_cls, is_cluster, get_command_args_fn, set_db_data_fn
21+
):
22+
# type: (Any, bool, Any, Callable[[Span, Any], None]) -> None
2223
old_execute = pipeline_cls.execute
2324

2425
async def _sentry_execute(self, *args, **kwargs):
@@ -32,22 +33,22 @@ async def _sentry_execute(self, *args, **kwargs):
3233
op=OP.DB_REDIS, description="redis.pipeline.execute"
3334
) as span:
3435
with capture_internal_exceptions():
35-
_set_db_data(span, self.connection_pool.connection_kwargs)
36+
set_db_data_fn(span, self)
3637
_set_pipeline_data(
3738
span,
38-
False,
39-
_get_redis_command_args,
40-
self.is_transaction,
41-
self.command_stack,
39+
is_cluster,
40+
get_command_args_fn,
41+
False if is_cluster else self.is_transaction,
42+
self._command_stack if is_cluster else self.command_stack,
4243
)
4344

4445
return await old_execute(self, *args, **kwargs)
4546

4647
pipeline_cls.execute = _sentry_execute
4748

4849

49-
def patch_redis_async_client(cls):
50-
# type: (Any) -> None
50+
def patch_redis_async_client(cls, is_cluster, set_db_data_fn):
51+
# type: (Any, bool, Callable[[Span, Any], None]) -> None
5152
old_execute_command = cls.execute_command
5253

5354
async def _sentry_execute_command(self, name, *args, **kwargs):
@@ -60,8 +61,8 @@ async def _sentry_execute_command(self, name, *args, **kwargs):
6061
description = _get_span_description(name, *args)
6162

6263
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
63-
_set_db_data(span, self.connection_pool.connection_kwargs)
64-
_set_client_data(span, False, name, *args)
64+
set_db_data_fn(span, self)
65+
_set_client_data(span, is_cluster, name, *args)
6566

6667
return await old_execute_command(self, name, *args, **kwargs)
6768

tests/integrations/redis/asyncio/test_redis_asyncio.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99

1010
@pytest.mark.asyncio
1111
async def test_async_basic(sentry_init, capture_events):
12-
sentry_init(integrations=[RedisIntegration()])
12+
sentry_init(
13+
integrations=[RedisIntegration()],
14+
traces_sample_rate=1.0,
15+
send_default_pii=True,
16+
)
1317
events = capture_events()
1418

1519
connection = FakeRedis()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import pytest
2+
3+
pytest.importorskip("redis.cluster")

0 commit comments

Comments
 (0)