11import functools
22from typing import TYPE_CHECKING
3+ from urllib3 .util import parse_url as urlparse
34
45from django import VERSION as DJANGO_VERSION
56from django .core .cache import CacheHandler
67
78import sentry_sdk
89from 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+ )
1015
1116
1217if TYPE_CHECKING :
1318 from typing import Any
1419 from typing import Callable
20+ from typing import Optional
1521
1622
1723METHODS_TO_INSTRUMENT = [
24+ "set" ,
25+ "set_many" ,
1826 "get" ,
1927 "get_many" ,
2028]
2129
2230
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 = ""
2634
2735 if args is not None and len (args ) >= 1 :
28- description += str ( args [0 ])
36+ key = args [0 ]
2937 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+
3148
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 )
3352
3453
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
3756 from sentry_sdk .integrations .django import DjangoIntegration
3857
3958 original_method = getattr (cache , method_name )
4059
4160 @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
4469 description = _get_span_description (method_name , args , kwargs )
4570
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 :
4972 value = original_method (* args , ** kwargs )
5073
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 )
59102
60103 return value
61104
62105 @functools .wraps (original_method )
63106 def sentry_method (* args , ** kwargs ):
64107 # 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+ )
66111
67112 setattr (cache , method_name , sentry_method )
68113
69114
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
72117 if not hasattr (cache , "_sentry_patched" ):
73118 for method_name in METHODS_TO_INSTRUMENT :
74- _patch_cache_method (cache , method_name )
119+ _patch_cache_method (cache , method_name , address , port )
75120 cache ._sentry_patched = True
76121
77122
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+
78149def patch_caching ():
79150 # type: () -> None
80151 from sentry_sdk .integrations .django import DjangoIntegration
@@ -90,7 +161,13 @@ def sentry_get_item(self, alias):
90161
91162 integration = sentry_sdk .get_client ().get_integration (DjangoIntegration )
92163 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 )
94171
95172 return cache
96173
@@ -107,7 +184,9 @@ def sentry_create_connection(self, alias):
107184
108185 integration = sentry_sdk .get_client ().get_integration (DjangoIntegration )
109186 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 )
111190
112191 return cache
113192
0 commit comments