1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

Fixed #31090 -- Logged transaction management queries.

Thanks to Petter Strandmark for the original idea and Mariusz Felisiak
for advice during the DjangoConUS 2022 Sprint!
This commit is contained in:
Ilya Bass 2022-10-20 15:14:35 -07:00 committed by Mariusz Felisiak
parent c0a93d3941
commit 798e38c2b9
7 changed files with 109 additions and 12 deletions

View File

@ -410,6 +410,7 @@ answer newbie questions, and generally made Django that much better:
Ifedapo Olarewaju <ifedapoolarewaju@gmail.com>
Igor Kolar <ike@email.si>
Illia Volochii <illia.volochii@gmail.com>
Ilya Bass
Ilya Semenov <semenov@inetss.com>
Ingo Klöcker <djangoproject@ingo-kloecker.de>
I.S. van Oostveen <v.oostveen@idca.nl>

View File

@ -8,6 +8,8 @@ import warnings
from collections import deque
from contextlib import contextmanager
from django.db.backends.utils import debug_transaction
try:
import zoneinfo
except ImportError:
@ -307,12 +309,12 @@ class BaseDatabaseWrapper:
def _commit(self):
if self.connection is not None:
with self.wrap_database_errors:
with debug_transaction(self, "COMMIT"), self.wrap_database_errors:
return self.connection.commit()
def _rollback(self):
if self.connection is not None:
with self.wrap_database_errors:
with debug_transaction(self, "ROLLBACK"), self.wrap_database_errors:
return self.connection.rollback()
def _close(self):
@ -488,9 +490,11 @@ class BaseDatabaseWrapper:
if start_transaction_under_autocommit:
self._start_transaction_under_autocommit()
else:
elif autocommit:
self._set_autocommit(autocommit)
else:
with debug_transaction(self, "BEGIN"):
self._set_autocommit(autocommit)
self.autocommit = autocommit
if autocommit and self.run_commit_hooks_on_set_autocommit_on:

View File

@ -13,6 +13,7 @@ from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import IntegrityError
from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.utils import debug_transaction
from django.utils.asyncio import async_unsafe
from django.utils.encoding import force_bytes, force_str
from django.utils.functional import cached_property
@ -306,7 +307,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def _commit(self):
if self.connection is not None:
with wrap_oracle_errors():
with debug_transaction(self, "COMMIT"), wrap_oracle_errors():
return self.connection.commit()
# Oracle doesn't support releasing savepoints. But we fake them when query

View File

@ -144,6 +144,35 @@ class CursorDebugWrapper(CursorWrapper):
)
@contextmanager
def debug_transaction(connection, sql):
start = time.monotonic()
try:
yield
finally:
if connection.queries_logged:
stop = time.monotonic()
duration = stop - start
connection.queries_log.append(
{
"sql": "%s" % sql,
"time": "%.3f" % duration,
}
)
logger.debug(
"(%.3f) %s; args=%s; alias=%s",
duration,
sql,
None,
connection.alias,
extra={
"duration": duration,
"sql": sql,
"alias": connection.alias,
},
)
def split_tzname_delta(tzname):
"""
Split a time zone name into a 3-tuple of (name, sign, offset).

View File

@ -196,9 +196,13 @@ For performance reasons, SQL logging is only enabled when
level or handlers that are installed.
This logging does not include framework-level initialization (e.g.
``SET TIMEZONE``) or transaction management queries (e.g. ``BEGIN``,
``COMMIT``, and ``ROLLBACK``). Turn on query logging in your database if you
wish to view all database queries.
``SET TIMEZONE``). Turn on query logging in your database if you wish to view
all database queries.
.. versionchanged:: 4.2
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
and ``ROLLBACK``) was added.
.. _django-security-logger:

View File

@ -199,7 +199,8 @@ Internationalization
Logging
~~~~~~~
* ...
* The :ref:`django-db-logger` logger now logs transaction management queries
(``BEGIN``, ``COMMIT``, and ``ROLLBACK``) at the ``DEBUG`` level.
Management Commands
~~~~~~~~~~~~~~~~~~~

View File

@ -1,10 +1,16 @@
from unittest.mock import MagicMock, patch
from django.db import DEFAULT_DB_ALIAS, connection, connections
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
from django.db.backends.base.base import BaseDatabaseWrapper
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
from django.test import (
SimpleTestCase,
TestCase,
TransactionTestCase,
skipUnlessDBFeature,
)
from django.test.utils import CaptureQueriesContext, override_settings
from ..models import Square
from ..models import Person, Square
class DatabaseWrapperTests(SimpleTestCase):
@ -55,6 +61,57 @@ class DatabaseWrapperTests(SimpleTestCase):
connection.check_database_version_supported()
class DatabaseWrapperLoggingTests(TransactionTestCase):
available_apps = []
@override_settings(DEBUG=True)
def test_commit_debug_log(self):
conn = connections[DEFAULT_DB_ALIAS]
with CaptureQueriesContext(conn):
with self.assertLogs("django.db.backends", "DEBUG") as cm:
with transaction.atomic():
Person.objects.create(first_name="first", last_name="last")
self.assertGreaterEqual(len(conn.queries_log), 3)
self.assertEqual(conn.queries_log[-3]["sql"], "BEGIN")
self.assertRegex(
cm.output[0],
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
rf"BEGIN; args=None; alias={DEFAULT_DB_ALIAS}",
)
self.assertEqual(conn.queries_log[-1]["sql"], "COMMIT")
self.assertRegex(
cm.output[-1],
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
rf"COMMIT; args=None; alias={DEFAULT_DB_ALIAS}",
)
@override_settings(DEBUG=True)
def test_rollback_debug_log(self):
conn = connections[DEFAULT_DB_ALIAS]
with CaptureQueriesContext(conn):
with self.assertLogs("django.db.backends", "DEBUG") as cm:
with self.assertRaises(Exception), transaction.atomic():
Person.objects.create(first_name="first", last_name="last")
raise Exception("Force rollback")
self.assertEqual(conn.queries_log[-1]["sql"], "ROLLBACK")
self.assertRegex(
cm.output[-1],
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
rf"ROLLBACK; args=None; alias={DEFAULT_DB_ALIAS}",
)
def test_no_logs_without_debug(self):
with self.assertNoLogs("django.db.backends", "DEBUG"):
with self.assertRaises(Exception), transaction.atomic():
Person.objects.create(first_name="first", last_name="last")
raise Exception("Force rollback")
conn = connections[DEFAULT_DB_ALIAS]
self.assertEqual(len(conn.queries_log), 0)
class ExecuteWrapperTests(TestCase):
@staticmethod
def call_execute(connection, params=None):