Skip to content

Commit 3255a93

Browse files
authored
Better handling of redis span/breadcrumb data (#2033)
- Arguments of the redis AUTH command is never collected (because it contains username and password) - When send_default_pii=False the arguments of all redis commands are redacted (except the first parameter, because it is always the "key" and thus important for debugging - Span descriptions and breadcrumb message are truncated to a max size of 1024 (the max size can be configured in a new argument to RedisIntegration(max_data_size=30) (if max_data_size is set to a falsie value (0 or None) then no truncation is done)
1 parent 6c68cf4 commit 3255a93

File tree

2 files changed

+206
-6
lines changed

2 files changed

+206
-6
lines changed

sentry_sdk/integrations/redis.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
from sentry_sdk import Hub
44
from sentry_sdk.consts import OP
5-
from sentry_sdk.utils import capture_internal_exceptions, logger
5+
from sentry_sdk.hub import _should_send_default_pii
6+
from sentry_sdk.utils import (
7+
SENSITIVE_DATA_SUBSTITUTE,
8+
capture_internal_exceptions,
9+
logger,
10+
)
611
from sentry_sdk.integrations import Integration, DidNotEnable
712

813
from sentry_sdk._types import TYPE_CHECKING
@@ -15,8 +20,13 @@
1520
)
1621
_MULTI_KEY_COMMANDS = frozenset(["del", "touch", "unlink"])
1722

18-
#: Trim argument lists to this many values
19-
_MAX_NUM_ARGS = 10
23+
_COMMANDS_INCLUDING_SENSITIVE_DATA = [
24+
"auth",
25+
]
26+
27+
_MAX_NUM_ARGS = 10 # Trim argument lists to this many values
28+
29+
_DEFAULT_MAX_DATA_SIZE = 1024
2030

2131

2232
def patch_redis_pipeline(pipeline_cls, is_cluster, get_command_args_fn):
@@ -96,6 +106,10 @@ def _patch_rediscluster():
96106
class RedisIntegration(Integration):
97107
identifier = "redis"
98108

109+
def __init__(self, max_data_size=_DEFAULT_MAX_DATA_SIZE):
110+
# type: (int) -> None
111+
self.max_data_size = max_data_size
112+
99113
@staticmethod
100114
def setup_once():
101115
# type: () -> None
@@ -139,8 +153,9 @@ def patch_redis_client(cls, is_cluster):
139153
def sentry_patched_execute_command(self, name, *args, **kwargs):
140154
# type: (Any, str, *Any, **Any) -> Any
141155
hub = Hub.current
156+
integration = hub.get_integration(RedisIntegration)
142157

143-
if hub.get_integration(RedisIntegration) is None:
158+
if integration is None:
144159
return old_execute_command(self, name, *args, **kwargs)
145160

146161
description = name
@@ -151,12 +166,33 @@ def sentry_patched_execute_command(self, name, *args, **kwargs):
151166
if i > _MAX_NUM_ARGS:
152167
break
153168

154-
description_parts.append(repr(arg))
169+
name_low = name.lower()
170+
171+
if name_low in _COMMANDS_INCLUDING_SENSITIVE_DATA:
172+
description_parts.append(SENSITIVE_DATA_SUBSTITUTE)
173+
continue
174+
175+
arg_is_the_key = i == 0
176+
if arg_is_the_key:
177+
description_parts.append(repr(arg))
178+
179+
else:
180+
if _should_send_default_pii():
181+
description_parts.append(repr(arg))
182+
else:
183+
description_parts.append(SENSITIVE_DATA_SUBSTITUTE)
155184

156185
description = " ".join(description_parts)
157186

187+
data_should_be_truncated = (
188+
integration.max_data_size and len(description) > integration.max_data_size
189+
)
190+
if data_should_be_truncated:
191+
description = description[: integration.max_data_size - len("...")] + "..."
192+
158193
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
159194
span.set_tag("redis.is_cluster", is_cluster)
195+
160196
if name:
161197
span.set_tag("redis.command", name)
162198

tests/integrations/redis/test_redis.py

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import mock
2+
13
from sentry_sdk import capture_message, start_transaction
24
from sentry_sdk.integrations.redis import RedisIntegration
35

@@ -37,7 +39,6 @@ def test_redis_pipeline(sentry_init, capture_events, is_transaction):
3739

3840
connection = FakeStrictRedis()
3941
with start_transaction():
40-
4142
pipeline = connection.pipeline(transaction=is_transaction)
4243
pipeline.get("foo")
4344
pipeline.set("bar", 1)
@@ -58,3 +59,166 @@ def test_redis_pipeline(sentry_init, capture_events, is_transaction):
5859
"redis.transaction": is_transaction,
5960
"redis.is_cluster": False,
6061
}
62+
63+
64+
def test_sensitive_data(sentry_init, capture_events):
65+
# fakeredis does not support the AUTH command, so we need to mock it
66+
with mock.patch(
67+
"sentry_sdk.integrations.redis._COMMANDS_INCLUDING_SENSITIVE_DATA", ["get"]
68+
):
69+
sentry_init(
70+
integrations=[RedisIntegration()],
71+
traces_sample_rate=1.0,
72+
send_default_pii=True,
73+
)
74+
events = capture_events()
75+
76+
connection = FakeStrictRedis()
77+
with start_transaction():
78+
connection.get(
79+
"this is super secret"
80+
) # because fakeredis does not support AUTH we use GET instead
81+
82+
(event,) = events
83+
spans = event["spans"]
84+
assert spans[0]["op"] == "db.redis"
85+
assert spans[0]["description"] == "GET [Filtered]"
86+
87+
88+
def test_pii_data_redacted(sentry_init, capture_events):
89+
sentry_init(
90+
integrations=[RedisIntegration()],
91+
traces_sample_rate=1.0,
92+
)
93+
events = capture_events()
94+
95+
connection = FakeStrictRedis()
96+
with start_transaction():
97+
connection.set("somekey1", "my secret string1")
98+
connection.set("somekey2", "my secret string2")
99+
connection.get("somekey2")
100+
connection.delete("somekey1", "somekey2")
101+
102+
(event,) = events
103+
spans = event["spans"]
104+
assert spans[0]["op"] == "db.redis"
105+
assert spans[0]["description"] == "SET 'somekey1' [Filtered]"
106+
assert spans[1]["description"] == "SET 'somekey2' [Filtered]"
107+
assert spans[2]["description"] == "GET 'somekey2'"
108+
assert spans[3]["description"] == "DEL 'somekey1' [Filtered]"
109+
110+
111+
def test_pii_data_sent(sentry_init, capture_events):
112+
sentry_init(
113+
integrations=[RedisIntegration()],
114+
traces_sample_rate=1.0,
115+
send_default_pii=True,
116+
)
117+
events = capture_events()
118+
119+
connection = FakeStrictRedis()
120+
with start_transaction():
121+
connection.set("somekey1", "my secret string1")
122+
connection.set("somekey2", "my secret string2")
123+
connection.get("somekey2")
124+
connection.delete("somekey1", "somekey2")
125+
126+
(event,) = events
127+
spans = event["spans"]
128+
assert spans[0]["op"] == "db.redis"
129+
assert spans[0]["description"] == "SET 'somekey1' 'my secret string1'"
130+
assert spans[1]["description"] == "SET 'somekey2' 'my secret string2'"
131+
assert spans[2]["description"] == "GET 'somekey2'"
132+
assert spans[3]["description"] == "DEL 'somekey1' 'somekey2'"
133+
134+
135+
def test_data_truncation(sentry_init, capture_events):
136+
sentry_init(
137+
integrations=[RedisIntegration()],
138+
traces_sample_rate=1.0,
139+
send_default_pii=True,
140+
)
141+
events = capture_events()
142+
143+
connection = FakeStrictRedis()
144+
with start_transaction():
145+
long_string = "a" * 100000
146+
connection.set("somekey1", long_string)
147+
short_string = "b" * 10
148+
connection.set("somekey2", short_string)
149+
150+
(event,) = events
151+
spans = event["spans"]
152+
assert spans[0]["op"] == "db.redis"
153+
assert spans[0]["description"] == "SET 'somekey1' '%s..." % (
154+
long_string[: 1024 - len("...") - len("SET 'somekey1' '")],
155+
)
156+
assert spans[1]["description"] == "SET 'somekey2' '%s'" % (short_string,)
157+
158+
159+
def test_data_truncation_custom(sentry_init, capture_events):
160+
sentry_init(
161+
integrations=[RedisIntegration(max_data_size=30)],
162+
traces_sample_rate=1.0,
163+
send_default_pii=True,
164+
)
165+
events = capture_events()
166+
167+
connection = FakeStrictRedis()
168+
with start_transaction():
169+
long_string = "a" * 100000
170+
connection.set("somekey1", long_string)
171+
short_string = "b" * 10
172+
connection.set("somekey2", short_string)
173+
174+
(event,) = events
175+
spans = event["spans"]
176+
assert spans[0]["op"] == "db.redis"
177+
assert spans[0]["description"] == "SET 'somekey1' '%s..." % (
178+
long_string[: 30 - len("...") - len("SET 'somekey1' '")],
179+
)
180+
assert spans[1]["description"] == "SET 'somekey2' '%s'" % (short_string,)
181+
182+
183+
def test_breadcrumbs(sentry_init, capture_events):
184+
185+
sentry_init(
186+
integrations=[RedisIntegration(max_data_size=30)],
187+
send_default_pii=True,
188+
)
189+
events = capture_events()
190+
191+
connection = FakeStrictRedis()
192+
193+
long_string = "a" * 100000
194+
connection.set("somekey1", long_string)
195+
short_string = "b" * 10
196+
connection.set("somekey2", short_string)
197+
198+
capture_message("hi")
199+
200+
(event,) = events
201+
crumbs = event["breadcrumbs"]["values"]
202+
203+
assert crumbs[0] == {
204+
"message": "SET 'somekey1' 'aaaaaaaaaaa...",
205+
"type": "redis",
206+
"category": "redis",
207+
"data": {
208+
"redis.is_cluster": False,
209+
"redis.command": "SET",
210+
"redis.key": "somekey1",
211+
},
212+
"timestamp": crumbs[0]["timestamp"],
213+
}
214+
assert crumbs[1] == {
215+
"message": "SET 'somekey2' 'bbbbbbbbbb'",
216+
"type": "redis",
217+
"category": "redis",
218+
"data": {
219+
"redis.is_cluster": False,
220+
"redis.command": "SET",
221+
"redis.key": "somekey2",
222+
},
223+
"timestamp": crumbs[1]["timestamp"],
224+
}

0 commit comments

Comments
 (0)