Skip to content

Commit e068db6

Browse files
feat(django): Instrument database commits (#5100)
Add spans for SQL commits issued when Django calls `commit()` on a PEP-249 database connection. Commit spans are generated for `transaction.atomic` blocks and for manual `transction.commit()` calls when auto-commit is disabled. Tests cover both cases, for SQLite and PostgreSQL, respectively.
1 parent 3c32bb4 commit e068db6

File tree

5 files changed

+520
-3
lines changed

5 files changed

+520
-3
lines changed

sentry_sdk/consts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ class INSTRUMENTER:
114114
OTEL = "otel"
115115

116116

117+
class SPANNAME:
118+
DB_COMMIT = "COMMIT"
119+
120+
117121
class SPANDATA:
118122
"""
119123
Additional information describing the type of the span.

sentry_sdk/integrations/django/__init__.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from importlib import import_module
66

77
import sentry_sdk
8-
from sentry_sdk.consts import OP, SPANDATA
8+
from sentry_sdk.consts import OP, SPANDATA, SPANNAME
99
from sentry_sdk.scope import add_global_event_processor, should_send_default_pii
1010
from sentry_sdk.serializer import add_global_repr_processor, add_repr_sequence_type
1111
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
@@ -132,6 +132,7 @@ def __init__(
132132
middleware_spans=True, # type: bool
133133
signals_spans=True, # type: bool
134134
cache_spans=False, # type: bool
135+
db_transaction_spans=False, # type: bool
135136
signals_denylist=None, # type: Optional[list[signals.Signal]]
136137
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
137138
):
@@ -148,6 +149,7 @@ def __init__(
148149
self.signals_denylist = signals_denylist or []
149150

150151
self.cache_spans = cache_spans
152+
self.db_transaction_spans = db_transaction_spans
151153

152154
self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
153155

@@ -633,6 +635,7 @@ def install_sql_hook():
633635
real_execute = CursorWrapper.execute
634636
real_executemany = CursorWrapper.executemany
635637
real_connect = BaseDatabaseWrapper.connect
638+
real_commit = BaseDatabaseWrapper._commit
636639
except AttributeError:
637640
# This won't work on Django versions < 1.6
638641
return
@@ -690,18 +693,37 @@ def connect(self):
690693
_set_db_data(span, self)
691694
return real_connect(self)
692695

696+
def _commit(self):
697+
# type: (BaseDatabaseWrapper) -> None
698+
integration = sentry_sdk.get_client().get_integration(DjangoIntegration)
699+
700+
if integration is None or not integration.db_transaction_spans:
701+
return real_commit(self)
702+
703+
with sentry_sdk.start_span(
704+
op=OP.DB,
705+
name=SPANNAME.DB_COMMIT,
706+
origin=DjangoIntegration.origin_db,
707+
) as span:
708+
_set_db_data(span, self, SPANNAME.DB_COMMIT)
709+
return real_commit(self)
710+
693711
CursorWrapper.execute = execute
694712
CursorWrapper.executemany = executemany
695713
BaseDatabaseWrapper.connect = connect
714+
BaseDatabaseWrapper._commit = _commit
696715
ignore_logger("django.db.backends")
697716

698717

699-
def _set_db_data(span, cursor_or_db):
700-
# type: (Span, Any) -> None
718+
def _set_db_data(span, cursor_or_db, db_operation=None):
719+
# type: (Span, Any, Optional[str]) -> None
701720
db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db
702721
vendor = db.vendor
703722
span.set_data(SPANDATA.DB_SYSTEM, vendor)
704723

724+
if db_operation is not None:
725+
span.set_data(SPANDATA.DB_OPERATION, db_operation)
726+
705727
# Some custom backends override `__getattr__`, making it look like `cursor_or_db`
706728
# actually has a `connection` and the `connection` has a `get_dsn_parameters`
707729
# attribute, only to throw an error once you actually want to call it.

tests/integrations/django/myapp/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ def path(path, *args, **kwargs):
6161
path("template-test4", views.template_test4, name="template_test4"),
6262
path("postgres-select", views.postgres_select, name="postgres_select"),
6363
path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"),
64+
path(
65+
"postgres-insert-no-autocommit",
66+
views.postgres_insert_orm_no_autocommit,
67+
name="postgres_insert_orm_no_autocommit",
68+
),
69+
path(
70+
"postgres-insert-atomic",
71+
views.postgres_insert_orm_atomic,
72+
name="postgres_insert_orm_atomic",
73+
),
6474
path(
6575
"postgres-select-slow-from-supplement",
6676
helper_views.postgres_select_orm,

tests/integrations/django/myapp/views.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import threading
44

5+
from django.db import transaction
56
from django.contrib.auth import login
67
from django.contrib.auth.models import User
78
from django.core.exceptions import PermissionDenied
@@ -246,6 +247,32 @@ def postgres_select_orm(request, *args, **kwargs):
246247
return HttpResponse("ok {}".format(user))
247248

248249

250+
@csrf_exempt
251+
def postgres_insert_orm_no_autocommit(request, *args, **kwargs):
252+
transaction.set_autocommit(False, using="postgres")
253+
try:
254+
user = User.objects.db_manager("postgres").create_user(
255+
username="user1",
256+
)
257+
transaction.commit(using="postgres")
258+
except Exception:
259+
transaction.rollback(using="postgres")
260+
transaction.set_autocommit(True, using="postgres")
261+
raise
262+
263+
transaction.set_autocommit(True, using="postgres")
264+
return HttpResponse("ok {}".format(user))
265+
266+
267+
@csrf_exempt
268+
def postgres_insert_orm_atomic(request, *args, **kwargs):
269+
with transaction.atomic(using="postgres"):
270+
user = User.objects.db_manager("postgres").create_user(
271+
username="user1",
272+
)
273+
return HttpResponse("ok {}".format(user))
274+
275+
249276
@csrf_exempt
250277
def permission_denied_exc(*args, **kwargs):
251278
raise PermissionDenied("bye")

0 commit comments

Comments
 (0)