Skip to content

Commit 8c33a76

Browse files
Redis Cache Module - 2 - Add Cache Spans (#3075)
* Some work to use same function for queries and caches module * Moved functions to better place * Added tests * Fix * Tests and linting * Thats important for Python 3.6 * Fixed some tests * Removed ipdb * more fixing * Cleanup * Async cache spans * Added async tests * Fixed async tests * Guard for not running async tests when there is no async fakeredis for that python version * linting * Use new names for ops * Renamed for consistency * fix _get_op() * Cleaning up unused properties/parameters * Use _get_safe_key in Django integration * Fixed typing * More tests * Only return the keys in set_many, makes more sense * Linting * Cleanup * fix(clickhouse): `_sentry_span` might be missing (#3096) We started auto-enabling the ClickHouse integration in 2.0+. This led to it getting auto-enabled also for folks using ClickHouse with Django via `django-clickhouse-backend`, but it turns out that the integration doesn't work properly with `django-clickhouse-backend` and leads to `AttributeError: 'Connection' object has no attribute '_sentry_span'`. * Make _get_safe_key work for all multi key methods in django and redis * Fixed kwargs case and updated tests * Updated tests * cache.set should be cache.put * Fix `cohere` testsuite for new release of `cohere`. (#3098) * Check for new class to signal end of stream --------- Co-authored-by: Ivana Kellyerova <ivana.kellyerova@sentry.io>
1 parent 8f23a9a commit 8c33a76

File tree

15 files changed

+680
-92
lines changed

15 files changed

+680
-92
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ class SPANDATA:
368368
class OP:
369369
ANTHROPIC_MESSAGES_CREATE = "ai.messages.create.anthropic"
370370
CACHE_GET = "cache.get"
371-
CACHE_SET = "cache.set"
371+
CACHE_PUT = "cache.put"
372372
COHERE_CHAT_COMPLETIONS_CREATE = "ai.chat_completions.create.cohere"
373373
COHERE_EMBEDDINGS_CREATE = "ai.embeddings.create.cohere"
374374
DB = "db"

sentry_sdk/integrations/clickhouse_driver.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def _wrap_end(f: Callable[P, T]) -> Callable[P, T]:
107107
def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T:
108108
res = f(*args, **kwargs)
109109
instance = args[0]
110-
span = instance.connection._sentry_span # type: ignore[attr-defined]
110+
span = getattr(instance.connection, "_sentry_span", None) # type: ignore[attr-defined]
111111

112112
if span is not None:
113113
if res is not None and should_send_default_pii():
@@ -129,14 +129,15 @@ def _wrap_send_data(f: Callable[P, T]) -> Callable[P, T]:
129129
def _inner_send_data(*args: P.args, **kwargs: P.kwargs) -> T:
130130
instance = args[0] # type: clickhouse_driver.client.Client
131131
data = args[2]
132-
span = instance.connection._sentry_span
132+
span = getattr(instance.connection, "_sentry_span", None)
133133

134-
_set_db_data(span, instance.connection)
134+
if span is not None:
135+
_set_db_data(span, instance.connection)
135136

136-
if should_send_default_pii():
137-
db_params = span._data.get("db.params", [])
138-
db_params.extend(data)
139-
span.set_data("db.params", db_params)
137+
if should_send_default_pii():
138+
db_params = span._data.get("db.params", [])
139+
db_params.extend(data)
140+
span.set_data("db.params", db_params)
140141

141142
return f(*args, **kwargs)
142143

sentry_sdk/integrations/cohere.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
try:
2323
from cohere.client import Client
2424
from cohere.base_client import BaseCohere
25-
from cohere import ChatStreamEndEvent, NonStreamedChatResponse
25+
from cohere import (
26+
ChatStreamEndEvent,
27+
NonStreamedChatResponse,
28+
StreamedChatResponse_StreamEnd,
29+
)
2630

2731
if TYPE_CHECKING:
2832
from cohere import StreamedChatResponse
@@ -181,7 +185,9 @@ def new_iterator():
181185

182186
with capture_internal_exceptions():
183187
for x in old_iterator:
184-
if isinstance(x, ChatStreamEndEvent):
188+
if isinstance(x, ChatStreamEndEvent) or isinstance(
189+
x, StreamedChatResponse_StreamEnd
190+
):
185191
collect_chat_response_fields(
186192
span,
187193
x.response,

sentry_sdk/integrations/django/caching.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
from typing import TYPE_CHECKING
3+
from sentry_sdk.integrations.redis.utils import _get_safe_key
34
from urllib3.util import parse_url as urlparse
45

56
from django import VERSION as DJANGO_VERSION
@@ -8,7 +9,6 @@
89
import sentry_sdk
910
from sentry_sdk.consts import OP, SPANDATA
1011
from sentry_sdk.utils import (
11-
SENSITIVE_DATA_SUBSTITUTE,
1212
capture_internal_exceptions,
1313
ensure_integration_enabled,
1414
)
@@ -28,27 +28,9 @@
2828
]
2929

3030

31-
def _get_key(args, kwargs):
32-
# type: (list[Any], dict[str, Any]) -> str
33-
key = ""
34-
35-
if args is not None and len(args) >= 1:
36-
key = args[0]
37-
elif kwargs is not None and "key" in kwargs:
38-
key = kwargs["key"]
39-
40-
if isinstance(key, dict):
41-
# Do not leak sensitive data
42-
# `set_many()` has a dict {"key1": "value1", "key2": "value2"} as first argument.
43-
# Those values could include sensitive data so we replace them with a placeholder
44-
key = {x: SENSITIVE_DATA_SUBSTITUTE for x in key}
45-
46-
return str(key)
47-
48-
4931
def _get_span_description(method_name, args, kwargs):
50-
# type: (str, list[Any], dict[str, Any]) -> str
51-
return _get_key(args, kwargs)
32+
# type: (str, tuple[Any], dict[str, Any]) -> str
33+
return _get_safe_key(method_name, args, kwargs)
5234

5335

5436
def _patch_cache_method(cache, method_name, address, port):
@@ -61,11 +43,11 @@ def _patch_cache_method(cache, method_name, address, port):
6143
def _instrument_call(
6244
cache, method_name, original_method, args, kwargs, address, port
6345
):
64-
# type: (CacheHandler, str, Callable[..., Any], list[Any], dict[str, Any], Optional[str], Optional[int]) -> Any
46+
# type: (CacheHandler, str, Callable[..., Any], tuple[Any, ...], dict[str, Any], Optional[str], Optional[int]) -> Any
6547
is_set_operation = method_name.startswith("set")
6648
is_get_operation = not is_set_operation
6749

68-
op = OP.CACHE_SET if is_set_operation else OP.CACHE_GET
50+
op = OP.CACHE_PUT if is_set_operation else OP.CACHE_GET
6951
description = _get_span_description(method_name, args, kwargs)
7052

7153
with sentry_sdk.start_span(op=op, description=description) as span:
@@ -78,7 +60,7 @@ def _instrument_call(
7860
if port is not None:
7961
span.set_data(SPANDATA.NETWORK_PEER_PORT, port)
8062

81-
key = _get_key(args, kwargs)
63+
key = _get_safe_key(method_name, args, kwargs)
8264
if key != "":
8365
span.set_data(SPANDATA.CACHE_KEY, key)
8466

sentry_sdk/integrations/redis/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from sentry_sdk._types import TYPE_CHECKING
12
from sentry_sdk.integrations import Integration, DidNotEnable
23
from sentry_sdk.integrations.redis.consts import _DEFAULT_MAX_DATA_SIZE
34
from sentry_sdk.integrations.redis.rb import _patch_rb
@@ -6,13 +7,17 @@
67
from sentry_sdk.integrations.redis.redis_py_cluster_legacy import _patch_rediscluster
78
from sentry_sdk.utils import logger
89

10+
if TYPE_CHECKING:
11+
from typing import Optional
12+
913

1014
class RedisIntegration(Integration):
1115
identifier = "redis"
1216

13-
def __init__(self, max_data_size=_DEFAULT_MAX_DATA_SIZE):
14-
# type: (int) -> None
17+
def __init__(self, max_data_size=_DEFAULT_MAX_DATA_SIZE, cache_prefixes=None):
18+
# type: (int, Optional[list[str]]) -> None
1519
self.max_data_size = max_data_size
20+
self.cache_prefixes = cache_prefixes if cache_prefixes is not None else []
1621

1722
@staticmethod
1823
def setup_once():

sentry_sdk/integrations/redis/_async_common.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from sentry_sdk._types import TYPE_CHECKING
22
from sentry_sdk.consts import OP
3+
from sentry_sdk.integrations.redis.modules.caches import (
4+
_compile_cache_span_properties,
5+
_set_cache_data,
6+
)
7+
from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
38
from sentry_sdk.integrations.redis.utils import (
4-
_get_span_description,
59
_set_client_data,
610
_set_pipeline_data,
711
)
@@ -56,15 +60,44 @@ def patch_redis_async_client(cls, is_cluster, set_db_data_fn):
5660

5761
async def _sentry_execute_command(self, name, *args, **kwargs):
5862
# type: (Any, str, *Any, **Any) -> Any
59-
if sentry_sdk.get_client().get_integration(RedisIntegration) is None:
63+
integration = sentry_sdk.get_client().get_integration(RedisIntegration)
64+
if integration is None:
6065
return await old_execute_command(self, name, *args, **kwargs)
6166

62-
description = _get_span_description(name, *args)
67+
cache_properties = _compile_cache_span_properties(
68+
name,
69+
args,
70+
kwargs,
71+
integration,
72+
)
6373

64-
with sentry_sdk.start_span(op=OP.DB_REDIS, description=description) as span:
65-
set_db_data_fn(span, self)
66-
_set_client_data(span, is_cluster, name, *args)
74+
cache_span = None
75+
if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
76+
cache_span = sentry_sdk.start_span(
77+
op=cache_properties["op"],
78+
description=cache_properties["description"],
79+
)
80+
cache_span.__enter__()
6781

68-
return await old_execute_command(self, name, *args, **kwargs)
82+
db_properties = _compile_db_span_properties(integration, name, args)
83+
84+
db_span = sentry_sdk.start_span(
85+
op=db_properties["op"],
86+
description=db_properties["description"],
87+
)
88+
db_span.__enter__()
89+
90+
set_db_data_fn(db_span, self)
91+
_set_client_data(db_span, is_cluster, name, *args)
92+
93+
value = await old_execute_command(self, name, *args, **kwargs)
94+
95+
db_span.__exit__(None, None, None)
96+
97+
if cache_span:
98+
_set_cache_data(cache_span, self, cache_properties, value)
99+
cache_span.__exit__(None, None, None)
100+
101+
return value
69102

70103
cls.execute_command = _sentry_execute_command # type: ignore

sentry_sdk/integrations/redis/_sync_common.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
from sentry_sdk._types import TYPE_CHECKING
22
from sentry_sdk.consts import OP
3+
from sentry_sdk.integrations.redis.modules.caches import (
4+
_compile_cache_span_properties,
5+
_set_cache_data,
6+
)
7+
from sentry_sdk.integrations.redis.modules.queries import _compile_db_span_properties
38
from sentry_sdk.integrations.redis.utils import (
4-
_get_span_description,
59
_set_client_data,
610
_set_pipeline_data,
711
)
812
from sentry_sdk.tracing import Span
913
from sentry_sdk.utils import capture_internal_exceptions
1014
import sentry_sdk
1115

16+
1217
if TYPE_CHECKING:
1318
from collections.abc import Callable
1419
from typing import Any
@@ -64,18 +69,40 @@ def sentry_patched_execute_command(self, name, *args, **kwargs):
6469
if integration is None:
6570
return old_execute_command(self, name, *args, **kwargs)
6671

67-
description = _get_span_description(name, *args)
72+
cache_properties = _compile_cache_span_properties(
73+
name,
74+
args,
75+
kwargs,
76+
integration,
77+
)
78+
79+
cache_span = None
80+
if cache_properties["is_cache_key"] and cache_properties["op"] is not None:
81+
cache_span = sentry_sdk.start_span(
82+
op=cache_properties["op"],
83+
description=cache_properties["description"],
84+
)
85+
cache_span.__enter__()
86+
87+
db_properties = _compile_db_span_properties(integration, name, args)
6888

69-
data_should_be_truncated = (
70-
integration.max_data_size and len(description) > integration.max_data_size
89+
db_span = sentry_sdk.start_span(
90+
op=db_properties["op"],
91+
description=db_properties["description"],
7192
)
72-
if data_should_be_truncated:
73-
description = description[: integration.max_data_size - len("...")] + "..."
93+
db_span.__enter__()
7494

75-
with sentry_sdk.start_span(op=OP.DB_REDIS, description=description) as span:
76-
set_db_data_fn(span, self)
77-
_set_client_data(span, is_cluster, name, *args)
95+
set_db_data_fn(db_span, self)
96+
_set_client_data(db_span, is_cluster, name, *args)
7897

79-
return old_execute_command(self, name, *args, **kwargs)
98+
value = old_execute_command(self, name, *args, **kwargs)
99+
100+
db_span.__exit__(None, None, None)
101+
102+
if cache_span:
103+
_set_cache_data(cache_span, self, cache_properties, value)
104+
cache_span.__exit__(None, None, None)
105+
106+
return value
80107

81108
cls.execute_command = sentry_patched_execute_command

sentry_sdk/integrations/redis/consts.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"],
33
)
44
_MULTI_KEY_COMMANDS = frozenset(
5-
["del", "touch", "unlink"],
5+
[
6+
"del",
7+
"touch",
8+
"unlink",
9+
"mget",
10+
],
611
)
712
_COMMANDS_INCLUDING_SENSITIVE_DATA = [
813
"auth",

0 commit comments

Comments
 (0)