Skip to content

Commit d2552ec

Browse files
feat(django): Instrument database commits
1 parent b8a0db2 commit d2552ec

File tree

3 files changed

+214
-6
lines changed

3 files changed

+214
-6
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 DBOPERATION:
118+
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: 20 additions & 4 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, DBOPERATION
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
@@ -633,6 +633,7 @@ def install_sql_hook():
633633
real_execute = CursorWrapper.execute
634634
real_executemany = CursorWrapper.executemany
635635
real_connect = BaseDatabaseWrapper.connect
636+
real_commit = BaseDatabaseWrapper.commit
636637
except AttributeError:
637638
# This won't work on Django versions < 1.6
638639
return
@@ -690,14 +691,26 @@ def connect(self):
690691
_set_db_data(span, self)
691692
return real_connect(self)
692693

694+
@ensure_integration_enabled(DjangoIntegration, real_commit)
695+
def commit(self):
696+
# type: (BaseDatabaseWrapper) -> None
697+
print("commiting")
698+
with sentry_sdk.start_span(
699+
op=OP.DB,
700+
name="commit", # DBOPERATION.COMMIT,
701+
origin=DjangoIntegration.origin_db,
702+
) as span:
703+
_set_db_data(span, self, DBOPERATION.COMMIT)
704+
return real_commit(self)
705+
693706
CursorWrapper.execute = execute
694707
CursorWrapper.executemany = executemany
695708
BaseDatabaseWrapper.connect = connect
696-
ignore_logger("django.db.backends")
709+
BaseDatabaseWrapper.commit = commit
697710

698711

699-
def _set_db_data(span, cursor_or_db):
700-
# type: (Span, Any) -> None
712+
def _set_db_data(span, cursor_or_db, db_operation=None):
713+
# type: (Span, Any, Optional[str]) -> None
701714
db = cursor_or_db.db if hasattr(cursor_or_db, "db") else cursor_or_db
702715
vendor = db.vendor
703716
span.set_data(SPANDATA.DB_SYSTEM, vendor)
@@ -735,6 +748,9 @@ def _set_db_data(span, cursor_or_db):
735748
if db_name is not None:
736749
span.set_data(SPANDATA.DB_NAME, db_name)
737750

751+
if db_operation is not None:
752+
span.set_data(SPANDATA.DB_OPERATION, db_operation)
753+
738754
server_address = connection_params.get("host")
739755
if server_address is not None:
740756
span.set_data(SPANDATA.SERVER_ADDRESS, server_address)

tests/integrations/django/test_db_query_data.py

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from unittest import mock
66

77
from django import VERSION as DJANGO_VERSION
8-
from django.db import connections
8+
from django.db import connection, connections, transaction
99

1010
try:
1111
from django.urls import reverse
@@ -15,7 +15,7 @@
1515
from werkzeug.test import Client
1616

1717
from sentry_sdk import start_transaction
18-
from sentry_sdk.consts import SPANDATA
18+
from sentry_sdk.consts import SPANDATA, DBOPERATION
1919
from sentry_sdk.integrations.django import DjangoIntegration
2020
from sentry_sdk.tracing_utils import record_sql_queries
2121

@@ -481,6 +481,7 @@ def test_db_span_origin_execute(sentry_init, client, capture_events):
481481
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
482482

483483
for span in event["spans"]:
484+
print("span is", span["op"], span["description"])
484485
if span["op"] == "db":
485486
assert span["origin"] == "auto.db.django"
486487
else:
@@ -524,3 +525,190 @@ def test_db_span_origin_executemany(sentry_init, client, capture_events):
524525

