Skip to content

Commit fc638fd

Browse files
authored
Connection attributes in redis database spans (#2398)
This adds db connection parameters like database host, database port, database name, database system ("redis" in this case) to all database spans that are created by our Redis integration. Works for async and sync connections to redis and redis cluster.
1 parent d0b1cf8 commit fc638fd

File tree

5 files changed

+241
-90
lines changed

5 files changed

+241
-90
lines changed

sentry_sdk/integrations/redis/__init__.py

Lines changed: 90 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,31 @@
22

33
from sentry_sdk import Hub
44
from sentry_sdk.consts import OP, SPANDATA
5+
from sentry_sdk._compat import text_type
56
from sentry_sdk.hub import _should_send_default_pii
7+
from sentry_sdk.integrations import Integration, DidNotEnable
8+
from sentry_sdk._types import TYPE_CHECKING
69
from sentry_sdk.utils import (
710
SENSITIVE_DATA_SUBSTITUTE,
811
capture_internal_exceptions,
912
logger,
1013
)
11-
from sentry_sdk.integrations import Integration, DidNotEnable
12-
13-
from sentry_sdk._types import TYPE_CHECKING
1414

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

1919
_SINGLE_KEY_COMMANDS = frozenset(
20-
["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"]
20+
["decr", "decrby", "get", "incr", "incrby", "pttl", "set", "setex", "setnx", "ttl"],
21+
)
22+
_MULTI_KEY_COMMANDS = frozenset(
23+
["del", "touch", "unlink"],
2124
)
22-
_MULTI_KEY_COMMANDS = frozenset(["del", "touch", "unlink"])
23-
2425
_COMMANDS_INCLUDING_SENSITIVE_DATA = [
2526
"auth",
2627
]
27-
2828
_MAX_NUM_ARGS = 10 # Trim argument lists to this many values
2929
_MAX_NUM_COMMANDS = 10 # Trim command lists to this many values
30-
3130
_DEFAULT_MAX_DATA_SIZE = 1024
3231

3332

@@ -59,6 +58,26 @@ def _get_safe_command(name, args):
5958
return command
6059

6160

61+
def _get_span_description(name, *args):
62+
# type: (str, *Any) -> str
63+
description = name
64+
65+
with capture_internal_exceptions():
66+
description = _get_safe_command(name, args)
67+
68+
return description
69+
70+
71+
def _get_redis_command_args(command):
72+
# type: (Any) -> Sequence[Any]
73+
return command[0]
74+
75+
76+
def _parse_rediscluster_command(command):
77+
# type: (Any) -> Sequence[Any]
78+
return command.args
79+
80+
6281
def _set_pipeline_data(
6382
span, is_cluster, get_command_args_fn, is_transaction, command_stack
6483
):
@@ -84,6 +103,38 @@ def _set_pipeline_data(
84103
)
85104

86105

106+
def _set_client_data(span, is_cluster, name, *args):
107+
# type: (Span, bool, str, *Any) -> None
108+
span.set_tag("redis.is_cluster", is_cluster)
109+
if name:
110+
span.set_tag("redis.command", name)
111+
span.set_tag(SPANDATA.DB_OPERATION, name)
112+
113+
if name and args:
114+
name_low = name.lower()
115+
if (name_low in _SINGLE_KEY_COMMANDS) or (
116+
name_low in _MULTI_KEY_COMMANDS and len(args) == 1
117+
):
118+
span.set_tag("redis.key", args[0])
119+
120+
121+
def _set_db_data(span, connection_params):
122+
# type: (Span, Dict[str, Any]) -> None
123+
span.set_data(SPANDATA.DB_SYSTEM, "redis")
124+
125+
db = connection_params.get("db")
126+
if db is not None:
127+
span.set_data(SPANDATA.DB_NAME, text_type(db))
128+
129+
host = connection_params.get("host")
130+
if host is not None:
131+
span.set_data(SPANDATA.SERVER_ADDRESS, host)
132+
133+
port = connection_params.get("port")
134+
if port is not None:
135+
span.set_data(SPANDATA.SERVER_PORT, port)
136+
137+
87138
def patch_redis_pipeline(pipeline_cls, is_cluster, get_command_args_fn):
88139
# type: (Any, bool, Any) -> None
89140
old_execute = pipeline_cls.execute
@@ -99,28 +150,51 @@ def sentry_patched_execute(self, *args, **kwargs):
99150
op=OP.DB_REDIS, description="redis.pipeline.execute"
100151
) as span:
101152
with capture_internal_exceptions():
153+
_set_db_data(span, self.connection_pool.connection_kwargs)
102154
_set_pipeline_data(
103155
span,
104156
is_cluster,
105157
get_command_args_fn,
106158
self.transaction,
107159
self.command_stack,
108160
)
109-
span.set_data(SPANDATA.DB_SYSTEM, "redis")
110161

