-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #138 from cloudblue/feature/LITE-27792
LITE-27792 Support for logging of timed out PG and MySQL queries
- Loading branch information
Showing
7 changed files
with
268 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import logging | ||
|
||
from django.conf import settings | ||
from django.db import OperationalError, transaction | ||
|
||
from dj_cqrs.constants import ( | ||
DB_VENDOR_MYSQL, | ||
DB_VENDOR_PG, | ||
MYSQL_TIMEOUT_ERROR_CODE, | ||
PG_TIMEOUT_FLAG, | ||
SUPPORTED_TIMEOUT_DB_VENDORS, | ||
) | ||
|
||
|
||
def install_last_query_capturer(model_cls): | ||
conn = _connection(model_cls) | ||
if not _get_last_query_capturer(conn): | ||
conn.execute_wrappers.append(_LastQueryCaptureWrapper()) | ||
|
||
|
||
def log_timed_out_queries(error, model_cls): # pragma: no cover | ||
log_q = bool(settings.CQRS['replica'].get('CQRS_LOG_TIMED_OUT_QUERIES', False)) | ||
if not (log_q and isinstance(error, OperationalError) and error.args): | ||
return | ||
|
||
conn = _connection(model_cls) | ||
conn_vendor = getattr(conn, 'vendor', '') | ||
if conn_vendor not in SUPPORTED_TIMEOUT_DB_VENDORS: | ||
return | ||
|
||
e_arg = error.args[0] | ||
is_timeout_error = bool( | ||
(conn_vendor == DB_VENDOR_MYSQL and e_arg == MYSQL_TIMEOUT_ERROR_CODE) | ||
or (conn_vendor == DB_VENDOR_PG and isinstance(e_arg, str) and PG_TIMEOUT_FLAG in e_arg) | ||
) | ||
if is_timeout_error: | ||
query = getattr(_get_last_query_capturer(conn), 'query', None) | ||
if query: | ||
logger_name = settings.CQRS['replica'].get('CQRS_QUERY_LOGGER', '') or 'django-cqrs' | ||
logger = logging.getLogger(logger_name) | ||
logger.error('Timed out query:\n%s', query) | ||
|
||
|
||
class _LastQueryCaptureWrapper: | ||
def __init__(self): | ||
self.query = None | ||
|
||
def __call__(self, execute, sql, params, many, context): | ||
try: | ||
execute(sql, params, many, context) | ||
finally: | ||
self.query = sql | ||
|
||
|
||
def _get_last_query_capturer(conn): | ||
return next((w for w in conn.execute_wrappers if isinstance(w, _LastQueryCaptureWrapper)), None) | ||
|
||
|
||
def _connection(model_cls): | ||
return transaction.get_connection(using=model_cls._default_manager.db) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import logging | ||
|
||
import pytest | ||
from django.db import ( | ||
DatabaseError, | ||
IntegrityError, | ||
OperationalError, | ||
connection, | ||
) | ||
|
||
from dj_cqrs.logger import ( | ||
_LastQueryCaptureWrapper, | ||
install_last_query_capturer, | ||
log_timed_out_queries, | ||
) | ||
from tests.dj_replica import models | ||
|
||
|
||
@pytest.mark.django_db(transaction=True) | ||
def test_install_last_query_capturer(): | ||
for _ in range(2): | ||
install_last_query_capturer(models.AuthorRef) | ||
|
||
assert len(connection.execute_wrappers) == 1 | ||
assert isinstance(connection.execute_wrappers[0], _LastQueryCaptureWrapper) | ||
|
||
with connection.cursor() as c: | ||
c.execute('SELECT 1') | ||
|
||
assert connection.execute_wrappers[0].query == 'SELECT 1' | ||
|
||
connection.execute_wrappers.pop() | ||
|
||
|
||
def test_log_timed_out_queries_not_supported(caplog): | ||
assert log_timed_out_queries(None, None) is None | ||
assert not caplog.record_tuples | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'error', | ||
[ | ||
IntegrityError('some error'), | ||
DatabaseError(), | ||
OperationalError(), | ||
], | ||
) | ||
def test_log_timed_out_queries_other_error(error, settings, caplog): | ||
settings.CQRS_LOG_TIMED_OUT_QUERIES = 1 | ||
|
||
assert log_timed_out_queries(error, None) is None | ||
assert not caplog.record_tuples | ||
|
||
|
||
@pytest.mark.django_db(transaction=True) | ||
@pytest.mark.parametrize( | ||
'engine, error, l_name, records', | ||
[ | ||
('sqlite', None, None, []), | ||
( | ||
'postgres', | ||
OperationalError('canceling statement due to statement timeout'), | ||
None, | ||
[ | ||
( | ||
'django-cqrs', | ||
logging.ERROR, | ||
'Timed out query:\nSELECT 1', | ||
) | ||
], | ||
), | ||
( | ||
'postgres', | ||
OperationalError('canceling statement due to statement timeout'), | ||
'long-query', | ||
[ | ||
( | ||
'long-query', | ||
logging.ERROR, | ||
'Timed out query:\nSELECT 1', | ||
) | ||
], | ||
), | ||
( | ||
'postgres', | ||
OperationalError('could not connect to server'), | ||
None, | ||
[], | ||
), | ||
( | ||
'postgres', | ||
OperationalError(125, 'Some error'), | ||
None, | ||
[], | ||
), | ||
( | ||
'mysql', | ||
OperationalError(3024), | ||
None, | ||
[ | ||
( | ||
'django-cqrs', | ||
logging.ERROR, | ||
'Timed out query:\nSELECT 1', | ||
) | ||
], | ||
), | ||
( | ||
'mysql', | ||
OperationalError( | ||
3024, 'Query exec was interrupted, max statement execution time exceeded' | ||
), | ||
'long-query-1', | ||
[ | ||
( | ||
'long-query-1', | ||
logging.ERROR, | ||
'Timed out query:\nSELECT 1', | ||
) | ||
], | ||
), | ||
( | ||
'mysql', | ||
OperationalError(1040, 'Too many connections'), | ||
None, | ||
[], | ||
), | ||
], | ||
) | ||
def test_apply_query_timeouts(settings, engine, l_name, error, records, caplog): | ||
if settings.DB_ENGINE != engine: | ||
return | ||
|
||
settings.CQRS['replica']['CQRS_LOG_TIMED_OUT_QUERIES'] = True | ||
settings.CQRS['replica']['CQRS_QUERY_LOGGER'] = l_name | ||
|
||
model_cls = models.BasicFieldsModelRef | ||
install_last_query_capturer(model_cls) | ||
|
||
with connection.cursor() as c: | ||
c.execute('SELECT 1') | ||
|
||
assert log_timed_out_queries(error, model_cls) is None | ||
assert caplog.record_tuples == records | ||
|
||
connection.execute_wrappers.pop() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters