1
1
import functools
2
2
from typing import TYPE_CHECKING
3
+ from urllib3 .util import parse_url as urlparse
3
4
4
5
from django import VERSION as DJANGO_VERSION
5
6
from django .core .cache import CacheHandler
6
7
7
8
import sentry_sdk
8
9
from sentry_sdk .consts import OP , SPANDATA
9
- from sentry_sdk .utils import ensure_integration_enabled
10
+ from sentry_sdk .utils import (
11
+ SENSITIVE_DATA_SUBSTITUTE ,
12
+ capture_internal_exceptions ,
13
+ ensure_integration_enabled ,
14
+ )
10
15
11
16
12
17
if TYPE_CHECKING :
13
18
from typing import Any
14
19
from typing import Callable
20
+ from typing import Optional
15
21
16
22
17
23
METHODS_TO_INSTRUMENT = [
24
+ "set" ,
25
+ "set_many" ,
18
26
"get" ,
19
27
"get_many" ,
20
28
]
21
29
22
30
23
- def _get_span_description ( method_name , args , kwargs ):
24
- # type: (str, Any , Any) -> str
25
- description = "{} " . format ( method_name )
31
+ def _get_key ( args , kwargs ):
32
+ # type: (list[Any], dict[str , Any] ) -> str
33
+ key = ""
26
34
27
35
if args is not None and len (args ) >= 1 :
28
- description += str ( args [0 ])
36
+ key = args [0 ]
29
37
elif kwargs is not None and "key" in kwargs :
30
- description += str (kwargs ["key" ])
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
+
31
48
32
- return description
49
+ def _get_span_description (method_name , args , kwargs ):
50
+ # type: (str, list[Any], dict[str, Any]) -> str
51
+ return _get_key (args , kwargs )
33
52
34
53
35
- def _patch_cache_method (cache , method_name ):
36
- # type: (CacheHandler, str) -> None
54
+ def _patch_cache_method (cache , method_name , address , port ):
55
+ # type: (CacheHandler, str, Optional[str], Optional[int] ) -> None
37
56
from sentry_sdk .integrations .django import DjangoIntegration
38
57
39
58
original_method = getattr (cache , method_name )
40
59
41
60
@ensure_integration_enabled (DjangoIntegration , original_method )
42
- def _instrument_call (cache , method_name , original_method , args , kwargs ):
43
- # type: (CacheHandler, str, Callable[..., Any], Any, Any) -> Any
61
+ def _instrument_call (
62
+ cache , method_name , original_method , args , kwargs , address , port
63
+ ):
64
+ # type: (CacheHandler, str, Callable[..., Any], list[Any], dict[str, Any], Optional[str], Optional[int]) -> Any
65
+ is_set_operation = method_name .startswith ("set" )
66
+ is_get_operation = not is_set_operation
67
+
68
+ op = OP .CACHE_SET if is_set_operation else OP .CACHE_GET
44
69
description = _get_span_description (method_name , args , kwargs )
45
70
46
- with sentry_sdk .start_span (
47
- op = OP .CACHE_GET_ITEM , description = description
48
- ) as span :
71
+ with sentry_sdk .start_span (op = op , description = description ) as span :
49
72
value = original_method (* args , ** kwargs )
50
73
51
- if value :
52
- span .set_data (SPANDATA .CACHE_HIT , True )
53
-
54
- size = len (str (value ))
55
- span .set_data (SPANDATA .CACHE_ITEM_SIZE , size )
56
-
57
- else :
58
- span .set_data (SPANDATA .CACHE_HIT , False )
74
+ with capture_internal_exceptions ():
75
+ if address is not None :
76
+ span .set_data (SPANDATA .NETWORK_PEER_ADDRESS , address )
77
+
78
+ if port is not None :
79
+ span .set_data (SPANDATA .NETWORK_PEER_PORT , port )
80
+
81
+ key = _get_key (args , kwargs )
82
+ if key != "" :
83
+ span .set_data (SPANDATA .CACHE_KEY , key )
84
+
85
+ item_size = None
86
+ if is_get_operation :
87
+ if value :
88
+ item_size = len (str (value ))
89
+ span .set_data (SPANDATA .CACHE_HIT , True )
90
+ else :
91
+ span .set_data (SPANDATA .CACHE_HIT , False )
92
+ else :
93
+ try :
94
+ # 'set' command
95
+ item_size = len (str (args [1 ]))
96
+ except IndexError :
97
+ # 'set_many' command
98
+ item_size = len (str (args [0 ]))
99
+
100
+ if item_size is not None :
101
+ span .set_data (SPANDATA .CACHE_ITEM_SIZE , item_size )
59
102
60
103
return value
61
104
62
105
@functools .wraps (original_method )
63
106
def sentry_method (* args , ** kwargs ):
64
107
# type: (*Any, **Any) -> Any
65
- return _instrument_call (cache , method_name , original_method , args , kwargs )
108
+ return _instrument_call (
109
+ cache , method_name , original_method , args , kwargs , address , port
110
+ )
66
111
67
112
setattr (cache , method_name , sentry_method )
68
113
69
114
70
- def _patch_cache (cache ):
71
- # type: (CacheHandler) -> None
115
+ def _patch_cache (cache , address = None , port = None ):
116
+ # type: (CacheHandler, Optional[str], Optional[int] ) -> None
72
117
if not hasattr (cache , "_sentry_patched" ):
73
118
for method_name in METHODS_TO_INSTRUMENT :
74
- _patch_cache_method (cache , method_name )
119
+ _patch_cache_method (cache , method_name , address , port )
75
120
cache ._sentry_patched = True
76
121
77
122
123
+ def _get_address_port (settings ):
124
+ # type: (dict[str, Any]) -> tuple[Optional[str], Optional[int]]
125
+ location = settings .get ("LOCATION" )
126
+
127
+ # TODO: location can also be an array of locations
128
+ # see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis
129
+ # GitHub issue: https://github.com/getsentry/sentry-python/issues/3062
130
+ if not isinstance (location , str ):
131
+ return None , None
132
+
133
+ if "://" in location :
134
+ parsed_url = urlparse (location )
135
+ # remove the username and password from URL to not leak sensitive data.
136
+ address = "{}://{}{}" .format (
137
+ parsed_url .scheme or "" ,
138
+ parsed_url .hostname or "" ,
139
+ parsed_url .path or "" ,
140
+ )
141
+ port = parsed_url .port
142
+ else :
143
+ address = location
144
+ port = None
145
+
146
+ return address , int (port ) if port is not None else None
147
+
148
+
78
149
def patch_caching ():
79
150
# type: () -> None
80
151
from sentry_sdk .integrations .django import DjangoIntegration
@@ -90,7 +161,13 @@ def sentry_get_item(self, alias):
90
161
91
162
integration = sentry_sdk .get_client ().get_integration (DjangoIntegration )
92
163
if integration is not None and integration .cache_spans :
93
- _patch_cache (cache )
164
+ from django .conf import settings
165
+
166
+ address , port = _get_address_port (
167
+ settings .CACHES [alias or "default" ]
168
+ )
169
+
170
+ _patch_cache (cache , address , port )
94
171
95
172
return cache
96
173
@@ -107,7 +184,9 @@ def sentry_create_connection(self, alias):
107
184
108
185
integration = sentry_sdk .get_client ().get_integration (DjangoIntegration )
109
186
if integration is not None and integration .cache_spans :
110
- _patch_cache (cache )
187
+ address , port = _get_address_port (self .settings [alias or "default" ])
188
+
189
+ _patch_cache (cache , address , port )
111
190
112
191
return cache
113
192
0 commit comments