Skip to content

Commit efa55d3

Browse files
authored
Add cache.hit and cache.item_size to Django (#2057)
In Django we want to add information to spans if a configured cache was hit or missed and if hit what the item_size of the object in the cache was.
1 parent 2610c66 commit efa55d3

File tree

8 files changed

+431
-42
lines changed

8 files changed

+431
-42
lines changed

sentry_sdk/consts.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,15 @@ class INSTRUMENTER:
5353

5454
# See: https://develop.sentry.dev/sdk/performance/span-data-conventions/
5555
class SPANDATA:
56+
# An identifier for the database management system (DBMS) product being used.
57+
# See: https://github.com/open-telemetry/opentelemetry-python/blob/e00306206ea25cf8549eca289e39e0b6ba2fa560/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py#L58
5658
DB_SYSTEM = "db.system"
59+
60+
# A boolean indicating whether the requested data was found in the cache.
61+
CACHE_HIT = "cache.hit"
62+
63+
# The size of the requested data in bytes.
64+
CACHE_ITEM_SIZE = "cache.item_size"
5765
"""
5866
An identifier for the database management system (DBMS) product being used.
5967
See: https://github.com/open-telemetry/opentelemetry-python/blob/e00306206ea25cf8549eca289e39e0b6ba2fa560/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py#L58
@@ -76,6 +84,7 @@ class SPANDATA:
7684

7785

7886
class OP:
87+
CACHE = "cache"
7988
DB = "db"
8089
DB_REDIS = "db.redis"
8190
EVENT_DJANGO = "event.django"

sentry_sdk/integrations/django/__init__.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
except ImportError:
4141
raise DidNotEnable("Django not installed")
4242

43-
4443
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
4544
from sentry_sdk.integrations.django.templates import (
4645
get_template_frame_from_exception,
@@ -50,6 +49,11 @@
5049
from sentry_sdk.integrations.django.signals_handlers import patch_signals
5150
from sentry_sdk.integrations.django.views import patch_views
5251

52+
if DJANGO_VERSION[:2] > (1, 8):
53+
from sentry_sdk.integrations.django.caching import patch_caching
54+
else:
55+
patch_caching = None # type: ignore
56+
5357

5458
if TYPE_CHECKING:
5559
from typing import Any
@@ -92,11 +96,16 @@ class DjangoIntegration(Integration):
9296
transaction_style = ""
9397
middleware_spans = None
9498
signals_spans = None
99+
cache_spans = None
95100

96101
def __init__(
97-
self, transaction_style="url", middleware_spans=True, signals_spans=True
102+
self,
103+
transaction_style="url",
104+
middleware_spans=True,
105+
signals_spans=True,
106+
cache_spans=True,
98107
):
99-
# type: (str, bool, bool) -> None
108+
# type: (str, bool, bool, bool) -> None
100109
if transaction_style not in TRANSACTION_STYLE_VALUES:
101110
raise ValueError(
102111
"Invalid value for transaction_style: %s (must be in %s)"
@@ -105,6 +114,7 @@ def __init__(
105114
self.transaction_style = transaction_style
106115
self.middleware_spans = middleware_spans
107116
self.signals_spans = signals_spans
117+
self.cache_spans = cache_spans
108118

109119
@staticmethod
110120
def setup_once():
@@ -224,6 +234,9 @@ def _django_queryset_repr(value, hint):
224234
patch_templates()
225235
patch_signals()
226236

237+
if patch_caching is not None:
238+
patch_caching()
239+
227240

228241
_DRF_PATCHED = False
229242
_DRF_PATCH_LOCK = threading.Lock()
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import functools
2+
from typing import TYPE_CHECKING
3+
4+
from django import VERSION as DJANGO_VERSION
5+
from django.core.cache import CacheHandler
6+
7+
from sentry_sdk import Hub
8+
from sentry_sdk.consts import OP, SPANDATA
9+
from sentry_sdk._compat import text_type
10+
11+
12+
if TYPE_CHECKING:
13+
from typing import Any
14+
from typing import Callable
15+
16+
17+
METHODS_TO_INSTRUMENT = [
18+
"get",
19+
"get_many",
20+
]
21+
22+
23+
def _patch_cache_method(cache, method_name):
24+
# type: (CacheHandler, str) -> None
25+
from sentry_sdk.integrations.django import DjangoIntegration
26+
27+
def _instrument_call(cache, method_name, original_method, args, kwargs):
28+
# type: (CacheHandler, str, Callable[..., Any], Any, Any) -> Any
29+
hub = Hub.current
30+
integration = hub.get_integration(DjangoIntegration)
31+
if integration is None or not integration.cache_spans:
32+
return original_method(*args, **kwargs)
33+
34+
description = "{} {}".format(method_name, " ".join(args))
35+
36+
with hub.start_span(op=OP.CACHE, description=description) as span:
37+
value = original_method(*args, **kwargs)
38+
39+
if value:
40+
span.set_data(SPANDATA.CACHE_HIT, True)
41+
42+
size = len(text_type(value).encode("utf-8"))
43+
span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)
44+
45+
else:
46+
span.set_data(SPANDATA.CACHE_HIT, False)
47+
48+
return value
49+
50+
original_method = getattr(cache, method_name)
51+
52+
@functools.wraps(original_method)
53+
def sentry_method(*args, **kwargs):
54+
# type: (*Any, **Any) -> Any
55+
return _instrument_call(cache, method_name, original_method, args, kwargs)
56+
57+
setattr(cache, method_name, sentry_method)
58+
59+
60+
def _patch_cache(cache):
61+
# type: (CacheHandler) -> None
62+
if not hasattr(cache, "_sentry_patched"):
63+
for method_name in METHODS_TO_INSTRUMENT:
64+
_patch_cache_method(cache, method_name)
65+
cache._sentry_patched = True
66+
67+
68+
def patch_caching():
69+
# type: () -> None
70+
from sentry_sdk.integrations.django import DjangoIntegration
71+
72+
if not hasattr(CacheHandler, "_sentry_patched"):
73+
if DJANGO_VERSION < (3, 2):
74+
original_get_item = CacheHandler.__getitem__
75+
76+
@functools.wraps(original_get_item)
77+
def sentry_get_item(self, alias):
78+
# type: (CacheHandler, str) -> Any
79+
cache = original_get_item(self, alias)
80+
81+
integration = Hub.current.get_integration(DjangoIntegration)
82+
if integration and integration.cache_spans:
83+
_patch_cache(cache)
84+
85+
return cache
86+
87+
CacheHandler.__getitem__ = sentry_get_item
88+
CacheHandler._sentry_patched = True
89+
90+
else:
91+
original_create_connection = CacheHandler.create_connection
92+
93+
@functools.wraps(original_create_connection)
94+
def sentry_create_connection(self, alias):
95+
# type: (CacheHandler, str) -> Any
96+
cache = original_create_connection(self, alias)
97+
98+
integration = Hub.current.get_integration(DjangoIntegration)
99+
if integration and integration.cache_spans:
100+
_patch_cache(cache)
101+
102+
return cache
103+
104+
CacheHandler.create_connection = sentry_create_connection
105+
CacheHandler._sentry_patched = True

tests/integrations/django/myapp/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ def path(path, *args, **kwargs):
2828

2929
urlpatterns = [
3030
path("view-exc", views.view_exc, name="view_exc"),
31+
path("cached-view", views.cached_view, name="cached_view"),
32+
path("not-cached-view", views.not_cached_view, name="not_cached_view"),
33+
path(
34+
"view-with-cached-template-fragment",
35+
views.view_with_cached_template_fragment,
36+
name="view_with_cached_template_fragment",
37+
),
3138
path(
3239
"read-body-and-view-exc",
3340
views.read_body_and_view_exc,

tests/integrations/django/myapp/views.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
from django.core.exceptions import PermissionDenied
88
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
99
from django.shortcuts import render
10+
from django.template import Context, Template
1011
from django.template.response import TemplateResponse
1112
from django.utils.decorators import method_decorator
13+
from django.views.decorators.cache import cache_page
1214
from django.views.decorators.csrf import csrf_exempt
1315
from django.views.generic import ListView
1416

17+
1518
try:
1619
from rest_framework.decorators import api_view
1720
from rest_framework.response import Response
@@ -49,6 +52,28 @@ def view_exc(request):
4952
1 / 0
5053

5154

55+
@cache_page(60)
56+
def cached_view(request):
57+
return HttpResponse("ok")
58+
59+
60+
def not_cached_view(request):
61+
return HttpResponse("ok")
62+
63+
64+
def view_with_cached_template_fragment(request):
65+
template = Template(
66+
"""{% load cache %}
67+
Not cached content goes here.
68+
{% cache 500 some_identifier %}
69+
And here some cached content.
70+
{% endcache %}
71+
"""
72+
)
73+
rendered = template.render(Context({}))
74+
return HttpResponse(rendered)
75+
76+
5277
# This is a "class based view" as previously found in the sentry codebase. The
5378
# interesting property of this one is that csrf_exempt, as a class attribute,
5479
# is not in __dict__, so regular use of functools.wraps will not forward the

0 commit comments

Comments
 (0)