Skip to content

Commit d521f30

Browse files
Pokapiecpawelusermocxrmx
authored
#2114 repeating query in django view (#2158)
* avoid making extensive queries by overriding queryset cache * move variable transformation to transform encoding util * handle django not installed, clearer failed query code * Cleanup pr * Run pre-commit * Update elasticapm/utils/encoding.py --------- Co-authored-by: p.okapiec <[email protected]> Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 4b0c8e9 commit d521f30

File tree

4 files changed

+47
-0
lines changed

4 files changed

+47
-0
lines changed

elasticapm/utils/encoding.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
import uuid
3737
from decimal import Decimal
3838

39+
try:
40+
from django.db.models import QuerySet as DjangoQuerySet
41+
except ImportError:
42+
DjangoQuerySet = None
43+
3944
from elasticapm.conf.constants import KEYWORD_MAX_LENGTH, LABEL_RE, LABEL_TYPES, LONG_FIELD_MAX_LENGTH
4045

4146
PROTECTED_TYPES = (int, type(None), float, Decimal, datetime.datetime, datetime.date, datetime.time)
@@ -144,6 +149,14 @@ class value_type(list):
144149
ret = float(value)
145150
elif isinstance(value, int):
146151
ret = int(value)
152+
elif (
153+
DjangoQuerySet is not None
154+
and isinstance(value, DjangoQuerySet)
155+
and getattr(value, "_result_cache", True) is None
156+
):
157+
# if we have a Django QuerySet a None result cache it may mean that the underlying query failed
158+
# so represent it as unevaluated instead of retrying the query again
159+
ret = "<%s `unevaluated`>" % (value.__class__.__name__)
147160
elif value is not None:
148161
try:
149162
ret = transform(repr(value))

tests/contrib/django/django_tests.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1250,6 +1250,23 @@ def test_capture_post_errors_dict(client, django_elasticapm_client):
12501250
assert error["context"]["request"]["body"] == "[REDACTED]"
12511251

12521252

1253+
@pytest.mark.parametrize(
1254+
"django_elasticapm_client",
1255+
[{"capture_body": "errors"}, {"capture_body": "transactions"}, {"capture_body": "all"}, {"capture_body": "off"}],
1256+
indirect=True,
1257+
)
1258+
def test_capture_django_orm_timeout_error(client, django_elasticapm_client):
1259+
with pytest.raises(DatabaseError):
1260+
client.get(reverse("elasticapm-django-orm-exc"))
1261+
1262+
errors = django_elasticapm_client.events[ERROR]
1263+
if django_elasticapm_client.config.capture_body in (constants.ERROR, "all"):
1264+
stacktrace = errors[0]["exception"]["stacktrace"]
1265+
frames = [frame for frame in stacktrace if frame["function"] == "django_queryset_error"]
1266+
qs_var = frames[0]["vars"]["qs"]
1267+
assert qs_var == "<CustomQuerySet `unevaluated`>"
1268+
1269+
12531270
def test_capture_body_config_is_dynamic_for_errors(client, django_elasticapm_client):
12541271
django_elasticapm_client.config.update(version="1", capture_body="all")
12551272
with pytest.raises(MyException):

tests/contrib/django/testapp/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def handler500(request):
6262
re_path(r"^trigger-500-ioerror$", views.raise_ioerror, name="elasticapm-raise-ioerror"),
6363
re_path(r"^trigger-500-decorated$", views.decorated_raise_exc, name="elasticapm-raise-exc-decor"),
6464
re_path(r"^trigger-500-django$", views.django_exc, name="elasticapm-django-exc"),
65+
re_path(r"^trigger-500-django-orm-exc$", views.django_queryset_error, name="elasticapm-django-orm-exc"),
6566
re_path(r"^trigger-500-template$", views.template_exc, name="elasticapm-template-exc"),
6667
re_path(r"^trigger-500-log-request$", views.logging_request_exc, name="elasticapm-log-request-exc"),
6768
re_path(r"^streaming$", views.streaming_view, name="elasticapm-streaming-view"),

tests/contrib/django/testapp/views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import time
3535

3636
from django.contrib.auth.models import User
37+
from django.db import DatabaseError
38+
from django.db.models import QuerySet
3739
from django.http import HttpResponse, StreamingHttpResponse
3840
from django.shortcuts import get_object_or_404, render
3941
from django.views import View
@@ -70,6 +72,20 @@ def django_exc(request):
7072
return get_object_or_404(MyException, pk=1)
7173

7274

75+
def django_queryset_error(request):
76+
"""Simulation of django ORM timeout"""
77+
78+
class CustomQuerySet(QuerySet):
79+
def all(self):
80+
raise DatabaseError()
81+
82+
def __repr__(self) -> str:
83+
return str(self._result_cache)
84+
85+
qs = CustomQuerySet()
86+
list(qs.all())
87+
88+
7389
def raise_exc(request):
7490
raise MyException(request.GET.get("message", "view exception"))
7591

0 commit comments

Comments
 (0)