Skip to content

Commit

Permalink
Add cache.hit and cache.item_size to Django (#2057)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
antonpirker committed May 5, 2023
1 parent 2610c66 commit efa55d3
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 42 deletions.
9 changes: 9 additions & 0 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ class INSTRUMENTER:

# See: https://develop.sentry.dev/sdk/performance/span-data-conventions/
class SPANDATA:
# An identifier for the database management system (DBMS) product being used.
# See: https://github.com/open-telemetry/opentelemetry-python/blob/e00306206ea25cf8549eca289e39e0b6ba2fa560/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py#L58
DB_SYSTEM = "db.system"

# A boolean indicating whether the requested data was found in the cache.
CACHE_HIT = "cache.hit"

# The size of the requested data in bytes.
CACHE_ITEM_SIZE = "cache.item_size"
"""
An identifier for the database management system (DBMS) product being used.
See: https://github.com/open-telemetry/opentelemetry-python/blob/e00306206ea25cf8549eca289e39e0b6ba2fa560/opentelemetry-semantic-conventions/src/opentelemetry/semconv/trace/__init__.py#L58
Expand All @@ -76,6 +84,7 @@ class SPANDATA:


class OP:
CACHE = "cache"
DB = "db"
DB_REDIS = "db.redis"
EVENT_DJANGO = "event.django"
Expand Down
19 changes: 16 additions & 3 deletions sentry_sdk/integrations/django/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
except ImportError:
raise DidNotEnable("Django not installed")


from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
from sentry_sdk.integrations.django.templates import (
get_template_frame_from_exception,
Expand All @@ -50,6 +49,11 @@
from sentry_sdk.integrations.django.signals_handlers import patch_signals
from sentry_sdk.integrations.django.views import patch_views

if DJANGO_VERSION[:2] > (1, 8):
from sentry_sdk.integrations.django.caching import patch_caching
else:
patch_caching = None # type: ignore


if TYPE_CHECKING:
from typing import Any
Expand Down Expand Up @@ -92,11 +96,16 @@ class DjangoIntegration(Integration):
transaction_style = ""
middleware_spans = None
signals_spans = None
cache_spans = None

def __init__(
self, transaction_style="url", middleware_spans=True, signals_spans=True
self,
transaction_style="url",
middleware_spans=True,
signals_spans=True,
cache_spans=True,
):
# type: (str, bool, bool) -> None
# type: (str, bool, bool, bool) -> None
if transaction_style not in TRANSACTION_STYLE_VALUES:
raise ValueError(
"Invalid value for transaction_style: %s (must be in %s)"
Expand All @@ -105,6 +114,7 @@ def __init__(
self.transaction_style = transaction_style
self.middleware_spans = middleware_spans
self.signals_spans = signals_spans
self.cache_spans = cache_spans

@staticmethod
def setup_once():
Expand Down Expand Up @@ -224,6 +234,9 @@ def _django_queryset_repr(value, hint):
patch_templates()
patch_signals()

if patch_caching is not None:
patch_caching()


_DRF_PATCHED = False
_DRF_PATCH_LOCK = threading.Lock()
Expand Down
105 changes: 105 additions & 0 deletions sentry_sdk/integrations/django/caching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import functools
from typing import TYPE_CHECKING

from django import VERSION as DJANGO_VERSION
from django.core.cache import CacheHandler

from sentry_sdk import Hub
from sentry_sdk.consts import OP, SPANDATA
from sentry_sdk._compat import text_type


if TYPE_CHECKING:
from typing import Any
from typing import Callable


METHODS_TO_INSTRUMENT = [
"get",
"get_many",
]


def _patch_cache_method(cache, method_name):
# type: (CacheHandler, str) -> None
from sentry_sdk.integrations.django import DjangoIntegration

def _instrument_call(cache, method_name, original_method, args, kwargs):
# type: (CacheHandler, str, Callable[..., Any], Any, Any) -> Any
hub = Hub.current
integration = hub.get_integration(DjangoIntegration)
if integration is None or not integration.cache_spans:
return original_method(*args, **kwargs)

description = "{} {}".format(method_name, " ".join(args))

with hub.start_span(op=OP.CACHE, description=description) as span:
value = original_method(*args, **kwargs)

if value:
span.set_data(SPANDATA.CACHE_HIT, True)

size = len(text_type(value).encode("utf-8"))
span.set_data(SPANDATA.CACHE_ITEM_SIZE, size)

else:
span.set_data(SPANDATA.CACHE_HIT, False)

return value

original_method = getattr(cache, method_name)

@functools.wraps(original_method)
def sentry_method(*args, **kwargs):
# type: (*Any, **Any) -> Any
return _instrument_call(cache, method_name, original_method, args, kwargs)

setattr(cache, method_name, sentry_method)


def _patch_cache(cache):
# type: (CacheHandler) -> None
if not hasattr(cache, "_sentry_patched"):
for method_name in METHODS_TO_INSTRUMENT:
_patch_cache_method(cache, method_name)
cache._sentry_patched = True


def patch_caching():
# type: () -> None
from sentry_sdk.integrations.django import DjangoIntegration

if not hasattr(CacheHandler, "_sentry_patched"):
if DJANGO_VERSION < (3, 2):
original_get_item = CacheHandler.__getitem__

@functools.wraps(original_get_item)
def sentry_get_item(self, alias):
# type: (CacheHandler, str) -> Any
cache = original_get_item(self, alias)

integration = Hub.current.get_integration(DjangoIntegration)
if integration and integration.cache_spans:
_patch_cache(cache)

return cache

CacheHandler.__getitem__ = sentry_get_item
CacheHandler._sentry_patched = True

else:
original_create_connection = CacheHandler.create_connection

@functools.wraps(original_create_connection)
def sentry_create_connection(self, alias):
# type: (CacheHandler, str) -> Any
cache = original_create_connection(self, alias)

integration = Hub.current.get_integration(DjangoIntegration)
if integration and integration.cache_spans:
_patch_cache(cache)

return cache

CacheHandler.create_connection = sentry_create_connection
CacheHandler._sentry_patched = True
7 changes: 7 additions & 0 deletions tests/integrations/django/myapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ def path(path, *args, **kwargs):

urlpatterns = [
path("view-exc", views.view_exc, name="view_exc"),
path("cached-view", views.cached_view, name="cached_view"),
path("not-cached-view", views.not_cached_view, name="not_cached_view"),
path(
"view-with-cached-template-fragment",
views.view_with_cached_template_fragment,
name="view_with_cached_template_fragment",
),
path(
"read-body-and-view-exc",
views.read_body_and_view_exc,
Expand Down
25 changes: 25 additions & 0 deletions tests/integrations/django/myapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse, HttpResponseNotFound, HttpResponseServerError
from django.shortcuts import render
from django.template import Context, Template
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView


try:
from rest_framework.decorators import api_view
from rest_framework.response import Response
Expand Down Expand Up @@ -49,6 +52,28 @@ def view_exc(request):
1 / 0


@cache_page(60)
def cached_view(request):
return HttpResponse("ok")


def not_cached_view(request):
return HttpResponse("ok")


def view_with_cached_template_fragment(request):
template = Template(
"""{% load cache %}
Not cached content goes here.
{% cache 500 some_identifier %}
And here some cached content.
{% endcache %}
"""
)
rendered = template.render(Context({}))
return HttpResponse(rendered)


# This is a "class based view" as previously found in the sentry codebase. The
# interesting property of this one is that csrf_exempt, as a class attribute,
# is not in __dict__, so regular use of functools.wraps will not forward the
Expand Down
Loading

0 comments on commit efa55d3

Please sign in to comment.