caching.py 6.8 KB

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