111162
return old_execute(self, *args, **kwargs)
112163

113164
pipeline_cls.execute = sentry_patched_execute
114165

115166

116-
def _get_redis_command_args(command):
117-
# type: (Any) -> Sequence[Any]
118-
return command[0]
167+
def patch_redis_client(cls, is_cluster):
168+
# type: (Any, bool) -> None
169+
"""
170+
This function can be used to instrument custom redis client classes or
171+
subclasses.
172+
"""
173+
old_execute_command = cls.execute_command
119174

175+
def sentry_patched_execute_command(self, name, *args, **kwargs):
176+
# type: (Any, str, *Any, **Any) -> Any
177+
hub = Hub.current
178+
integration = hub.get_integration(RedisIntegration)
120179

121-
def _parse_rediscluster_command(command):
122-
# type: (Any) -> Sequence[Any]
123-
return command.args
180+
if integration is None:
181+
return old_execute_command(self, name, *args, **kwargs)
182+
183+
description = _get_span_description(name, *args)
184+
185+
data_should_be_truncated = (
186+
integration.max_data_size and len(description) > integration.max_data_size
187+
)
188+
if data_should_be_truncated:
189+
description = description[: integration.max_data_size - len("...")] + "..."
190+
191+
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
192+
_set_db_data(span, self.connection_pool.connection_kwargs)
193+
_set_client_data(span, is_cluster, name, *args)
194+
195+
return old_execute_command(self, name, *args, **kwargs)
196+
197+
cls.execute_command = sentry_patched_execute_command
124198

125199

126200
def _patch_redis(StrictRedis, client): # noqa: N803
@@ -206,61 +280,3 @@ def setup_once():
206280
_patch_rediscluster()
207281
except Exception:
208282
logger.exception("Error occurred while patching `rediscluster` library")
209-
210-
211-
def _get_span_description(name, *args):
212-
# type: (str, *Any) -> str
213-
description = name
214-
215-
with capture_internal_exceptions():
216-
description = _get_safe_command(name, args)
217-
218-
return description
219-
220-
221-
def _set_client_data(span, is_cluster, name, *args):
222-
# type: (Span, bool, str, *Any) -> None
223-
span.set_data(SPANDATA.DB_SYSTEM, "redis")
224-
span.set_tag("redis.is_cluster", is_cluster)
225-
if name:
226-
span.set_tag("redis.command", name)
227-
span.set_tag(SPANDATA.DB_OPERATION, name)
228-
229-
if name and args:
230-
name_low = name.lower()
231-
if (name_low in _SINGLE_KEY_COMMANDS) or (
232-
name_low in _MULTI_KEY_COMMANDS and len(args) == 1
233-
):
234-
span.set_tag("redis.key", args[0])
235-
236-
237-
def patch_redis_client(cls, is_cluster):
238-
# type: (Any, bool) -> None
239-
"""
240-
This function can be used to instrument custom redis client classes or
241-
subclasses.
242-
"""
243-
old_execute_command = cls.execute_command
244-
245-
def sentry_patched_execute_command(self, name, *args, **kwargs):
246-
# type: (Any, str, *Any, **Any) -> Any
247-
hub = Hub.current
248-
integration = hub.get_integration(RedisIntegration)
249-
250-
if integration is None:
251-
return old_execute_command(self, name, *args, **kwargs)
252-
253-
description = _get_span_description(name, *args)
254-
255-
data_should_be_truncated = (
256-
integration.max_data_size and len(description) > integration.max_data_size
257-
)
258-
if data_should_be_truncated:
259-
description = description[: integration.max_data_size - len("...")] + "..."
260-
261-
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
262-
_set_client_data(span, is_cluster, name, *args)
263-
264-
return old_execute_command(self, name, *args, **kwargs)
265-
266-
cls.execute_command = sentry_patched_execute_command

sentry_sdk/integrations/redis/asyncio.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,18 @@
22

33
from sentry_sdk import Hub
44
from sentry_sdk.consts import OP
5-
from sentry_sdk.utils import capture_internal_exceptions
65
from sentry_sdk.integrations.redis import (
76
RedisIntegration,
87
_get_redis_command_args,
98
_get_span_description,
109
_set_client_data,
10+
_set_db_data,
1111
_set_pipeline_data,
1212
)
13+
from sentry_sdk._types import TYPE_CHECKING
14+
from sentry_sdk.utils import capture_internal_exceptions
1315

14-
15-
from sentry_sdk._types import MYPY
16-
17-
if MYPY:
16+
if TYPE_CHECKING:
1817
from typing import Any
1918

2019

@@ -33,6 +32,7 @@ async def _sentry_execute(self, *args, **kwargs):
3332
op=OP.DB_REDIS, description="redis.pipeline.execute"
3433
) as span:
3534
with capture_internal_exceptions():
35+
_set_db_data(span, self.connection_pool.connection_kwargs)
3636
_set_pipeline_data(
3737
span,
3838
False,
@@ -60,6 +60,7 @@ async def _sentry_execute_command(self, name, *args, **kwargs):
6060
description = _get_span_description(name, *args)
6161

6262
with hub.start_span(op=OP.DB_REDIS, description=description) as span:
63+
_set_db_data(span, self.connection_pool.connection_kwargs)
6364
_set_client_data(span, False, name, *args)
6465

6566
return await old_execute_command(self, name, *args, **kwargs)

tests/integrations/redis/asyncio/test_redis_asyncio.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from sentry_sdk import capture_message, start_transaction
4+
from sentry_sdk.consts import SPANDATA
45
from sentry_sdk.integrations.redis import RedisIntegration
56

67
from fakeredis.aioredis import FakeRedis
@@ -67,7 +68,13 @@ async def test_async_redis_pipeline(
6768
"redis.commands": {
6869
"count": 3,
6970
"first_ten": expected_first_ten,
70-
}
71+
},
72+
SPANDATA.DB_SYSTEM: "redis",
73+
SPANDATA.DB_NAME: "0",
74+
SPANDATA.SERVER_ADDRESS: connection.connection_pool.connection_kwargs.get(
75+
"host"
76+
),
77+
SPANDATA.SERVER_PORT: 6379,
7178
}
7279
assert span["tags"] == {
7380
"redis.transaction": is_transaction,

tests/integrations/redis/test_redis.py

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
import mock # python < 3.3
1313

1414

15+
MOCK_CONNECTION_POOL = mock.MagicMock()
16+
MOCK_CONNECTION_POOL.connection_kwargs = {
17+
"host": "localhost",
18+
"port": 63791,
19+
"db": 1,
20+
}
21+
22+
1523
def test_basic(sentry_init, capture_events):
1624
sentry_init(integrations=[RedisIntegration()])
1725
events = capture_events()
@@ -67,12 +75,10 @@ def test_redis_pipeline(
6775
(span,) = event["spans"]
6876
assert span["op"] == "db.redis"
6977
assert span["description"] == "redis.pipeline.execute"
70-
assert span["data"] == {
71-
"redis.commands": {
72-
"count": 3,
73-
"first_ten": expected_first_ten,
74-
},
75-
SPANDATA.DB_SYSTEM: "redis",
78+
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
79+
assert span["data"]["redis.commands"] == {
80+
"count": 3,
81+
"first_ten": expected_first_ten,
7682
}
7783
assert span["tags"] == {
7884
"redis.transaction": is_transaction,
@@ -242,3 +248,51 @@ def test_breadcrumbs(sentry_init, capture_events):
242248
},
243249
"timestamp": crumbs[1]["timestamp"],
244250
}
251+
252+
253+
def test_db_connection_attributes_client(sentry_init, capture_events):
254+
sentry_init(
255+
traces_sample_rate=1.0,
256+
integrations=[RedisIntegration()],
257+
)
258+
events = capture_events()
259+
260+
with start_transaction():
261+
connection = FakeStrictRedis(connection_pool=MOCK_CONNECTION_POOL)
262+
connection.get("foobar")
263+
264+
(event,) = events
265+
(span,) = event["spans"]
266+
267+
assert span["op"] == "db.redis"
268+
assert span["description"] == "GET 'foobar'"
269+
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
270+
assert span["data"][SPANDATA.DB_NAME] == "1"
271+
assert span["data"][SPANDATA.SERVER_ADDRESS] == "localhost"
272+
assert span["data"][SPANDATA.SERVER_PORT] == 63791
273+
274+
275+
def test_db_connection_attributes_pipeline(sentry_init, capture_events):
276+
sentry_init(
277+
traces_sample_rate=1.0,
278+
integrations=[RedisIntegration()],
279+
)
280+
events = capture_events()
281+
282+
with start_transaction():
283+
connection = FakeStrictRedis(connection_pool=MOCK_CONNECTION_POOL)
284+
pipeline = connection.pipeline(transaction=False)
285+
pipeline.get("foo")
286+
pipeline.set("bar", 1)
287+
pipeline.set("baz", 2)
288+
pipeline.execute()
289+
290+
(event,) = events
291+
(span,) = event["spans"]
292+
293+
assert span["op"] == "db.redis"
294+
assert span["description"] == "redis.pipeline.execute"
295+
assert span["data"][SPANDATA.DB_SYSTEM] == "redis"
296+
assert span["data"][SPANDATA.DB_NAME] == "1"
297+
assert span["data"][SPANDATA.SERVER_ADDRESS] == "localhost"
298+
assert span["data"][SPANDATA.SERVER_PORT] == 63791

0 commit comments

Comments
 (0)