525526
assert event["contexts"]["trace"]["origin"] == "manual"
526527
assert event["spans"][0]["origin"] == "auto.db.django"
528+
529+
commit_spans = [
530+
span
531+
for span in event["spans"]
532+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT
533+
]
534+
assert len(commit_spans) == 1
535+
commit_span = commit_spans[0]
536+
assert commit_span["origin"] == "auto.db.django"
537+
538+
539+
@pytest.mark.forked
540+
@pytest_mark_django_db_decorator(transaction=True)
541+
def test_db_no_autocommit_execute(sentry_init, client, capture_events):
542+
"""
543+
Verify we record a breadcrumb when opening a new database.
544+
"""
545+
sentry_init(
546+
integrations=[DjangoIntegration()],
547+
traces_sample_rate=1.0,
548+
)
549+
550+
if "postgres" not in connections:
551+
pytest.skip("postgres tests disabled")
552+
553+
# trigger Django to open a new connection by marking the existing one as None.
554+
connections["postgres"].connection = None
555+
556+
events = capture_events()
557+
558+
client.get(reverse("postgres_select_orm_no_autocommit"))
559+
560+
(event,) = events
561+
562+
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
563+
564+
for span in event["spans"]:
565+
if span["op"] == "db":
566+
assert span["origin"] == "auto.db.django"
567+
else:
568+
assert span["origin"] == "auto.http.django"
569+
570+
commit_spans = [
571+
span
572+
for span in event["spans"]
573+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT
574+
]
575+
assert len(commit_spans) == 1
576+
commit_span = commit_spans[0]
577+
assert commit_span["origin"] == "auto.db.django"
578+
579+
580+
@pytest.mark.forked
581+
@pytest_mark_django_db_decorator(transaction=True)
582+
def test_db_no_autocommit_executemany(sentry_init, client, capture_events):
583+
sentry_init(
584+
integrations=[DjangoIntegration()],
585+
traces_sample_rate=1.0,
586+
)
587+
588+
events = capture_events()
589+
590+
if "postgres" not in connections:
591+
pytest.skip("postgres tests disabled")
592+
593+
with start_transaction(name="test_transaction"):
594+
from django.db import connection, transaction
595+
596+
cursor = connection.cursor()
597+
598+
query = """UPDATE auth_user SET username = %s where id = %s;"""
599+
query_list = (
600+
(
601+
"test1",
602+
1,
603+
),
604+
(
605+
"test2",
606+
2,
607+
),
608+
)
609+
cursor.executemany(query, query_list)
610+
611+
transaction.commit()
612+
613+
(event,) = events
614+
615+
assert event["contexts"]["trace"]["origin"] == "manual"
616+
assert event["spans"][0]["origin"] == "auto.db.django"
617+
618+
commit_spans = [
619+
span
620+
for span in event["spans"]
621+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT
622+
]
623+
assert len(commit_spans) == 1
624+
commit_span = commit_spans[0]
625+
assert commit_span["origin"] == "auto.db.django"
626+
627+
628+
@pytest.mark.forked
629+
@pytest_mark_django_db_decorator(transaction=True)
630+
def test_db_atomic_execute(sentry_init, client, capture_events):
631+
"""
632+
Verify we record a breadcrumb when opening a new database.
633+
"""
634+
sentry_init(
635+
integrations=[DjangoIntegration()],
636+
send_default_pii=True,
637+
traces_sample_rate=1.0,
638+
)
639+
640+
if "postgres" not in connections:
641+
pytest.skip("postgres tests disabled")
642+
643+
# trigger Django to open a new connection by marking the existing one as None.
644+
connections["postgres"].connection = None
645+
646+
events = capture_events()
647+
648+
with transaction.atomic():
649+
client.get(reverse("postgres_select_orm_atomic"))
650+
connections["postgres"].commit()
651+
652+
(event,) = events
653+
654+
assert event["contexts"]["trace"]["origin"] == "auto.http.django"
655+
656+
commit_spans = [
657+
span
658+
for span in event["spans"]
659+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT
660+
]
661+
assert len(commit_spans) == 1
662+
commit_span = commit_spans[0]
663+
assert commit_span["origin"] == "auto.db.django"
664+
665+
666+
@pytest.mark.forked
667+
@pytest_mark_django_db_decorator(transaction=True)
668+
def test_db_atomic_executemany(sentry_init, client, capture_events):
669+
"""
670+
Verify we record a breadcrumb when opening a new database.
671+
"""
672+
sentry_init(
673+
integrations=[DjangoIntegration()],
674+
send_default_pii=True,
675+
traces_sample_rate=1.0,
676+
)
677+
678+
if "postgres" not in connections:
679+
pytest.skip("postgres tests disabled")
680+
681+
# trigger Django to open a new connection by marking the existing one as None.
682+
connections["postgres"].connection = None
683+
684+
events = capture_events()
685+
686+
with start_transaction(name="test_transaction"):
687+
with transaction.atomic():
688+
cursor = connection.cursor()
689+
690+
query = """UPDATE auth_user SET username = %s where id = %s;"""
691+
query_list = (
692+
(
693+
"test1",
694+
1,
695+
),
696+
(
697+
"test2",
698+
2,
699+
),
700+
)
701+
cursor.executemany(query, query_list)
702+
703+
(event,) = events
704+
705+
assert event["contexts"]["trace"]["origin"] == "manual"
706+
707+
commit_spans = [
708+
span
709+
for span in event["spans"]
710+
if span["data"].get(SPANDATA.DB_OPERATION) == DBOPERATION.COMMIT
711+
]
712+
assert len(commit_spans) == 1
713+
commit_span = commit_spans[0]
714+
assert commit_span["origin"] == "auto.db.django"

0 commit comments

Comments
 (0)