sentry-sdk 0.7.5__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -30
  2. sentry_sdk/_compat.py +74 -61
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +289 -0
  8. sentry_sdk/_types.py +338 -0
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +134 -0
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +141 -0
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +112 -68
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,118 @@
1
+ """
2
+ Code used for the Caches module in Sentry
3
+ """
4
+
5
+ from sentry_sdk.consts import OP, SPANDATA
6
+ from sentry_sdk.integrations.redis.utils import _get_safe_key, _key_as_string
7
+ from sentry_sdk.utils import capture_internal_exceptions
8
+
9
+ GET_COMMANDS = ("get", "mget")
10
+ SET_COMMANDS = ("set", "setex")
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from sentry_sdk.integrations.redis import RedisIntegration
16
+ from sentry_sdk.tracing import Span
17
+ from typing import Any, Optional
18
+
19
+
20
+ def _get_op(name):
21
+ # type: (str) -> Optional[str]
22
+ op = None
23
+ if name.lower() in GET_COMMANDS:
24
+ op = OP.CACHE_GET
25
+ elif name.lower() in SET_COMMANDS:
26
+ op = OP.CACHE_PUT
27
+
28
+ return op
29
+
30
+
31
+ def _compile_cache_span_properties(redis_command, args, kwargs, integration):
32
+ # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> dict[str, Any]
33
+ key = _get_safe_key(redis_command, args, kwargs)
34
+ key_as_string = _key_as_string(key)
35
+ keys_as_string = key_as_string.split(", ")
36
+
37
+ is_cache_key = False
38
+ for prefix in integration.cache_prefixes:
39
+ for kee in keys_as_string:
40
+ if kee.startswith(prefix):
41
+ is_cache_key = True
42
+ break
43
+ if is_cache_key:
44
+ break
45
+
46
+ value = None
47
+ if redis_command.lower() in SET_COMMANDS:
48
+ value = args[-1]
49
+
50
+ properties = {
51
+ "op": _get_op(redis_command),
52
+ "description": _get_cache_span_description(
53
+ redis_command, args, kwargs, integration
54
+ ),
55
+ "key": key,
56
+ "key_as_string": key_as_string,
57
+ "redis_command": redis_command.lower(),
58
+ "is_cache_key": is_cache_key,
59
+ "value": value,
60
+ }
61
+
62
+ return properties
63
+
64
+
65
+ def _get_cache_span_description(redis_command, args, kwargs, integration):
66
+ # type: (str, tuple[Any, ...], dict[str, Any], RedisIntegration) -> str
67
+ description = _key_as_string(_get_safe_key(redis_command, args, kwargs))
68
+
69
+ if integration.max_data_size and len(description) > integration.max_data_size:
70
+ description = description[: integration.max_data_size - len("...")] + "..."
71
+
72
+ return description
73
+
74
+
75
+ def _set_cache_data(span, redis_client, properties, return_value):
76
+ # type: (Span, Any, dict[str, Any], Optional[Any]) -> None
77
+ with capture_internal_exceptions():
78
+ span.set_data(SPANDATA.CACHE_KEY, properties["key"])
79
+
80
+ if properties["redis_command"] in GET_COMMANDS:
81
+ if return_value is not None:
82
+ span.set_data(SPANDATA.CACHE_HIT, True)
83
+ size = (
84
+ len(str(return_value).encode("utf-8"))
85
+ if not isinstance(return_value, bytes)
86
+ else len(return_value)
87
+ )
88
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
89
+ else:
90
+ span.set_data(SPANDATA.CACHE_HIT, False)
91
+
92
+ elif properties["redis_command"] in SET_COMMANDS:
93
+ if properties["value"] is not None:
94
+ size = (
95
+ len(properties["value"].encode("utf-8"))
96
+ if not isinstance(properties["value"], bytes)
97
+ else len(properties["value"])
98
+ )
99
+ span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
100
+
101
+ try:
102
+ connection_params = redis_client.connection_pool.connection_kwargs
103
+ except AttributeError:
104
+ # If it is a cluster, there is no connection_pool attribute so we
105
+ # need to get the default node from the cluster instance
106
+ default_node = redis_client.get_default_node()
107
+ connection_params = {
108
+ "host": default_node.host,
109
+ "port": default_node.port,
110
+ }
111
+
112
+ host = connection_params.get("host")
113
+ if host is not None:
114
+ span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, host)
115
+
116
+ port = connection_params.get("port")
117
+ if port is not None:
118
+ span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
@@ -0,0 +1,65 @@
1
+ """
2
+ Code used for the Queries module in Sentry
3
+ """
4
+
5
+ from sentry_sdk.consts import OP, SPANDATA
6
+ from sentry_sdk.integrations.redis.utils import _get_safe_command
7
+ from sentry_sdk.utils import capture_internal_exceptions
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from redis import Redis
13
+ from sentry_sdk.integrations.redis import RedisIntegration
14
+ from sentry_sdk.tracing import Span
15
+ from typing import Any
16
+
17
+
18
+ def _compile_db_span_properties(integration, redis_command, args):
19
+ # type: (RedisIntegration, str, tuple[Any, ...]) -> dict[str, Any]
20
+ description = _get_db_span_description(integration, redis_command, args)
21
+
22
+ properties = {
23
+ "op": OP.DB_REDIS,
24
+ "description": description,
25
+ }
26
+
27
+ return properties
28
+
29
+
30
+ def _get_db_span_description(integration, command_name, args):
31
+ # type: (RedisIntegration, str, tuple[Any, ...]) -> str
32
+ description = command_name
33
+
34
+ with capture_internal_exceptions():
35
+ description = _get_safe_command(command_name, args)
36
+
37
+ if integration.max_data_size and len(description) > integration.max_data_size:
38
+ description = description[: integration.max_data_size - len("...")] + "..."
39
+
40
+ return description
41
+
42
+
43
+ def _set_db_data_on_span(span, connection_params):
44
+ # type: (Span, dict[str, Any]) -> None
45
+ span.set_data(SPANDATA.DB_SYSTEM, "redis")
46
+
47
+ db = connection_params.get("db")
48
+ if db is not None:
49
+ span.set_data(SPANDATA.DB_NAME, str(db))
50
+
51
+ host = connection_params.get("host")
52
+ if host is not None:
53
+ span.set_data(SPANDATA.SERVER_ADDRESS, host)
54
+
55
+ port = connection_params.get("port")
56
+ if port is not None:
57
+ span.set_data(SPANDATA.SERVER_PORT, port)
58
+
59
+
60
+ def _set_db_data(span, redis_instance):
61
+ # type: (Span, Redis[Any]) -> None
62
+ try:
63
+ _set_db_data_on_span(span, redis_instance.connection_pool.connection_kwargs)
64
+ except AttributeError:
65
+ pass # connections_kwargs may be missing in some cases
@@ -0,0 +1,32 @@
1
+ """
2
+ Instrumentation for Redis Blaster (rb)
3
+
4
+ https://github.com/getsentry/rb
5
+ """
6
+
7
+ from sentry_sdk.integrations.redis._sync_common import patch_redis_client
8
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data
9
+
10
+
11
+ def _patch_rb():
12
+ # type: () -> None
13
+ try:
14
+ import rb.clients # type: ignore
15
+ except ImportError:
16
+ pass
17
+ else:
18
+ patch_redis_client(
19
+ rb.clients.FanoutClient,
20
+ is_cluster=False,
21
+ set_db_data_fn=_set_db_data,
22
+ )
23
+ patch_redis_client(
24
+ rb.clients.MappingClient,
25
+ is_cluster=False,
26
+ set_db_data_fn=_set_db_data,
27
+ )
28
+ patch_redis_client(
29
+ rb.clients.RoutingClient,
30
+ is_cluster=False,
31
+ set_db_data_fn=_set_db_data,
32
+ )
@@ -0,0 +1,69 @@
1
+ """
2
+ Instrumentation for Redis
3
+
4
+ https://github.com/redis/redis-py
5
+ """
6
+
7
+ from sentry_sdk.integrations.redis._sync_common import (
8
+ patch_redis_client,
9
+ patch_redis_pipeline,
10
+ )
11
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data
12
+
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Sequence
17
+
18
+
19
+ def _get_redis_command_args(command):
20
+ # type: (Any) -> Sequence[Any]
21
+ return command[0]
22
+
23
+
24
+ def _patch_redis(StrictRedis, client): # noqa: N803
25
+ # type: (Any, Any) -> None
26
+ patch_redis_client(
27
+ StrictRedis,
28
+ is_cluster=False,
29
+ set_db_data_fn=_set_db_data,
30
+ )
31
+ patch_redis_pipeline(
32
+ client.Pipeline,
33
+ is_cluster=False,
34
+ get_command_args_fn=_get_redis_command_args,
35
+ set_db_data_fn=_set_db_data,
36
+ )
37
+ try:
38
+ strict_pipeline = client.StrictPipeline
39
+ except AttributeError:
40
+ pass
41
+ else:
42
+ patch_redis_pipeline(
43
+ strict_pipeline,
44
+ is_cluster=False,
45
+ get_command_args_fn=_get_redis_command_args,
46
+ set_db_data_fn=_set_db_data,
47
+ )
48
+
49
+ try:
50
+ import redis.asyncio
51
+ except ImportError:
52
+ pass
53
+ else:
54
+ from sentry_sdk.integrations.redis._async_common import (
55
+ patch_redis_async_client,
56
+ patch_redis_async_pipeline,
57
+ )
58
+
59
+ patch_redis_async_client(
60
+ redis.asyncio.client.StrictRedis,
61
+ is_cluster=False,
62
+ set_db_data_fn=_set_db_data,
63
+ )
64
+ patch_redis_async_pipeline(
65
+ redis.asyncio.client.Pipeline,
66
+ False,
67
+ _get_redis_command_args,
68
+ set_db_data_fn=_set_db_data,
69
+ )
@@ -0,0 +1,107 @@
1
+ """
2
+ Instrumentation for RedisCluster
3
+ This is part of the main redis-py client.
4
+
5
+ https://github.com/redis/redis-py/blob/master/redis/cluster.py
6
+ """
7
+
8
+ from sentry_sdk.integrations.redis._sync_common import (
9
+ patch_redis_client,
10
+ patch_redis_pipeline,
11
+ )
12
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data_on_span
13
+ from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
14
+
15
+ from sentry_sdk.utils import capture_internal_exceptions
16
+
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from typing import Any
21
+ from redis import RedisCluster
22
+ from redis.asyncio.cluster import (
23
+ RedisCluster as AsyncRedisCluster,
24
+ ClusterPipeline as AsyncClusterPipeline,
25
+ )
26
+ from sentry_sdk.tracing import Span
27
+
28
+
29
+ def _set_async_cluster_db_data(span, async_redis_cluster_instance):
30
+ # type: (Span, AsyncRedisCluster[Any]) -> None
31
+ default_node = async_redis_cluster_instance.get_default_node()
32
+ if default_node is not None and default_node.connection_kwargs is not None:
33
+ _set_db_data_on_span(span, default_node.connection_kwargs)
34
+
35
+
36
+ def _set_async_cluster_pipeline_db_data(span, async_redis_cluster_pipeline_instance):
37
+ # type: (Span, AsyncClusterPipeline[Any]) -> None
38
+ with capture_internal_exceptions():
39
+ client = getattr(async_redis_cluster_pipeline_instance, "cluster_client", None)
40
+ if client is None:
41
+ # In older redis-py versions, the AsyncClusterPipeline had a `_client`
42
+ # attr but it is private so potentially problematic and mypy does not
43
+ # recognize it - see
44
+ # https://github.com/redis/redis-py/blame/v5.0.0/redis/asyncio/cluster.py#L1386
45
+ client = (
46
+ async_redis_cluster_pipeline_instance._client # type: ignore[attr-defined]
47
+ )
48
+
49
+ _set_async_cluster_db_data(
50
+ span,
51
+ client,
52
+ )
53
+
54
+
55
+ def _set_cluster_db_data(span, redis_cluster_instance):
56
+ # type: (Span, RedisCluster[Any]) -> None
57
+ default_node = redis_cluster_instance.get_default_node()
58
+
59
+ if default_node is not None:
60
+ connection_params = {
61
+ "host": default_node.host,
62
+ "port": default_node.port,
63
+ }
64
+ _set_db_data_on_span(span, connection_params)
65
+
66
+
67
+ def _patch_redis_cluster():
68
+ # type: () -> None
69
+ """Patches the cluster module on redis SDK (as opposed to rediscluster library)"""
70
+ try:
71
+ from redis import RedisCluster, cluster
72
+ except ImportError:
73
+ pass
74
+ else:
75
+ patch_redis_client(
76
+ RedisCluster,
77
+ is_cluster=True,
78
+ set_db_data_fn=_set_cluster_db_data,
79
+ )
80
+ patch_redis_pipeline(
81
+ cluster.ClusterPipeline,
82
+ is_cluster=True,
83
+ get_command_args_fn=_parse_rediscluster_command,
84
+ set_db_data_fn=_set_cluster_db_data,
85
+ )
86
+
87
+ try:
88
+ from redis.asyncio import cluster as async_cluster
89
+ except ImportError:
90
+ pass
91
+ else:
92
+ from sentry_sdk.integrations.redis._async_common import (
93
+ patch_redis_async_client,
94
+ patch_redis_async_pipeline,
95
+ )
96
+
97
+ patch_redis_async_client(
98
+ async_cluster.RedisCluster,
99
+ is_cluster=True,
100
+ set_db_data_fn=_set_async_cluster_db_data,
101
+ )
102
+ patch_redis_async_pipeline(
103
+ async_cluster.ClusterPipeline,
104
+ is_cluster=True,
105
+ get_command_args_fn=_parse_rediscluster_command,
106
+ set_db_data_fn=_set_async_cluster_pipeline_db_data,
107
+ )
@@ -0,0 +1,50 @@
1
+ """
2
+ Instrumentation for redis-py-cluster
3
+ The project redis-py-cluster is EOL and was integrated into redis-py starting from version 4.1.0 (Dec 26, 2021).
4
+
5
+ https://github.com/grokzen/redis-py-cluster
6
+ """
7
+
8
+ from sentry_sdk.integrations.redis._sync_common import (
9
+ patch_redis_client,
10
+ patch_redis_pipeline,
11
+ )
12
+ from sentry_sdk.integrations.redis.modules.queries import _set_db_data
13
+ from sentry_sdk.integrations.redis.utils import _parse_rediscluster_command
14
+
15
+
16
+ def _patch_rediscluster():
17
+ # type: () -> None
18
+ try:
19
+ import rediscluster # type: ignore
20
+ except ImportError:
21
+ return
22
+
23
+ patch_redis_client(
24
+ rediscluster.RedisCluster,
25
+ is_cluster=True,
26
+ set_db_data_fn=_set_db_data,
27
+ )
28
+
29
+ # up to v1.3.6, __version__ attribute is a tuple
30
+ # from v2.0.0, __version__ is a string and VERSION a tuple
31
+ version = getattr(rediscluster, "VERSION", rediscluster.__version__)
32
+
33
+ # StrictRedisCluster was introduced in v0.2.0 and removed in v2.0.0
34
+ # https://github.com/Grokzen/redis-py-cluster/blob/master/docs/release-notes.rst
35
+ if (0, 2, 0) < version < (2, 0, 0):
36
+ pipeline_cls = rediscluster.pipeline.StrictClusterPipeline
37
+ patch_redis_client(
38
+ rediscluster.StrictRedisCluster,
39
+ is_cluster=True,
40
+ set_db_data_fn=_set_db_data,
41
+ )
42
+ else:
43
+ pipeline_cls = rediscluster.pipeline.ClusterPipeline
44
+
45
+ patch_redis_pipeline(
46
+ pipeline_cls,
47
+ is_cluster=True,
48
+ get_command_args_fn=_parse_rediscluster_command,
49
+ set_db_data_fn=_set_db_data,
50
+ )
@@ -0,0 +1,148 @@
1
+ from sentry_sdk.consts import SPANDATA
2
+ from sentry_sdk.integrations.redis.consts import (
3
+ _COMMANDS_INCLUDING_SENSITIVE_DATA,
4
+ _MAX_NUM_ARGS,
5
+ _MAX_NUM_COMMANDS,
6
+ _MULTI_KEY_COMMANDS,
7
+ _SINGLE_KEY_COMMANDS,
8
+ )
9
+ from sentry_sdk.scope import should_send_default_pii
10
+ from sentry_sdk.utils import SENSITIVE_DATA_SUBSTITUTE
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ if TYPE_CHECKING:
15
+ from typing import Any, Optional, Sequence
16
+ from sentry_sdk.tracing import Span
17
+
18
+
19
+ def _get_safe_command(name, args):
20
+ # type: (str, Sequence[Any]) -> str
21
+ command_parts = [name]
22
+
23
+ name_low = name.lower()
24
+ send_default_pii = should_send_default_pii()
25
+
26
+ for i, arg in enumerate(args):
27
+ if i > _MAX_NUM_ARGS:
28
+ break
29
+
30
+ if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA:
31
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
32
+ continue
33
+
34
+ arg_is_the_key = i == 0
35
+ if arg_is_the_key:
36
+ command_parts.append(repr(arg))
37
+ else:
38
+ if send_default_pii:
39
+ command_parts.append(repr(arg))
40
+ else:
41
+ command_parts.append(SENSITIVE_DATA_SUBSTITUTE)
42
+
43
+ command = " ".join(command_parts)
44
+ return command
45
+
46
+
47
+ def _safe_decode(key):
48
+ # type: (Any) -> str
49
+ if isinstance(key, bytes):
50
+ try:
51
+ return key.decode()
52
+ except UnicodeDecodeError:
53
+ return ""
54
+
55
+ return str(key)
56
+
57
+
58
+ def _key_as_string(key):
59
+ # type: (Any) -> str
60
+ if isinstance(key, (dict, list, tuple)):
61
+ key = ", ".join(_safe_decode(x) for x in key)
62
+ elif isinstance(key, bytes):
63
+ key = _safe_decode(key)
64
+ elif key is None:
65
+ key = ""
66
+ else:
67
+ key = str(key)
68
+
69
+ return key
70
+
71
+
72
+ def _get_safe_key(method_name, args, kwargs):
73
+ # type: (str, Optional[tuple[Any, ...]], Optional[dict[str, Any]]) -> Optional[tuple[str, ...]]
74
+ """
75
+ Gets the key (or keys) from the given method_name.
76
+ The method_name could be a redis command or a django caching command
77
+ """
78
+ key = None
79
+
80
+ if args is not None and method_name.lower() in _MULTI_KEY_COMMANDS:
81
+ # for example redis "mget"
82
+ key = tuple(args)
83
+
84
+ elif args is not None and len(args) >= 1:
85
+ # for example django "set_many/get_many" or redis "get"
86
+ if isinstance(args[0], (dict, list, tuple)):
87
+ key = tuple(args[0])
88
+ else:
89
+ key = (args[0],)
90
+
91
+ elif kwargs is not None and "key" in kwargs:
92
+ # this is a legacy case for older versions of Django
93
+ if isinstance(kwargs["key"], (list, tuple)):
94
+ if len(kwargs["key"]) > 0:
95
+ key = tuple(kwargs["key"])
96
+ else:
97
+ if kwargs["key"] is not None:
98
+ key = (kwargs["key"],)
99
+
100
+ return key
101
+
102
+
103
+ def _parse_rediscluster_command(command):
104
+ # type: (Any) -> Sequence[Any]
105
+ return command.args
106
+
107
+
108
+ def _set_pipeline_data(
109
+ span,
110
+ is_cluster,
111
+ get_command_args_fn,
112
+ is_transaction,
113
+ commands_seq,
114
+ ):
115
+ # type: (Span, bool, Any, bool, Sequence[Any]) -> None
116
+ span.set_tag("redis.is_cluster", is_cluster)
117
+ span.set_tag("redis.transaction", is_transaction)
118
+
119
+ commands = []
120
+ for i, arg in enumerate(commands_seq):
121
+ if i >= _MAX_NUM_COMMANDS:
122
+ break
123
+
124
+ command = get_command_args_fn(arg)
125
+ commands.append(_get_safe_command(command[0], command[1:]))
126
+
127
+ span.set_data(
128
+ "redis.commands",
129
+ {
130
+ "count": len(commands_seq),
131
+ "first_ten": commands,
132
+ },
133
+ )
134
+
135
+
136
+ def _set_client_data(span, is_cluster, name, *args):
137
+ # type: (Span, bool, str, *Any) -> None
138
+ span.set_tag("redis.is_cluster", is_cluster)
139
+ if name:
140
+ span.set_tag("redis.command", name)
141
+ span.set_tag(SPANDATA.DB_OPERATION, name)
142
+
143
+ if name and args:
144
+ name_low = name.lower()
145
+ if (name_low in _SINGLE_KEY_COMMANDS) or (
146
+ name_low in _MULTI_KEY_COMMANDS and len(args) == 1
147
+ ):
148
+ span.set_tag("redis.key", args[0])