-
Notifications
You must be signed in to change notification settings - Fork 554
Django caching instrumentation update #3009
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d848184
8926c93
618157b
5ab4ce0
ae7a995
11f7e15
9ba4ccf
7a4e0cd
8768373
8d51038
9791ec8
3d5d08a
fa47da2
94421d2
be1d4fd
c8d86d4
8ec780e
c9bff4b
34743e0
93b539c
3a50d66
5a03f12
2e41ae9
11a662e
0fad52b
2dcdeb9
b398172
bcf702c
cefe47f
c051197
11be8fa
3718f41
66e1c6a
16a9edf
1ad1cb8
3b89394
0115843
827858e
cc0f1af
2de1d03
00e1aa5
f2d8c77
ca65d17
7adb5a1
7232c44
a319a51
a691861
5dbdf35
8b710ab
fe52b14
747e0c8
4fa92d0
e7986b8
66eff13
2648759
7d3a2a6
2b84023
a001e15
7ac3c62
dbdf1e9
891b453
941ca77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,80 +1,151 @@ | ||||||
import functools | ||||||
from typing import TYPE_CHECKING | ||||||
from urllib3.util import parse_url as urlparse | ||||||
|
||||||
from django import VERSION as DJANGO_VERSION | ||||||
from django.core.cache import CacheHandler | ||||||
|
||||||
import sentry_sdk | ||||||
from sentry_sdk.consts import OP, SPANDATA | ||||||
from sentry_sdk.utils import ensure_integration_enabled | ||||||
from sentry_sdk.utils import ( | ||||||
SENSITIVE_DATA_SUBSTITUTE, | ||||||
capture_internal_exceptions, | ||||||
ensure_integration_enabled, | ||||||
) | ||||||
|
||||||
|
||||||
if TYPE_CHECKING: | ||||||
from typing import Any | ||||||
from typing import Callable | ||||||
from typing import Optional | ||||||
|
||||||
|
||||||
METHODS_TO_INSTRUMENT = [ | ||||||
"set", | ||||||
"set_many", | ||||||
"get", | ||||||
"get_many", | ||||||
] | ||||||
|
||||||
|
||||||
def _get_span_description(method_name, args, kwargs): | ||||||
# type: (str, Any, Any) -> str | ||||||
description = "{} ".format(method_name) | ||||||
def _get_key(args, kwargs): | ||||||
# type: (list[Any], dict[str, Any]) -> str | ||||||
key = "" | ||||||
|
||||||
if args is not None and len(args) >= 1: | ||||||
description += str(args[0]) | ||||||
key = args[0] | ||||||
elif kwargs is not None and "key" in kwargs: | ||||||
description += str(kwargs["key"]) | ||||||
key = kwargs["key"] | ||||||
|
||||||
if isinstance(key, dict): | ||||||
# Do not leak sensitive data | ||||||
# `set_many()` has a dict {"key1": "value1", "key2": "value2"} as first argument. | ||||||
# Those values could include sensitive data so we replace them with a placeholder | ||||||
key = {x: SENSITIVE_DATA_SUBSTITUTE for x in key} | ||||||
|
||||||
return str(key) | ||||||
|
||||||
|
||||||
return description | ||||||
def _get_span_description(method_name, args, kwargs): | ||||||
# type: (str, list[Any], dict[str, Any]) -> str | ||||||
return _get_key(args, kwargs) | ||||||
|
||||||
|
||||||
def _patch_cache_method(cache, method_name): | ||||||
# type: (CacheHandler, str) -> None | ||||||
def _patch_cache_method(cache, method_name, address, port): | ||||||
# type: (CacheHandler, str, Optional[str], Optional[int]) -> None | ||||||
from sentry_sdk.integrations.django import DjangoIntegration | ||||||
|
||||||
original_method = getattr(cache, method_name) | ||||||
|
||||||
@ensure_integration_enabled(DjangoIntegration, original_method) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think it makes more sense to place this on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, probably better a new PR. Can you please create a ticket @szokeasaurusrex ? |
||||||
def _instrument_call(cache, method_name, original_method, args, kwargs): | ||||||
# type: (CacheHandler, str, Callable[..., Any], Any, Any) -> Any | ||||||
def _instrument_call( | ||||||
cache, method_name, original_method, args, kwargs, address, port | ||||||
): | ||||||
# type: (CacheHandler, str, Callable[..., Any], list[Any], dict[str, Any], Optional[str], Optional[int]) -> Any | ||||||
is_set_operation = method_name.startswith("set") | ||||||
is_get_operation = not is_set_operation | ||||||
|
||||||
op = OP.CACHE_SET if is_set_operation else OP.CACHE_GET | ||||||
description = _get_span_description(method_name, args, kwargs) | ||||||
|
||||||
with sentry_sdk.start_span( | ||||||
op=OP.CACHE_GET_ITEM, description=description | ||||||
) as span: | ||||||
with sentry_sdk.start_span(op=op, description=description) as span: | ||||||
value = original_method(*args, **kwargs) | ||||||
|
||||||
if value: | ||||||
span.set_data(SPANDATA.CACHE_HIT, True) | ||||||
|
||||||
size = len(str(value)) | ||||||
span.set_data(SPANDATA.CACHE_ITEM_SIZE, size) | ||||||
|
||||||
else: | ||||||
span.set_data(SPANDATA.CACHE_HIT, False) | ||||||
with capture_internal_exceptions(): | ||||||
if address is not None: | ||||||
span.set_data(SPANDATA.NETWORK_PEER_ADDRESS, address) | ||||||
|
||||||
if port is not None: | ||||||
span.set_data(SPANDATA.NETWORK_PEER_PORT, port) | ||||||
|
||||||
key = _get_key(args, kwargs) | ||||||
szokeasaurusrex marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
if key != "": | ||||||
span.set_data(SPANDATA.CACHE_KEY, key) | ||||||
|
||||||
item_size = None | ||||||
if is_get_operation: | ||||||
if value: | ||||||
antonpirker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
item_size = len(str(value)) | ||||||
span.set_data(SPANDATA.CACHE_HIT, True) | ||||||
else: | ||||||
span.set_data(SPANDATA.CACHE_HIT, False) | ||||||
else: | ||||||
try: | ||||||
# 'set' command | ||||||
item_size = len(str(args[1])) | ||||||
except IndexError: | ||||||
# 'set_many' command | ||||||
item_size = len(str(args[0])) | ||||||
antonpirker marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
if item_size is not None: | ||||||
span.set_data(SPANDATA.CACHE_ITEM_SIZE, item_size) | ||||||
|
||||||
return value | ||||||
|
||||||
@functools.wraps(original_method) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
To be applied alongside my comment on (original version) line 41; also, likely makes sense to do in separate PR. |
||||||
def sentry_method(*args, **kwargs): | ||||||
# type: (*Any, **Any) -> Any | ||||||
return _instrument_call(cache, method_name, original_method, args, kwargs) | ||||||
return _instrument_call( | ||||||
cache, method_name, original_method, args, kwargs, address, port | ||||||
) | ||||||
|
||||||
setattr(cache, method_name, sentry_method) | ||||||
|
||||||
|
||||||
def _patch_cache(cache): | ||||||
# type: (CacheHandler) -> None | ||||||
def _patch_cache(cache, address=None, port=None): | ||||||
# type: (CacheHandler, Optional[str], Optional[int]) -> None | ||||||
if not hasattr(cache, "_sentry_patched"): | ||||||
for method_name in METHODS_TO_INSTRUMENT: | ||||||
_patch_cache_method(cache, method_name) | ||||||
_patch_cache_method(cache, method_name, address, port) | ||||||
cache._sentry_patched = True | ||||||
|
||||||
|
||||||
def _get_address_port(settings): | ||||||
# type: (dict[str, Any]) -> tuple[Optional[str], Optional[int]] | ||||||
location = settings.get("LOCATION") | ||||||
|
||||||
# TODO: location can also be an array of locations | ||||||
# see: https://docs.djangoproject.com/en/5.0/topics/cache/#redis | ||||||
# GitHub issue: https://github.com/getsentry/sentry-python/issues/3062 | ||||||
if not isinstance(location, str): | ||||||
return None, None | ||||||
|
||||||
if "://" in location: | ||||||
parsed_url = urlparse(location) | ||||||
# remove the username and password from URL to not leak sensitive data. | ||||||
address = "{}://{}{}".format( | ||||||
parsed_url.scheme or "", | ||||||
parsed_url.hostname or "", | ||||||
parsed_url.path or "", | ||||||
) | ||||||
port = parsed_url.port | ||||||
else: | ||||||
address = location | ||||||
port = None | ||||||
|
||||||
return address, int(port) if port is not None else None | ||||||
|
||||||
|
||||||
def patch_caching(): | ||||||
# type: () -> None | ||||||
from sentry_sdk.integrations.django import DjangoIntegration | ||||||
|
@@ -90,7 +161,13 @@ def sentry_get_item(self, alias): | |||||
|
||||||
integration = sentry_sdk.get_client().get_integration(DjangoIntegration) | ||||||
if integration is not None and integration.cache_spans: | ||||||
_patch_cache(cache) | ||||||
from django.conf import settings | ||||||
|
||||||
address, port = _get_address_port( | ||||||
settings.CACHES[alias or "default"] | ||||||
) | ||||||
|
||||||
_patch_cache(cache, address, port) | ||||||
|
||||||
return cache | ||||||
|
||||||
|
@@ -107,7 +184,9 @@ def sentry_create_connection(self, alias): | |||||
|
||||||
integration = sentry_sdk.get_client().get_integration(DjangoIntegration) | ||||||
if integration is not None and integration.cache_spans: | ||||||
_patch_cache(cache) | ||||||
address, port = _get_address_port(self.settings[alias or "default"]) | ||||||
|
||||||
_patch_cache(cache, address, port) | ||||||
|
||||||
return cache | ||||||
|
||||||
|
Uh oh!
There was an error while loading. Please reload this page.