diff --git a/elasticapm/utils/encoding.py b/elasticapm/utils/encoding.py index 4455f2685..fedaa8d63 100644 --- a/elasticapm/utils/encoding.py +++ b/elasticapm/utils/encoding.py @@ -36,6 +36,11 @@ import uuid from decimal import Decimal +try: + from django.db.models import QuerySet as DjangoQuerySet +except ImportError: + DjangoQuerySet = None + from elasticapm.conf.constants import KEYWORD_MAX_LENGTH, LABEL_RE, LABEL_TYPES, LONG_FIELD_MAX_LENGTH PROTECTED_TYPES = (int, type(None), float, Decimal, datetime.datetime, datetime.date, datetime.time) @@ -144,6 +149,14 @@ class value_type(list): ret = float(value) elif isinstance(value, int): ret = int(value) + elif ( + DjangoQuerySet is not None + and isinstance(value, DjangoQuerySet) + and getattr(value, "_result_cache", True) is None + ): + # if we have a Django QuerySet a None result cache it may mean that the underlying query failed + # so represent it as unevaluated instead of retrying the query again + ret = "<%s `unevaluated`>" % (value.__class__.__name__) elif value is not None: try: ret = transform(repr(value)) diff --git a/tests/contrib/django/django_tests.py b/tests/contrib/django/django_tests.py index 535729bcf..72c791280 100644 --- a/tests/contrib/django/django_tests.py +++ b/tests/contrib/django/django_tests.py @@ -1318,6 +1318,23 @@ def test_capture_post_errors_dict(client, django_elasticapm_client): assert error["context"]["request"]["body"] == "[REDACTED]" +@pytest.mark.parametrize( + "django_elasticapm_client", + [{"capture_body": "errors"}, {"capture_body": "transactions"}, {"capture_body": "all"}, {"capture_body": "off"}], + indirect=True, +) +def test_capture_django_orm_timeout_error(client, django_elasticapm_client): + with pytest.raises(DatabaseError): + client.get(reverse("elasticapm-django-orm-exc")) + + errors = django_elasticapm_client.events[ERROR] + if django_elasticapm_client.config.capture_body in (constants.ERROR, "all"): + stacktrace = errors[0]["exception"]["stacktrace"] + frames = [frame for frame in stacktrace if frame["function"] == "django_queryset_error"] + qs_var = frames[0]["vars"]["qs"] + assert qs_var == "" + + def test_capture_body_config_is_dynamic_for_errors(client, django_elasticapm_client): django_elasticapm_client.config.update(version="1", capture_body="all") with pytest.raises(MyException): diff --git a/tests/contrib/django/testapp/urls.py b/tests/contrib/django/testapp/urls.py index 857215280..92302e313 100644 --- a/tests/contrib/django/testapp/urls.py +++ b/tests/contrib/django/testapp/urls.py @@ -62,6 +62,7 @@ def handler500(request): re_path(r"^trigger-500-ioerror$", views.raise_ioerror, name="elasticapm-raise-ioerror"), re_path(r"^trigger-500-decorated$", views.decorated_raise_exc, name="elasticapm-raise-exc-decor"), re_path(r"^trigger-500-django$", views.django_exc, name="elasticapm-django-exc"), + re_path(r"^trigger-500-django-orm-exc$", views.django_queryset_error, name="elasticapm-django-orm-exc"), re_path(r"^trigger-500-template$", views.template_exc, name="elasticapm-template-exc"), re_path(r"^trigger-500-log-request$", views.logging_request_exc, name="elasticapm-log-request-exc"), re_path(r"^streaming$", views.streaming_view, name="elasticapm-streaming-view"), diff --git a/tests/contrib/django/testapp/views.py b/tests/contrib/django/testapp/views.py index 5a11b0961..906a8c2df 100644 --- a/tests/contrib/django/testapp/views.py +++ b/tests/contrib/django/testapp/views.py @@ -34,6 +34,8 @@ import time from django.contrib.auth.models import User +from django.db import DatabaseError +from django.db.models import QuerySet from django.http import HttpResponse, StreamingHttpResponse from django.shortcuts import get_object_or_404, render from django.views import View @@ -70,6 +72,20 @@ def django_exc(request): return get_object_or_404(MyException, pk=1) +def django_queryset_error(request): + """Simulation of django ORM timeout""" + + class CustomQuerySet(QuerySet): + def all(self): + raise DatabaseError() + + def __repr__(self) -> str: + return str(self._result_cache) + + qs = CustomQuerySet() + list(qs.all()) + + def raise_exc(request): raise MyException(request.GET.get("message", "view exception"))