aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis')
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py38
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py108
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py113
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py19
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py121
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py68
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py32
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py69
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py99
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py50
-rw-r--r--.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py144
12 files changed, 861 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py
new file mode 100644
index 00000000..f4431382
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/__init__.py
@@ -0,0 +1,38 @@
+from sentry_sdk.integrations import Integration, DidNotEnable
+from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE
+from sentry_sdk.integrations.redis.rb import _patch_rb
+from sentry_sdk.integrations.redis.redis import _patch_redis
+from sentry_sdk.integrations.redis.redis_cluster import _patch_redis_cluster
+from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster
+from sentry_sdk.utils import logger
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Optional
+
+
+class RedisIntegration(Integration):
+ identifier = "redis"
+
+ def __init__(self, max_data_size=_DEFAULT_MAX_DATA_SIZE, cache_prefixes=None):
+ # type: (int, Optional[list[str]]) -> None
+ self.max_data_size = max_data_size
+ self.cache_prefixes = cache_prefixes if cache_prefixes is not None else []
+
+ @staticmethod
+ def setup_once():
+ # type: () -> None
+ try:
+ from redis import StrictRedis, client
+ except ImportError:
+ raise DidNotEnable("Redis client not installed")
+
+ _patch_redis(StrictRedis, client)
+ _patch_redis_cluster()
+ _patch_rb()
+
+ try:
+ _patch_rediscluster()
+ except Exception:
+ logger.exception("Error occurred while patching `rediscluster` library")
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py
new file mode 100644
index 00000000..196e85e7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_async_common.py
@@ -0,0 +1,108 @@
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN
+from sentry_sdk.integrations.redis.modules.caches import (
+ _compile_cache_span_properties,
+ _set_cache_data,
+)
+from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
+from sentry_sdk.integrations.redis.utils import (
+ _set_client_data,
+ _set_pipeline_data,
+)
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any, Union
+ from redis.asyncio.client import Pipeline, StrictRedis
+ from redis.asyncio.cluster import ClusterPipeline, RedisCluster
+
+
+def patch_redis_async_pipeline(
+ pipeline_cls, is_cluster, get_command_args_fn, set_db_data_fn
+):
+ # type: (Union[type[Pipeline[Any]], type[ClusterPipeline[Any]]], bool, Any, Callable[[Span, Any], None]) -> None
+ old_execute = pipeline_cls.execute
+
+ from sentry_sdk.integrations.redis import RedisIntegration
+
+ async def _sentry_execute(self, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ if sentry_sdk.get_client().get_integration(RedisIntegration) is None:
+ return await old_execute(self, *args, **kwargs)
+
+ with sentry_sdk.start_span(
+ op=OP.DB_REDIS,
+ name="redis.pipeline.execute",
+ origin=SPAN_ORIGIN,
+ ) as span:
+ with capture_internal_exceptions():
+ set_db_data_fn(span, self)
+ _set_pipeline_data(
+ span,
+ is_cluster,
+ get_command_args_fn,
+ False if is_cluster else self.is_transaction,
+ self._command_stack if is_cluster else self.command_stack,
+ )
+
+ return await old_execute(self, *args, **kwargs)
+
+ pipeline_cls.execute = _sentry_execute # type: ignore
+
+
+def patch_redis_async_client(cls, is_cluster, set_db_data_fn):
+ # type: (Union[type[StrictRedis[Any]], type[RedisCluster[Any]]], bool, Callable[[Span, Any], None]) -> None
+ old_execute_command = cls.execute_command
+
+ from sentry_sdk.integrations.redis import RedisIntegration
+
+ async def _sentry_execute_command(self, name, *args, **kwargs):
+ # type: (Any, str, *Any, **Any) -> Any
+ integration = sentry_sdk.get_client().get_integration(RedisIntegration)
+ if integration is None:
+ return await old_execute_command(self, name, *args, **kwargs)
+
+ cache_properties = _compile_cache_span_properties(
+ name,
+ args,
+ kwargs,
+ integration,
+ )
+
+ cache_span = None
+ if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
+ cache_span = sentry_sdk.start_span(
+ op=cache_properties["op"],
+ name=cache_properties["description"],
+ origin=SPAN_ORIGIN,
+ )
+ cache_span.__enter__()
+
+ db_properties = _compile_db_span_properties(integration, name, args)
+
+ db_span = sentry_sdk.start_span(
+ op=db_properties["op"],
+ name=db_properties["description"],
+ origin=SPAN_ORIGIN,
+ )
+ db_span.__enter__()
+
+ set_db_data_fn(db_span, self)
+ _set_client_data(db_span, is_cluster, name, *args)
+
+ value = await old_execute_command(self, name, *args, **kwargs)
+
+ db_span.__exit__(None, None, None)
+
+ if cache_span:
+ _set_cache_data(cache_span, self, cache_properties, value)
+ cache_span.__exit__(None, None, None)
+
+ return value
+
+ cls.execute_command = _sentry_execute_command # type: ignore
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py
new file mode 100644
index 00000000..ef10e9e4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/_sync_common.py
@@ -0,0 +1,113 @@
+import sentry_sdk
+from sentry_sdk.consts import OP
+from sentry_sdk.integrations.redis.consts import SPAN_ORIGIN
+from sentry_sdk.integrations.redis.modules.caches import (
+ _compile_cache_span_properties,
+ _set_cache_data,
+)
+from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
+from sentry_sdk.integrations.redis.utils import (
+ _set_client_data,
+ _set_pipeline_data,
+)
+from sentry_sdk.tracing import Span
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Any
+
+
+def patch_redis_pipeline(
+ pipeline_cls,
+ is_cluster,
+ get_command_args_fn,
+ set_db_data_fn,
+):
+ # type: (Any, bool, Any, Callable[[Span, Any], None]) -> None
+ old_execute = pipeline_cls.execute
+
+ from sentry_sdk.integrations.redis import RedisIntegration
+
+ def sentry_patched_execute(self, *args, **kwargs):
+ # type: (Any, *Any, **Any) -> Any
+ if sentry_sdk.get_client().get_integration(RedisIntegration) is None:
+ return old_execute(self, *args, **kwargs)
+
+ with sentry_sdk.start_span(
+ op=OP.DB_REDIS,
+ name="redis.pipeline.execute",
+ origin=SPAN_ORIGIN,
+ ) as span:
+ with capture_internal_exceptions():
+ set_db_data_fn(span, self)
+ _set_pipeline_data(
+ span,
+ is_cluster,
+ get_command_args_fn,
+ False if is_cluster else self.transaction,
+ self.command_stack,
+ )
+
+ return old_execute(self, *args, **kwargs)
+
+ pipeline_cls.execute = sentry_patched_execute
+
+
+def patch_redis_client(cls, is_cluster, set_db_data_fn):
+ # type: (Any, bool, Callable[[Span, Any], None]) -> None
+ """
+ This function can be used to instrument custom redis client classes or
+ subclasses.
+ """
+ old_execute_command = cls.execute_command
+
+ from sentry_sdk.integrations.redis import RedisIntegration
+
+ def sentry_patched_execute_command(self, name, *args, **kwargs):
+ # type: (Any, str, *Any, **Any) -> Any
+ integration = sentry_sdk.get_client().get_integration(RedisIntegration)
+ if integration is None:
+ return old_execute_command(self, name, *args, **kwargs)
+
+ cache_properties = _compile_cache_span_properties(
+ name,
+ args,
+ kwargs,
+ integration,
+ )
+
+ cache_span = None
+ if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
+ cache_span = sentry_sdk.start_span(
+ op=cache_properties["op"],
+ name=cache_properties["description"],
+ origin=SPAN_ORIGIN,
+ )
+ cache_span.__enter__()
+
+ db_properties = _compile_db_span_properties(integration, name, args)
+
+ db_span = sentry_sdk.start_span(
+ op=db_properties["op"],
+ name=db_properties["description"],
+ origin=SPAN_ORIGIN,
+ )
+ db_span.__enter__()
+
+ set_db_data_fn(db_span, self)
+ _set_client_data(db_span, is_cluster, name, *args)
+
+ value = old_execute_command(self, name, *args, **kwargs)
+
+ db_span.__exit__(None, None, None)
+
+ if cache_span:
+ _set_cache_data(cache_span, self, cache_properties, value)
+ cache_span.__exit__(None, None, None)
+
+ return value
+
+ cls.execute_command = sentry_patched_execute_command
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py
new file mode 100644
index 00000000..737e8297
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/consts.py
@@ -0,0 +1,19 @@
+SPAN_ORIGIN = "auto.db.redis"
+
+_SINGLE_KEY_COMMANDS = frozenset(
+ ["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"],
+)
+_MULTI_KEY_COMMANDS = frozenset(
+ [
+ "del",
+ "touch",
+ "unlink",
+ "mget",
+ ],
+)
+_COMMANDS_INCLUDING_SENSITIVE_DATA = [
+ "auth",
+]
+_MAX_NUM_ARGS = 10 # Trim argument lists to this many values
+_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values
+_DEFAULT_MAX_DATA_SIZE = 1024
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py
new file mode 100644
index 00000000..c6fc19f5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/caches.py
@@ -0,0 +1,121 @@
+"""
+Code used for the Caches module in Sentry
+"""
+
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
+from sentry_sdk.utils import capture_internal_exceptions
+
+GET_COMMANDS = ("get", "mget")
+SET_COMMANDS = ("set", "setex")
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from sentry_sdk.integrations.redis import RedisIntegration
+ from sentry_sdk.tracing import Span
+ from typing import Any, Optional
+
+
+def _get_op(name):
+ # type: (str) -> Optional[str]
+ op = None
+ if name.lower() in GET_COMMANDS:
+ op = OP.CACHE_GET
+ elif name.lower() in SET_COMMANDS:
+ op = OP.CACHE_PUT
+
+ return op
+
+
+def _compile_cache_span_properties(redis_command, args, kwargs, integration):
+ # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> dict[str, Any]
+ key = _get_safe_key(redis_command, args, kwargs)
+ key_as_string = _key_as_string(key)
+ keys_as_string = key_as_string.split(", ")
+
+ is_cache_key = False
+ for prefix in integration.cache_prefixes:
+ for kee in keys_as_string:
+ if kee.startswith(prefix):
+ is_cache_key = True
+ break
+ if is_cache_key:
+ break
+
+ value = None
+ if redis_command.lower() in SET_COMMANDS:
+ value = args[-1]
+
+ properties = {
+ "op": _get_op(redis_command),
+ "description": _get_cache_span_description(
+ redis_command, args, kwargs, integration
+ ),
+ "key": key,
+ "key_as_string": key_as_string,
+ "redis_command": redis_command.lower(),
+ "is_cache_key": is_cache_key,
+ "value": value,
+ }
+
+ return properties
+
+
+def _get_cache_span_description(redis_command, args, kwargs, integration):
+ # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> str
+ description = _key_as_string(_get_safe_key(redis_command, args, kwargs))
+
+ data_should_be_truncated = (
+ integration.max_data_size and len(description) > integration.max_data_size
+ )
+ if data_should_be_truncated:
+ description = description[: integration.max_data_size - len("...")] + "..."
+
+ return description
+
+
+def _set_cache_data(span, redis_client, properties, return_value):
+ # type: (Span, Any, dict[str, Any], Optional[Any]) -> None
+ with capture_internal_exceptions():
+ span.set_data(SPANDATA.CACHE_KEY, properties["key"])
+
+ if properties["redis_command"] in GET_COMMANDS:
+ if return_value is not None:
+ span.set_data(SPANDATA.CACHE_HIT, True)
+ size = (
+ len(str(return_value).encode("utf-8"))
+ if not isinstance(return_value, bytes)
+ else len(return_value)
+ )
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
+ else:
+ span.set_data(SPANDATA.CACHE_HIT, False)
+
+ elif properties["redis_command"] in SET_COMMANDS:
+ if properties["value"] is not None:
+ size = (
+ len(properties["value"].encode("utf-8"))
+ if not isinstance(properties["value"], bytes)
+ else len(properties["value"])
+ )
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
+
+ try:
+ connection_params = redis_client.connection_pool.connection_kwargs
+ except AttributeError:
+ # If it is a cluster, there is no connection_pool attribute so we
+ # need to get the default node from the cluster instance
+ default_node = redis_client.get_default_node()
+ connection_params = {
+ "host": default_node.host,
+ "port": default_node.port,
+ }
+
+ host = connection_params.get("host")
+ if host is not None:
+ span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, host)
+
+ port = connection_params.get("port")
+ if port is not None:
+ span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py
new file mode 100644
index 00000000..e0d85a4e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/modules/queries.py
@@ -0,0 +1,68 @@
+"""
+Code used for the Queries module in Sentry
+"""
+
+from sentry_sdk.consts import OP, SPANDATA
+from sentry_sdk.integrations.redis.utils import _get_safe_command
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from redis import Redis
+ from sentry_sdk.integrations.redis import RedisIntegration
+ from sentry_sdk.tracing import Span
+ from typing import Any
+
+
+def _compile_db_span_properties(integration, redis_command, args):
+ # type: (RedisIntegration, str, tuple[Any, ...]) -> dict[str, Any]
+ description = _get_db_span_description(integration, redis_command, args)
+
+ properties = {
+ "op": OP.DB_REDIS,
+ "description": description,
+ }
+
+ return properties
+
+
+def _get_db_span_description(integration, command_name, args):
+ # type: (RedisIntegration, str, tuple[Any, ...]) -> str
+ description = command_name
+
+ with capture_internal_exceptions():
+ description = _get_safe_command(command_name, args)
+
+ data_should_be_truncated = (
+ integration.max_data_size and len(description) > integration.max_data_size
+ )
+ if data_should_be_truncated:
+ description = description[: integration.max_data_size - len("...")] + "..."
+
+ return description
+
+
+def _set_db_data_on_span(span, connection_params):
+ # type: (Span, dict[str, Any]) -> None
+ span.set_data(SPANDATA.DB_SYSTEM, "redis")
+
+ db = connection_params.get("db")
+ if db is not None:
+ span.set_data(SPANDATA.DB_NAME, str(db))
+
+ host = connection_params.get("host")
+ if host is not None:
+ span.set_data(SPANDATA.SERVER_ADDRESS, host)
+
+ port = connection_params.get("port")
+ if port is not None:
+ span.set_data(SPANDATA.SERVER_PORT, port)
+
+
+def _set_db_data(span, redis_instance):
+ # type: (Span, Redis[Any]) -> None
+ try:
+ _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs)
+ except AttributeError:
+ pass # connections_kwargs may be missing in some cases
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py
new file mode 100644
index 00000000..1b3e2e53
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/rb.py
@@ -0,0 +1,32 @@
+"""
+Instrumentation for Redis Blaster (rb)
+
+https://github.com/getsentry/rb
+"""
+
+from sentry_sdk.integrations.redis._sync_common import patch_redis_client
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+
+
+def _patch_rb():
+ # type: () -> None
+ try:
+ import rb.clients # type: ignore
+ except ImportError:
+ pass
+ else:
+ patch_redis_client(
+ rb.clients.FanoutClient,
+ is_cluster=False,
+ set_db_data_fn=_set_db_data,
+ )
+ patch_redis_client(
+ rb.clients.MappingClient,
+ is_cluster=False,
+ set_db_data_fn=_set_db_data,
+ )
+ patch_redis_client(
+ rb.clients.RoutingClient,
+ is_cluster=False,
+ set_db_data_fn=_set_db_data,
+ )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py
new file mode 100644
index 00000000..c92958a3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis.py
@@ -0,0 +1,69 @@
+"""
+Instrumentation for Redis
+
+https://github.com/redis/redis-py
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+ patch_redis_client,
+ patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any, Sequence
+
+
+def _get_redis_command_args(command):
+ # type: (Any) -> Sequence[Any]
+ return command[0]
+
+
+def _patch_redis(StrictRedis, client): # noqa: N803
+ # type: (Any, Any) -> None
+ patch_redis_client(
+ StrictRedis,
+ is_cluster=False,
+ set_db_data_fn=_set_db_data,
+ )
+ patch_redis_pipeline(
+ client.Pipeline,
+ is_cluster=False,
+ get_command_args_fn=_get_redis_command_args,
+ set_db_data_fn=_set_db_data,
+ )
+ try:
+ strict_pipeline = client.StrictPipeline
+ except AttributeError:
+ pass
+ else:
+ patch_redis_pipeline(
+ strict_pipeline,
+ is_cluster=False,
+ get_command_args_fn=_get_redis_command_args,
+ set_db_data_fn=_set_db_data,
+ )
+
+ try:
+ import redis.asyncio
+ except ImportError:
+ pass
+ else:
+ from sentry_sdk.integrations.redis._async_common import (
+ patch_redis_async_client,
+ patch_redis_async_pipeline,
+ )
+
+ patch_redis_async_client(
+ redis.asyncio.client.StrictRedis,
+ is_cluster=False,
+ set_db_data_fn=_set_db_data,
+ )
+ patch_redis_async_pipeline(
+ redis.asyncio.client.Pipeline,
+ False,
+ _get_redis_command_args,
+ set_db_data_fn=_set_db_data,
+ )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py
new file mode 100644
index 00000000..80cdc723
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_cluster.py
@@ -0,0 +1,99 @@
+"""
+Instrumentation for RedisCluster
+This is part of the main redis-py client.
+
+https://github.com/redis/redis-py/blob/master/redis/cluster.py
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+ patch_redis_client,
+ patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span
+from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
+
+from sentry_sdk.utils import capture_internal_exceptions
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any
+ from redis import RedisCluster
+ from redis.asyncio.cluster import (
+ RedisCluster as AsyncRedisCluster,
+ ClusterPipeline as AsyncClusterPipeline,
+ )
+ from sentry_sdk.tracing import Span
+
+
+def _set_async_cluster_db_data(span, async_redis_cluster_instance):
+ # type: (Span, AsyncRedisCluster[Any]) -> None
+ default_node = async_redis_cluster_instance.get_default_node()
+ if default_node is not None and default_node.connection_kwargs is not None:
+ _set_db_data_on_span(span, default_node.connection_kwargs)
+
+
+def _set_async_cluster_pipeline_db_data(span, async_redis_cluster_pipeline_instance):
+ # type: (Span, AsyncClusterPipeline[Any]) -> None
+ with capture_internal_exceptions():
+ _set_async_cluster_db_data(
+ span,
+ # the AsyncClusterPipeline has always had a `_client` attr but it is private so potentially problematic and mypy
+ # does not recognize it - see https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
+ async_redis_cluster_pipeline_instance._client, # type: ignore[attr-defined]
+ )
+
+
+def _set_cluster_db_data(span, redis_cluster_instance):
+ # type: (Span, RedisCluster[Any]) -> None
+ default_node = redis_cluster_instance.get_default_node()
+
+ if default_node is not None:
+ connection_params = {
+ "host": default_node.host,
+ "port": default_node.port,
+ }
+ _set_db_data_on_span(span, connection_params)
+
+
+def _patch_redis_cluster():
+ # type: () -> None
+ """Patches the cluster module on redis SDK (as opposed to rediscluster library)"""
+ try:
+ from redis import RedisCluster, cluster
+ except ImportError:
+ pass
+ else:
+ patch_redis_client(
+ RedisCluster,
+ is_cluster=True,
+ set_db_data_fn=_set_cluster_db_data,
+ )
+ patch_redis_pipeline(
+ cluster.ClusterPipeline,
+ is_cluster=True,
+ get_command_args_fn=_parse_rediscluster_command,
+ set_db_data_fn=_set_cluster_db_data,
+ )
+
+ try:
+ from redis.asyncio import cluster as async_cluster
+ except ImportError:
+ pass
+ else:
+ from sentry_sdk.integrations.redis._async_common import (
+ patch_redis_async_client,
+ patch_redis_async_pipeline,
+ )
+
+ patch_redis_async_client(
+ async_cluster.RedisCluster,
+ is_cluster=True,
+ set_db_data_fn=_set_async_cluster_db_data,
+ )
+ patch_redis_async_pipeline(
+ async_cluster.ClusterPipeline,
+ is_cluster=True,
+ get_command_args_fn=_parse_rediscluster_command,
+ set_db_data_fn=_set_async_cluster_pipeline_db_data,
+ )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py
new file mode 100644
index 00000000..ad1c2363
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/redis_py_cluster_legacy.py
@@ -0,0 +1,50 @@
+"""
+Instrumentation for redis-py-cluster
+The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021).
+
+https://github.com/grokzen/redis-py-cluster
+"""
+
+from sentry_sdk.integrations.redis._sync_common import (
+ patch_redis_client,
+ patch_redis_pipeline,
+)
+from sentry_sdk.integrations.redis.modules.queries import _set_db_data
+from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
+
+
+def _patch_rediscluster():
+ # type: () -> None
+ try:
+ import rediscluster # type: ignore
+ except ImportError:
+ return
+
+ patch_redis_client(
+ rediscluster.RedisCluster,
+ is_cluster=True,
+ set_db_data_fn=_set_db_data,
+ )
+
+ # up to v1.3.6, __version__ attribute is a tuple
+ # from v2.0.0, __version__ is a string and VERSION a tuple
+ version = getattr(rediscluster, "VERSION", rediscluster.__version__)
+
+ # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0
+ # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst
+ if (0, 2, 0) < version < (2, 0, 0):
+ pipeline_cls = rediscluster.pipeline.StrictClusterPipeline
+ patch_redis_client(
+ rediscluster.StrictRedisCluster,
+ is_cluster=True,
+ set_db_data_fn=_set_db_data,
+ )
+ else:
+ pipeline_cls = rediscluster.pipeline.ClusterPipeline
+
+ patch_redis_pipeline(
+ pipeline_cls,
+ is_cluster=True,
+ get_command_args_fn=_parse_rediscluster_command,
+ set_db_data_fn=_set_db_data,
+ )
diff --git a/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py
new file mode 100644
index 00000000..27fae1e8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/sentry_sdk/integrations/redis/utils.py
@@ -0,0 +1,144 @@
+from sentry_sdk.consts import SPANDATA
+from sentry_sdk.integrations.redis.consts import (
+ _COMMANDS_INCLUDING_SENSITIVE_DATA,
+ _MAX_NUM_ARGS,
+ _MAX_NUM_COMMANDS,
+ _MULTI_KEY_COMMANDS,
+ _SINGLE_KEY_COMMANDS,
+)
+from sentry_sdk.scope import should_send_default_pii
+from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from typing import Any, Optional, Sequence
+ from sentry_sdk.tracing import Span
+
+
+def _get_safe_command(name, args):
+ # type: (str, Sequence[Any]) -> str
+ command_parts = [name]
+
+ for i, arg in enumerate(args):
+ if i > _MAX_NUM_ARGS:
+ break
+
+ name_low = name.lower()
+
+ if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA:
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
+ continue
+
+ arg_is_the_key = i == 0
+ if arg_is_the_key:
+ command_parts.append(repr(arg))
+
+ else:
+ if should_send_default_pii():
+ command_parts.append(repr(arg))
+ else:
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
+
+ command = " ".join(command_parts)
+ return command
+
+
+def _safe_decode(key):
+ # type: (Any) -> str
+ if isinstance(key, bytes):
+ try:
+ return key.decode()
+ except UnicodeDecodeError:
+ return ""
+
+ return str(key)
+
+
+def _key_as_string(key):
+ # type: (Any) -> str
+ if isinstance(key, (dict, list, tuple)):
+ key = ", ".join(_safe_decode(x) for x in key)
+ elif isinstance(key, bytes):
+ key = _safe_decode(key)
+ elif key is None:
+ key = ""
+ else:
+ key = str(key)
+
+ return key
+
+
+def _get_safe_key(method_name, args, kwargs):
+ # type: (str, Optional[tuple[Any, ...]], Optional[dict[str, Any]]) -> Optional[tuple[str, ...]]
+ """
+ Gets the key (or keys) from the given method_name.
+ The method_name could be a redis command or a django caching command
+ """
+ key = None
+
+ if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS:
+ # for example redis "mget"
+ key = tuple(args)
+
+ elif args is not None and len(args) >= 1:
+ # for example django "set_many/get_many" or redis "get"
+ if isinstance(args[0], (dict, list, tuple)):
+ key = tuple(args[0])
+ else:
+ key = (args[0],)
+
+ elif kwargs is not None and "key" in kwargs:
+ # this is a legacy case for older versions of Django
+ if isinstance(kwargs["key"], (list, tuple)):
+ if len(kwargs["key"]) > 0:
+ key = tuple(kwargs["key"])
+ else:
+ if kwargs["key"] is not None:
+ key = (kwargs["key"],)
+
+ return key
+
+
+def _parse_rediscluster_command(command):
+ # type: (Any) -> Sequence[Any]
+ return command.args
+
+
+def _set_pipeline_data(
+ span, is_cluster, get_command_args_fn, is_transaction, command_stack
+):
+ # type: (Span, bool, Any, bool, Sequence[Any]) -> None
+ span.set_tag("redis.is_cluster", is_cluster)
+ span.set_tag("redis.transaction", is_transaction)
+
+ commands = []
+ for i, arg in enumerate(command_stack):
+ if i >= _MAX_NUM_COMMANDS:
+ break
+
+ command = get_command_args_fn(arg)
+ commands.append(_get_safe_command(command[0], command[1:]))
+
+ span.set_data(
+ "redis.commands",
+ {
+ "count": len(command_stack),
+ "first_ten": commands,
+ },
+ )
+
+
+def _set_client_data(span, is_cluster, name, *args):
+ # type: (Span, bool, str, *Any) -> None
+ span.set_tag("redis.is_cluster", is_cluster)
+ if name:
+ span.set_tag("redis.command", name)
+ span.set_tag(SPANDATA.DB_OPERATION, name)
+
+ if name and args:
+ name_low = name.lower()
+ if (name_low in _SINGLE_KEY_COMMANDS) or (
+ name_low in _MULTI_KEY_COMMANDS and len(args) == 1
+ ):
+ span.set_tag("redis.key", args[0])