From d70ff4f028be740e2988950f6bdf4b4ac2b6486d Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 23 Jul 2024 16:29:36 +0000 Subject: [PATCH 01/92] Creating a test for the deadlock --- sqlserver/datadog_checks/sqlserver/queries.py | 13 ++ .../datadog_checks/sqlserver/sqlserver.py | 12 ++ sqlserver/tests/compose/setup.sql | 6 + sqlserver/tests/test_activity.py | 119 ++++++++++++++++++ sqlserver/tests/test_integration.py | 1 - 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 230b5152f7867..f5c631c2689e6 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -428,3 +428,16 @@ def get_query_file_stats(sqlserver_major_version, sqlserver_engine_edition): ] + metric_columns, } + +DETECT_DEADLOCK_QUERY = """ +SELECT xdr.value('@timestamp', 'datetime') AS [Date], + xdr.query('.') AS [Event_Data] +FROM (SELECT CAST([target_data] AS XML) AS Target_Data + FROM sys.dm_xe_session_targets AS xt + INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address + WHERE xs.name = N'system_health' + AND xt.target_name = N'ring_buffer' + ) AS XML_Data +CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +ORDER BY [Date] DESC; +""" \ No newline at end of file diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index c2f9599b308d5..d64b152c9d847 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -80,6 +80,7 @@ QUERY_LOG_SHIPPING_PRIMARY, QUERY_LOG_SHIPPING_SECONDARY, QUERY_SERVER_STATIC_INFO, + DEADLOCK_QUERY, get_query_ao_availability_groups, get_query_file_stats, ) @@ -756,6 +757,16 @@ def _check_database_conns(self): else: self._check_connections_by_use_db() + # do we need to send like new deadlocks or the whiole table whats the retention ? + # lets assume we send all + def detect_deadlocks(self): + # send query here + with self.connection.open_managed_default_connection(): + with self.connection.get_managed_cursor() as cursor: + quey = cursor.execute(DEADLOCK_QUERY) + result = cursor.fetchone() + print(result) + def check(self, _): if self.do_check: # configure custom queries for the check @@ -785,6 +796,7 @@ def check(self, _): self.activity.run_job_loop(self.tags) self.sql_metadata.run_job_loop(self.tags) self._schemas.run_job_loop(self.tags) + self.detect_deadlocks() else: self.log.debug("Skipping check") diff --git a/sqlserver/tests/compose/setup.sql b/sqlserver/tests/compose/setup.sql index c8749702677a9..1e4271bc8e5a1 100644 --- a/sqlserver/tests/compose/setup.sql +++ b/sqlserver/tests/compose/setup.sql @@ -104,6 +104,12 @@ CREATE USER fred FOR LOGIN fred; CREATE CLUSTERED INDEX thingsindex ON [datadog_test-1].dbo.ϑings (name); GO +-- Create a simple table for deadlocks +CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); + +INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) +GO + EXEC sp_addrolemember 'db_datareader', 'bob' EXEC sp_addrolemember 'db_datareader', 'fred' EXEC sp_addrolemember 'db_datawriter', 'bob' diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 7e8d73375afde..7398cfae3a242 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -906,3 +906,122 @@ def test_sanitize_activity_row(dbm_instance, row): row = check.activity._obfuscate_and_sanitize_row(row) assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_deadlocks(aggregator, dd_run_check, init_config, instance_docker, dbm_enabled): + + instance_docker['dbm'] = True + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) + +#--session 1 +BEGIN TRAN foo; +UPDATE t2 SET b = b+ 10 WHERE a = 1; + + first_query = "SELECT * FROM testdb.users WHERE id = 1 FOR UPDATE;" + second_query = "SELECT * FROM testdb.users WHERE id = 2 FOR UPDATE;" + def run_first_deadlock_query(conn, event1, event2): + conn.begin() + try: + conn.cursor().execute("BEGIN TRAN foo;") + conn.cursor().execute("UPDATE t2 SET b = b+ 10 WHERE a = 1;") + event1.set() + event2.wait() + conn.cursor().execute(second_query) + conn.cursor().execute("COMMIT;") + except Exception as e: + # Exception is expected due to a deadlock + print(e) + pass + conn.commit() + def run_second_deadlock_query(conn, event1, event2): + conn.begin() + try: + event1.wait() + conn.cursor().execute("START TRANSACTION;") + conn.cursor().execute(second_query) + event2.set() + conn.cursor().execute(first_query) + conn.cursor().execute("COMMIT;") + except Exception as e: + # Exception is expected due to a deadlock + print(e) + pass + conn.commit() + bob_conn = _get_conn_for_user('bob') + fred_conn = _get_conn_for_user('fred') +@pytest.mark.skipif( + environ.get('MYSQL_FLAVOR') == 'mariadb' or MYSQL_VERSION_PARSED < parse_version('8.0'), + reason='Deadock count is not updated in MariaDB or older MySQL versions', +) +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_deadlocks(aggregator, dd_run_check, dbm_instance): + check = MySql(CHECK_NAME, {}, [dbm_instance]) + dd_run_check(check) + deadlocks_start = 0 + deadlock_metric_start = aggregator.metrics("mysql.innodb.deadlocks") + + assert len(deadlock_metric_start) == 1, "there should be one deadlock metric" + + deadlocks_start = deadlock_metric_start[0].value + + first_query = "SELECT * FROM testdb.users WHERE id = 1 FOR UPDATE;" + second_query = "SELECT * FROM testdb.users WHERE id = 2 FOR UPDATE;" + + def run_first_deadlock_query(conn, event1, event2): + conn.begin() + try: + conn.cursor().execute("START TRANSACTION;") + conn.cursor().execute(first_query) + event1.set() + event2.wait() + conn.cursor().execute(second_query) + conn.cursor().execute("COMMIT;") + except Exception as e: + # Exception is expected due to a deadlock + print(e) + pass + conn.commit() + + def run_second_deadlock_query(conn, event1, event2): + conn.begin() + try: + event1.wait() + conn.cursor().execute("START TRANSACTION;") + conn.cursor().execute(second_query) + event2.set() + conn.cursor().execute(first_query) + conn.cursor().execute("COMMIT;") + except Exception as e: + # Exception is expected due to a deadlock + print(e) + pass + conn.commit() + + bob_conn = _get_conn_for_user('bob') + fred_conn = _get_conn_for_user('fred') + + executor = concurrent.futures.thread.ThreadPoolExecutor(2) + + event1 = Event() + event2 = Event() + + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) + futures_first_query.result() + futures_second_query.result() + # Make sure innodb is updated. + time.sleep(0.3) + bob_conn.close() + fred_conn.close() + executor.shutdown() + + dd_run_check(check) + + deadlock_metric_end = aggregator.metrics("mysql.innodb.deadlocks") + + assert ( + len(deadlock_metric_end) == 2 and deadlock_metric_end[1].value - deadlocks_start == 1 + ), "there should be one new deadlock" diff --git a/sqlserver/tests/test_integration.py b/sqlserver/tests/test_integration.py index 152344a5dd997..e9f20cd711a2e 100644 --- a/sqlserver/tests/test_integration.py +++ b/sqlserver/tests/test_integration.py @@ -69,7 +69,6 @@ def test_check_dbm_enabled_config(aggregator, dd_run_check, init_config, instanc sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) assert isinstance(sqlserver_check._config.dbm_enabled, bool) - @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @pytest.mark.parametrize( From 3237a1635bea19b0da0bafdce69208b71ae1c0dc Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 24 Jul 2024 15:13:48 +0000 Subject: [PATCH 02/92] added test --- .../datadog_checks/sqlserver/sqlserver.py | 9 +- sqlserver/tests/test_activity.py | 107 ++++-------------- 2 files changed, 28 insertions(+), 88 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index d64b152c9d847..eb9afe00ee677 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -28,7 +28,7 @@ from datadog_checks.sqlserver.statements import SqlserverStatementMetrics from datadog_checks.sqlserver.stored_procedures import SqlserverProcedureMetrics from datadog_checks.sqlserver.utils import Database, construct_use_statement, parse_sqlserver_major_version - +import pdb try: import datadog_agent except ImportError: @@ -80,7 +80,7 @@ QUERY_LOG_SHIPPING_PRIMARY, QUERY_LOG_SHIPPING_SECONDARY, QUERY_SERVER_STATIC_INFO, - DEADLOCK_QUERY, + DETECT_DEADLOCK_QUERY, get_query_ao_availability_groups, get_query_file_stats, ) @@ -763,8 +763,9 @@ def detect_deadlocks(self): # send query here with self.connection.open_managed_default_connection(): with self.connection.get_managed_cursor() as cursor: - quey = cursor.execute(DEADLOCK_QUERY) - result = cursor.fetchone() + cursor.execute(DETECT_DEADLOCK_QUERY) + pdb.set_trace() + result = cursor.fetchall() print(result) def check(self, _): diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 7398cfae3a242..51d07c3906a80 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -12,6 +12,7 @@ import threading import time from concurrent.futures.thread import ThreadPoolExecutor +from threading import Event from copy import copy import mock @@ -915,35 +916,30 @@ def test_deadlocks(aggregator, dd_run_check, init_config, instance_docker, dbm_e instance_docker['dbm'] = True sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) -#--session 1 -BEGIN TRAN foo; -UPDATE t2 SET b = b+ 10 WHERE a = 1; - - first_query = "SELECT * FROM testdb.users WHERE id = 1 FOR UPDATE;" - second_query = "SELECT * FROM testdb.users WHERE id = 2 FOR UPDATE;" def run_first_deadlock_query(conn, event1, event2): - conn.begin() + #conn.begin() try: conn.cursor().execute("BEGIN TRAN foo;") conn.cursor().execute("UPDATE t2 SET b = b+ 10 WHERE a = 1;") event1.set() event2.wait() - conn.cursor().execute(second_query) - conn.cursor().execute("COMMIT;") + conn.cursor().execute("UPDATE t2 SET b = b + 100 WHERE a = 2;") + #conn.cursor().execute("GO;") except Exception as e: # Exception is expected due to a deadlock print(e) pass conn.commit() def run_second_deadlock_query(conn, event1, event2): - conn.begin() + #conn.begin() try: event1.wait() - conn.cursor().execute("START TRANSACTION;") - conn.cursor().execute(second_query) + conn.cursor().execute("BEGIN TRAN bar;") + conn.cursor().execute("UPDATE t2 SET b = b+ 10 WHERE a = 2;") event2.set() - conn.cursor().execute(first_query) - conn.cursor().execute("COMMIT;") + conn.cursor().execute("UPDATE t2 SET b = b + 20 WHERE a = 1;") + #may be Go ? + #conn.cursor().execute("COMMIT;") except Exception as e: # Exception is expected due to a deadlock print(e) @@ -951,77 +947,20 @@ def run_second_deadlock_query(conn, event1, event2): conn.commit() bob_conn = _get_conn_for_user('bob') fred_conn = _get_conn_for_user('fred') -@pytest.mark.skipif( - environ.get('MYSQL_FLAVOR') == 'mariadb' or MYSQL_VERSION_PARSED < parse_version('8.0'), - reason='Deadock count is not updated in MariaDB or older MySQL versions', -) -@pytest.mark.integration -@pytest.mark.usefixtures('dd_environment') -def test_deadlocks(aggregator, dd_run_check, dbm_instance): - check = MySql(CHECK_NAME, {}, [dbm_instance]) - dd_run_check(check) - deadlocks_start = 0 - deadlock_metric_start = aggregator.metrics("mysql.innodb.deadlocks") - - assert len(deadlock_metric_start) == 1, "there should be one deadlock metric" - - deadlocks_start = deadlock_metric_start[0].value - - first_query = "SELECT * FROM testdb.users WHERE id = 1 FOR UPDATE;" - second_query = "SELECT * FROM testdb.users WHERE id = 2 FOR UPDATE;" - - def run_first_deadlock_query(conn, event1, event2): - conn.begin() - try: - conn.cursor().execute("START TRANSACTION;") - conn.cursor().execute(first_query) - event1.set() - event2.wait() - conn.cursor().execute(second_query) - conn.cursor().execute("COMMIT;") - except Exception as e: - # Exception is expected due to a deadlock - print(e) - pass - conn.commit() - def run_second_deadlock_query(conn, event1, event2): - conn.begin() - try: - event1.wait() - conn.cursor().execute("START TRANSACTION;") - conn.cursor().execute(second_query) - event2.set() - conn.cursor().execute(first_query) - conn.cursor().execute("COMMIT;") - except Exception as e: - # Exception is expected due to a deadlock - print(e) - pass - conn.commit() + executor = concurrent.futures.thread.ThreadPoolExecutor(2) - bob_conn = _get_conn_for_user('bob') - fred_conn = _get_conn_for_user('fred') + event1 = Event() + event2 = Event() - executor = concurrent.futures.thread.ThreadPoolExecutor(2) - - event1 = Event() - event2 = Event() - - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) - futures_first_query.result() - futures_second_query.result() - # Make sure innodb is updated. - time.sleep(0.3) - bob_conn.close() - fred_conn.close() - executor.shutdown() - - dd_run_check(check) - - deadlock_metric_end = aggregator.metrics("mysql.innodb.deadlocks") + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) + futures_first_query.result() + futures_second_query.result() + # Make sure deadlock is killed and db is updated + time.sleep(1) + bob_conn.close() + fred_conn.close() + executor.shutdown() - assert ( - len(deadlock_metric_end) == 2 and deadlock_metric_end[1].value - deadlocks_start == 1 - ), "there should be one new deadlock" + dd_run_check(sqlserver_check) From 3107f2e5703b699314df94e4d9ca8f339ef52bac Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 30 Jul 2024 10:03:26 +0000 Subject: [PATCH 03/92] Deadlocks first impl --- .../datadog_checks/sqlserver/sqlserver.py | 3 ++ sqlserver/tests/compose/setup.sql | 9 ++++ sqlserver/tests/test_activity.py | 51 +++++++++---------- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index eb9afe00ee677..01e182e510294 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -761,6 +761,8 @@ def _check_database_conns(self): # lets assume we send all def detect_deadlocks(self): # send query here + #TODO here get the time of the deadlock and later discard in the query + #all deadlocks that have occured earlier than this date with self.connection.open_managed_default_connection(): with self.connection.get_managed_cursor() as cursor: cursor.execute(DETECT_DEADLOCK_QUERY) @@ -769,6 +771,7 @@ def detect_deadlocks(self): print(result) def check(self, _): + pdb.set_trace() if self.do_check: # configure custom queries for the check if self._query_manager is None: diff --git a/sqlserver/tests/compose/setup.sql b/sqlserver/tests/compose/setup.sql index 1e4271bc8e5a1..5cb9b50a42e11 100644 --- a/sqlserver/tests/compose/setup.sql +++ b/sqlserver/tests/compose/setup.sql @@ -108,6 +108,15 @@ GO CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) + +-- Grant permissions to bob and fred to update the deadlocks table +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO bob; + +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO fred; GO EXEC sp_addrolemember 'db_datareader', 'bob' diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 51d07c3906a80..0c2c8fcd71f9f 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -26,7 +26,7 @@ from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION from .conftest import DEFAULT_TIMEOUT - +import pdb try: import pyodbc except ImportError: @@ -911,8 +911,8 @@ def test_sanitize_activity_row(dbm_instance, row): @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') -def test_deadlocks(aggregator, dd_run_check, init_config, instance_docker, dbm_enabled): - +def test_deadlocks(dd_run_check, init_config, instance_docker): + pdb.set_trace() instance_docker['dbm'] = True sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) @@ -920,10 +920,10 @@ def run_first_deadlock_query(conn, event1, event2): #conn.begin() try: conn.cursor().execute("BEGIN TRAN foo;") - conn.cursor().execute("UPDATE t2 SET b = b+ 10 WHERE a = 1;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b+ 10 WHERE a = 1;") event1.set() event2.wait() - conn.cursor().execute("UPDATE t2 SET b = b + 100 WHERE a = 2;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") #conn.cursor().execute("GO;") except Exception as e: # Exception is expected due to a deadlock @@ -935,9 +935,9 @@ def run_second_deadlock_query(conn, event1, event2): try: event1.wait() conn.cursor().execute("BEGIN TRAN bar;") - conn.cursor().execute("UPDATE t2 SET b = b+ 10 WHERE a = 2;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b+ 10 WHERE a = 2;") event2.set() - conn.cursor().execute("UPDATE t2 SET b = b + 20 WHERE a = 1;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") #may be Go ? #conn.cursor().execute("COMMIT;") except Exception as e: @@ -945,22 +945,21 @@ def run_second_deadlock_query(conn, event1, event2): print(e) pass conn.commit() - bob_conn = _get_conn_for_user('bob') - fred_conn = _get_conn_for_user('fred') - - executor = concurrent.futures.thread.ThreadPoolExecutor(2) - - event1 = Event() - event2 = Event() - - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) - futures_first_query.result() - futures_second_query.result() - # Make sure deadlock is killed and db is updated - time.sleep(1) - bob_conn.close() - fred_conn.close() - executor.shutdown() - - dd_run_check(sqlserver_check) + pdb.set_trace() + bob_conn = _get_conn_for_user(instance_docker, 'bob') + fred_conn = _get_conn_for_user(instance_docker, 'fred') + executor = concurrent.futures.thread.ThreadPoolExecutor(2) + event1 = Event() + event2 = Event() + + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) + futures_first_query.result() + futures_second_query.result() + # Make sure deadlock is killed and db is updated + time.sleep(1) + bob_conn.close() + fred_conn.close() + executor.shutdown() + pdb.set_trace() + dd_run_check(sqlserver_check) From 425f728fc8b5c015b0ca1b80c935f8b31da77ee8 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Sat, 3 Aug 2024 22:41:26 +0000 Subject: [PATCH 04/92] first full version --- .../datadog_checks/base/checks/base.py | 7 ++ .../datadog_checks/sqlserver/activity.py | 112 ++++++++++++++++-- sqlserver/datadog_checks/sqlserver/config.py | 1 + sqlserver/datadog_checks/sqlserver/queries.py | 7 +- .../datadog_checks/sqlserver/sqlserver.py | 8 +- sqlserver/tests/test_activity.py | 41 +++++-- 6 files changed, 149 insertions(+), 27 deletions(-) diff --git a/datadog_checks_base/datadog_checks/base/checks/base.py b/datadog_checks_base/datadog_checks/base/checks/base.py index ccfbced9231ca..4dba59c1b34ff 100644 --- a/datadog_checks_base/datadog_checks/base/checks/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/base.py @@ -687,6 +687,13 @@ def database_monitoring_query_activity(self, raw_event): aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), "dbm-activity") + def database_monitoring_deadlocks(self, raw_event): + # type: (str) -> None + if raw_event is None: + return + + aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), "dbm-deadlocks") + def database_monitoring_metadata(self, raw_event): # type: (str) -> None if raw_event is None: diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 63186f680ccd1..f5cc822a37e7a 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -15,13 +15,15 @@ from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name - +from datadog_checks.sqlserver.deadlocks import Deadlocks +import pdb try: import datadog_agent except ImportError: from ..stubs import datadog_agent -DEFAULT_COLLECTION_INTERVAL = 10 +DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 +DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 CONNECTIONS_QUERY = """\ @@ -146,7 +148,24 @@ def _hash_to_hex(hash) -> str: def agent_check_getter(self): return self._check +""" self._databases_data_enabled = is_affirmative(config.schemas_config.get("enabled", False)) + self._databases_data_collection_interval = config.schemas_config.get( + "collection_interval", DEFAULT_DATABASES_DATA_COLLECTION_INTERVAL + ) + self._settings_enabled = is_affirmative(config.settings_config.get('enabled', False)) + + self._settings_collection_interval = float( + config.settings_config.get('collection_interval', DEFAULT_SETTINGS_COLLECTION_INTERVAL) + ) + if self._databases_data_enabled and not self._settings_enabled: + self.collection_interval = self._databases_data_collection_interval + elif not self._databases_data_enabled and self._settings_enabled: + self.collection_interval = self._settings_collection_interval + else: + self.collection_interval = min(self._databases_data_collection_interval, self._settings_collection_interval) + + self.enabled = self._databases_data_enabled or self._settings_enabled""" class SqlserverActivity(DBMAsyncJob): """Collects query metrics and plans""" @@ -156,33 +175,88 @@ def __init__(self, check, config: SQLServerConfig): self.tags = [t for t in check.tags if not t.startswith('dd.internal')] self.log = check.log self._config = config - collection_interval = float( - self._config.activity_config.get('collection_interval', DEFAULT_COLLECTION_INTERVAL) + + self._last_deadlocks_collection_time = 0 + self._last_activity_collection_time = 0 + + self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", False)) + self._deadlocks_collection_interval = config.deadlocks_config.get( + "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL ) - if collection_interval <= 0: - collection_interval = DEFAULT_COLLECTION_INTERVAL - self.collection_interval = collection_interval + if self._deadlocks_collection_interval <= 0: + self._deadlocks_collection_interval = DEFAULT_DEADLOCKS_COLLECTION_INTERVAL + + self._activity_collection_enabled = is_affirmative(config.activity_config.get("enabled", False)) + self._activity_collection_interval = config.activity_config.get( + "collection_interval", DEFAULT_ACTIVITY_COLLECTION_INTERVAL + ) + if self._activity_collection_enabled <= 0: + self._activity_collection_enabled = DEFAULT_ACTIVITY_COLLECTION_INTERVAL + + if self._deadlocks_collection_enabled and not self._activity_collection_enabled: + self.collection_interval = self._deadlocks_collection_interval + elif not self._deadlocks_collection_enabled and self._activity_collection_enabled: + self.collection_interval = self._activity_collection_interval + else: + self.collection_interval = min(self._deadlocks_collection_interval, self._activity_collection_interval) + + self.enabled = self._deadlocks_collection_enabled or self._activity_collection_enabled + super(SqlserverActivity, self).__init__( check, run_sync=is_affirmative(self._config.activity_config.get('run_sync', False)), - enabled=is_affirmative(self._config.activity_config.get('enabled', True)), + enabled=self.enabled, expected_db_exceptions=(), min_collection_interval=self._config.min_collection_interval, dbms="sqlserver", - rate_limit=1 / float(collection_interval), + rate_limit=1 / float(self.collection_interval), job_name="query-activity", shutdown_callback=self._close_db_conn, ) self._conn_key_prefix = "dbm-activity-" self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES self._exec_requests_cols_cached = None + obfuscate_sql = lambda sql: obfuscate_sql_with_metadata(sql, self._config.obfuscator_options, replace_null_character=True) + self._deadlocks = Deadlocks(check, config, self._conn_key_prefix, obfuscate_sql) def _close_db_conn(self): pass def run_job(self): - self.collect_activity() + elapsed_time_activity = time.time() - self._last_activity_collection_time + if self._activity_collection_enabled and elapsed_time_activity >= self._activity_collection_interval: + self._last_activity_collection_time = time.time() + try: + self.collect_activity() + except Exception as e: + self._log.error( + """An error occurred while collecting sqlserver activity. + This may be unavailable until the error is resolved. The error - {}""".format( + e + ) + ) + pdb.set_trace() + elapsed_time_deadlocks = time.time() - self._last_deadlocks_collection_time + if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: + self._last_deadlocks_collection_time = time.time() + try: + self._collect_deadlocks() + except Exception as e: + self._log.error( + """An error occurred while collecting sqlserver deadlocks. + This may be unavailable until the error is resolved. The error - {}""".format( + e + ) + ) + @tracked_method(agent_check_getter=agent_check_getter) + def _collect_deadlocks(self): + pdb.set_trace() + deadlock_xmls = self._deadlocks.collect_deadlocks() + deadlocks_event = self._create_deadlock_event(deadlock_xmls) + payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + self._check.database_monitoring_deadlocks(payload) + @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): self.log.debug("collecting sql server current connections") @@ -350,7 +424,7 @@ def _create_activity_event(self, active_sessions, active_connections): "ddagentversion": datadog_agent.get_version(), "ddsource": "sqlserver", "dbm_type": "activity", - "collection_interval": self.collection_interval, + "collection_interval": self._activity_collection_interval, #TODO is it important for whatever reason to have very precise int ? "ddtags": self.tags, "timestamp": time.time() * 1000, 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), @@ -361,6 +435,22 @@ def _create_activity_event(self, active_sessions, active_connections): } return event + def _create_deadlock_event(self, deadlock_xmls): + event = { + "host": self._check.resolved_hostname, + "ddagentversion": datadog_agent.get_version(), + "ddsource": "sqlserver", + "dbm_type": "deadlocks", + "collection_interval": self._deadlocks_collection_interval, + "ddtags": self.tags, + "timestamp": time.time() * 1000, + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), + "cloud_metadata": self._config.cloud_metadata, + "sqlserver_deadlocks": deadlock_xmls, + } + return event + @tracked_method(agent_check_getter=agent_check_getter) def collect_activity(self): """ diff --git a/sqlserver/datadog_checks/sqlserver/config.py b/sqlserver/datadog_checks/sqlserver/config.py index a26440949a82d..e6d4cee27fe16 100644 --- a/sqlserver/datadog_checks/sqlserver/config.py +++ b/sqlserver/datadog_checks/sqlserver/config.py @@ -50,6 +50,7 @@ def __init__(self, init_config, instance, log): self.settings_config: dict = instance.get('collect_settings', {}) or {} self.activity_config: dict = instance.get('query_activity', {}) or {} self.schema_config: dict = instance.get('schemas_collection', {}) or {} + self.deadlocks_config: dict = instance.get('deadlocks', {}) or {} self.cloud_metadata: dict = {} aws: dict = instance.get('aws', {}) or {} gcp: dict = instance.get('gcp', {}) or {} diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index f5c631c2689e6..339083b29efd7 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -428,10 +428,10 @@ def get_query_file_stats(sqlserver_major_version, sqlserver_engine_edition): ] + metric_columns, } - +#shell we limit the query , like if there 100 deadlocks ? DETECT_DEADLOCK_QUERY = """ SELECT xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [Event_Data] + xdr.query('.') AS [Event_Data] FROM (SELECT CAST([target_data] AS XML) AS Target_Data FROM sys.dm_xe_session_targets AS xt INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address @@ -439,5 +439,6 @@ def get_query_file_stats(sqlserver_major_version, sqlserver_engine_edition): AND xt.target_name = N'ring_buffer' ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +WHERE xdr.value('@timestamp', 'datetime') >= ? ORDER BY [Date] DESC; -""" \ No newline at end of file +""" diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 01e182e510294..d28e718e78f33 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -28,6 +28,7 @@ from datadog_checks.sqlserver.statements import SqlserverStatementMetrics from datadog_checks.sqlserver.stored_procedures import SqlserverProcedureMetrics from datadog_checks.sqlserver.utils import Database, construct_use_statement, parse_sqlserver_major_version +from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata import pdb try: import datadog_agent @@ -763,6 +764,11 @@ def detect_deadlocks(self): # send query here #TODO here get the time of the deadlock and later discard in the query #all deadlocks that have occured earlier than this date + pdb.set_trace() + res1 = obfuscate_sql_with_metadata("\nunknown", self._config.obfuscator_options, replace_null_character=True) + print(res1) + res2 = obfuscate_sql_with_metadata("\nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;", self._config.obfuscator_options, replace_null_character=True) + print(res2) with self.connection.open_managed_default_connection(): with self.connection.get_managed_cursor() as cursor: cursor.execute(DETECT_DEADLOCK_QUERY) @@ -800,7 +806,7 @@ def check(self, _): self.activity.run_job_loop(self.tags) self.sql_metadata.run_job_loop(self.tags) self._schemas.run_job_loop(self.tags) - self.detect_deadlocks() + #self.detect_deadlocks() else: self.log.debug("Skipping check") diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 0c2c8fcd71f9f..39ee46516c5f7 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -911,20 +911,25 @@ def test_sanitize_activity_row(dbm_instance, row): @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') -def test_deadlocks(dd_run_check, init_config, instance_docker): +def test_deadlocks(dd_run_check, init_config, dbm_instance): pdb.set_trace() - instance_docker['dbm'] = True - sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) + dbm_instance['deadlocks'] = { + 'enabled': True, + 'run_sync': True, #TODO oups run_sync what should be the logic for 2 jobs ? + 'collection_interval': 0.1, + } + dbm_instance['query_activity']['enabled'] = False + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) def run_first_deadlock_query(conn, event1, event2): #conn.begin() try: conn.cursor().execute("BEGIN TRAN foo;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b+ 10 WHERE a = 1;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 1;") event1.set() event2.wait() conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") - #conn.cursor().execute("GO;") except Exception as e: # Exception is expected due to a deadlock print(e) @@ -935,19 +940,16 @@ def run_second_deadlock_query(conn, event1, event2): try: event1.wait() conn.cursor().execute("BEGIN TRAN bar;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b+ 10 WHERE a = 2;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 2;") event2.set() conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") - #may be Go ? - #conn.cursor().execute("COMMIT;") except Exception as e: # Exception is expected due to a deadlock print(e) pass conn.commit() - pdb.set_trace() - bob_conn = _get_conn_for_user(instance_docker, 'bob') - fred_conn = _get_conn_for_user(instance_docker, 'fred') + bob_conn = _get_conn_for_user(dbm_instance, 'bob') + fred_conn = _get_conn_for_user(dbm_instance, 'fred') executor = concurrent.futures.thread.ThreadPoolExecutor(2) event1 = Event() event2 = Event() @@ -958,8 +960,23 @@ def run_second_deadlock_query(conn, event1, event2): futures_second_query.result() # Make sure deadlock is killed and db is updated time.sleep(1) + + bob_conn.close() + fred_conn.close() + bob_conn = _get_conn_for_user(dbm_instance, 'bob') + fred_conn = _get_conn_for_user(dbm_instance, 'fred') + event3 = Event() + event4 = Event() + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event3, event4) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event3, event4) + futures_first_query.result() + futures_second_query.result() + + time.sleep(1) + bob_conn.close() fred_conn.close() executor.shutdown() - pdb.set_trace() dd_run_check(sqlserver_check) + pdb.set_trace() + print("Set trace before end to keep sqlserver alive") \ No newline at end of file From bdda946a3b693069c3101e2f8440f1454a92cc93 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Mon, 5 Aug 2024 10:06:15 +0000 Subject: [PATCH 05/92] Added checks --- .../datadog_checks/sqlserver/activity.py | 5 ++--- .../datadog_checks/sqlserver/sqlserver.py | 20 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index f5cc822a37e7a..150924ca18131 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -21,7 +21,7 @@ import datadog_agent except ImportError: from ..stubs import datadog_agent - +import pdb DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 @@ -235,7 +235,6 @@ def run_job(self): e ) ) - pdb.set_trace() elapsed_time_deadlocks = time.time() - self._last_deadlocks_collection_time if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: self._last_deadlocks_collection_time = time.time() @@ -251,10 +250,10 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): - pdb.set_trace() deadlock_xmls = self._deadlocks.collect_deadlocks() deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + pdb.set_trace() self._check.database_monitoring_deadlocks(payload) @tracked_method(agent_check_getter=agent_check_getter) diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index d28e718e78f33..81bcfee10cc12 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -758,26 +758,7 @@ def _check_database_conns(self): else: self._check_connections_by_use_db() - # do we need to send like new deadlocks or the whiole table whats the retention ? - # lets assume we send all - def detect_deadlocks(self): - # send query here - #TODO here get the time of the deadlock and later discard in the query - #all deadlocks that have occured earlier than this date - pdb.set_trace() - res1 = obfuscate_sql_with_metadata("\nunknown", self._config.obfuscator_options, replace_null_character=True) - print(res1) - res2 = obfuscate_sql_with_metadata("\nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;", self._config.obfuscator_options, replace_null_character=True) - print(res2) - with self.connection.open_managed_default_connection(): - with self.connection.get_managed_cursor() as cursor: - cursor.execute(DETECT_DEADLOCK_QUERY) - pdb.set_trace() - result = cursor.fetchall() - print(result) - def check(self, _): - pdb.set_trace() if self.do_check: # configure custom queries for the check if self._query_manager is None: @@ -806,7 +787,6 @@ def check(self, _): self.activity.run_job_loop(self.tags) self.sql_metadata.run_job_loop(self.tags) self._schemas.run_job_loop(self.tags) - #self.detect_deadlocks() else: self.log.debug("Skipping check") From 132eabb3b55cd00fecc64fd811e0c622bd0abe00 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Mon, 5 Aug 2024 10:08:45 +0000 Subject: [PATCH 06/92] Added the deadlock file --- .../datadog_checks/sqlserver/deadlocks.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 sqlserver/datadog_checks/sqlserver/deadlocks.py diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py new file mode 100644 index 0000000000000..fa8ab9cbcad86 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -0,0 +1,75 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import xml.etree.ElementTree as ET +from datetime import datetime + +from datadog_checks.sqlserver.queries import ( + DETECT_DEADLOCK_QUERY, +) + +import pdb + +class Deadlocks: + + + def __init__(self, check, config, conn_prefix, obfuscate_sql): + #may be dont need a check + self._check = check + self._log = check.log + self._conn_key_prefix = conn_prefix + self._obfuscate_sql = obfuscate_sql + self._last_deadlock_timestamp = '1900-07-01 00:00:26.363' + + def obfuscate_xml(self, root): + s= ET.tostring(root, encoding='unicode') + print(s) + process_list = root.find(".//process-list") + + # Iterate through elements and apply function F + for process in process_list.findall('process'): + inputbuf = process.find('inputbuf') + inputbuf.text = self._obfuscate_sql(inputbuf.text)['query'] + for frame in process.findall('.//frame'): + frame.text = self._obfuscate_sql(frame.text)['query'] + print(s) + + + def collect_deadlocks(self): + with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): + with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: + #todo check if 0 works or needs to be converted to 0.0.0 etc ... '2024-07-31 15:05:26.363' + time_offset = self._last_deadlock_timestamp + cursor.execute(DETECT_DEADLOCK_QUERY, (time_offset,)) + results = cursor.fetchall() + last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') + converted_xmls = [] + for result in results: + root = ET.fromstring(result[1]) + datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') + if last_deadlock_datetime < datetime_obj: + last_deadlock_datetime = datetime_obj + #apply obfuscator loop throur resources + self.obfuscate_xml(root) + s= ET.tostring(root, encoding='unicode') + print(s) + converted_xmls.append(ET.tostring(root, encoding='unicode')) + #TODO check conversion doesnt loose precision + self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + print(converted_xmls) + return converted_xmls + #todo extract timestamp if any + + #put int da event + + + + +# Parse the XML data +#root = ET.fromstring(xml_data) + +# Extract the timestamp attribute +#timestamp = root.get('timestamp') +#obfuscate , serialize in text or compress ? +#print("Timestamp:", timestamp) From f32097c263dc14ec5e8249846ff7aea76b8385a2 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Mon, 5 Aug 2024 14:17:33 +0000 Subject: [PATCH 07/92] Added exception for truncated xml --- .../datadog_checks/sqlserver/activity.py | 12 ++-- .../datadog_checks/sqlserver/deadlocks.py | 71 +++++++++---------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 150924ca18131..d1946fb680db7 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -16,12 +16,12 @@ from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name from datadog_checks.sqlserver.deadlocks import Deadlocks -import pdb + try: import datadog_agent except ImportError: from ..stubs import datadog_agent -import pdb + DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 @@ -216,8 +216,7 @@ def __init__(self, check, config: SQLServerConfig): self._conn_key_prefix = "dbm-activity-" self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES self._exec_requests_cols_cached = None - obfuscate_sql = lambda sql: obfuscate_sql_with_metadata(sql, self._config.obfuscator_options, replace_null_character=True) - self._deadlocks = Deadlocks(check, config, self._conn_key_prefix, obfuscate_sql) + self._deadlocks = Deadlocks(check, self._conn_key_prefix, self._config) def _close_db_conn(self): pass @@ -230,7 +229,7 @@ def run_job(self): self.collect_activity() except Exception as e: self._log.error( - """An error occurred while collecting sqlserver activity. + """An error occurred while collecting SQLServer activity. This may be unavailable until the error is resolved. The error - {}""".format( e ) @@ -242,7 +241,7 @@ def run_job(self): self._collect_deadlocks() except Exception as e: self._log.error( - """An error occurred while collecting sqlserver deadlocks. + """An error occurred while collecting SQLServer deadlocks. This may be unavailable until the error is resolved. The error - {}""".format( e ) @@ -253,7 +252,6 @@ def _collect_deadlocks(self): deadlock_xmls = self._deadlocks.collect_deadlocks() deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - pdb.set_trace() self._check.database_monitoring_deadlocks(payload) @tracked_method(agent_check_getter=agent_check_getter) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index fa8ab9cbcad86..9c7edefbb3557 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -5,71 +5,64 @@ import xml.etree.ElementTree as ET from datetime import datetime +from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata + from datadog_checks.sqlserver.queries import ( DETECT_DEADLOCK_QUERY, ) - +#TODO temp imports: import pdb +import time class Deadlocks: - - def __init__(self, check, config, conn_prefix, obfuscate_sql): - #may be dont need a check + def __init__(self, check, conn_prefix, config): self._check = check - self._log = check.log + self._log = self._check.log self._conn_key_prefix = conn_prefix - self._obfuscate_sql = obfuscate_sql - self._last_deadlock_timestamp = '1900-07-01 00:00:26.363' + self._config = config + self._last_deadlock_timestamp = '1900-01-01 01:01:01.111' def obfuscate_xml(self, root): - s= ET.tostring(root, encoding='unicode') - print(s) + # TODO put exception here if not found as this would signal in a format change process_list = root.find(".//process-list") - - # Iterate through elements and apply function F for process in process_list.findall('process'): inputbuf = process.find('inputbuf') - inputbuf.text = self._obfuscate_sql(inputbuf.text)['query'] + inputbuf.text = obfuscate_sql_with_metadata(inputbuf.text, self._config.obfuscator_options, replace_null_character=True)['query'] for frame in process.findall('.//frame'): - frame.text = self._obfuscate_sql(frame.text)['query'] - print(s) - + frame.text = obfuscate_sql_with_metadata(frame.text, self._config.obfuscator_options, replace_null_character=True)['query'] def collect_deadlocks(self): + pdb.set_trace() with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: - #todo check if 0 works or needs to be converted to 0.0.0 etc ... '2024-07-31 15:05:26.363' - time_offset = self._last_deadlock_timestamp - cursor.execute(DETECT_DEADLOCK_QUERY, (time_offset,)) + #Q test this query for 1000 deadlocks ? speed , truncation ? + #Q shell we may be limit amount of data, 1000 deadlock is 4MB but do we need more than .. 50 ?(conf parameter) + cursor.execute(DETECT_DEADLOCK_QUERY, (self._last_deadlock_timestamp,)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') converted_xmls = [] for result in results: - root = ET.fromstring(result[1]) + #TODO if this fails what we do , can be obfuscate it or just drop and notify backend? + #TODO speed of serialization deciarialization + try: + root = ET.fromstring(result[1]) + except Exception as e: + #TODO notify backend ? try to check manually for process list tag and processes + # say if we can find and and and we could + # still try to do something but my feeling just to notify backend + + # Other thing do we want to suggest to set ring buffer to 1MB ? + self._log.error( + """An error occurred while collecting SQLServer deadlocks. + One of the deadlock XMLs couldn't be parsed. The error: {}""".format(e) + ) + datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') if last_deadlock_datetime < datetime_obj: last_deadlock_datetime = datetime_obj - #apply obfuscator loop throur resources self.obfuscate_xml(root) - s= ET.tostring(root, encoding='unicode') - print(s) converted_xmls.append(ET.tostring(root, encoding='unicode')) - #TODO check conversion doesnt loose precision self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] - print(converted_xmls) - return converted_xmls - #todo extract timestamp if any - - #put int da event - - - - -# Parse the XML data -#root = ET.fromstring(xml_data) - -# Extract the timestamp attribute -#timestamp = root.get('timestamp') -#obfuscate , serialize in text or compress ? -#print("Timestamp:", timestamp) + pdb.set_trace() + return converted_xmls From 3cd70e32733b50daed762192e75a6cdc46f4910f Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 6 Aug 2024 17:31:00 +0000 Subject: [PATCH 08/92] Added exception handling when obfuscating --- .../datadog_checks/sqlserver/activity.py | 5 +++ .../datadog_checks/sqlserver/deadlocks.py | 35 ++++++++++++------- sqlserver/datadog_checks/sqlserver/queries.py | 30 ++++++++-------- 3 files changed, 43 insertions(+), 27 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index d1946fb680db7..dfd413e2306da 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -433,6 +433,11 @@ def _create_activity_event(self, active_sessions, active_connections): return event def _create_deadlock_event(self, deadlock_xmls): + #TODO WHAT if deadlock xml is just too long ? + #MAX_PAYLOAD_BYTES ? + #TODO compression , couldnt reallly see it in Go code so far but may be its somewhere deeper. + # may be flushAndSerializeInParallel FlushAndSerializeInParallel in aggregator.go ? + # compression is Trade off CPU , network ? event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 9c7edefbb3557..b99768883b7ff 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -12,7 +12,8 @@ ) #TODO temp imports: import pdb -import time + +MAX_DEADLOCKS = 100 class Deadlocks: @@ -22,37 +23,45 @@ def __init__(self, check, conn_prefix, config): self._conn_key_prefix = conn_prefix self._config = config self._last_deadlock_timestamp = '1900-01-01 01:01:01.111' - + self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) + + + def obfuscate_no_except_wrapper(self, sql_text): + try: + sql_text = obfuscate_sql_with_metadata(sql_text, self._config.obfuscator_options, replace_null_character=True)['query'] + except Exception as e: + if self._config.log_unobfuscated_queries: + self.log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) + else: + self.log.debug("Failed to obfuscate sql text within a deadlock | err=[%s]", e) + sql_text = "ERROR: failed to obfuscate" + return sql_text + def obfuscate_xml(self, root): # TODO put exception here if not found as this would signal in a format change + pdb.set_trace() process_list = root.find(".//process-list") for process in process_list.findall('process'): inputbuf = process.find('inputbuf') - inputbuf.text = obfuscate_sql_with_metadata(inputbuf.text, self._config.obfuscator_options, replace_null_character=True)['query'] + #TODO inputbuf.text can be truncated, check when live ? + inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) for frame in process.findall('.//frame'): - frame.text = obfuscate_sql_with_metadata(frame.text, self._config.obfuscator_options, replace_null_character=True)['query'] + frame.text = self.obfuscate_no_except_wrapper(frame.text) def collect_deadlocks(self): pdb.set_trace() with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: - #Q test this query for 1000 deadlocks ? speed , truncation ? - #Q shell we may be limit amount of data, 1000 deadlock is 4MB but do we need more than .. 50 ?(conf parameter) - cursor.execute(DETECT_DEADLOCK_QUERY, (self._last_deadlock_timestamp,)) + cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') converted_xmls = [] for result in results: - #TODO if this fails what we do , can be obfuscate it or just drop and notify backend? - #TODO speed of serialization deciarialization try: root = ET.fromstring(result[1]) except Exception as e: - #TODO notify backend ? try to check manually for process list tag and processes - # say if we can find and and and we could - # still try to do something but my feeling just to notify backend - # Other thing do we want to suggest to set ring buffer to 1MB ? + # TODO notify backend ? How ? make a collection_errors array like in metadata json self._log.error( """An error occurred while collecting SQLServer deadlocks. One of the deadlock XMLs couldn't be parsed. The error: {}""".format(e) diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 339083b29efd7..a5216b8862705 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,6 +214,22 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ +DETECT_DEADLOCK_QUERY = """ +DECLARE @limit INT = ?; +SELECT TOP (@limit) + xdr.value('@timestamp', 'datetime') AS [Date], + xdr.query('.') AS [Event_Data] +FROM + (SELECT CAST([target_data] AS XML) AS Target_Data + FROM sys.dm_xe_session_targets AS xt + INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address + WHERE xs.name = N'system_health' + AND xt.target_name = N'ring_buffer' + ) AS XML_Data +CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +WHERE xdr.value('@timestamp', 'datetime') >= ? +ORDER BY [Date] DESC; +""" def get_query_ao_availability_groups(sqlserver_major_version): """ @@ -428,17 +444,3 @@ def get_query_file_stats(sqlserver_major_version, sqlserver_engine_edition): ] + metric_columns, } -#shell we limit the query , like if there 100 deadlocks ? -DETECT_DEADLOCK_QUERY = """ -SELECT xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [Event_Data] -FROM (SELECT CAST([target_data] AS XML) AS Target_Data - FROM sys.dm_xe_session_targets AS xt - INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address - WHERE xs.name = N'system_health' - AND xt.target_name = N'ring_buffer' - ) AS XML_Data -CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE xdr.value('@timestamp', 'datetime') >= ? -ORDER BY [Date] DESC; -""" From 0a0a85cce9151b5399685aadac7e2a818379616f Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Thu, 8 Aug 2024 08:39:40 +0000 Subject: [PATCH 09/92] adopted oto the activity pipeline --- sqlserver/datadog_checks/sqlserver/activity.py | 16 +++++++++++----- sqlserver/datadog_checks/sqlserver/deadlocks.py | 2 -- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index dfd413e2306da..505abe537067b 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -16,7 +16,7 @@ from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name from datadog_checks.sqlserver.deadlocks import Deadlocks - +import pdb try: import datadog_agent except ImportError: @@ -250,9 +250,14 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): deadlock_xmls = self._deadlocks.collect_deadlocks() - deadlocks_event = self._create_deadlock_event(deadlock_xmls) - payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - self._check.database_monitoring_deadlocks(payload) + #deadlocks_event = self._create_deadlock_event(deadlock_xmls) + #payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + #self._check.database_monitoring_deadlocks(payload) + + + deadlocks_event_activity = self._create_activity_event([], [], deadlock_xmls) + payload = json.dumps(deadlocks_event_activity, default=default_json_event_encoding) + self._check.database_monitoring_query_activity(payload) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): @@ -415,7 +420,7 @@ def _sanitize_row(row, obfuscated_statement=None): def _get_estimated_row_size_bytes(row): return len(str(row)) - def _create_activity_event(self, active_sessions, active_connections): + def _create_activity_event(self, active_sessions, active_connections, deadlocks = []): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -429,6 +434,7 @@ def _create_activity_event(self, active_sessions, active_connections): "cloud_metadata": self._config.cloud_metadata, "sqlserver_activity": active_sessions, "sqlserver_connections": active_connections, + "sqlserver_deadlocks": deadlocks, } return event diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index b99768883b7ff..88e298fa1b385 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -39,7 +39,6 @@ def obfuscate_no_except_wrapper(self, sql_text): def obfuscate_xml(self, root): # TODO put exception here if not found as this would signal in a format change - pdb.set_trace() process_list = root.find(".//process-list") for process in process_list.findall('process'): inputbuf = process.find('inputbuf') @@ -49,7 +48,6 @@ def obfuscate_xml(self, root): frame.text = self.obfuscate_no_except_wrapper(frame.text) def collect_deadlocks(self): - pdb.set_trace() with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) From f95318fb0c0910f7a56efefb6fc21b969d150b83 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Thu, 8 Aug 2024 09:12:43 +0000 Subject: [PATCH 10/92] Use activity event pipline for the deadlock event --- sqlserver/datadog_checks/sqlserver/activity.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 505abe537067b..bdbf11624f0f0 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -250,14 +250,14 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): deadlock_xmls = self._deadlocks.collect_deadlocks() - #deadlocks_event = self._create_deadlock_event(deadlock_xmls) - #payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - #self._check.database_monitoring_deadlocks(payload) + deadlocks_event = self._create_deadlock_event(deadlock_xmls) + payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + self._check.database_monitoring_query_activity(payload) - deadlocks_event_activity = self._create_activity_event([], [], deadlock_xmls) - payload = json.dumps(deadlocks_event_activity, default=default_json_event_encoding) - self._check.database_monitoring_query_activity(payload) + #deadlocks_event_activity = self._create_activity_event([], [], deadlock_xmls) + #payload = json.dumps(deadlocks_event_activity, default=default_json_event_encoding) + #self._check.database_monitoring_query_activity(payload) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): @@ -420,7 +420,7 @@ def _sanitize_row(row, obfuscated_statement=None): def _get_estimated_row_size_bytes(row): return len(str(row)) - def _create_activity_event(self, active_sessions, active_connections, deadlocks = []): + def _create_activity_event(self, active_sessions, active_connections): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -434,7 +434,6 @@ def _create_activity_event(self, active_sessions, active_connections, deadlocks "cloud_metadata": self._config.cloud_metadata, "sqlserver_activity": active_sessions, "sqlserver_connections": active_connections, - "sqlserver_deadlocks": deadlocks, } return event From 3af8b681f119b9a38dc7bc91fdc012dd5ae70474 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Thu, 8 Aug 2024 12:46:00 +0000 Subject: [PATCH 11/92] Removed pdb --- .../datadog_checks/sqlserver/activity.py | 19 +++++++++---------- .../datadog_checks/sqlserver/deadlocks.py | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index bdbf11624f0f0..d1e93deb3622b 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -16,7 +16,7 @@ from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name from datadog_checks.sqlserver.deadlocks import Deadlocks -import pdb + try: import datadog_agent except ImportError: @@ -179,7 +179,8 @@ def __init__(self, check, config: SQLServerConfig): self._last_deadlocks_collection_time = 0 self._last_activity_collection_time = 0 - self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", False)) + #TODO put back false + self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", True)) self._deadlocks_collection_interval = config.deadlocks_config.get( "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL ) @@ -253,11 +254,6 @@ def _collect_deadlocks(self): deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) - - - #deadlocks_event_activity = self._create_activity_event([], [], deadlock_xmls) - #payload = json.dumps(deadlocks_event_activity, default=default_json_event_encoding) - #self._check.database_monitoring_query_activity(payload) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): @@ -420,6 +416,9 @@ def _sanitize_row(row, obfuscated_statement=None): def _get_estimated_row_size_bytes(row): return len(str(row)) + + + def _create_activity_event(self, active_sessions, active_connections): event = { "host": self._check.resolved_hostname, @@ -429,7 +428,7 @@ def _create_activity_event(self, active_sessions, active_connections): "collection_interval": self._activity_collection_interval, #TODO is it important for whatever reason to have very precise int ? "ddtags": self.tags, "timestamp": time.time() * 1000, - 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_activity": active_sessions, @@ -451,8 +450,8 @@ def _create_deadlock_event(self, deadlock_xmls): "collection_interval": self._deadlocks_collection_interval, "ddtags": self.tags, "timestamp": time.time() * 1000, - 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), - 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), + #TODO ? 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + #TODO ? 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, } diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 88e298fa1b385..90e874b862c26 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -11,7 +11,7 @@ DETECT_DEADLOCK_QUERY, ) #TODO temp imports: -import pdb +#import pdb MAX_DEADLOCKS = 100 @@ -71,5 +71,5 @@ def collect_deadlocks(self): self.obfuscate_xml(root) converted_xmls.append(ET.tostring(root, encoding='unicode')) self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] - pdb.set_trace() + #pdb.set_trace() return converted_xmls From 0627c7bf1e0572c85127246f37adaf639cf3ce29 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Thu, 8 Aug 2024 13:05:18 +0000 Subject: [PATCH 12/92] Added logs --- sqlserver/datadog_checks/sqlserver/activity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index d1e93deb3622b..90cda706e165b 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -251,7 +251,11 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): deadlock_xmls = self._deadlocks.collect_deadlocks() + if len(deadlock_xmls) == 0: + self._log.error("Collected 0 DEADLOCKS") + return deadlocks_event = self._create_deadlock_event(deadlock_xmls) + self._log.error("DEADLOCK EVENTS TO BE SENT: {}".format(deadlocks_event)) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) From da6ef0d4b437c653befec7014f736033ee5316fc Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Mon, 12 Aug 2024 10:30:32 +0000 Subject: [PATCH 13/92] Improved queries --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 2 +- sqlserver/datadog_checks/sqlserver/queries.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 90e874b862c26..27ec52a8ab605 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -70,6 +70,6 @@ def collect_deadlocks(self): last_deadlock_datetime = datetime_obj self.obfuscate_xml(root) converted_xmls.append(ET.tostring(root, encoding='unicode')) - self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] #pdb.set_trace() return converted_xmls diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index a5216b8862705..b5a78fad53026 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -227,7 +227,7 @@ AND xt.target_name = N'ring_buffer' ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE xdr.value('@timestamp', 'datetime') >= ? +WHERE xdr.value('@timestamp', 'datetime') > ? ORDER BY [Date] DESC; """ From 61772e649fd3e7b36d946c8085cb3191339dba99 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Sun, 18 Aug 2024 15:30:08 +0000 Subject: [PATCH 14/92] Added a new query --- .../datadog_checks/sqlserver/activity.py | 5 +- .../datadog_checks/sqlserver/deadlocks.py | 3 +- sqlserver/datadog_checks/sqlserver/queries.py | 21 ++++++- sqlserver/tests/test_activity.py | 60 +++++++++---------- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 90cda706e165b..c862e212ce2b3 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -239,6 +239,7 @@ def run_job(self): if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: self._last_deadlocks_collection_time = time.time() try: + self._log.error("EXECUTING COLLECT DEADLOCKS") self._collect_deadlocks() except Exception as e: self._log.error( @@ -250,14 +251,16 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): + start_time = time.time() deadlock_xmls = self._deadlocks.collect_deadlocks() if len(deadlock_xmls) == 0: self._log.error("Collected 0 DEADLOCKS") - return + return deadlocks_event = self._create_deadlock_event(deadlock_xmls) self._log.error("DEADLOCK EVENTS TO BE SENT: {}".format(deadlocks_event)) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) + self._log.error("DEADLOCK COlLECTED {} in {} time".format(len(deadlock_xmls), time.time() - start_time)) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 27ec52a8ab605..70ceabd18e3fa 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -11,7 +11,7 @@ DETECT_DEADLOCK_QUERY, ) #TODO temp imports: -#import pdb + MAX_DEADLOCKS = 100 @@ -71,5 +71,4 @@ def collect_deadlocks(self): self.obfuscate_xml(root) converted_xmls.append(ET.tostring(root, encoding='unicode')) self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - #pdb.set_trace() return converted_xmls diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index b5a78fad53026..88991e18508f1 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,7 +214,7 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ -DETECT_DEADLOCK_QUERY = """ +DETECT_DEADLOCK_QUERY2 = """ DECLARE @limit INT = ?; SELECT TOP (@limit) xdr.value('@timestamp', 'datetime') AS [Date], @@ -231,6 +231,25 @@ ORDER BY [Date] DESC; """ +DETECT_DEADLOCK_QUERY = """ +DECLARE @limit INT = ?; +SELECT TOP (@limit) + xdr.value('@timestamp', 'datetime') AS [Date], + xdr.query('.') AS [Event_Data] +FROM + sys.dm_xe_sessions AS xs + INNER JOIN sys.dm_xe_session_targets AS xt + ON xs.address = xt.event_session_address + CROSS APPLY (SELECT CAST(xt.target_data AS XML) AS Target_Data) AS XML_Data + CROSS APPLY XML_Data.Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +WHERE + xs.name = N'DeadlockMonitoring' + AND xt.target_name = N'ring_buffer' + AND xdr.value('@timestamp', 'datetime') > ? +ORDER BY [Date] DESC; +OPTION (FORCE ORDER); +""" + def get_query_ao_availability_groups(sqlserver_major_version): """ Construct the sys.availability_groups QueryExecutor configuration based on the SQL Server major version diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 39ee46516c5f7..91a32b8cf8f13 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -31,7 +31,7 @@ import pyodbc except ImportError: pyodbc = None - +import time ACTIVITY_JSON_PLANS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "activity") @@ -751,8 +751,8 @@ def _get_conn_for_user(instance_docker, user, _autocommit=False): conn_str = 'DRIVER={};Server={};Database=master;UID={};PWD={};TrustServerCertificate=yes;'.format( instance_docker['driver'], instance_docker['host'], user, "Password12!" ) - conn = pyodbc.connect(conn_str, timeout=DEFAULT_TIMEOUT, autocommit=_autocommit) - conn.timeout = DEFAULT_TIMEOUT + conn = pyodbc.connect(conn_str, timeout=1, autocommit=_autocommit) + conn.timeout = 1 return conn @@ -948,35 +948,31 @@ def run_second_deadlock_query(conn, event1, event2): print(e) pass conn.commit() - bob_conn = _get_conn_for_user(dbm_instance, 'bob') - fred_conn = _get_conn_for_user(dbm_instance, 'fred') - executor = concurrent.futures.thread.ThreadPoolExecutor(2) - event1 = Event() - event2 = Event() - - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) - futures_first_query.result() - futures_second_query.result() - # Make sure deadlock is killed and db is updated - time.sleep(1) - - bob_conn.close() - fred_conn.close() - bob_conn = _get_conn_for_user(dbm_instance, 'bob') - fred_conn = _get_conn_for_user(dbm_instance, 'fred') - event3 = Event() - event4 = Event() - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event3, event4) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event3, event4) - futures_first_query.result() - futures_second_query.result() - - time.sleep(1) - - bob_conn.close() - fred_conn.close() - executor.shutdown() + def create_deadlock(): + bob_conn = _get_conn_for_user(dbm_instance, 'bob') + fred_conn = _get_conn_for_user(dbm_instance, 'fred') + + executor = concurrent.futures.thread.ThreadPoolExecutor(2) + event1 = Event() + event2 = Event() + + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) + futures_first_query.result() + futures_second_query.result() + # Make sure deadlock is killed and db is updated + time.sleep(1) + + bob_conn.close() + fred_conn.close() + executor.shutdown() + s = time.time() + for i in range(0,700): + if i % 70 == 0: + spent_from_start = time.time() - s + #pdb.set_trace() + print("created some deadlocks {}", spent_from_start) + create_deadlock() dd_run_check(sqlserver_check) pdb.set_trace() print("Set trace before end to keep sqlserver alive") \ No newline at end of file From 8835210664e1acb5855f479deb22d41f78c64204 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Sun, 18 Aug 2024 15:59:15 +0000 Subject: [PATCH 15/92] fix import --- sqlserver/datadog_checks/sqlserver/activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index c862e212ce2b3..82fc0e50c4f60 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -21,7 +21,7 @@ import datadog_agent except ImportError: from ..stubs import datadog_agent - +import time DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 From d6cc03a404777b553419084a8bf1590316399479 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Mon, 19 Aug 2024 12:50:55 +0000 Subject: [PATCH 16/92] use temp table --- .../datadog_checks/sqlserver/deadlocks.py | 2 + sqlserver/datadog_checks/sqlserver/queries.py | 47 ++++++++----------- sqlserver/tests/test_integration.py | 1 + 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 70ceabd18e3fa..128439a356078 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -8,6 +8,7 @@ from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata from datadog_checks.sqlserver.queries import ( + CREATE_DEADLOCK_TEMP_TABLE_QUERY, DETECT_DEADLOCK_QUERY, ) #TODO temp imports: @@ -50,6 +51,7 @@ def obfuscate_xml(self, root): def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: + cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 88991e18508f1..68bb2264b3dc8 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,40 +214,31 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ -DETECT_DEADLOCK_QUERY2 = """ -DECLARE @limit INT = ?; -SELECT TOP (@limit) - xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [Event_Data] +CREATE_DEADLOCK_TEMP_TABLE_QUERY = """ +SELECT + CAST([target_data] AS XML) AS Target_Data +INTO + TempXMLDatadogData FROM - (SELECT CAST([target_data] AS XML) AS Target_Data - FROM sys.dm_xe_session_targets AS xt - INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address - WHERE xs.name = N'system_health' - AND xt.target_name = N'ring_buffer' - ) AS XML_Data -CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE xdr.value('@timestamp', 'datetime') > ? -ORDER BY [Date] DESC; + sys.dm_xe_session_targets AS xt +INNER JOIN + sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address +WHERE + xs.name = N'system_health' +AND + xt.target_name = N'ring_buffer'; """ DETECT_DEADLOCK_QUERY = """ -DECLARE @limit INT = ?; -SELECT TOP (@limit) - xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [Event_Data] -FROM - sys.dm_xe_sessions AS xs - INNER JOIN sys.dm_xe_session_targets AS xt - ON xs.address = xt.event_session_address - CROSS APPLY (SELECT CAST(xt.target_data AS XML) AS Target_Data) AS XML_Data - CROSS APPLY XML_Data.Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +SELECT TOP (?) + xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] +FROM + TempXMLDatadogData +CROSS APPLY + Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE - xs.name = N'DeadlockMonitoring' - AND xt.target_name = N'ring_buffer' - AND xdr.value('@timestamp', 'datetime') > ? + xdr.value('@timestamp', 'datetime') > ? ORDER BY [Date] DESC; -OPTION (FORCE ORDER); """ def get_query_ao_availability_groups(sqlserver_major_version): diff --git a/sqlserver/tests/test_integration.py b/sqlserver/tests/test_integration.py index e9f20cd711a2e..152344a5dd997 100644 --- a/sqlserver/tests/test_integration.py +++ b/sqlserver/tests/test_integration.py @@ -69,6 +69,7 @@ def test_check_dbm_enabled_config(aggregator, dd_run_check, init_config, instanc sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) assert isinstance(sqlserver_check._config.dbm_enabled, bool) + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @pytest.mark.parametrize( From 2de0f13190a63f7b90e407a534c7e0b86c109444 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 12:33:45 +0000 Subject: [PATCH 17/92] Made test stable --- .../datadog_checks/sqlserver/activity.py | 26 ++++++----- .../datadog_checks/sqlserver/deadlocks.py | 3 +- sqlserver/datadog_checks/sqlserver/queries.py | 4 +- sqlserver/tests/utils.py | 44 +++++++++++++++++++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 82fc0e50c4f60..635d048f9c9d3 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -25,7 +25,7 @@ DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 - +import pdb CONNECTIONS_QUERY = """\ SELECT login_name AS user_name, @@ -218,6 +218,7 @@ def __init__(self, check, config: SQLServerConfig): self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES self._exec_requests_cols_cached = None self._deadlocks = Deadlocks(check, self._conn_key_prefix, self._config) + self._deadlock__payload_max_bytes = MAX_PAYLOAD_BYTES def _close_db_conn(self): pass @@ -252,7 +253,18 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): start_time = time.time() - deadlock_xmls = self._deadlocks.collect_deadlocks() + deadlock_xmls_collected = self._deadlocks.collect_deadlocks() + deadlock_xmls = [] + total_number_of_characters = 0 + pdb.set_trace() + for i, deadlock in enumerate(deadlock_xmls_collected): + total_number_of_characters += len(deadlock) + if total_number_of_characters > self._deadlock__payload_max_bytes: + self._log.warning("We've dropped {} deadlocks from a total of {} deadlocks as the max deadlock payload of {} bytes was exceeded.".format(len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes)) + break + else: + deadlock_xmls.append(deadlock) + #TODO REMOVE log error if len(deadlock_xmls) == 0: self._log.error("Collected 0 DEADLOCKS") return @@ -423,9 +435,6 @@ def _sanitize_row(row, obfuscated_statement=None): def _get_estimated_row_size_bytes(row): return len(str(row)) - - - def _create_activity_event(self, active_sessions, active_connections): event = { "host": self._check.resolved_hostname, @@ -446,9 +455,6 @@ def _create_activity_event(self, active_sessions, active_connections): def _create_deadlock_event(self, deadlock_xmls): #TODO WHAT if deadlock xml is just too long ? #MAX_PAYLOAD_BYTES ? - #TODO compression , couldnt reallly see it in Go code so far but may be its somewhere deeper. - # may be flushAndSerializeInParallel FlushAndSerializeInParallel in aggregator.go ? - # compression is Trade off CPU , network ? event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -457,8 +463,8 @@ def _create_deadlock_event(self, deadlock_xmls): "collection_interval": self._deadlocks_collection_interval, "ddtags": self.tags, "timestamp": time.time() * 1000, - #TODO ? 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), - #TODO ? 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, } diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 128439a356078..4c01fd64cb6c0 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -12,7 +12,7 @@ DETECT_DEADLOCK_QUERY, ) #TODO temp imports: - +import pdb MAX_DEADLOCKS = 100 @@ -51,6 +51,7 @@ def obfuscate_xml(self, root): def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: + pdb.set_trace() cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 68bb2264b3dc8..020fad1085189 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -218,7 +218,7 @@ SELECT CAST([target_data] AS XML) AS Target_Data INTO - TempXMLDatadogData + #TempXMLDatadogData FROM sys.dm_xe_session_targets AS xt INNER JOIN @@ -233,7 +233,7 @@ SELECT TOP (?) xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] FROM - TempXMLDatadogData + #TempXMLDatadogData CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE diff --git a/sqlserver/tests/utils.py b/sqlserver/tests/utils.py index 17b6f03fbc887..87ae66b52f595 100644 --- a/sqlserver/tests/utils.py +++ b/sqlserver/tests/utils.py @@ -6,6 +6,8 @@ import threading from copy import copy from random import choice, randint, shuffle +import concurrent +from threading import Event import pyodbc import pytest @@ -244,6 +246,48 @@ def normalize_indexes_columns(actual_payload): sorted_columns = sorted(columns) index['column_names'] = ','.join(sorted_columns) +def run_first_deadlock_query(conn, event1, event2): + exception_text = "" + try: + conn.cursor().execute("BEGIN TRAN foo;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 1;") + event1.set() + event2.wait() + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") + except Exception as e: + # Exception is expected due to a deadlock + exception_text = str(e) + pass + conn.commit() + return exception_text + +def run_second_deadlock_query(conn, event1, event2): + exception_text = "" + try: + event1.wait() + conn.cursor().execute("BEGIN TRAN bar;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 2;") + event2.set() + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") + except Exception as e: + # Exception is expected due to a deadlock + exception_text = str(e) + pass + conn.commit() + return exception_text + +def create_deadlock(bob_conn, fred_conn): + executor = concurrent.futures.thread.ThreadPoolExecutor(2) + event1 = Event() + event2 = Event() + + futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) + exception_1_text = futures_first_query.result() + exception_2_text = futures_second_query.result() + executor.shutdown() + return "deadlock" in exception_1_text or "deadlock" in exception_2_text + def deep_compare(obj1, obj2): if isinstance(obj1, dict) and isinstance(obj2, dict): From a82f04a09110a04ae98c1ac7fb66c837da0c70f1 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 15:21:37 +0000 Subject: [PATCH 18/92] Added obfuscation unit test --- .../datadog_checks/sqlserver/deadlocks.py | 1 - sqlserver/tests/test_activity.py | 127 +-- sqlserver/tests/test_unit.py | 893 ------------------ 3 files changed, 69 insertions(+), 952 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 4c01fd64cb6c0..1ac9ccf857310 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -26,7 +26,6 @@ def __init__(self, check, conn_prefix, config): self._last_deadlock_timestamp = '1900-01-01 01:01:01.111' self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) - def obfuscate_no_except_wrapper(self, sql_text): try: sql_text = obfuscate_sql_with_metadata(sql_text, self._config.obfuscator_options, replace_null_character=True)['query'] diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 91a32b8cf8f13..44b74d169be15 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -12,8 +12,8 @@ import threading import time from concurrent.futures.thread import ThreadPoolExecutor -from threading import Event from copy import copy +import xml.etree.ElementTree as ET import mock import pytest @@ -26,6 +26,7 @@ from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION from .conftest import DEFAULT_TIMEOUT +from .utils import create_deadlock import pdb try: import pyodbc @@ -746,13 +747,13 @@ def _load_test_activity_json(filename): return json.load(f) -def _get_conn_for_user(instance_docker, user, _autocommit=False): +def _get_conn_for_user(instance_docker, user, timeout = 1, _autocommit=False): # Make DB connection conn_str = 'DRIVER={};Server={};Database=master;UID={};PWD={};TrustServerCertificate=yes;'.format( instance_docker['driver'], instance_docker['host'], user, "Password12!" ) - conn = pyodbc.connect(conn_str, timeout=1, autocommit=_autocommit) - conn.timeout = 1 + conn = pyodbc.connect(conn_str, timeout=timeout, autocommit=_autocommit) + conn.timeout = timeout return conn @@ -908,11 +909,21 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) +#plan - first we need to catch deadlock exception if not try again ? + +# there are often 2 deadlocks lets check for that + +# test1 - just that we collect deadlocks and its in stub events +# test2 - time test that we take deadlocks in delta +# some crazy scenario if possible like 3 query involved ? +# deadlock too long ? +# we cannot test that real obfuscator is called at least check that its called by unit test + +#TEST That we at least try to apply obfuscation to all required fields ! @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') -def test_deadlocks(dd_run_check, init_config, dbm_instance): - pdb.set_trace() +def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['deadlocks'] = { 'enabled': True, 'run_sync': True, #TODO oups run_sync what should be the logic for 2 jobs ? @@ -922,57 +933,57 @@ def test_deadlocks(dd_run_check, init_config, dbm_instance): sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - def run_first_deadlock_query(conn, event1, event2): - #conn.begin() - try: - conn.cursor().execute("BEGIN TRAN foo;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 1;") - event1.set() - event2.wait() - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") - except Exception as e: - # Exception is expected due to a deadlock - print(e) - pass - conn.commit() - def run_second_deadlock_query(conn, event1, event2): - #conn.begin() - try: - event1.wait() - conn.cursor().execute("BEGIN TRAN bar;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 2;") - event2.set() - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") - except Exception as e: - # Exception is expected due to a deadlock - print(e) - pass - conn.commit() - def create_deadlock(): - bob_conn = _get_conn_for_user(dbm_instance, 'bob') - fred_conn = _get_conn_for_user(dbm_instance, 'fred') - - executor = concurrent.futures.thread.ThreadPoolExecutor(2) - event1 = Event() - event2 = Event() - - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) - futures_first_query.result() - futures_second_query.result() - # Make sure deadlock is killed and db is updated - time.sleep(1) - + created_deadlock = False + #Rarely instead of a deadlock one of the transactions time outs + for i in range(0,3): + bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) + fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) + created_deadlock = create_deadlock(bob_conn, fred_conn) bob_conn.close() fred_conn.close() - executor.shutdown() - s = time.time() - for i in range(0,700): - if i % 70 == 0: - spent_from_start = time.time() - s - #pdb.set_trace() - print("created some deadlocks {}", spent_from_start) - create_deadlock() - dd_run_check(sqlserver_check) - pdb.set_trace() - print("Set trace before end to keep sqlserver alive") \ No newline at end of file + if created_deadlock: + break + assert created_deadlock, "Couldn't create a deadlock, exiting" + + def execut_test(): + dd_run_check(sqlserver_check) + + dbm_activity = aggregator.get_event_platform_events("dbm-activity") + if not dbm_activity: + return False, "should have collected at least one activity event" + matched_event = [] + + for event in dbm_activity: + if "sqlserver_deadlocks" in event: + matched_event.append(event) + + if len(matched_event) != 1: + return False, "should have collected one deadlock payload" + deadlocks = matched_event[0]["sqlserver_deadlocks"] + if len(deadlocks) < 1: + return False, "should have collected one or more deadlock in the payload" + found = False + for d in deadlocks: + root = ET.fromstring(d) + pdb.set_trace() + process_list = root.find(".//process-list") + for process in process_list.findall('process'): + if process.find('inputbuf').text == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;": + found = True + err = "" + if not found: + err = "Should've collected produced deadlock" + return found, err + + # Sometimes deadlock takes a bit longer to arrive to the ring buffer. + # We can may be give it 3 tries + err = "" + for i in range(0,3): + time.sleep(3) + res, err = execut_test() + if res: + return + assert False, err + + + \ No newline at end of file diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 4aadfa462af2f..e69de29bb2d1d 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -1,893 +0,0 @@ -# (C) Datadog, Inc. 2018-present -# All rights reserved -# Licensed under a 3-clause BSD style license (see LICENSE) -import copy -import json -import os -import re -import time -from collections import namedtuple - -import mock -import pytest - -from datadog_checks.dev import EnvVars -from datadog_checks.sqlserver import SQLServer -from datadog_checks.sqlserver.connection import split_sqlserver_host_port -from datadog_checks.sqlserver.metrics import SqlFractionMetric, SqlMasterDatabaseFileStats -from datadog_checks.sqlserver.schemas import Schemas, SubmitData -from datadog_checks.sqlserver.sqlserver import SQLConnectionError -from datadog_checks.sqlserver.utils import ( - Database, - extract_sql_comments_and_procedure_name, - get_unixodbc_sysconfig, - is_non_empty_file, - parse_sqlserver_major_version, - set_default_driver_conf, -) - -from .common import CHECK_NAME, DOCKER_SERVER, assert_metrics -from .utils import deep_compare, not_windows_ci, windows_ci - -try: - import pyodbc -except ImportError: - pyodbc = None - -# mark the whole module -pytestmark = pytest.mark.unit - - -def test_get_cursor(instance_docker): - """ - Ensure we don't leak connection info in case of a KeyError when the - connection pool is empty or the params for `get_cursor` are invalid. - """ - check = SQLServer(CHECK_NAME, {}, [instance_docker]) - check.initialize_connection() - with pytest.raises(SQLConnectionError): - check.connection.get_cursor('foo') - - -def test_missing_db(instance_docker, dd_run_check): - instance = copy.copy(instance_docker) - instance['ignore_missing_database'] = False - - with mock.patch( - 'datadog_checks.sqlserver.connection.Connection.open_managed_default_connection', - side_effect=SQLConnectionError(Exception("couldnt connect")), - ): - with pytest.raises(SQLConnectionError): - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - - instance['ignore_missing_database'] = True - with mock.patch('datadog_checks.sqlserver.connection.Connection.check_database', return_value=(False, 'db')): - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - dd_run_check(check) - assert check.do_check is False - - -@mock.patch('datadog_checks.sqlserver.connection.Connection.open_managed_default_database') -@mock.patch('datadog_checks.sqlserver.connection.Connection.get_cursor') -def test_db_exists(get_cursor, mock_connect, instance_docker_defaults, dd_run_check): - Row = namedtuple('Row', 'name,collation_name') - db_results = [ - Row('master', 'SQL_Latin1_General_CP1_CI_AS'), - Row('tempdb', 'SQL_Latin1_General_CP1_CI_AS'), - Row('AdventureWorks2017', 'SQL_Latin1_General_CP1_CI_AS'), - Row('CaseSensitive2018', 'SQL_Latin1_General_CP1_CS_AS'), - Row('OfflineDB', None), - ] - - mock_connect.__enter__ = mock.Mock(return_value='foo') - - mock_results = mock.MagicMock() - mock_results.fetchall.return_value = db_results - get_cursor.return_value = mock_results - - instance = copy.copy(instance_docker_defaults) - # make sure check doesn't try to add metrics - instance['stored_procedure'] = 'fake_proc' - instance['ignore_missing_database'] = True - - # check base case of lowercase for lowercase and case-insensitive db - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is True - # check all caps for case insensitive db - instance['database'] = 'MASTER' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is True - - # check mixed case against mixed case but case-insensitive db - instance['database'] = 'AdventureWORKS2017' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is True - - # check case sensitive but matched db - instance['database'] = 'CaseSensitive2018' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is True - - # check case sensitive but mismatched db - instance['database'] = 'cASEsENSITIVE2018' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is False - - # check offline but exists db - instance['database'] = 'Offlinedb' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - assert check.do_check is True - - -@mock.patch('datadog_checks.sqlserver.connection.Connection.open_managed_default_database') -@mock.patch('datadog_checks.sqlserver.connection.Connection.get_cursor') -def test_azure_cross_database_queries_excluded(get_cursor, mock_connect, instance_docker_defaults, dd_run_check): - Row = namedtuple('Row', 'name,collation_name') - db_results = [ - Row('master', 'SQL_Latin1_General_CP1_CI_AS'), - Row('tempdb', 'SQL_Latin1_General_CP1_CI_AS'), - Row('AdventureWorks2017', 'SQL_Latin1_General_CP1_CI_AS'), - Row('CaseSensitive2018', 'SQL_Latin1_General_CP1_CS_AS'), - Row('OfflineDB', None), - ] - - mock_connect.__enter__ = mock.Mock(return_value='foo') - - mock_results = mock.MagicMock() - mock_results.fetchall.return_value = db_results - get_cursor.return_value = mock_results - - instance = copy.copy(instance_docker_defaults) - instance['stored_procedure'] = 'fake_proc' - check = SQLServer(CHECK_NAME, {}, [instance]) - check.initialize_connection() - check.make_metric_list_to_collect() - cross_database_metrics = [ - metric - for metric in check.instance_metrics - if metric.__class__.TABLE not in ['msdb.dbo.backupset', 'sys.dm_db_file_space_usage'] - ] - assert len(cross_database_metrics) == 0 - - -def test_autodiscovery_matches_all_by_default(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list() - all_dbs = {Database(r.name) for r in fetchall_results} - # check base case of default filters - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == all_dbs - - -def test_azure_autodiscovery_matches_all_by_default(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list_azure() - all_dbs = {Database(r.name, r.physical_database_name) for r in fetchall_results} - - # check base case of default filters - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == all_dbs - - -def test_autodiscovery_matches_none(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list() - # check missing additions, but no exclusions - mock_cursor.fetchall.return_value = iter(fetchall_results) # reset the mock results - instance_autodiscovery['autodiscovery_include'] = ['missingdb', 'fakedb'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == set() - - -def test_azure_autodiscovery_matches_none(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list_azure() - # check missing additions, but no exclusions - mock_cursor.fetchall.return_value = iter(fetchall_results) # reset the mock results - instance_autodiscovery['autodiscovery_include'] = ['missingdb', 'fakedb'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == set() - - -def test_autodiscovery_matches_some(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list() - instance_autodiscovery['autodiscovery_include'] = ['master', 'fancy2020db', 'missingdb', 'fakedb'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - dbs = [Database(name) for name in ['master', 'Fancy2020db']] - assert check.databases == set(dbs) - - -def test_azure_autodiscovery_matches_some(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list_azure() - instance_autodiscovery['autodiscovery_include'] = ['master', 'fancy2020db', 'missingdb', 'fakedb'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - dbs = [Database(name, pys_db) for name, pys_db in {'master': 'master', 'Fancy2020db': '40e688a7e268'}.items()] - assert check.databases == set(dbs) - - -def test_autodiscovery_exclude_some(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list() - instance_autodiscovery['autodiscovery_include'] = ['.*'] # replace default `.*` - instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - dbs = [Database(name) for name in ['tempdb', 'AdventureWorks2017', 'CaseSensitive2018']] - assert check.databases == set(dbs) - - -def test_azure_autodiscovery_exclude_some(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list_azure() - instance_autodiscovery['autodiscovery_include'] = ['.*'] # replace default `.*` - instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - db_dict = {'tempdb': 'tempdb', 'AdventureWorks2017': 'fce04774', 'CaseSensitive2018': 'jub3j8kh'} - dbs = [Database(name, pys_db) for name, pys_db in db_dict.items()] - assert check.databases == set(dbs) - - -def test_autodiscovery_exclude_override(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list() - instance_autodiscovery['autodiscovery_include'] = ['t.*', 'master'] # remove default `.*` - instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == {Database("tempdb")} - - -def test_azure_autodiscovery_exclude_override(instance_autodiscovery): - fetchall_results, mock_cursor = _mock_database_list_azure() - instance_autodiscovery['autodiscovery_include'] = ['t.*', 'master'] # remove default `.*` - instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] - check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) - check.autodiscover_databases(mock_cursor) - assert check.databases == {Database("tempdb", "tempdb")} - - -@pytest.mark.parametrize( - 'col_val_row_1, col_val_row_2, col_val_row_3', - [ - pytest.param(256, 1024, 1720, id='Valid column value 0'), - pytest.param(0, None, 1024, id='NoneType column value 1, should not raise error'), - pytest.param(512, 0, 256, id='Valid column value 2'), - pytest.param(None, 256, 0, id='NoneType column value 3, should not raise error'), - ], -) -def test_SqlMasterDatabaseFileStats_fetch_metric(col_val_row_1, col_val_row_2, col_val_row_3): - Row = namedtuple('Row', ['name', 'file_id', 'type', 'physical_name', 'size', 'max_size', 'state', 'state_desc']) - mock_rows = [ - Row('master', 1, 0, '/var/opt/mssql/data/master.mdf', col_val_row_1, -1, 0, 'ONLINE'), - Row('tempdb', 1, 0, '/var/opt/mssql/data/tempdb.mdf', col_val_row_2, -1, 0, 'ONLINE'), - Row('msdb', 1, 0, '/var/opt/mssql/data/MSDBData.mdf', col_val_row_3, -1, 0, 'ONLINE'), - ] - mock_cols = ['name', 'file_id', 'type', 'physical_name', 'size', 'max_size', 'state', 'state_desc'] - mock_metric_obj = SqlMasterDatabaseFileStats( - cfg_instance=mock.MagicMock(dict), - base_name=None, - report_function=mock.MagicMock(), - column='size', - logger=None, - ) - with mock.patch.object( - SqlMasterDatabaseFileStats, 'fetch_metric', wraps=mock_metric_obj.fetch_metric - ) as mock_fetch_metric: - errors = 0 - try: - mock_fetch_metric(mock_rows, mock_cols) - except Exception as e: - errors += 1 - raise AssertionError('{}'.format(e)) - assert errors < 1 - - -@pytest.mark.parametrize( - 'base_name', - [ - pytest.param('Buffer cache hit ratio base', id='base_name valid'), - pytest.param(None, id='base_name None'), - ], -) -def test_SqlFractionMetric_base(caplog, base_name): - Row = namedtuple('Row', ['counter_name', 'cntr_type', 'cntr_value', 'instance_name', 'object_name']) - fetchall_results = [ - Row('Buffer cache hit ratio', 537003264, 33453, '', 'SQLServer:Buffer Manager'), - Row('Buffer cache hit ratio base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), - Row('some random counter', 1073939712, 1111, '', 'SQLServer:Buffer Manager'), - Row('some random counter base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), - ] - mock_cursor = mock.MagicMock() - mock_cursor.fetchall.return_value = fetchall_results - - report_function = mock.MagicMock() - metric_obj = SqlFractionMetric( - cfg_instance={ - 'name': 'sqlserver.buffer.cache_hit_ratio', - 'counter_name': 'Buffer cache hit ratio', - 'instance_name': '', - 'physical_db_name': None, - 'tags': ['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], - 'hostname': 'stubbed.hostname', - }, - base_name=base_name, - report_function=report_function, - column=None, - logger=mock.MagicMock(), - ) - results_rows, results_cols = SqlFractionMetric.fetch_all_values( - mock_cursor, ['Buffer cache hit ratio', base_name], mock.mock.MagicMock() - ) - metric_obj.fetch_metric(results_rows, results_cols) - if base_name: - report_function.assert_called_with( - 'sqlserver.buffer.cache_hit_ratio', - 0.9976737943992127, - raw=True, - hostname='stubbed.hostname', - tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], - ) - else: - report_function.assert_not_called() - - -def test_SqlFractionMetric_group_by_instance(caplog): - Row = namedtuple('Row', ['counter_name', 'cntr_type', 'cntr_value', 'instance_name', 'object_name']) - fetchall_results = [ - Row('Buffer cache hit ratio', 537003264, 33453, '', 'SQLServer:Buffer Manager'), - Row('Buffer cache hit ratio base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), - Row('Foo counter', 537003264, 1, 'bar', 'SQLServer:Buffer Manager'), - Row('Foo counter base', 1073939712, 50, 'bar', 'SQLServer:Buffer Manager'), - Row('Foo counter', 537003264, 5, 'zoo', 'SQLServer:Buffer Manager'), - Row('Foo counter base', 1073939712, 100, 'zoo', 'SQLServer:Buffer Manager'), - ] - mock_cursor = mock.MagicMock() - mock_cursor.fetchall.return_value = fetchall_results - - report_function = mock.MagicMock() - metric_obj = SqlFractionMetric( - cfg_instance={ - 'name': 'sqlserver.test.metric', - 'counter_name': 'Foo counter', - 'instance_name': 'ALL', - 'physical_db_name': None, - 'tags': ['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], - 'hostname': 'stubbed.hostname', - 'tag_by': 'db', - }, - base_name='Foo counter base', - report_function=report_function, - column=None, - logger=mock.MagicMock(), - ) - results_rows, results_cols = SqlFractionMetric.fetch_all_values( - mock_cursor, ['Foo counter base', 'Foo counter'], mock.mock.MagicMock() - ) - metric_obj.fetch_metric(results_rows, results_cols) - report_function.assert_any_call( - 'sqlserver.test.metric', - 0.02, - raw=True, - hostname='stubbed.hostname', - tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname', 'db:bar'], - ) - report_function.assert_any_call( - 'sqlserver.test.metric', - 0.05, - raw=True, - hostname='stubbed.hostname', - tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname', 'db:zoo'], - ) - - -def _mock_database_list(): - Row = namedtuple('Row', 'name') - fetchall_results = [ - Row('master'), - Row('tempdb'), - Row('msdb'), - Row('AdventureWorks2017'), - Row('CaseSensitive2018'), - Row('Fancy2020db'), - ] - mock_cursor = mock.MagicMock() - mock_cursor.fetchall.return_value = iter(fetchall_results) - # check excluded overrides included - mock_cursor.fetchall.return_value = iter(fetchall_results) - return fetchall_results, mock_cursor - - -def _mock_database_list_azure(): - Row = namedtuple('Row', ['name', 'physical_database_name']) - fetchall_results = [ - Row('master', 'master'), - Row('tempdb', 'tempdb'), - Row('msdb', 'msdb'), - Row('AdventureWorks2017', 'fce04774'), - Row('CaseSensitive2018', 'jub3j8kh'), - Row('Fancy2020db', '40e688a7e268'), - ] - mock_cursor = mock.MagicMock() - mock_cursor.fetchall.return_value = iter(fetchall_results) - # check excluded overrides included - mock_cursor.fetchall.return_value = iter(fetchall_results) - return fetchall_results, mock_cursor - - -def test_set_default_driver_conf(): - # Docker Agent with ODBCSYSINI env var - # The only case where we set ODBCSYSINI to the the default odbcinst.ini folder - with EnvVars({'DOCKER_DD_AGENT': 'true'}, ignore=['ODBCSYSINI']): - set_default_driver_conf() - assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) - - with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): - with EnvVars({}, ignore=['ODBCSYSINI']): - set_default_driver_conf() - assert 'ODBCSYSINI' in os.environ, "ODBCSYSINI should be set" - assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) - - # `set_default_driver_conf` have no effect on the cases below - with EnvVars({'ODBCSYSINI': 'ABC', 'DOCKER_DD_AGENT': 'true'}): - set_default_driver_conf() - assert os.environ['ODBCSYSINI'] == 'ABC' - - with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): - with EnvVars({}): - set_default_driver_conf() - assert 'ODBCSYSINI' in os.environ - assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) - - with EnvVars({'ODBCSYSINI': 'ABC'}): - set_default_driver_conf() - assert os.environ['ODBCSYSINI'] == 'ABC' - - -@not_windows_ci -def test_set_default_driver_conf_linux(): - odbc_config_dir = os.path.expanduser('~') - with mock.patch("datadog_checks.sqlserver.utils.get_unixodbc_sysconfig", return_value=odbc_config_dir): - with EnvVars({}, ignore=['ODBCSYSINI']): - odbc_inst = os.path.join(odbc_config_dir, "odbcinst.ini") - odbc_ini = os.path.join(odbc_config_dir, "odbc.ini") - for file in [odbc_inst, odbc_ini]: - if os.path.exists(file): - os.remove(file) - with open(odbc_ini, "x") as file: - file.write("dummy-content") - set_default_driver_conf() - assert is_non_empty_file(odbc_inst), "odbc_inst should have been created when a non empty odbc.ini exists" - - -@windows_ci -def test_check_local(aggregator, dd_run_check, init_config, instance_docker): - sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) - dd_run_check(sqlserver_check) - check_tags = instance_docker.get('tags', []) - expected_tags = check_tags + [ - 'sqlserver_host:{}'.format(sqlserver_check.resolved_hostname), - 'connection_host:{}'.format(DOCKER_SERVER), - 'db:master', - ] - assert_metrics(instance_docker, aggregator, check_tags, expected_tags, hostname=sqlserver_check.resolved_hostname) - - -SQL_SERVER_2012_VERSION_EXAMPLE = """\ -Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64) - Oct 20 2015 15:36:27 - Copyright (c) Microsoft Corporation - Express Edition (64-bit) on Windows NT 6.3 (Build 17763: ) (Hypervisor) -""" - -SQL_SERVER_2019_VERSION_EXAMPLE = """\ -Microsoft SQL Server 2019 (RTM-CU12) (KB5004524) - 15.0.4153.1 (X64) - Jul 19 2021 15:37:34 - Copyright (C) 2019 Microsoft Corporation - Standard Edition (64-bit) on Windows Server 2016 Datacenter 10.0 (Build 14393: ) (Hypervisor) -""" - - -@pytest.mark.parametrize( - "version,expected_major_version", [(SQL_SERVER_2012_VERSION_EXAMPLE, 2012), (SQL_SERVER_2019_VERSION_EXAMPLE, 2019)] -) -def test_parse_sqlserver_major_version(version, expected_major_version): - assert parse_sqlserver_major_version(version) == expected_major_version - - -@pytest.mark.parametrize( - "instance_host,split_host,split_port", - [ - ("localhost,1433,some-typo", "localhost", "1433"), - ("localhost, 1433,some-typo", "localhost", "1433"), - ("localhost,1433", "localhost", "1433"), - ("localhost", "localhost", None), - ], -) -def test_split_sqlserver_host(instance_host, split_host, split_port): - s_host, s_port = split_sqlserver_host_port(instance_host) - assert (s_host, s_port) == (split_host, split_port) - - -def test_database_state(aggregator, dd_run_check, init_config, instance_docker): - instance_docker['database'] = 'mAsTeR' - sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) - dd_run_check(sqlserver_check) - expected_tags = instance_docker.get('tags', []) + [ - 'database_recovery_model_desc:SIMPLE', - 'database_state_desc:ONLINE', - 'database:{}'.format(instance_docker['database']), - 'db:{}'.format(instance_docker['database']), - ] - aggregator.assert_metric('sqlserver.database.state', tags=expected_tags, hostname=sqlserver_check.resolved_hostname) - - -@pytest.mark.parametrize( - "query,expected_comments,is_proc,expected_name", - [ - [ - None, - [], - False, - None, - ], - [ - "", - [], - False, - None, - ], - [ - "/*", - [], - False, - None, - ], - [ - "--", - [], - False, - None, - ], - [ - "/*justonecomment*/", - ["/*justonecomment*/"], - False, - None, - ], - [ - """\ - /* a comment */ - -- Single comment - """, - ["/* a comment */", "-- Single comment"], - False, - None, - ], - [ - "/*tag=foo*/ SELECT * FROM foo;", - ["/*tag=foo*/"], - False, - None, - ], - [ - "/*tag=foo*/ SELECT * FROM /*other=tag,incomment=yes*/ foo;", - ["/*tag=foo*/", "/*other=tag,incomment=yes*/"], - False, - None, - ], - [ - "/*tag=foo*/ SELECT * FROM /*other=tag,incomment=yes*/ foo /*lastword=yes*/", - ["/*tag=foo*/", "/*other=tag,incomment=yes*/", "/*lastword=yes*/"], - False, - None, - ], - [ - """\ - -- My Comment - CREATE PROCEDURE bobProcedure - BEGIN - SELECT name FROM bob - END; - """, - ["-- My Comment"], - True, - "bobProcedure", - ], - [ - """\ - -- My procedure - CREATE PROCEDURE bobProcedure - BEGIN - SELECT name FROM bob - END; - """, - ["-- My procedure"], - True, - "bobProcedure", - ], - [ - """\ - -- My Comment - CREATE PROCEDURE bobProcedure - -- In the middle - BEGIN - SELECT name FROM bob - END; - """, - ["-- My Comment", "-- In the middle"], - True, - "bobProcedure", - ], - [ - """\ - -- My Comment - CREATE PROCEDURE bobProcedure - -- this procedure does foo - BEGIN - SELECT name FROM bob - END; - """, - ["-- My Comment", "-- this procedure does foo"], - True, - "bobProcedure", - ], - [ - """\ - -- My Comment - CREATE PROCEDURE bobProcedure - -- In the middle - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - ["-- My Comment", "-- In the middle", "-- And at the end"], - True, - "bobProcedure", - ], - [ - """\ - -- My Comment - CREATE PROCEDURE bobProcedure - -- In the middle - /*mixed with mult-line foo*/ - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - ["-- My Comment", "-- In the middle", "/*mixed with mult-line foo*/", "-- And at the end"], - True, - "bobProcedure", - ], - [ - """\ - -- My procedure - CREATE PROCEDURE bobProcedure - -- In the middle - /*mixed with procedure foo*/ - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - ["-- My procedure", "-- In the middle", "/*mixed with procedure foo*/", "-- And at the end"], - True, - "bobProcedure", - ], - [ - """\ - /* hello - this is a mult-line-comment - tag=foo,blah=tag - */ - /* - second multi-line - comment - */ - CREATE PROCEDURE bobProcedure - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - [ - "/* hello this is a mult-line-comment tag=foo,blah=tag */", - "/* second multi-line comment */", - "-- And at the end", - ], - True, - "bobProcedure", - ], - [ - """\ - /* hello - this is a mult-line-comment - tag=foo,blah=tag - */ - /* - second multi-line - for procedure foo - */ - CREATE PROCEDURE bobProcedure - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - [ - "/* hello this is a mult-line-comment tag=foo,blah=tag */", - "/* second multi-line for procedure foo */", - "-- And at the end", - ], - True, - "bobProcedure", - ], - [ - """\ - /* hello - this is a mult-line-commet - tag=foo,blah=tag - */ - CREATE PROCEDURE bobProcedure - -- In the middle - /*mixed with mult-line foo*/ - BEGIN - SELECT name FROM bob - END; - -- And at the end - """, - [ - "/* hello this is a mult-line-commet tag=foo,blah=tag */", - "-- In the middle", - "/*mixed with mult-line foo*/", - "-- And at the end", - ], - True, - "bobProcedure", - ], - ], -) -def test_extract_sql_comments_and_procedure_name(query, expected_comments, is_proc, expected_name): - comments, p, name = extract_sql_comments_and_procedure_name(query) - assert comments == expected_comments - assert p == is_proc - assert re.match(name, expected_name, re.IGNORECASE) if expected_name else expected_name == name - - -class DummyLogger: - def debug(*args): - pass - - def error(*args): - pass - - -def set_up_submitter_unit_test(): - submitted_data = [] - base_event = { - "host": "some", - "agent_version": 0, - "dbms": "sqlserver", - "kind": "sqlserver_databases", - "collection_interval": 1200, - "dbms_version": "some", - "tags": "some", - "cloud_metadata": "some", - } - - def submitData(data): - submitted_data.append(data) - - dataSubmitter = SubmitData(submitData, base_event, DummyLogger()) - return dataSubmitter, submitted_data - - -def test_submit_data(): - - dataSubmitter, submitted_data = set_up_submitter_unit_test() - - dataSubmitter.store_db_infos([{"id": 3, "name": "test_db1"}, {"id": 4, "name": "test_db2"}]) - schema1 = {"id": "1"} - schema2 = {"id": "2"} - schema3 = {"id": "3"} - - dataSubmitter.store("test_db1", schema1, [1, 2], 5) - dataSubmitter.store("test_db2", schema3, [1, 2], 5) - assert dataSubmitter.columns_since_last_submit() == 10 - dataSubmitter.store("test_db1", schema2, [1, 2], 10) - - dataSubmitter.submit() - - assert dataSubmitter.columns_since_last_submit() == 0 - - expected_data = { - "host": "some", - "agent_version": 0, - "dbms": "sqlserver", - "kind": "sqlserver_databases", - "collection_interval": 1200, - "dbms_version": "some", - "tags": "some", - "cloud_metadata": "some", - "metadata": [ - {"id": 3, "name": "test_db1", "schemas": [{"id": "1", "tables": [1, 2]}, {"id": "2", "tables": [1, 2]}]}, - {"id": 4, "name": "test_db2", "schemas": [{"id": "3", "tables": [1, 2]}]}, - ], - } - data = json.loads(submitted_data[0]) - data.pop("timestamp") - assert deep_compare(data, expected_data) - - -def test_fetch_throws(instance_docker): - check = SQLServer(CHECK_NAME, {}, [instance_docker]) - schemas = Schemas(check, check._config) - with mock.patch('time.time', side_effect=[0, 9999999]), mock.patch( - 'datadog_checks.sqlserver.schemas.Schemas._query_schema_information', return_value={"id": 1} - ), mock.patch('datadog_checks.sqlserver.schemas.Schemas._get_tables', return_value=[1, 2]): - with pytest.raises(StopIteration): - schemas._fetch_schema_data("dummy_cursor", time.time(), "my_db") - - -def test_submit_is_called_if_too_many_columns(instance_docker): - check = SQLServer(CHECK_NAME, {}, [instance_docker]) - schemas = Schemas(check, check._config) - with mock.patch('time.time', side_effect=[0, 0]), mock.patch( - 'datadog_checks.sqlserver.schemas.Schemas._query_schema_information', return_value={"id": 1} - ), mock.patch('datadog_checks.sqlserver.schemas.Schemas._get_tables', return_value=[1, 2]), mock.patch( - 'datadog_checks.sqlserver.schemas.SubmitData.submit' - ) as mocked_submit, mock.patch( - 'datadog_checks.sqlserver.schemas.Schemas._get_tables_data', return_value=(1000_000, {"id": 1}) - ): - with pytest.raises(StopIteration): - schemas._fetch_schema_data("dummy_cursor", time.time(), "my_db") - mocked_submit.called_once() - - -def test_exception_handling_by_do_for_dbs(instance_docker): - check = SQLServer(CHECK_NAME, {}, [instance_docker]) - check.initialize_connection() - schemas = Schemas(check, check._config) - mock_cursor = mock.MagicMock() - with mock.patch( - 'datadog_checks.sqlserver.schemas.Schemas._fetch_schema_data', side_effect=Exception("Can't connect to DB") - ), mock.patch('datadog_checks.sqlserver.sqlserver.SQLServer.get_databases', return_value=["db1"]), mock.patch( - 'cachetools.TTLCache.get', return_value="dummy" - ), mock.patch( - 'datadog_checks.sqlserver.connection.Connection.open_managed_default_connection' - ), mock.patch( - 'datadog_checks.sqlserver.connection.Connection.get_managed_cursor', return_value=mock_cursor - ), mock.patch( - 'datadog_checks.sqlserver.utils.is_azure_sql_database', return_value={} - ): - schemas._fetch_for_databases() - - -def test_get_unixodbc_sysconfig(): - etc_dir = os.path.sep - for dir in ["opt", "datadog-agent", "embedded", "bin", "python"]: - etc_dir = os.path.join(etc_dir, dir) - assert get_unixodbc_sysconfig(etc_dir).split(os.path.sep) == [ - "", - "opt", - "datadog-agent", - "embedded", - "etc", - ], "incorrect unix odbc config dir" From 9365ae3af6ac9126f6f254e8eb06af7e5b08b4b5 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 15:48:26 +0000 Subject: [PATCH 19/92] Applied formatting --- .../datadog_checks/sqlserver/activity.py | 35 +- .../datadog_checks/sqlserver/deadlocks.py | 20 +- sqlserver/datadog_checks/sqlserver/queries.py | 21 +- .../datadog_checks/sqlserver/sqlserver.py | 10 +- sqlserver/tests/test_activity.py | 47 +- sqlserver/tests/test_unit.py | 922 ++++++++++++++++++ sqlserver/tests/utils.py | 5 +- 7 files changed, 1001 insertions(+), 59 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 635d048f9c9d3..5d7662a4e8f7d 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -14,18 +14,18 @@ from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION -from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name from datadog_checks.sqlserver.deadlocks import Deadlocks +from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name try: import datadog_agent except ImportError: from ..stubs import datadog_agent -import time + DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 MAX_PAYLOAD_BYTES = 19e6 -import pdb + CONNECTIONS_QUERY = """\ SELECT login_name AS user_name, @@ -148,6 +148,8 @@ def _hash_to_hex(hash) -> str: def agent_check_getter(self): return self._check + + """ self._databases_data_enabled = is_affirmative(config.schemas_config.get("enabled", False)) self._databases_data_collection_interval = config.schemas_config.get( "collection_interval", DEFAULT_DATABASES_DATA_COLLECTION_INTERVAL @@ -167,6 +169,7 @@ def agent_check_getter(self): self.enabled = self._databases_data_enabled or self._settings_enabled""" + class SqlserverActivity(DBMAsyncJob): """Collects query metrics and plans""" @@ -179,7 +182,7 @@ def __init__(self, check, config: SQLServerConfig): self._last_deadlocks_collection_time = 0 self._last_activity_collection_time = 0 - #TODO put back false + # TODO put back false self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", True)) self._deadlocks_collection_interval = config.deadlocks_config.get( "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL @@ -193,7 +196,7 @@ def __init__(self, check, config: SQLServerConfig): ) if self._activity_collection_enabled <= 0: self._activity_collection_enabled = DEFAULT_ACTIVITY_COLLECTION_INTERVAL - + if self._deadlocks_collection_enabled and not self._activity_collection_enabled: self.collection_interval = self._deadlocks_collection_interval elif not self._deadlocks_collection_enabled and self._activity_collection_enabled: @@ -256,15 +259,19 @@ def _collect_deadlocks(self): deadlock_xmls_collected = self._deadlocks.collect_deadlocks() deadlock_xmls = [] total_number_of_characters = 0 - pdb.set_trace() for i, deadlock in enumerate(deadlock_xmls_collected): total_number_of_characters += len(deadlock) if total_number_of_characters > self._deadlock__payload_max_bytes: - self._log.warning("We've dropped {} deadlocks from a total of {} deadlocks as the max deadlock payload of {} bytes was exceeded.".format(len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes)) + self._log.warning( + """We've dropped {} deadlocks from a total of {} deadlocks as the + max deadlock payload of {} bytes was exceeded.""".format( + len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes + ) + ) break else: deadlock_xmls.append(deadlock) - #TODO REMOVE log error + # TODO REMOVE log error if len(deadlock_xmls) == 0: self._log.error("Collected 0 DEADLOCKS") return @@ -273,7 +280,7 @@ def _collect_deadlocks(self): payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) self._log.error("DEADLOCK COlLECTED {} in {} time".format(len(deadlock_xmls), time.time() - start_time)) - + @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): self.log.debug("collecting sql server current connections") @@ -441,10 +448,10 @@ def _create_activity_event(self, active_sessions, active_connections): "ddagentversion": datadog_agent.get_version(), "ddsource": "sqlserver", "dbm_type": "activity", - "collection_interval": self._activity_collection_interval, #TODO is it important for whatever reason to have very precise int ? + "collection_interval": self._activity_collection_interval, "ddtags": self.tags, "timestamp": time.time() * 1000, - 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_activity": active_sessions, @@ -453,8 +460,8 @@ def _create_activity_event(self, active_sessions, active_connections): return event def _create_deadlock_event(self, deadlock_xmls): - #TODO WHAT if deadlock xml is just too long ? - #MAX_PAYLOAD_BYTES ? + # TODO WHAT if deadlock xml is just too long ? + # MAX_PAYLOAD_BYTES ? event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -463,7 +470,7 @@ def _create_deadlock_event(self, deadlock_xmls): "collection_interval": self._deadlocks_collection_interval, "ddtags": self.tags, "timestamp": time.time() * 1000, - 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 1ac9ccf857310..c1153b7a8fce6 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -2,20 +2,20 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +# TODO temp imports: +import pdb import xml.etree.ElementTree as ET from datetime import datetime from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata - from datadog_checks.sqlserver.queries import ( CREATE_DEADLOCK_TEMP_TABLE_QUERY, DETECT_DEADLOCK_QUERY, ) -#TODO temp imports: -import pdb MAX_DEADLOCKS = 100 + class Deadlocks: def __init__(self, check, conn_prefix, config): @@ -28,7 +28,9 @@ def __init__(self, check, conn_prefix, config): def obfuscate_no_except_wrapper(self, sql_text): try: - sql_text = obfuscate_sql_with_metadata(sql_text, self._config.obfuscator_options, replace_null_character=True)['query'] + sql_text = obfuscate_sql_with_metadata( + sql_text, self._config.obfuscator_options, replace_null_character=True + )['query'] except Exception as e: if self._config.log_unobfuscated_queries: self.log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) @@ -42,7 +44,7 @@ def obfuscate_xml(self, root): process_list = root.find(".//process-list") for process in process_list.findall('process'): inputbuf = process.find('inputbuf') - #TODO inputbuf.text can be truncated, check when live ? + # TODO inputbuf.text can be truncated, check when live ? inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) for frame in process.findall('.//frame'): frame.text = self.obfuscate_no_except_wrapper(frame.text) @@ -60,11 +62,13 @@ def collect_deadlocks(self): try: root = ET.fromstring(result[1]) except Exception as e: - # Other thing do we want to suggest to set ring buffer to 1MB ? + # Other thing do we want to suggest to set ring buffer to 1MB ? # TODO notify backend ? How ? make a collection_errors array like in metadata json self._log.error( - """An error occurred while collecting SQLServer deadlocks. - One of the deadlock XMLs couldn't be parsed. The error: {}""".format(e) + """An error occurred while collecting SQLServer deadlocks. + One of the deadlock XMLs couldn't be parsed. The error: {}""".format( + e + ) ) datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 020fad1085189..76815e360cdc4 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -215,32 +215,33 @@ """ CREATE_DEADLOCK_TEMP_TABLE_QUERY = """ -SELECT +SELECT CAST([target_data] AS XML) AS Target_Data -INTO +INTO #TempXMLDatadogData -FROM +FROM sys.dm_xe_session_targets AS xt -INNER JOIN +INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address -WHERE +WHERE xs.name = N'system_health' -AND +AND xt.target_name = N'ring_buffer'; """ DETECT_DEADLOCK_QUERY = """ -SELECT TOP (?) +SELECT TOP (?) xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] -FROM +FROM #TempXMLDatadogData -CROSS APPLY +CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE +WHERE xdr.value('@timestamp', 'datetime') > ? ORDER BY [Date] DESC; """ + def get_query_ao_availability_groups(sqlserver_major_version): """ Construct the sys.availability_groups QueryExecutor configuration based on the SQL Server major version diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 81bcfee10cc12..e0927bcfd0405 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -12,7 +12,11 @@ from datadog_checks.base import AgentCheck from datadog_checks.base.config import is_affirmative from datadog_checks.base.utils.db import QueryExecutor, QueryManager -from datadog_checks.base.utils.db.utils import default_json_event_encoding, resolve_db_host, tracked_query +from datadog_checks.base.utils.db.utils import ( + default_json_event_encoding, + resolve_db_host, + tracked_query, +) from datadog_checks.base.utils.serialization import json from datadog_checks.sqlserver.activity import SqlserverActivity from datadog_checks.sqlserver.agent_history import SqlserverAgentHistory @@ -28,8 +32,7 @@ from datadog_checks.sqlserver.statements import SqlserverStatementMetrics from datadog_checks.sqlserver.stored_procedures import SqlserverProcedureMetrics from datadog_checks.sqlserver.utils import Database, construct_use_statement, parse_sqlserver_major_version -from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata -import pdb + try: import datadog_agent except ImportError: @@ -81,7 +84,6 @@ QUERY_LOG_SHIPPING_PRIMARY, QUERY_LOG_SHIPPING_SECONDARY, QUERY_SERVER_STATIC_INFO, - DETECT_DEADLOCK_QUERY, get_query_ao_availability_groups, get_query_file_stats, ) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 44b74d169be15..97010646c3216 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -8,12 +8,13 @@ import datetime import json import os +import pdb import re import threading import time +import xml.etree.ElementTree as ET from concurrent.futures.thread import ThreadPoolExecutor from copy import copy -import xml.etree.ElementTree as ET import mock import pytest @@ -27,12 +28,12 @@ from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION from .conftest import DEFAULT_TIMEOUT from .utils import create_deadlock -import pdb + try: import pyodbc except ImportError: pyodbc = None -import time + ACTIVITY_JSON_PLANS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "activity") @@ -747,7 +748,7 @@ def _load_test_activity_json(filename): return json.load(f) -def _get_conn_for_user(instance_docker, user, timeout = 1, _autocommit=False): +def _get_conn_for_user(instance_docker, user, timeout=1, _autocommit=False): # Make DB connection conn_str = 'DRIVER={};Server={};Database=master;UID={};PWD={};TrustServerCertificate=yes;'.format( instance_docker['driver'], instance_docker['host'], user, "Password12!" @@ -909,24 +910,26 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) -#plan - first we need to catch deadlock exception if not try again ? -# there are often 2 deadlocks lets check for that +# plan - first we need to catch deadlock exception if not try again ? + +# there are often 2 deadlocks lets check for that # test1 - just that we collect deadlocks and its in stub events # test2 - time test that we take deadlocks in delta # some crazy scenario if possible like 3 query involved ? -# deadlock too long ? -# we cannot test that real obfuscator is called at least check that its called by unit test +# deadlock too long ? +# we cannot test that real obfuscator is called at least check that its called by unit test + +# TEST That we at least try to apply obfuscation to all required fields ! -#TEST That we at least try to apply obfuscation to all required fields ! @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['deadlocks'] = { 'enabled': True, - 'run_sync': True, #TODO oups run_sync what should be the logic for 2 jobs ? + 'run_sync': True, # TODO oups run_sync what should be the logic for 2 jobs ? 'collection_interval': 0.1, } dbm_instance['query_activity']['enabled'] = False @@ -934,8 +937,8 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) created_deadlock = False - #Rarely instead of a deadlock one of the transactions time outs - for i in range(0,3): + # Rarely instead of a deadlock one of the transactions time outs + for i in range(0, 3): bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) created_deadlock = create_deadlock(bob_conn, fred_conn) @@ -947,12 +950,12 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): def execut_test(): dd_run_check(sqlserver_check) - + dbm_activity = aggregator.get_event_platform_events("dbm-activity") if not dbm_activity: return False, "should have collected at least one activity event" matched_event = [] - + for event in dbm_activity: if "sqlserver_deadlocks" in event: matched_event.append(event) @@ -961,29 +964,29 @@ def execut_test(): return False, "should have collected one deadlock payload" deadlocks = matched_event[0]["sqlserver_deadlocks"] if len(deadlocks) < 1: - return False, "should have collected one or more deadlock in the payload" + return False, "should have collected one or more deadlock in the payload" found = False for d in deadlocks: root = ET.fromstring(d) pdb.set_trace() process_list = root.find(".//process-list") for process in process_list.findall('process'): - if process.find('inputbuf').text == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;": - found = True + if ( + process.find('inputbuf').text + == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" + ): + found = True err = "" if not found: err = "Should've collected produced deadlock" return found, err - + # Sometimes deadlock takes a bit longer to arrive to the ring buffer. # We can may be give it 3 tries err = "" - for i in range(0,3): + for i in range(0, 3): time.sleep(3) res, err = execut_test() if res: return assert False, err - - - \ No newline at end of file diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index e69de29bb2d1d..e9a23a8b4bc4b 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -0,0 +1,922 @@ +# (C) Datadog, Inc. 2018-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) +import copy +import json +import os +import re +import time +from collections import namedtuple + +import mock +import pytest +from deepdiff import DeepDiff + + +from datadog_checks.dev import EnvVars +from datadog_checks.sqlserver import SQLServer +from datadog_checks.sqlserver.connection import split_sqlserver_host_port +from datadog_checks.sqlserver.deadlocks import Deadlocks +from datadog_checks.sqlserver.metrics import SqlFractionMetric, SqlMasterDatabaseFileStats +from datadog_checks.sqlserver.schemas import Schemas, SubmitData +from datadog_checks.sqlserver.sqlserver import SQLConnectionError +from datadog_checks.sqlserver.utils import ( + Database, + extract_sql_comments_and_procedure_name, + parse_sqlserver_major_version, + set_default_driver_conf, +) + +from .common import CHECK_NAME, DOCKER_SERVER, assert_metrics +from .utils import windows_ci + + +try: + import pyodbc +except ImportError: + pyodbc = None + +# mark the whole module +pytestmark = pytest.mark.unit + + +def test_get_cursor(instance_docker): + """ + Ensure we don't leak connection info in case of a KeyError when the + connection pool is empty or the params for `get_cursor` are invalid. + """ + check = SQLServer(CHECK_NAME, {}, [instance_docker]) + check.initialize_connection() + with pytest.raises(SQLConnectionError): + check.connection.get_cursor('foo') + + +def test_missing_db(instance_docker, dd_run_check): + instance = copy.copy(instance_docker) + instance['ignore_missing_database'] = False + + with mock.patch( + 'datadog_checks.sqlserver.connection.Connection.open_managed_default_connection', + side_effect=SQLConnectionError(Exception("couldnt connect")), + ): + with pytest.raises(SQLConnectionError): + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + + instance['ignore_missing_database'] = True + with mock.patch('datadog_checks.sqlserver.connection.Connection.check_database', return_value=(False, 'db')): + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + dd_run_check(check) + assert check.do_check is False + + +@mock.patch('datadog_checks.sqlserver.connection.Connection.open_managed_default_database') +@mock.patch('datadog_checks.sqlserver.connection.Connection.get_cursor') +def test_db_exists(get_cursor, mock_connect, instance_docker_defaults, dd_run_check): + Row = namedtuple('Row', 'name,collation_name') + db_results = [ + Row('master', 'SQL_Latin1_General_CP1_CI_AS'), + Row('tempdb', 'SQL_Latin1_General_CP1_CI_AS'), + Row('AdventureWorks2017', 'SQL_Latin1_General_CP1_CI_AS'), + Row('CaseSensitive2018', 'SQL_Latin1_General_CP1_CS_AS'), + Row('OfflineDB', None), + ] + + mock_connect.__enter__ = mock.Mock(return_value='foo') + + mock_results = mock.MagicMock() + mock_results.fetchall.return_value = db_results + get_cursor.return_value = mock_results + + instance = copy.copy(instance_docker_defaults) + # make sure check doesn't try to add metrics + instance['stored_procedure'] = 'fake_proc' + instance['ignore_missing_database'] = True + + # check base case of lowercase for lowercase and case-insensitive db + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is True + # check all caps for case insensitive db + instance['database'] = 'MASTER' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is True + + # check mixed case against mixed case but case-insensitive db + instance['database'] = 'AdventureWORKS2017' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is True + + # check case sensitive but matched db + instance['database'] = 'CaseSensitive2018' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is True + + # check case sensitive but mismatched db + instance['database'] = 'cASEsENSITIVE2018' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is False + + # check offline but exists db + instance['database'] = 'Offlinedb' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + assert check.do_check is True + + +@mock.patch('datadog_checks.sqlserver.connection.Connection.open_managed_default_database') +@mock.patch('datadog_checks.sqlserver.connection.Connection.get_cursor') +def test_azure_cross_database_queries_excluded(get_cursor, mock_connect, instance_docker_defaults, dd_run_check): + Row = namedtuple('Row', 'name,collation_name') + db_results = [ + Row('master', 'SQL_Latin1_General_CP1_CI_AS'), + Row('tempdb', 'SQL_Latin1_General_CP1_CI_AS'), + Row('AdventureWorks2017', 'SQL_Latin1_General_CP1_CI_AS'), + Row('CaseSensitive2018', 'SQL_Latin1_General_CP1_CS_AS'), + Row('OfflineDB', None), + ] + + mock_connect.__enter__ = mock.Mock(return_value='foo') + + mock_results = mock.MagicMock() + mock_results.fetchall.return_value = db_results + get_cursor.return_value = mock_results + + instance = copy.copy(instance_docker_defaults) + instance['stored_procedure'] = 'fake_proc' + check = SQLServer(CHECK_NAME, {}, [instance]) + check.initialize_connection() + check.make_metric_list_to_collect() + cross_database_metrics = [ + metric + for metric in check.instance_metrics + if metric.__class__.TABLE not in ['msdb.dbo.backupset', 'sys.dm_db_file_space_usage'] + ] + assert len(cross_database_metrics) == 0 + + +def test_autodiscovery_matches_all_by_default(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list() + all_dbs = {Database(r.name) for r in fetchall_results} + # check base case of default filters + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == all_dbs + + +def test_azure_autodiscovery_matches_all_by_default(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list_azure() + all_dbs = {Database(r.name, r.physical_database_name) for r in fetchall_results} + + # check base case of default filters + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == all_dbs + + +def test_autodiscovery_matches_none(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list() + # check missing additions, but no exclusions + mock_cursor.fetchall.return_value = iter(fetchall_results) # reset the mock results + instance_autodiscovery['autodiscovery_include'] = ['missingdb', 'fakedb'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == set() + + +def test_azure_autodiscovery_matches_none(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list_azure() + # check missing additions, but no exclusions + mock_cursor.fetchall.return_value = iter(fetchall_results) # reset the mock results + instance_autodiscovery['autodiscovery_include'] = ['missingdb', 'fakedb'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == set() + + +def test_autodiscovery_matches_some(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list() + instance_autodiscovery['autodiscovery_include'] = ['master', 'fancy2020db', 'missingdb', 'fakedb'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + dbs = [Database(name) for name in ['master', 'Fancy2020db']] + assert check.databases == set(dbs) + + +def test_azure_autodiscovery_matches_some(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list_azure() + instance_autodiscovery['autodiscovery_include'] = ['master', 'fancy2020db', 'missingdb', 'fakedb'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + dbs = [Database(name, pys_db) for name, pys_db in {'master': 'master', 'Fancy2020db': '40e688a7e268'}.items()] + assert check.databases == set(dbs) + + +def test_autodiscovery_exclude_some(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list() + instance_autodiscovery['autodiscovery_include'] = ['.*'] # replace default `.*` + instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + dbs = [Database(name) for name in ['tempdb', 'AdventureWorks2017', 'CaseSensitive2018']] + assert check.databases == set(dbs) + + +def test_azure_autodiscovery_exclude_some(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list_azure() + instance_autodiscovery['autodiscovery_include'] = ['.*'] # replace default `.*` + instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + db_dict = {'tempdb': 'tempdb', 'AdventureWorks2017': 'fce04774', 'CaseSensitive2018': 'jub3j8kh'} + dbs = [Database(name, pys_db) for name, pys_db in db_dict.items()] + assert check.databases == set(dbs) + + +def test_autodiscovery_exclude_override(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list() + instance_autodiscovery['autodiscovery_include'] = ['t.*', 'master'] # remove default `.*` + instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == {Database("tempdb")} + + +def test_azure_autodiscovery_exclude_override(instance_autodiscovery): + fetchall_results, mock_cursor = _mock_database_list_azure() + instance_autodiscovery['autodiscovery_include'] = ['t.*', 'master'] # remove default `.*` + instance_autodiscovery['autodiscovery_exclude'] = ['.*2020db$', 'm.*'] + check = SQLServer(CHECK_NAME, {}, [instance_autodiscovery]) + check.autodiscover_databases(mock_cursor) + assert check.databases == {Database("tempdb", "tempdb")} + + +@pytest.mark.parametrize( + 'col_val_row_1, col_val_row_2, col_val_row_3', + [ + pytest.param(256, 1024, 1720, id='Valid column value 0'), + pytest.param(0, None, 1024, id='NoneType column value 1, should not raise error'), + pytest.param(512, 0, 256, id='Valid column value 2'), + pytest.param(None, 256, 0, id='NoneType column value 3, should not raise error'), + ], +) +def test_SqlMasterDatabaseFileStats_fetch_metric(col_val_row_1, col_val_row_2, col_val_row_3): + Row = namedtuple('Row', ['name', 'file_id', 'type', 'physical_name', 'size', 'max_size', 'state', 'state_desc']) + mock_rows = [ + Row('master', 1, 0, '/var/opt/mssql/data/master.mdf', col_val_row_1, -1, 0, 'ONLINE'), + Row('tempdb', 1, 0, '/var/opt/mssql/data/tempdb.mdf', col_val_row_2, -1, 0, 'ONLINE'), + Row('msdb', 1, 0, '/var/opt/mssql/data/MSDBData.mdf', col_val_row_3, -1, 0, 'ONLINE'), + ] + mock_cols = ['name', 'file_id', 'type', 'physical_name', 'size', 'max_size', 'state', 'state_desc'] + mock_metric_obj = SqlMasterDatabaseFileStats( + cfg_instance=mock.MagicMock(dict), + base_name=None, + report_function=mock.MagicMock(), + column='size', + logger=None, + ) + with mock.patch.object( + SqlMasterDatabaseFileStats, 'fetch_metric', wraps=mock_metric_obj.fetch_metric + ) as mock_fetch_metric: + errors = 0 + try: + mock_fetch_metric(mock_rows, mock_cols) + except Exception as e: + errors += 1 + raise AssertionError('{}'.format(e)) + assert errors < 1 + + +@pytest.mark.parametrize( + 'base_name', + [ + pytest.param('Buffer cache hit ratio base', id='base_name valid'), + pytest.param(None, id='base_name None'), + ], +) +def test_SqlFractionMetric_base(caplog, base_name): + Row = namedtuple('Row', ['counter_name', 'cntr_type', 'cntr_value', 'instance_name', 'object_name']) + fetchall_results = [ + Row('Buffer cache hit ratio', 537003264, 33453, '', 'SQLServer:Buffer Manager'), + Row('Buffer cache hit ratio base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), + Row('some random counter', 1073939712, 1111, '', 'SQLServer:Buffer Manager'), + Row('some random counter base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), + ] + mock_cursor = mock.MagicMock() + mock_cursor.fetchall.return_value = fetchall_results + + report_function = mock.MagicMock() + metric_obj = SqlFractionMetric( + cfg_instance={ + 'name': 'sqlserver.buffer.cache_hit_ratio', + 'counter_name': 'Buffer cache hit ratio', + 'instance_name': '', + 'physical_db_name': None, + 'tags': ['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], + 'hostname': 'stubbed.hostname', + }, + base_name=base_name, + report_function=report_function, + column=None, + logger=mock.MagicMock(), + ) + results_rows, results_cols = SqlFractionMetric.fetch_all_values( + mock_cursor, ['Buffer cache hit ratio', base_name], mock.mock.MagicMock() + ) + metric_obj.fetch_metric(results_rows, results_cols) + if base_name: + report_function.assert_called_with( + 'sqlserver.buffer.cache_hit_ratio', + 0.9976737943992127, + raw=True, + hostname='stubbed.hostname', + tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], + ) + else: + report_function.assert_not_called() + + +def test_SqlFractionMetric_group_by_instance(caplog): + Row = namedtuple('Row', ['counter_name', 'cntr_type', 'cntr_value', 'instance_name', 'object_name']) + fetchall_results = [ + Row('Buffer cache hit ratio', 537003264, 33453, '', 'SQLServer:Buffer Manager'), + Row('Buffer cache hit ratio base', 1073939712, 33531, '', 'SQLServer:Buffer Manager'), + Row('Foo counter', 537003264, 1, 'bar', 'SQLServer:Buffer Manager'), + Row('Foo counter base', 1073939712, 50, 'bar', 'SQLServer:Buffer Manager'), + Row('Foo counter', 537003264, 5, 'zoo', 'SQLServer:Buffer Manager'), + Row('Foo counter base', 1073939712, 100, 'zoo', 'SQLServer:Buffer Manager'), + ] + mock_cursor = mock.MagicMock() + mock_cursor.fetchall.return_value = fetchall_results + + report_function = mock.MagicMock() + metric_obj = SqlFractionMetric( + cfg_instance={ + 'name': 'sqlserver.test.metric', + 'counter_name': 'Foo counter', + 'instance_name': 'ALL', + 'physical_db_name': None, + 'tags': ['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname'], + 'hostname': 'stubbed.hostname', + 'tag_by': 'db', + }, + base_name='Foo counter base', + report_function=report_function, + column=None, + logger=mock.MagicMock(), + ) + results_rows, results_cols = SqlFractionMetric.fetch_all_values( + mock_cursor, ['Foo counter base', 'Foo counter'], mock.mock.MagicMock() + ) + metric_obj.fetch_metric(results_rows, results_cols) + report_function.assert_any_call( + 'sqlserver.test.metric', + 0.02, + raw=True, + hostname='stubbed.hostname', + tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname', 'db:bar'], + ) + report_function.assert_any_call( + 'sqlserver.test.metric', + 0.05, + raw=True, + hostname='stubbed.hostname', + tags=['optional:tag1', 'dd.internal.resource:database_instance:stubbed.hostname', 'db:zoo'], + ) + + +def _mock_database_list(): + Row = namedtuple('Row', 'name') + fetchall_results = [ + Row('master'), + Row('tempdb'), + Row('msdb'), + Row('AdventureWorks2017'), + Row('CaseSensitive2018'), + Row('Fancy2020db'), + ] + mock_cursor = mock.MagicMock() + mock_cursor.fetchall.return_value = iter(fetchall_results) + # check excluded overrides included + mock_cursor.fetchall.return_value = iter(fetchall_results) + return fetchall_results, mock_cursor + + +def _mock_database_list_azure(): + Row = namedtuple('Row', ['name', 'physical_database_name']) + fetchall_results = [ + Row('master', 'master'), + Row('tempdb', 'tempdb'), + Row('msdb', 'msdb'), + Row('AdventureWorks2017', 'fce04774'), + Row('CaseSensitive2018', 'jub3j8kh'), + Row('Fancy2020db', '40e688a7e268'), + ] + mock_cursor = mock.MagicMock() + mock_cursor.fetchall.return_value = iter(fetchall_results) + # check excluded overrides included + mock_cursor.fetchall.return_value = iter(fetchall_results) + return fetchall_results, mock_cursor + + +def test_set_default_driver_conf(): + # Docker Agent with ODBCSYSINI env var + # The only case where we set ODBCSYSINI to the the default odbcinst.ini folder + with EnvVars({'DOCKER_DD_AGENT': 'true'}, ignore=['ODBCSYSINI']): + set_default_driver_conf() + assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) + + # `set_default_driver_conf` have no effect on the cases below + with EnvVars({'ODBCSYSINI': 'ABC', 'DOCKER_DD_AGENT': 'true'}): + set_default_driver_conf() + assert os.environ['ODBCSYSINI'] == 'ABC' + + with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): + with EnvVars({}): + set_default_driver_conf() + assert 'ODBCSYSINI' in os.environ + assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) + + with EnvVars({}, ignore=['ODBCSYSINI']): + with mock.patch("os.path.exists", return_value=True): + # odbcinst.ini or odbc.ini exists in agent embedded directory + set_default_driver_conf() + assert 'ODBCSYSINI' not in os.environ + + with EnvVars({}, ignore=['ODBCSYSINI']): + set_default_driver_conf() + assert 'ODBCSYSINI' in os.environ # ODBCSYSINI is set by the integration + if pyodbc is not None: + assert pyodbc.drivers() is not None + + with EnvVars({'ODBCSYSINI': 'ABC'}): + set_default_driver_conf() + assert os.environ['ODBCSYSINI'] == 'ABC' + + +@windows_ci +def test_check_local(aggregator, dd_run_check, init_config, instance_docker): + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) + dd_run_check(sqlserver_check) + check_tags = instance_docker.get('tags', []) + expected_tags = check_tags + [ + 'sqlserver_host:{}'.format(sqlserver_check.resolved_hostname), + 'connection_host:{}'.format(DOCKER_SERVER), + 'db:master', + ] + assert_metrics(instance_docker, aggregator, check_tags, expected_tags, hostname=sqlserver_check.resolved_hostname) + + +SQL_SERVER_2012_VERSION_EXAMPLE = """\ +Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64) + Oct 20 2015 15:36:27 + Copyright (c) Microsoft Corporation + Express Edition (64-bit) on Windows NT 6.3 (Build 17763: ) (Hypervisor) +""" + +SQL_SERVER_2019_VERSION_EXAMPLE = """\ +Microsoft SQL Server 2019 (RTM-CU12) (KB5004524) - 15.0.4153.1 (X64) + Jul 19 2021 15:37:34 + Copyright (C) 2019 Microsoft Corporation + Standard Edition (64-bit) on Windows Server 2016 Datacenter 10.0 (Build 14393: ) (Hypervisor) +""" + + +@pytest.mark.parametrize( + "version,expected_major_version", [(SQL_SERVER_2012_VERSION_EXAMPLE, 2012), (SQL_SERVER_2019_VERSION_EXAMPLE, 2019)] +) +def test_parse_sqlserver_major_version(version, expected_major_version): + assert parse_sqlserver_major_version(version) == expected_major_version + + +@pytest.mark.parametrize( + "instance_host,split_host,split_port", + [ + ("localhost,1433,some-typo", "localhost", "1433"), + ("localhost, 1433,some-typo", "localhost", "1433"), + ("localhost,1433", "localhost", "1433"), + ("localhost", "localhost", None), + ], +) +def test_split_sqlserver_host(instance_host, split_host, split_port): + s_host, s_port = split_sqlserver_host_port(instance_host) + assert (s_host, s_port) == (split_host, split_port) + + +def test_database_state(aggregator, dd_run_check, init_config, instance_docker): + instance_docker['database'] = 'mAsTeR' + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) + dd_run_check(sqlserver_check) + expected_tags = instance_docker.get('tags', []) + [ + 'database_recovery_model_desc:SIMPLE', + 'database_state_desc:ONLINE', + 'database:{}'.format(instance_docker['database']), + 'db:{}'.format(instance_docker['database']), + ] + aggregator.assert_metric('sqlserver.database.state', tags=expected_tags, hostname=sqlserver_check.resolved_hostname) + + +@pytest.mark.parametrize( + "query,expected_comments,is_proc,expected_name", + [ + [ + None, + [], + False, + None, + ], + [ + "", + [], + False, + None, + ], + [ + "/*", + [], + False, + None, + ], + [ + "--", + [], + False, + None, + ], + [ + "/*justonecomment*/", + ["/*justonecomment*/"], + False, + None, + ], + [ + """\ + /* a comment */ + -- Single comment + """, + ["/* a comment */", "-- Single comment"], + False, + None, + ], + [ + "/*tag=foo*/ SELECT * FROM foo;", + ["/*tag=foo*/"], + False, + None, + ], + [ + "/*tag=foo*/ SELECT * FROM /*other=tag,incomment=yes*/ foo;", + ["/*tag=foo*/", "/*other=tag,incomment=yes*/"], + False, + None, + ], + [ + "/*tag=foo*/ SELECT * FROM /*other=tag,incomment=yes*/ foo /*lastword=yes*/", + ["/*tag=foo*/", "/*other=tag,incomment=yes*/", "/*lastword=yes*/"], + False, + None, + ], + [ + """\ + -- My Comment + CREATE PROCEDURE bobProcedure + BEGIN + SELECT name FROM bob + END; + """, + ["-- My Comment"], + True, + "bobProcedure", + ], + [ + """\ + -- My procedure + CREATE PROCEDURE bobProcedure + BEGIN + SELECT name FROM bob + END; + """, + ["-- My procedure"], + True, + "bobProcedure", + ], + [ + """\ + -- My Comment + CREATE PROCEDURE bobProcedure + -- In the middle + BEGIN + SELECT name FROM bob + END; + """, + ["-- My Comment", "-- In the middle"], + True, + "bobProcedure", + ], + [ + """\ + -- My Comment + CREATE PROCEDURE bobProcedure + -- this procedure does foo + BEGIN + SELECT name FROM bob + END; + """, + ["-- My Comment", "-- this procedure does foo"], + True, + "bobProcedure", + ], + [ + """\ + -- My Comment + CREATE PROCEDURE bobProcedure + -- In the middle + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + ["-- My Comment", "-- In the middle", "-- And at the end"], + True, + "bobProcedure", + ], + [ + """\ + -- My Comment + CREATE PROCEDURE bobProcedure + -- In the middle + /*mixed with mult-line foo*/ + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + ["-- My Comment", "-- In the middle", "/*mixed with mult-line foo*/", "-- And at the end"], + True, + "bobProcedure", + ], + [ + """\ + -- My procedure + CREATE PROCEDURE bobProcedure + -- In the middle + /*mixed with procedure foo*/ + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + ["-- My procedure", "-- In the middle", "/*mixed with procedure foo*/", "-- And at the end"], + True, + "bobProcedure", + ], + [ + """\ + /* hello + this is a mult-line-comment + tag=foo,blah=tag + */ + /* + second multi-line + comment + */ + CREATE PROCEDURE bobProcedure + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + [ + "/* hello this is a mult-line-comment tag=foo,blah=tag */", + "/* second multi-line comment */", + "-- And at the end", + ], + True, + "bobProcedure", + ], + [ + """\ + /* hello + this is a mult-line-comment + tag=foo,blah=tag + */ + /* + second multi-line + for procedure foo + */ + CREATE PROCEDURE bobProcedure + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + [ + "/* hello this is a mult-line-comment tag=foo,blah=tag */", + "/* second multi-line for procedure foo */", + "-- And at the end", + ], + True, + "bobProcedure", + ], + [ + """\ + /* hello + this is a mult-line-commet + tag=foo,blah=tag + */ + CREATE PROCEDURE bobProcedure + -- In the middle + /*mixed with mult-line foo*/ + BEGIN + SELECT name FROM bob + END; + -- And at the end + """, + [ + "/* hello this is a mult-line-commet tag=foo,blah=tag */", + "-- In the middle", + "/*mixed with mult-line foo*/", + "-- And at the end", + ], + True, + "bobProcedure", + ], + ], +) +def test_extract_sql_comments_and_procedure_name(query, expected_comments, is_proc, expected_name): + comments, p, name = extract_sql_comments_and_procedure_name(query) + assert comments == expected_comments + assert p == is_proc + assert re.match(name, expected_name, re.IGNORECASE) if expected_name else expected_name == name + + +class DummyLogger: + def debug(*args): + pass + + def error(*args): + pass + + +def set_up_submitter_unit_test(): + submitted_data = [] + base_event = { + "host": "some", + "agent_version": 0, + "dbms": "sqlserver", + "kind": "sqlserver_databases", + "collection_interval": 1200, + "dbms_version": "some", + "tags": "some", + "cloud_metadata": "some", + } + + def submitData(data): + submitted_data.append(data) + + dataSubmitter = SubmitData(submitData, base_event, DummyLogger()) + return dataSubmitter, submitted_data + + +def test_submit_data(): + + dataSubmitter, submitted_data = set_up_submitter_unit_test() + + dataSubmitter.store_db_infos([{"id": 3, "name": "test_db1"}, {"id": 4, "name": "test_db2"}]) + schema1 = {"id": "1"} + schema2 = {"id": "2"} + schema3 = {"id": "3"} + + dataSubmitter.store("test_db1", schema1, [1, 2], 5) + dataSubmitter.store("test_db2", schema3, [1, 2], 5) + assert dataSubmitter.columns_since_last_submit() == 10 + dataSubmitter.store("test_db1", schema2, [1, 2], 10) + + dataSubmitter.submit() + + assert dataSubmitter.columns_since_last_submit() == 0 + + expected_data = { + "host": "some", + "agent_version": 0, + "dbms": "sqlserver", + "kind": "sqlserver_databases", + "collection_interval": 1200, + "dbms_version": "some", + "tags": "some", + "cloud_metadata": "some", + "metadata": [ + {"id": 3, "name": "test_db1", "schemas": [{"id": "1", "tables": [1, 2]}, {"id": "2", "tables": [1, 2]}]}, + {"id": 4, "name": "test_db2", "schemas": [{"id": "3", "tables": [1, 2]}]}, + ], + "timestamp": 1.1, + } + difference = DeepDiff( + json.loads(submitted_data[0]), expected_data, exclude_paths="root['timestamp']", ignore_order=True + ) + assert len(difference) == 0 + + +def test_fetch_throws(instance_docker): + check = SQLServer(CHECK_NAME, {}, [instance_docker]) + schemas = Schemas(check, check._config) + with mock.patch('time.time', side_effect=[0, 9999999]), mock.patch( + 'datadog_checks.sqlserver.schemas.Schemas._query_schema_information', return_value={"id": 1} + ), mock.patch('datadog_checks.sqlserver.schemas.Schemas._get_tables', return_value=[1, 2]): + with pytest.raises(StopIteration): + schemas._fetch_schema_data("dummy_cursor", time.time(), "my_db") + + +def test_submit_is_called_if_too_many_columns(instance_docker): + check = SQLServer(CHECK_NAME, {}, [instance_docker]) + schemas = Schemas(check, check._config) + with mock.patch('time.time', side_effect=[0, 0]), mock.patch( + 'datadog_checks.sqlserver.schemas.Schemas._query_schema_information', return_value={"id": 1} + ), mock.patch('datadog_checks.sqlserver.schemas.Schemas._get_tables', return_value=[1, 2]), mock.patch( + 'datadog_checks.sqlserver.schemas.SubmitData.submit' + ) as mocked_submit, mock.patch( + 'datadog_checks.sqlserver.schemas.Schemas._get_tables_data', return_value=(1000_000, {"id": 1}) + ): + with pytest.raises(StopIteration): + schemas._fetch_schema_data("dummy_cursor", time.time(), "my_db") + mocked_submit.called_once() + + +def test_exception_handling_by_do_for_dbs(instance_docker): + check = SQLServer(CHECK_NAME, {}, [instance_docker]) + check.initialize_connection() + schemas = Schemas(check, check._config) + mock_cursor = mock.MagicMock() + with mock.patch( + 'datadog_checks.sqlserver.schemas.Schemas._fetch_schema_data', side_effect=Exception("Can't connect to DB") + ), mock.patch('datadog_checks.sqlserver.sqlserver.SQLServer.get_databases', return_value=["db1"]), mock.patch( + 'cachetools.TTLCache.get', return_value="dummy" + ), mock.patch( + 'datadog_checks.sqlserver.connection.Connection.open_managed_default_connection' + ), mock.patch( + 'datadog_checks.sqlserver.connection.Connection.get_managed_cursor', return_value=mock_cursor + ), mock.patch( + 'datadog_checks.sqlserver.utils.is_azure_sql_database', return_value={} + ): + schemas._fetch_for_databases() + + +# obfuscate_no_except_wrapper +def test_deadlock_calls_obfuscator(instance_docker): + check = SQLServer(CHECK_NAME, {}, [instance_docker]) + test_xml = """ + + + + + + + + + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2; + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1; + + + + + + + """ + expected_xml_string = ' obfuscated obfuscated obfuscated obfuscated obfuscated obfuscated ' + with mock.patch( + 'datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated" + ): + config = type("Config", (object,), {"deadlocks_config": {"max_deadlocks": 5}}) + deadlocks = Deadlocks(check, "", config) + root = ET.fromstring(test_xml) + deadlocks.obfuscate_xml(root) + result_string = ET.tostring(root, encoding='unicode') + result_string = result_string.replace('\t', '').replace('\n', '') + result_string = re.sub(r'\s{2,}', ' ', result_string) + assert expected_xml_string == result_string + diff --git a/sqlserver/tests/utils.py b/sqlserver/tests/utils.py index 87ae66b52f595..184ce1c267a15 100644 --- a/sqlserver/tests/utils.py +++ b/sqlserver/tests/utils.py @@ -1,12 +1,12 @@ # (C) Datadog, Inc. 2019-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +import concurrent import os import string import threading from copy import copy from random import choice, randint, shuffle -import concurrent from threading import Event import pyodbc @@ -246,6 +246,7 @@ def normalize_indexes_columns(actual_payload): sorted_columns = sorted(columns) index['column_names'] = ','.join(sorted_columns) + def run_first_deadlock_query(conn, event1, event2): exception_text = "" try: @@ -261,6 +262,7 @@ def run_first_deadlock_query(conn, event1, event2): conn.commit() return exception_text + def run_second_deadlock_query(conn, event1, event2): exception_text = "" try: @@ -276,6 +278,7 @@ def run_second_deadlock_query(conn, event1, event2): conn.commit() return exception_text + def create_deadlock(bob_conn, fred_conn): executor = concurrent.futures.thread.ThreadPoolExecutor(2) event1 = Event() From 5bd49d676c51966891235e84bc223567a0203ea5 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 16:31:58 +0000 Subject: [PATCH 20/92] Fixed linter errors --- sqlserver/tests/test_activity.py | 11 ++-- sqlserver/tests/test_unit.py | 92 +++++++++++++++++++++----------- 2 files changed, 67 insertions(+), 36 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 97010646c3216..1f95d60a61df4 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -26,7 +26,6 @@ from datadog_checks.sqlserver.activity import DM_EXEC_REQUESTS_COLS, _hash_to_hex from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION -from .conftest import DEFAULT_TIMEOUT from .utils import create_deadlock try: @@ -938,7 +937,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): created_deadlock = False # Rarely instead of a deadlock one of the transactions time outs - for i in range(0, 3): + for _ in range(0, 3): bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) created_deadlock = create_deadlock(bob_conn, fred_conn) @@ -984,9 +983,11 @@ def execut_test(): # Sometimes deadlock takes a bit longer to arrive to the ring buffer. # We can may be give it 3 tries err = "" - for i in range(0, 3): + success = False + for _ in range(0, 3): time.sleep(3) res, err = execut_test() if res: - return - assert False, err + success = True + break + assert success, err diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index e9a23a8b4bc4b..6014cf8cc0953 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -874,40 +874,70 @@ def test_exception_handling_by_do_for_dbs(instance_docker): schemas._fetch_for_databases() -# obfuscate_no_except_wrapper def test_deadlock_calls_obfuscator(instance_docker): check = SQLServer(CHECK_NAME, {}, [instance_docker]) test_xml = """ - - - - - - - - - - - - \nunknown - \nunknown - - \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2; - - - - \nunknown - \nunknown - - \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1; - - - - - - - """ - expected_xml_string = ' obfuscated obfuscated obfuscated obfuscated obfuscated obfuscated ' + + + + + + + + + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2; + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1; + + + + + + + """ + + expected_xml_string = ( + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "obfuscated " + "obfuscated " + " " + "obfuscated " + " " + " " + " " + "obfuscated " + "obfuscated " + " " + "obfuscated " + " " + " " + " " + " " + " " + "" + ) + with mock.patch( 'datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated" ): From 1a93253235f867d12a3c2b1e566204f6a8336cbc Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 19:31:50 +0000 Subject: [PATCH 21/92] propagated errors --- .../datadog_checks/sqlserver/activity.py | 13 +++----- .../datadog_checks/sqlserver/deadlocks.py | 30 ++++++++++--------- sqlserver/tests/test_activity.py | 8 ----- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 5d7662a4e8f7d..2d89805529888 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -256,7 +256,7 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): start_time = time.time() - deadlock_xmls_collected = self._deadlocks.collect_deadlocks() + deadlock_xmls_collected, errors = self._deadlocks.collect_deadlocks() deadlock_xmls = [] total_number_of_characters = 0 for i, deadlock in enumerate(deadlock_xmls_collected): @@ -271,11 +271,7 @@ def _collect_deadlocks(self): break else: deadlock_xmls.append(deadlock) - # TODO REMOVE log error - if len(deadlock_xmls) == 0: - self._log.error("Collected 0 DEADLOCKS") - return - deadlocks_event = self._create_deadlock_event(deadlock_xmls) + deadlocks_event = self._create_deadlock_event(deadlock_xmls, errors) self._log.error("DEADLOCK EVENTS TO BE SENT: {}".format(deadlocks_event)) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) @@ -459,9 +455,7 @@ def _create_activity_event(self, active_sessions, active_connections): } return event - def _create_deadlock_event(self, deadlock_xmls): - # TODO WHAT if deadlock xml is just too long ? - # MAX_PAYLOAD_BYTES ? + def _create_deadlock_event(self, deadlock_xmls, errors): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -474,6 +468,7 @@ def _create_deadlock_event(self, deadlock_xmls): 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, + "sqlserver_deadlock_errors": errors, } return event diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index c1153b7a8fce6..f65be441e1187 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -2,8 +2,6 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) -# TODO temp imports: -import pdb import xml.etree.ElementTree as ET from datetime import datetime @@ -40,41 +38,45 @@ def obfuscate_no_except_wrapper(self, sql_text): return sql_text def obfuscate_xml(self, root): - # TODO put exception here if not found as this would signal in a format change process_list = root.find(".//process-list") + if process_list is None: + return "process-list element not found. The deadlock XML is in an unexpected format." for process in process_list.findall('process'): - inputbuf = process.find('inputbuf') - # TODO inputbuf.text can be truncated, check when live ? - inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) + for inputbuf in process.findall('.//inputbuf'): + if inputbuf.text is not None: + inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) for frame in process.findall('.//frame'): - frame.text = self.obfuscate_no_except_wrapper(frame.text) + if frame.text is not None: + frame.text = self.obfuscate_no_except_wrapper(frame.text) + return None def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: - pdb.set_trace() cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') converted_xmls = [] + errors = [] for result in results: try: root = ET.fromstring(result[1]) except Exception as e: - # Other thing do we want to suggest to set ring buffer to 1MB ? - # TODO notify backend ? How ? make a collection_errors array like in metadata json self._log.error( """An error occurred while collecting SQLServer deadlocks. One of the deadlock XMLs couldn't be parsed. The error: {}""".format( e ) ) - + errors.append("Truncated deadlock xml - {}".format(result[:50])) datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') if last_deadlock_datetime < datetime_obj: last_deadlock_datetime = datetime_obj - self.obfuscate_xml(root) - converted_xmls.append(ET.tostring(root, encoding='unicode')) + error = self.obfuscate_xml(root) + if not error: + converted_xmls.append(ET.tostring(root, encoding='unicode')) + else: + errors.append(error) self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - return converted_xmls + return converted_xmls, errors diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 1f95d60a61df4..ff23a23e0cb22 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -910,17 +910,9 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_plan_hash'], str) -# plan - first we need to catch deadlock exception if not try again ? - -# there are often 2 deadlocks lets check for that - -# test1 - just that we collect deadlocks and its in stub events # test2 - time test that we take deadlocks in delta # some crazy scenario if possible like 3 query involved ? # deadlock too long ? -# we cannot test that real obfuscator is called at least check that its called by unit test - -# TEST That we at least try to apply obfuscation to all required fields ! @pytest.mark.integration From 44d506c302116f68a506859789342714a51acc6e Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 19:34:53 +0000 Subject: [PATCH 22/92] removed old imports --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index f65be441e1187..44b8181c02417 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -13,7 +13,6 @@ MAX_DEADLOCKS = 100 - class Deadlocks: def __init__(self, check, conn_prefix, config): @@ -79,4 +78,4 @@ def collect_deadlocks(self): else: errors.append(error) self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - return converted_xmls, errors + return converted_xmls, errors \ No newline at end of file From f657179e75c608c2bdbd419e1b02cc183a62d6b5 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Tue, 20 Aug 2024 19:44:47 +0000 Subject: [PATCH 23/92] Added setup to all variants --- sqlserver/tests/compose-ha/sql/aoag_primary.sql | 15 +++++++++++++++ .../compose-high-cardinality-windows/setup.sql | 15 +++++++++++++++ .../tests/compose-high-cardinality/setup.sql | 15 +++++++++++++++ sqlserver/tests/compose-windows/setup.sql | 15 +++++++++++++++ 4 files changed, 60 insertions(+) diff --git a/sqlserver/tests/compose-ha/sql/aoag_primary.sql b/sqlserver/tests/compose-ha/sql/aoag_primary.sql index bc6574d39e2f6..3a3193f6dd662 100644 --- a/sqlserver/tests/compose-ha/sql/aoag_primary.sql +++ b/sqlserver/tests/compose-ha/sql/aoag_primary.sql @@ -121,6 +121,21 @@ CREATE USER bob FOR LOGIN bob; CREATE USER fred FOR LOGIN fred; GO +-- Create a simple table for deadlocks +CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); + +INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) + +-- Grant permissions to bob and fred to update the deadlocks table +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO bob; + +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO fred; +GO + EXEC sp_addrolemember 'db_datareader', 'bob' EXEC sp_addrolemember 'db_datareader', 'fred' EXEC sp_addrolemember 'db_datawriter', 'bob' diff --git a/sqlserver/tests/compose-high-cardinality-windows/setup.sql b/sqlserver/tests/compose-high-cardinality-windows/setup.sql index f33ceff2df42e..4446dbdb52c70 100644 --- a/sqlserver/tests/compose-high-cardinality-windows/setup.sql +++ b/sqlserver/tests/compose-high-cardinality-windows/setup.sql @@ -118,6 +118,21 @@ CREATE USER fred FOR LOGIN fred; -- database so it's copied by default to new databases GO +-- Create a simple table for deadlocks +CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); + +INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) + +-- Grant permissions to bob and fred to update the deadlocks table +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO bob; + +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO fred; +GO + EXEC sp_addrolemember 'db_datareader', 'bob' EXEC sp_addrolemember 'db_datareader', 'fred' EXEC sp_addrolemember 'db_datawriter', 'bob' diff --git a/sqlserver/tests/compose-high-cardinality/setup.sql b/sqlserver/tests/compose-high-cardinality/setup.sql index 839fd7c690679..f9d89b4a5a9df 100644 --- a/sqlserver/tests/compose-high-cardinality/setup.sql +++ b/sqlserver/tests/compose-high-cardinality/setup.sql @@ -251,6 +251,21 @@ CREATE CLUSTERED INDEX thingsindex ON [datadog_test-1].dbo.ϑings (name); DECLARE @table_prefix VARCHAR(100) = 'CREATE TABLE [datadog_test-1].dbo.' DECLARE @table_columns VARCHAR(500) = ' (id INT NOT NULL IDENTITY, col1_txt TEXT, col2_txt TEXT, col3_txt TEXT, col4_txt TEXT, col5_txt TEXT, col6_txt TEXT, col7_txt TEXT, col8_txt TEXT, col9_txt TEXT, col10_txt TEXT, col11_float FLOAT, col12_float FLOAT, col13_float FLOAT, col14_int INT, col15_int INT, col16_int INT, col17_date DATE, PRIMARY KEY(id));'; +-- Create a simple table for deadlocks +CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); + +INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) + +-- Grant permissions to bob and fred to update the deadlocks table +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO bob; + +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO fred; +GO + -- Create a main table which contains high cardinality data for testing. DECLARE @main_table_query VARCHAR(600) = @table_prefix + 'high_cardinality' + @table_columns; EXEC (@main_table_query); diff --git a/sqlserver/tests/compose-windows/setup.sql b/sqlserver/tests/compose-windows/setup.sql index da285a3535baf..5160b7012f40d 100644 --- a/sqlserver/tests/compose-windows/setup.sql +++ b/sqlserver/tests/compose-windows/setup.sql @@ -120,6 +120,21 @@ CREATE USER fred FOR LOGIN fred; -- database so it's copied by default to new databases GO +-- Create a simple table for deadlocks +CREATE TABLE [datadog_test-1].dbo.deadlocks (a int PRIMARY KEY not null ,b int null); + +INSERT INTO [datadog_test-1].dbo.deadlocks VALUES (1,10),(2,20),(3,30) + +-- Grant permissions to bob and fred to update the deadlocks table +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO bob; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO bob; + +GRANT INSERT ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT UPDATE ON [datadog_test-1].dbo.deadlocks TO fred; +GRANT DELETE ON [datadog_test-1].dbo.deadlocks TO fred; +GO + EXEC sp_addrolemember 'db_datareader', 'bob' EXEC sp_addrolemember 'db_datareader', 'fred' EXEC sp_addrolemember 'db_datawriter', 'bob' From 9add15cf5998fa7a30f6cb0c3e57afe47771ad6c Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 08:41:48 +0000 Subject: [PATCH 24/92] Remove unused changes --- datadog_checks_base/datadog_checks/base/checks/base.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/datadog_checks_base/datadog_checks/base/checks/base.py b/datadog_checks_base/datadog_checks/base/checks/base.py index 4dba59c1b34ff..ccfbced9231ca 100644 --- a/datadog_checks_base/datadog_checks/base/checks/base.py +++ b/datadog_checks_base/datadog_checks/base/checks/base.py @@ -687,13 +687,6 @@ def database_monitoring_query_activity(self, raw_event): aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), "dbm-activity") - def database_monitoring_deadlocks(self, raw_event): - # type: (str) -> None - if raw_event is None: - return - - aggregator.submit_event_platform_event(self, self.check_id, to_native_string(raw_event), "dbm-deadlocks") - def database_monitoring_metadata(self, raw_event): # type: (str) -> None if raw_event is None: From 52beecb2e1b77f0be0867ede0ccc944369820581 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 08:46:52 +0000 Subject: [PATCH 25/92] Added changelog --- sqlserver/changelog.d/18108.added | 1 + 1 file changed, 1 insertion(+) create mode 100644 sqlserver/changelog.d/18108.added diff --git a/sqlserver/changelog.d/18108.added b/sqlserver/changelog.d/18108.added new file mode 100644 index 0000000000000..15ae883141b60 --- /dev/null +++ b/sqlserver/changelog.d/18108.added @@ -0,0 +1 @@ +Added deadlock history collection feature to the SQL Server integration. From a7715e3993b767acb92876ee96aee230ea865dc4 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 09:35:46 +0000 Subject: [PATCH 26/92] deadlocks spec --- sqlserver/assets/configuration/spec.yaml | 27 +++++++++++++++++++ .../datadog_checks/sqlserver/activity.py | 20 -------------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index c5d7643bd8815..80a36a3557b2d 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -760,6 +760,33 @@ files: value: type: number example: 10 + - name: deadlocks + hidden: True + description: | + Configure the collection of historical deadlock data. + options: + - name: enabled + description: | + Enable the collection of historical deadlock data. + value: + type: boolean + example: false + + - name: collection_interval + description: | + Set the interval for collecting deadlock data, in seconds. Defaults to 5 seconds. + value: + type: number + example: 5 + - name: max_deadlocks + description: | + Set the maximum number of deadlocks to retrieve. + value: + type: number + example: 100 + + + - template: instances/default - template: logs example: diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 2d89805529888..1a53a1f9b02de 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -150,26 +150,6 @@ def agent_check_getter(self): return self._check -""" self._databases_data_enabled = is_affirmative(config.schemas_config.get("enabled", False)) - self._databases_data_collection_interval = config.schemas_config.get( - "collection_interval", DEFAULT_DATABASES_DATA_COLLECTION_INTERVAL - ) - self._settings_enabled = is_affirmative(config.settings_config.get('enabled', False)) - - self._settings_collection_interval = float( - config.settings_config.get('collection_interval', DEFAULT_SETTINGS_COLLECTION_INTERVAL) - ) - - if self._databases_data_enabled and not self._settings_enabled: - self.collection_interval = self._databases_data_collection_interval - elif not self._databases_data_enabled and self._settings_enabled: - self.collection_interval = self._settings_collection_interval - else: - self.collection_interval = min(self._databases_data_collection_interval, self._settings_collection_interval) - - self.enabled = self._databases_data_enabled or self._settings_enabled""" - - class SqlserverActivity(DBMAsyncJob): """Collects query metrics and plans""" From 7fde1c7b6ce681e804d8b1b987234349dbdeda33 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 09:37:22 +0000 Subject: [PATCH 27/92] formatted spce --- sqlserver/assets/configuration/spec.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index 80a36a3557b2d..6b5ae99b94f89 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -784,9 +784,6 @@ files: value: type: number example: 100 - - - - template: instances/default - template: logs example: From 43e57180e2c634713af15c65ea8dbce5223781f6 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 11:15:08 +0000 Subject: [PATCH 28/92] Improced changelog --- sqlserver/assets/configuration/spec.yaml | 5 ++--- sqlserver/changelog.d/18108.added | 2 +- sqlserver/datadog_checks/sqlserver/activity.py | 13 ++++--------- sqlserver/datadog_checks/sqlserver/deadlocks.py | 3 +++ 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index 6b5ae99b94f89..3b82419953497 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -763,15 +763,14 @@ files: - name: deadlocks hidden: True description: | - Configure the collection of historical deadlock data. + Configure the collection of deadlock data. options: - name: enabled description: | - Enable the collection of historical deadlock data. + Enable the collection of deadlock data. value: type: boolean example: false - - name: collection_interval description: | Set the interval for collecting deadlock data, in seconds. Defaults to 5 seconds. diff --git a/sqlserver/changelog.d/18108.added b/sqlserver/changelog.d/18108.added index 15ae883141b60..a75d5681d83d1 100644 --- a/sqlserver/changelog.d/18108.added +++ b/sqlserver/changelog.d/18108.added @@ -1 +1 @@ -Added deadlock history collection feature to the SQL Server integration. +Added deadlock collection feature to the SQL Server integration. diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 1a53a1f9b02de..0462a3a2dc628 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -162,15 +162,14 @@ def __init__(self, check, config: SQLServerConfig): self._last_deadlocks_collection_time = 0 self._last_activity_collection_time = 0 - # TODO put back false - self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", True)) + self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", False)) self._deadlocks_collection_interval = config.deadlocks_config.get( "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL ) if self._deadlocks_collection_interval <= 0: self._deadlocks_collection_interval = DEFAULT_DEADLOCKS_COLLECTION_INTERVAL - self._activity_collection_enabled = is_affirmative(config.activity_config.get("enabled", False)) + self._activity_collection_enabled = is_affirmative(config.activity_config.get("enabled", True)) self._activity_collection_interval = config.activity_config.get( "collection_interval", DEFAULT_ACTIVITY_COLLECTION_INTERVAL ) @@ -201,7 +200,7 @@ def __init__(self, check, config: SQLServerConfig): self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES self._exec_requests_cols_cached = None self._deadlocks = Deadlocks(check, self._conn_key_prefix, self._config) - self._deadlock__payload_max_bytes = MAX_PAYLOAD_BYTES + self._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES def _close_db_conn(self): pass @@ -223,7 +222,6 @@ def run_job(self): if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: self._last_deadlocks_collection_time = time.time() try: - self._log.error("EXECUTING COLLECT DEADLOCKS") self._collect_deadlocks() except Exception as e: self._log.error( @@ -235,13 +233,12 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): - start_time = time.time() deadlock_xmls_collected, errors = self._deadlocks.collect_deadlocks() deadlock_xmls = [] total_number_of_characters = 0 for i, deadlock in enumerate(deadlock_xmls_collected): total_number_of_characters += len(deadlock) - if total_number_of_characters > self._deadlock__payload_max_bytes: + if total_number_of_characters > self._deadlock_payload_max_bytes: self._log.warning( """We've dropped {} deadlocks from a total of {} deadlocks as the max deadlock payload of {} bytes was exceeded.""".format( @@ -252,10 +249,8 @@ def _collect_deadlocks(self): else: deadlock_xmls.append(deadlock) deadlocks_event = self._create_deadlock_event(deadlock_xmls, errors) - self._log.error("DEADLOCK EVENTS TO BE SENT: {}".format(deadlocks_event)) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) - self._log.error("DEADLOCK COlLECTED {} in {} time".format(len(deadlock_xmls), time.time() - start_time)) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 44b8181c02417..3b65cacfb16cc 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -52,7 +52,10 @@ def obfuscate_xml(self, root): def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: + self._log.debug("collecting sql server deadlocks") + self._log.debug("Running query [%s]", CREATE_DEADLOCK_TEMP_TABLE_QUERY) cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) + self._log.debug("Running query [%s]", DETECT_DEADLOCK_QUERY) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') From 2035822bda0efa298b1ddb24c312a2f1b8bf3f13 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 11:59:13 +0000 Subject: [PATCH 29/92] Improved deadlock test --- .../datadog_checks/sqlserver/deadlocks.py | 3 +- sqlserver/tests/test_activity.py | 28 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 3b65cacfb16cc..913c859cc424b 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -13,6 +13,7 @@ MAX_DEADLOCKS = 100 + class Deadlocks: def __init__(self, check, conn_prefix, config): @@ -81,4 +82,4 @@ def collect_deadlocks(self): else: errors.append(error) self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] - return converted_xmls, errors \ No newline at end of file + return converted_xmls, errors diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index ff23a23e0cb22..4a46d06c37c65 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -8,7 +8,6 @@ import datetime import json import os -import pdb import re import threading import time @@ -910,17 +909,12 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_plan_hash'], str) -# test2 - time test that we take deadlocks in delta -# some crazy scenario if possible like 3 query involved ? -# deadlock too long ? - - @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['deadlocks'] = { 'enabled': True, - 'run_sync': True, # TODO oups run_sync what should be the logic for 2 jobs ? + 'run_sync': True, 'collection_interval': 0.1, } dbm_instance['query_activity']['enabled'] = False @@ -928,7 +922,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) created_deadlock = False - # Rarely instead of a deadlock one of the transactions time outs + # Rarely instead of creating a deadlock one of the transactions time outs for _ in range(0, 3): bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) @@ -939,7 +933,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): break assert created_deadlock, "Couldn't create a deadlock, exiting" - def execut_test(): + def execute_test(): dd_run_check(sqlserver_check) dbm_activity = aggregator.get_event_platform_events("dbm-activity") @@ -956,30 +950,34 @@ def execut_test(): deadlocks = matched_event[0]["sqlserver_deadlocks"] if len(deadlocks) < 1: return False, "should have collected one or more deadlock in the payload" - found = False + found = 0 for d in deadlocks: root = ET.fromstring(d) - pdb.set_trace() process_list = root.find(".//process-list") for process in process_list.findall('process'): if ( process.find('inputbuf').text == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" ): - found = True + found += 1 err = "" if not found: err = "Should've collected produced deadlock" return found, err # Sometimes deadlock takes a bit longer to arrive to the ring buffer. - # We can may be give it 3 tries + # We give it 3 tries err = "" success = False for _ in range(0, 3): time.sleep(3) - res, err = execut_test() - if res: + res, err = execute_test() + if res == 1: success = True break assert success, err + + # After a deadlock has been collected, we need to ensure that the agent + # does not collect the same deadlock again. + res, err = execute_test() + assert res == 1, "Produced deadlock should be collected only once" From 36448ae3a230b6fd4511f4c837c05aa534b12b95 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 13:29:41 +0000 Subject: [PATCH 30/92] Improved unit tests --- sqlserver/assets/configuration/spec.yaml | 4 +- .../datadog_checks/sqlserver/activity.py | 2 +- .../datadog_checks/sqlserver/deadlocks.py | 14 +++- sqlserver/tests/test_unit.py | 77 ------------------- 4 files changed, 13 insertions(+), 84 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index 3b82419953497..7ae2086c97c15 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -773,10 +773,10 @@ files: example: false - name: collection_interval description: | - Set the interval for collecting deadlock data, in seconds. Defaults to 5 seconds. + Set the interval for collecting deadlock data, in seconds. Defaults to 10 seconds. value: type: number - example: 5 + example: 10 - name: max_deadlocks description: | Set the maximum number of deadlocks to retrieve. diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 0462a3a2dc628..36f0542491fcf 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -23,7 +23,7 @@ from ..stubs import datadog_agent DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 -DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 5 +DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 10 MAX_PAYLOAD_BYTES = 19e6 CONNECTIONS_QUERY = """\ diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 913c859cc424b..4fd96b05f7808 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -25,16 +25,16 @@ def __init__(self, check, conn_prefix, config): self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) def obfuscate_no_except_wrapper(self, sql_text): + sql_text = "ERROR: failed to obfuscate" try: sql_text = obfuscate_sql_with_metadata( sql_text, self._config.obfuscator_options, replace_null_character=True )['query'] except Exception as e: if self._config.log_unobfuscated_queries: - self.log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) + self._log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) else: - self.log.debug("Failed to obfuscate sql text within a deadlock | err=[%s]", e) - sql_text = "ERROR: failed to obfuscate" + self._log.debug("Failed to obfuscate sql text within a deadlock | err=[%s]", e) return sql_text def obfuscate_xml(self, root): @@ -56,7 +56,12 @@ def collect_deadlocks(self): self._log.debug("collecting sql server deadlocks") self._log.debug("Running query [%s]", CREATE_DEADLOCK_TEMP_TABLE_QUERY) cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) - self._log.debug("Running query [%s]", DETECT_DEADLOCK_QUERY) + self._log.debug( + "Running query [%s] with max deadlocks %s and timestamp %s", + DETECT_DEADLOCK_QUERY, + self._max_deadlocks, + self._last_deadlock_timestamp, + ) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) results = cursor.fetchall() last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') @@ -73,6 +78,7 @@ def collect_deadlocks(self): ) ) errors.append("Truncated deadlock xml - {}".format(result[:50])) + continue datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') if last_deadlock_datetime < datetime_obj: last_deadlock_datetime = datetime_obj diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 6014cf8cc0953..813f8a739c293 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -873,80 +873,3 @@ def test_exception_handling_by_do_for_dbs(instance_docker): ): schemas._fetch_for_databases() - -def test_deadlock_calls_obfuscator(instance_docker): - check = SQLServer(CHECK_NAME, {}, [instance_docker]) - test_xml = """ - - - - - - - - - - - - \nunknown - \nunknown - - \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2; - - - - \nunknown - \nunknown - - \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1; - - - - - - - """ - - expected_xml_string = ( - " " - " " - " " - " " - " " - " " - " " - " " - " " - " " - " " - "obfuscated " - "obfuscated " - " " - "obfuscated " - " " - " " - " " - "obfuscated " - "obfuscated " - " " - "obfuscated " - " " - " " - " " - " " - " " - "" - ) - - with mock.patch( - 'datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated" - ): - config = type("Config", (object,), {"deadlocks_config": {"max_deadlocks": 5}}) - deadlocks = Deadlocks(check, "", config) - root = ET.fromstring(test_xml) - deadlocks.obfuscate_xml(root) - result_string = ET.tostring(root, encoding='unicode') - result_string = result_string.replace('\t', '').replace('\n', '') - result_string = re.sub(r'\s{2,}', ' ', result_string) - assert expected_xml_string == result_string - From 516431c0d0aeefc17749e1eedb8d3a301303ad39 Mon Sep 17 00:00:00 2001 From: Boris Kozlov Date: Wed, 21 Aug 2024 13:48:05 +0000 Subject: [PATCH 31/92] Fixed data model --- sqlserver/assets/configuration/spec.yaml | 46 +++++++++---------- .../sqlserver/config_models/instance.py | 11 +++++ 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index 7ae2086c97c15..c7b3894d32455 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -760,29 +760,29 @@ files: value: type: number example: 10 - - name: deadlocks - hidden: True - description: | - Configure the collection of deadlock data. - options: - - name: enabled - description: | - Enable the collection of deadlock data. - value: - type: boolean - example: false - - name: collection_interval - description: | - Set the interval for collecting deadlock data, in seconds. Defaults to 10 seconds. - value: - type: number - example: 10 - - name: max_deadlocks - description: | - Set the maximum number of deadlocks to retrieve. - value: - type: number - example: 100 + - name: deadlocks + hidden: True + description: | + Configure the collection of deadlock data. + options: + - name: enabled + description: | + Enable the collection of deadlock data. + value: + type: boolean + example: false + - name: collection_interval + description: | + Set the interval for collecting deadlock data, in seconds. Defaults to 10 seconds. + value: + type: number + example: 10 + - name: max_deadlocks + description: | + Set the maximum number of deadlocks to retrieve per collection. + value: + type: number + example: 100 - template: instances/default - template: logs example: diff --git a/sqlserver/datadog_checks/sqlserver/config_models/instance.py b/sqlserver/datadog_checks/sqlserver/config_models/instance.py index d37ad477a430f..57a1002064276 100644 --- a/sqlserver/datadog_checks/sqlserver/config_models/instance.py +++ b/sqlserver/datadog_checks/sqlserver/config_models/instance.py @@ -68,6 +68,16 @@ class CustomQuery(BaseModel): tags: Optional[tuple[str, ...]] = None +class Deadlocks(BaseModel): + model_config = ConfigDict( + arbitrary_types_allowed=True, + frozen=True, + ) + collection_interval: Optional[float] = None + enabled: Optional[bool] = None + max_deadlocks: Optional[float] = None + + class Gcp(BaseModel): model_config = ConfigDict( arbitrary_types_allowed=True, @@ -185,6 +195,7 @@ class InstanceConfig(BaseModel): database_instance_collection_interval: Optional[float] = None db_fragmentation_object_names: Optional[tuple[str, ...]] = None dbm: Optional[bool] = None + deadlocks: Optional[Deadlocks] = None disable_generic_tags: Optional[bool] = None driver: Optional[str] = None dsn: Optional[str] = None From ce69f16c780d61d583222e048c56275b73ddb541 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:33:52 +0200 Subject: [PATCH 32/92] print the number of deadlocks found in test case --- sqlserver/tests/test_activity.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 4a46d06c37c65..b66ee214d569b 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -944,9 +944,11 @@ def execute_test(): for event in dbm_activity: if "sqlserver_deadlocks" in event: matched_event.append(event) - - if len(matched_event) != 1: - return False, "should have collected one deadlock payload" + + collected_deadlocks = len(matched_event) + if collected_deadlocks != 1: + return False, "should have collected one deadlock payload. collected: {}".format(collected_deadlocks) + deadlocks = matched_event[0]["sqlserver_deadlocks"] if len(deadlocks) < 1: return False, "should have collected one or more deadlock in the payload" From bf45452c2a4d035962eb17fcaa83766b230c7e00 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:40:04 +0200 Subject: [PATCH 33/92] test for empty payloads --- sqlserver/tests/test_activity.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index b66ee214d569b..e178c5770262b 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -9,6 +9,7 @@ import json import os import re +import csv import threading import time import xml.etree.ElementTree as ET @@ -908,6 +909,23 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): + dbm_instance['deadlocks'] = { + 'enabled': True, + 'run_sync': True, + 'collection_interval': 0.1, + } + dbm_instance['query_activity']['enabled'] = False + sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) + dd_run_check(sqlserver_check) + dbm_activity = aggregator.get_event_platform_events("dbm-activity") + deadlock_event_found = False + for event in dbm_activity: + if "sqlserver_deadlocks" in event: + deadlock_event_found = True + assert not deadlock_event_found, "shouldn't have collected a deadlock event" @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @@ -947,7 +965,10 @@ def execute_test(): collected_deadlocks = len(matched_event) if collected_deadlocks != 1: - return False, "should have collected one deadlock payload. collected: {}".format(collected_deadlocks) + with open("/tmp/output.csv", "w", newline="") as file: + for row in matched_event: + file.write(str(row) + "\n") + return False, "Should have collected one deadlock payload, but collected: {}. Events {}".format(collected_deadlocks, matched_event) deadlocks = matched_event[0]["sqlserver_deadlocks"] if len(deadlocks) < 1: From b58ff3a6b0937a245a8da953544a5dca42780200 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:57:04 +0200 Subject: [PATCH 34/92] send payload only if deadlocks found --- sqlserver/datadog_checks/sqlserver/activity.py | 9 ++++++--- sqlserver/tests/test_activity.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 36f0542491fcf..6c6ef36e409eb 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -248,9 +248,12 @@ def _collect_deadlocks(self): break else: deadlock_xmls.append(deadlock) - deadlocks_event = self._create_deadlock_event(deadlock_xmls, errors) - payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - self._check.database_monitoring_query_activity(payload) + + # Send payload only if deadlocks found + if deadlock_xmls: + deadlocks_event = self._create_deadlock_event(deadlock_xmls, errors) + payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + self._check.database_monitoring_query_activity(payload) @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index e178c5770262b..e9f93b5a2217b 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -7,9 +7,9 @@ import concurrent import datetime import json +import logging import os import re -import csv import threading import time import xml.etree.ElementTree as ET @@ -919,12 +919,14 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): } dbm_instance['query_activity']['enabled'] = False sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) + dd_run_check(sqlserver_check) dbm_activity = aggregator.get_event_platform_events("dbm-activity") deadlock_event_found = False for event in dbm_activity: if "sqlserver_deadlocks" in event: deadlock_event_found = True + logging.error("Deadlock event found: %s", event) assert not deadlock_event_found, "shouldn't have collected a deadlock event" @pytest.mark.integration From fd53695bca5c2fad8bacb26f0ee81414cce4add3 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:20:01 +0200 Subject: [PATCH 35/92] refactor deadlock event extraction --- sqlserver/tests/test_activity.py | 45 +++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index e9f93b5a2217b..2939e96fd8f9f 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -909,6 +909,19 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) +def run_check_and_return_deadlocks(dd_run_check, check, aggregator): + dd_run_check(check) + dbm_activity = aggregator.get_event_platform_events("dbm-activity") + if not dbm_activity: + return None + matched_event = [] + for event in dbm_activity: + if "sqlserver_deadlocks" in event: + matched_event.append(event) + if not matched_event: + return None + return matched_event + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): @@ -920,14 +933,34 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['query_activity']['enabled'] = False sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - dd_run_check(sqlserver_check) - dbm_activity = aggregator.get_event_platform_events("dbm-activity") - deadlock_event_found = False + deadlocks = run_check_and_return_deadlocks(dd_run_check, sqlserver_check, aggregator) + assert not deadlocks, "shouldn't have sent a deadlock payload without a deadlock" + + created_deadlock = False + # Rarely instead of creating a deadlock one of the transactions time outs + for _ in range(0, 3): + bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) + fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) + created_deadlock = create_deadlock(bob_conn, fred_conn) + bob_conn.close() + fred_conn.close() + if created_deadlock: + break + try: + assert created_deadlock, "Couldn't create a deadlock, exiting" + except AssertionError as e: + raise e + + ''' + collected_deadlocks = [] for event in dbm_activity: if "sqlserver_deadlocks" in event: - deadlock_event_found = True - logging.error("Deadlock event found: %s", event) - assert not deadlock_event_found, "shouldn't have collected a deadlock event" + collected_deadlocks.append(event) + assert len(collected_deadlocks) == 1, "Should have collected one deadlock payload, but collected: {}. Events {}".format(len(collected_deadlocks), collected_deadlocks) + ''' + + + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') From 2f362e8749b11344f947b2180bef9ace6903ad5e Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 14:22:26 +0200 Subject: [PATCH 36/92] refactor deadlock event extraction after deadlock create --- sqlserver/tests/test_activity.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 2939e96fd8f9f..ac3ee28ee913a 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -951,13 +951,8 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: raise e - ''' - collected_deadlocks = [] - for event in dbm_activity: - if "sqlserver_deadlocks" in event: - collected_deadlocks.append(event) - assert len(collected_deadlocks) == 1, "Should have collected one deadlock payload, but collected: {}. Events {}".format(len(collected_deadlocks), collected_deadlocks) - ''' + deadlocks = run_check_and_return_deadlocks(dd_run_check, sqlserver_check, aggregator) + assert len(deadlocks) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) From 3719d4e17db71acb99d53a65155d6f00f2431cae Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 15:43:35 +0200 Subject: [PATCH 37/92] deadlock events to deadlock payloads --- sqlserver/tests/test_activity.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index ac3ee28ee913a..823f5f540d79d 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -909,18 +909,18 @@ def test_sanitize_activity_row(dbm_instance, row): assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) -def run_check_and_return_deadlocks(dd_run_check, check, aggregator): +def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): dd_run_check(check) dbm_activity = aggregator.get_event_platform_events("dbm-activity") if not dbm_activity: return None - matched_event = [] + matched = [] for event in dbm_activity: if "sqlserver_deadlocks" in event: - matched_event.append(event) - if not matched_event: + matched.append(event) + if not matched: return None - return matched_event + return matched @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @@ -933,8 +933,8 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['query_activity']['enabled'] = False sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - deadlocks = run_check_and_return_deadlocks(dd_run_check, sqlserver_check, aggregator) - assert not deadlocks, "shouldn't have sent a deadlock payload without a deadlock" + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) + assert not deadlock_payloads, "shouldn't have sent a deadlock payload without a deadlock" created_deadlock = False # Rarely instead of creating a deadlock one of the transactions time outs @@ -951,8 +951,8 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: raise e - deadlocks = run_check_and_return_deadlocks(dd_run_check, sqlserver_check, aggregator) - assert len(deadlocks) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) + assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) From b4e8778b9bf07c0d17b62c9a9acc4ceba5c77ab9 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:21:59 +0200 Subject: [PATCH 38/92] test for obfuscation errors --- sqlserver/tests/test_activity.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 823f5f540d79d..ceeac848b2392 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -952,10 +952,25 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): raise e deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) - assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) - - + try: + assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) + except AssertionError as e: + raise e + found = 0 + deadlocks = deadlock_payloads[0] + assert not "ERROR" in deadlocks, "Shouldn't have generated an error" + + for d in deadlocks: + root = ET.fromstring(d) + process_list = root.find(".//process-list") + for process in process_list.findall('process'): + if ( + process.find('inputbuf').text + == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" + ): + found += 1 + assert found == 1, "Should haveve collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') From f5161ae9da1ae2899c6200769a3a98b99eab904a Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:29:19 +0200 Subject: [PATCH 39/92] test for obfuscation errors --- sqlserver/tests/test_activity.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index ceeac848b2392..5431afb0bbee4 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -958,11 +958,15 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): raise e found = 0 - deadlocks = deadlock_payloads[0] + deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] assert not "ERROR" in deadlocks, "Shouldn't have generated an error" for d in deadlocks: - root = ET.fromstring(d) + try: + root = ET.fromstring(d) + except ET.ParseError as e: + logging.error("deadlock events: %s", str(deadlocks)) + raise e process_list = root.find(".//process-list") for process in process_list.findall('process'): if ( From 7d84e25a1337017959536d8bbacfdfc8ee76f38b Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 16:41:30 +0200 Subject: [PATCH 40/92] test refactoring completed --- sqlserver/tests/test_activity.py | 91 +++----------------------------- 1 file changed, 8 insertions(+), 83 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 5431afb0bbee4..bbb69ad8cb850 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -924,7 +924,7 @@ def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') -def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): +def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): dbm_instance['deadlocks'] = { 'enabled': True, 'run_sync': True, @@ -957,11 +957,10 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: raise e - found = 0 deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] - assert not "ERROR" in deadlocks, "Shouldn't have generated an error" - + found = 0 for d in deadlocks: + assert not "ERROR" in d, "Shouldn't have generated an error" try: root = ET.fromstring(d) except ET.ParseError as e: @@ -974,82 +973,8 @@ def test_deadlocks_2(aggregator, dd_run_check, init_config, dbm_instance): == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" ): found += 1 - assert found == 1, "Should haveve collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) - -@pytest.mark.integration -@pytest.mark.usefixtures('dd_environment') -def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): - dbm_instance['deadlocks'] = { - 'enabled': True, - 'run_sync': True, - 'collection_interval': 0.1, - } - dbm_instance['query_activity']['enabled'] = False - - sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - - created_deadlock = False - # Rarely instead of creating a deadlock one of the transactions time outs - for _ in range(0, 3): - bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) - fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) - created_deadlock = create_deadlock(bob_conn, fred_conn) - bob_conn.close() - fred_conn.close() - if created_deadlock: - break - assert created_deadlock, "Couldn't create a deadlock, exiting" - - def execute_test(): - dd_run_check(sqlserver_check) - - dbm_activity = aggregator.get_event_platform_events("dbm-activity") - if not dbm_activity: - return False, "should have collected at least one activity event" - matched_event = [] - - for event in dbm_activity: - if "sqlserver_deadlocks" in event: - matched_event.append(event) - - collected_deadlocks = len(matched_event) - if collected_deadlocks != 1: - with open("/tmp/output.csv", "w", newline="") as file: - for row in matched_event: - file.write(str(row) + "\n") - return False, "Should have collected one deadlock payload, but collected: {}. Events {}".format(collected_deadlocks, matched_event) - - deadlocks = matched_event[0]["sqlserver_deadlocks"] - if len(deadlocks) < 1: - return False, "should have collected one or more deadlock in the payload" - found = 0 - for d in deadlocks: - root = ET.fromstring(d) - process_list = root.find(".//process-list") - for process in process_list.findall('process'): - if ( - process.find('inputbuf').text - == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" - ): - found += 1 - err = "" - if not found: - err = "Should've collected produced deadlock" - return found, err - - # Sometimes deadlock takes a bit longer to arrive to the ring buffer. - # We give it 3 tries - err = "" - success = False - for _ in range(0, 3): - time.sleep(3) - res, err = execute_test() - if res == 1: - success = True - break - assert success, err - - # After a deadlock has been collected, we need to ensure that the agent - # does not collect the same deadlock again. - res, err = execute_test() - assert res == 1, "Produced deadlock should be collected only once" + try: + assert found == 1, "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) + except AssertionError as e: + logging.error("deadlock XML: %s", str(d)) + raise e From 1c96319a3095170318a33614d9311483d500ba2a Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 2 Sep 2024 18:06:37 +0200 Subject: [PATCH 41/92] obfuscation bug fix --- sqlserver/datadog_checks/sqlserver/activity.py | 6 ++++++ sqlserver/datadog_checks/sqlserver/deadlocks.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 6c6ef36e409eb..539abb65b0a6e 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -234,6 +234,12 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): deadlock_xmls_collected, errors = self._deadlocks.collect_deadlocks() + if errors: + print( + """An error occurred while parsing SQLServer deadlocks. The error - {}""".format( + errors + ) + ) deadlock_xmls = [] total_number_of_characters = 0 for i, deadlock in enumerate(deadlock_xmls_collected): diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 4fd96b05f7808..04ea845d9e5d7 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -25,16 +25,16 @@ def __init__(self, check, conn_prefix, config): self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) def obfuscate_no_except_wrapper(self, sql_text): - sql_text = "ERROR: failed to obfuscate" try: sql_text = obfuscate_sql_with_metadata( sql_text, self._config.obfuscator_options, replace_null_character=True )['query'] except Exception as e: + sql_text = "ERROR: failed to obfuscate" if self._config.log_unobfuscated_queries: self._log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) else: - self._log.debug("Failed to obfuscate sql text within a deadlock | err=[%s]", e) + self._log.warning("Failed to obfuscate sql text within a deadlock | err=[%s]", e) return sql_text def obfuscate_xml(self, root): From d99ba510ced2215e6993cfcbbd2827a2f42e206e Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 3 Sep 2024 08:55:18 +0200 Subject: [PATCH 42/92] test with dbm: false --- sqlserver/tests/test_activity.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index bbb69ad8cb850..6d3550651a39e 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import concurrent +import copy import datetime import json import logging @@ -14,7 +15,6 @@ import time import xml.etree.ElementTree as ET from concurrent.futures.thread import ThreadPoolExecutor -from copy import copy import mock import pytest @@ -56,7 +56,7 @@ def dbm_instance(instance_docker): instance_docker['query_metrics'] = {'enabled': False} instance_docker['procedure_metrics'] = {'enabled': False} instance_docker['collect_settings'] = {'enabled': False} - return copy(instance_docker) + return copy.copy(instance_docker) @pytest.mark.integration @@ -912,14 +912,10 @@ def test_sanitize_activity_row(dbm_instance, row): def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): dd_run_check(check) dbm_activity = aggregator.get_event_platform_events("dbm-activity") - if not dbm_activity: - return None matched = [] for event in dbm_activity: if "sqlserver_deadlocks" in event: matched.append(event) - if not matched: - return None return matched @pytest.mark.integration @@ -931,10 +927,11 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): 'collection_interval': 0.1, } dbm_instance['query_activity']['enabled'] = False + sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) - assert not deadlock_payloads, "shouldn't have sent a deadlock payload without a deadlock" + assert not deadlock_payloads, "shouldn't have sent an empty payload" created_deadlock = False # Rarely instead of creating a deadlock one of the transactions time outs @@ -951,9 +948,16 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: raise e + dbm_instance_no_dbm = copy.deepcopy(dbm_instance) + dbm_instance_no_dbm['dbm'] = False + sqlserver_check_no_dbm = SQLServer(CHECK_NAME, init_config, [dbm_instance_no_dbm]) + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check_no_dbm, aggregator) + assert len(deadlock_payloads) == 0, "deadlock should be behind dbm" + + dbm_instance['dbm_enabled'] = True deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) try: - assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlocks)) + assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlock_payloads)) except AssertionError as e: raise e From e1130a85ee50502823a96058d69cd82e8e860b6f Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:58:54 +0200 Subject: [PATCH 43/92] removed reliance on temp table --- .../datadog_checks/sqlserver/deadlocks.py | 8 ++--- sqlserver/datadog_checks/sqlserver/queries.py | 36 ++++++------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 04ea845d9e5d7..3e7bfeb861ff1 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -6,10 +6,8 @@ from datetime import datetime from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata -from datadog_checks.sqlserver.queries import ( - CREATE_DEADLOCK_TEMP_TABLE_QUERY, - DETECT_DEADLOCK_QUERY, -) +from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY + MAX_DEADLOCKS = 100 @@ -54,8 +52,6 @@ def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: self._log.debug("collecting sql server deadlocks") - self._log.debug("Running query [%s]", CREATE_DEADLOCK_TEMP_TABLE_QUERY) - cursor.execute(CREATE_DEADLOCK_TEMP_TABLE_QUERY) self._log.debug( "Running query [%s] with max deadlocks %s and timestamp %s", DETECT_DEADLOCK_QUERY, diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 76815e360cdc4..a7c5b8e69f356 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,32 +214,18 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ -CREATE_DEADLOCK_TEMP_TABLE_QUERY = """ -SELECT - CAST([target_data] AS XML) AS Target_Data -INTO - #TempXMLDatadogData -FROM - sys.dm_xe_session_targets AS xt -INNER JOIN - sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address -WHERE - xs.name = N'system_health' -AND - xt.target_name = N'ring_buffer'; -""" - DETECT_DEADLOCK_QUERY = """ -SELECT TOP (?) - xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] -FROM - #TempXMLDatadogData -CROSS APPLY - Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE - xdr.value('@timestamp', 'datetime') > ? -ORDER BY [Date] DESC; -""" +SELECT TOP(?) xdr.value('@timestamp', 'datetime') AS [Date], + xdr.query('.') AS [Event_Data] +FROM (SELECT CAST([target_data] AS XML) AS Target_Data + FROM sys.dm_xe_session_targets AS xt + INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address + WHERE xs.name = N'system_health' + AND xt.target_name = N'ring_buffer' + ) AS XML_Data +CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) +WHERE xdr.value('@timestamp', 'datetime') > ? +ORDER BY [Date] DESC;""" def get_query_ao_availability_groups(sqlserver_major_version): From a91a3745e7d558cd6b7adc4a95cf541a5562f641 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 3 Sep 2024 10:40:57 +0200 Subject: [PATCH 44/92] replace last date with offset --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 12 +++++------- sqlserver/datadog_checks/sqlserver/queries.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 3e7bfeb861ff1..6172736875187 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -4,6 +4,7 @@ import xml.etree.ElementTree as ET from datetime import datetime +from time import time from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY @@ -19,7 +20,7 @@ def __init__(self, check, conn_prefix, config): self._log = self._check.log self._conn_key_prefix = conn_prefix self._config = config - self._last_deadlock_timestamp = '1900-01-01 01:01:01.111' + self._last_deadlock_timestamp = time() self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) def obfuscate_no_except_wrapper(self, sql_text): @@ -58,9 +59,9 @@ def collect_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, self._last_deadlock_timestamp)) + cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) results = cursor.fetchall() - last_deadlock_datetime = datetime.strptime(self._last_deadlock_timestamp, '%Y-%m-%d %H:%M:%S.%f') + last_deadlock_datetime = time() converted_xmls = [] errors = [] for result in results: @@ -75,13 +76,10 @@ def collect_deadlocks(self): ) errors.append("Truncated deadlock xml - {}".format(result[:50])) continue - datetime_obj = datetime.strptime(root.get('timestamp'), '%Y-%m-%dT%H:%M:%S.%fZ') - if last_deadlock_datetime < datetime_obj: - last_deadlock_datetime = datetime_obj error = self.obfuscate_xml(root) if not error: converted_xmls.append(ET.tostring(root, encoding='unicode')) else: errors.append(error) - self._last_deadlock_timestamp = last_deadlock_datetime.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + self._last_deadlock_timestamp = time() return converted_xmls, errors diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index a7c5b8e69f356..2ed895d4ff918 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -224,7 +224,7 @@ AND xt.target_name = N'ring_buffer' ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) -WHERE xdr.value('@timestamp', 'datetime') > ? +WHERE xdr.value('@timestamp', 'datetime') >= DATEADD(SECOND, ?, GETDATE()) ORDER BY [Date] DESC;""" From 95ee9b73010a7c8b4466890a22a229865d111420 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 3 Sep 2024 11:38:28 +0200 Subject: [PATCH 45/92] default interval --- sqlserver/assets/configuration/spec.yaml | 4 ++-- sqlserver/datadog_checks/sqlserver/activity.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index c7b3894d32455..e2a7647e6cd82 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -773,10 +773,10 @@ files: example: false - name: collection_interval description: | - Set the interval for collecting deadlock data, in seconds. Defaults to 10 seconds. + Set the interval for collecting deadlock data, in seconds. Defaults to 600 seconds. value: type: number - example: 10 + example: 600 - name: max_deadlocks description: | Set the maximum number of deadlocks to retrieve per collection. diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 539abb65b0a6e..1e53608c70e98 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -23,7 +23,7 @@ from ..stubs import datadog_agent DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 -DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 10 +DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 600 MAX_PAYLOAD_BYTES = 19e6 CONNECTIONS_QUERY = """\ From 9afd2c6e42442568191b0a75655d0e1fde404eff Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:51:46 +0200 Subject: [PATCH 46/92] improved error handling --- .../datadog_checks/sqlserver/activity.py | 13 +++------- .../datadog_checks/sqlserver/deadlocks.py | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 1e53608c70e98..1481c1b12478d 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -233,13 +233,7 @@ def run_job(self): @tracked_method(agent_check_getter=agent_check_getter) def _collect_deadlocks(self): - deadlock_xmls_collected, errors = self._deadlocks.collect_deadlocks() - if errors: - print( - """An error occurred while parsing SQLServer deadlocks. The error - {}""".format( - errors - ) - ) + deadlock_xmls_collected = self._deadlocks.collect_deadlocks() deadlock_xmls = [] total_number_of_characters = 0 for i, deadlock in enumerate(deadlock_xmls_collected): @@ -257,7 +251,7 @@ def _collect_deadlocks(self): # Send payload only if deadlocks found if deadlock_xmls: - deadlocks_event = self._create_deadlock_event(deadlock_xmls, errors) + deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._check.database_monitoring_query_activity(payload) @@ -439,7 +433,7 @@ def _create_activity_event(self, active_sessions, active_connections): } return event - def _create_deadlock_event(self, deadlock_xmls, errors): + def _create_deadlock_event(self, deadlock_xmls): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -452,7 +446,6 @@ def _create_deadlock_event(self, deadlock_xmls, errors): 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, - "sqlserver_deadlock_errors": errors, } return event diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 6172736875187..afa28d2ae31b3 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -39,7 +39,7 @@ def obfuscate_no_except_wrapper(self, sql_text): def obfuscate_xml(self, root): process_list = root.find(".//process-list") if process_list is None: - return "process-list element not found. The deadlock XML is in an unexpected format." + raise Exception("process-list element not found. The deadlock XML is in an unexpected format.") for process in process_list.findall('process'): for inputbuf in process.findall('.//inputbuf'): if inputbuf.text is not None: @@ -47,7 +47,7 @@ def obfuscate_xml(self, root): for frame in process.findall('.//frame'): if frame.text is not None: frame.text = self.obfuscate_no_except_wrapper(frame.text) - return None + return def collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): @@ -61,7 +61,6 @@ def collect_deadlocks(self): ) cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) results = cursor.fetchall() - last_deadlock_datetime = time() converted_xmls = [] errors = [] for result in results: @@ -70,16 +69,18 @@ def collect_deadlocks(self): except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. - One of the deadlock XMLs couldn't be parsed. The error: {}""".format( - e + One of the deadlock XMLs couldn't be parsed. The error: {}. XML: {}""".format( + e, result ) ) - errors.append("Truncated deadlock xml - {}".format(result[:50])) continue - error = self.obfuscate_xml(root) - if not error: - converted_xmls.append(ET.tostring(root, encoding='unicode')) - else: - errors.append(error) + try: + self.obfuscate_xml(root) + except Exception as e: + error = "An error occurred while obfuscating SQLServer deadlocks. The error: {}".format(e) + self._log.error(error) + continue + + converted_xmls.append(ET.tostring(root, encoding='unicode')) self._last_deadlock_timestamp = time() - return converted_xmls, errors + return converted_xmls From 1ab5f4a337df252150079ddfa2b937ec47ee70c8 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:24:54 +0200 Subject: [PATCH 47/92] fixes --- sqlserver/assets/configuration/spec.yaml | 2 +- sqlserver/datadog_checks/sqlserver/activity.py | 3 ++- sqlserver/datadog_checks/sqlserver/deadlocks.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index e2a7647e6cd82..f1a7a428bd30b 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -767,7 +767,7 @@ files: options: - name: enabled description: | - Enable the collection of deadlock data. + Enable the collection of deadlock data. Requires `dbm: true`. Enabled by default. value: type: boolean example: false diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 1481c1b12478d..37532face32ed 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -162,7 +162,7 @@ def __init__(self, check, config: SQLServerConfig): self._last_deadlocks_collection_time = 0 self._last_activity_collection_time = 0 - self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", False)) + self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", True)) self._deadlocks_collection_interval = config.deadlocks_config.get( "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL ) @@ -253,6 +253,7 @@ def _collect_deadlocks(self): if deadlock_xmls: deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + self.log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) @tracked_method(agent_check_getter=agent_check_getter) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index afa28d2ae31b3..dfc6cd90e7c86 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -62,7 +62,6 @@ def collect_deadlocks(self): cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) results = cursor.fetchall() converted_xmls = [] - errors = [] for result in results: try: root = ET.fromstring(result[1]) From 7a60fcd465961abcd3f21e39e75ba09683832017 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:46:58 +0200 Subject: [PATCH 48/92] Update sqlserver/assets/configuration/spec.yaml Co-authored-by: Justin --- sqlserver/assets/configuration/spec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index f1a7a428bd30b..cd587763ebe13 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -760,7 +760,7 @@ files: value: type: number example: 10 - - name: deadlocks + - name: deadlocks_collection hidden: True description: | Configure the collection of deadlock data. From f64d44a2198b1f7bc2b7b7a1caa8df3fba2e0250 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 12:50:49 +0200 Subject: [PATCH 49/92] renamed config to deadlocks_collection --- sqlserver/datadog_checks/sqlserver/config.py | 2 +- sqlserver/datadog_checks/sqlserver/config_models/instance.py | 2 +- sqlserver/tests/test_activity.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/config.py b/sqlserver/datadog_checks/sqlserver/config.py index e6d4cee27fe16..166299b3c2bb6 100644 --- a/sqlserver/datadog_checks/sqlserver/config.py +++ b/sqlserver/datadog_checks/sqlserver/config.py @@ -50,7 +50,7 @@ def __init__(self, init_config, instance, log): self.settings_config: dict = instance.get('collect_settings', {}) or {} self.activity_config: dict = instance.get('query_activity', {}) or {} self.schema_config: dict = instance.get('schemas_collection', {}) or {} - self.deadlocks_config: dict = instance.get('deadlocks', {}) or {} + self.deadlocks_config: dict = instance.get('deadlocks_collection', {}) or {} self.cloud_metadata: dict = {} aws: dict = instance.get('aws', {}) or {} gcp: dict = instance.get('gcp', {}) or {} diff --git a/sqlserver/datadog_checks/sqlserver/config_models/instance.py b/sqlserver/datadog_checks/sqlserver/config_models/instance.py index 57a1002064276..daa899025fdee 100644 --- a/sqlserver/datadog_checks/sqlserver/config_models/instance.py +++ b/sqlserver/datadog_checks/sqlserver/config_models/instance.py @@ -195,7 +195,7 @@ class InstanceConfig(BaseModel): database_instance_collection_interval: Optional[float] = None db_fragmentation_object_names: Optional[tuple[str, ...]] = None dbm: Optional[bool] = None - deadlocks: Optional[Deadlocks] = None + deadlocks_collection: Optional[Deadlocks] = None disable_generic_tags: Optional[bool] = None driver: Optional[str] = None dsn: Optional[str] = None diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 6d3550651a39e..f1297f168da7e 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -921,7 +921,7 @@ def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): - dbm_instance['deadlocks'] = { + dbm_instance['deadlocks_collection'] = { 'enabled': True, 'run_sync': True, 'collection_interval': 0.1, From ca0ba5f8c7b2db3e177acbbf7350831de06fb4ec Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:37:30 +0200 Subject: [PATCH 50/92] refactor all deadlock funcs in the deadlock.py --- .../datadog_checks/sqlserver/activity.py | 27 +----------------- .../datadog_checks/sqlserver/deadlocks.py | 28 +++++++++++++++++++ sqlserver/tests/test_activity.py | 5 ++-- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index 37532face32ed..e2e6c0906a227 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -222,7 +222,7 @@ def run_job(self): if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: self._last_deadlocks_collection_time = time.time() try: - self._collect_deadlocks() + self._deadlocks.collect_deadlocks_wrapper() except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. @@ -231,31 +231,6 @@ def run_job(self): ) ) - @tracked_method(agent_check_getter=agent_check_getter) - def _collect_deadlocks(self): - deadlock_xmls_collected = self._deadlocks.collect_deadlocks() - deadlock_xmls = [] - total_number_of_characters = 0 - for i, deadlock in enumerate(deadlock_xmls_collected): - total_number_of_characters += len(deadlock) - if total_number_of_characters > self._deadlock_payload_max_bytes: - self._log.warning( - """We've dropped {} deadlocks from a total of {} deadlocks as the - max deadlock payload of {} bytes was exceeded.""".format( - len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes - ) - ) - break - else: - deadlock_xmls.append(deadlock) - - # Send payload only if deadlocks found - if deadlock_xmls: - deadlocks_event = self._create_deadlock_event(deadlock_xmls) - payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - self.log.debug("Deadlocks payload: %s", str(payload)) - self._check.database_monitoring_query_activity(payload) - @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): self.log.debug("collecting sql server current connections") diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index dfc6cd90e7c86..64c56332610da 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -7,11 +7,14 @@ from time import time from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata +from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY MAX_DEADLOCKS = 100 +def agent_check_getter(self): + return self._check class Deadlocks: @@ -83,3 +86,28 @@ def collect_deadlocks(self): converted_xmls.append(ET.tostring(root, encoding='unicode')) self._last_deadlock_timestamp = time() return converted_xmls + + @tracked_method(agent_check_getter=agent_check_getter) + def collect_deadlocks_wrapper(self): + deadlock_xmls_collected = self._deadlocks.collect_deadlocks() + deadlock_xmls = [] + total_number_of_characters = 0 + for i, deadlock in enumerate(deadlock_xmls_collected): + total_number_of_characters += len(deadlock) + if total_number_of_characters > self._deadlock_payload_max_bytes: + self._log.warning( + """We've dropped {} deadlocks from a total of {} deadlocks as the + max deadlock payload of {} bytes was exceeded.""".format( + len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes + ) + ) + break + else: + deadlock_xmls.append(deadlock) + + # Send payload only if deadlocks found + if deadlock_xmls: + deadlocks_event = self._create_deadlock_event(deadlock_xmls) + payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + self.log.debug("Deadlocks payload: %s", str(payload)) + self._check.database_monitoring_query_activity(payload) diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index f1297f168da7e..606dba76cde5e 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -960,8 +960,9 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlock_payloads)) except AssertionError as e: raise e - - deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] + assert isinstance(deadlock_payloads, dict), "Should have collected a dictionary" + #deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] + deadlocks = deadlock_payloads['sqlserver_deadlocks'] found = 0 for d in deadlocks: assert not "ERROR" in d, "Shouldn't have generated an error" From a0edfa6f06c0636c4993c11059dd3dc23ef805e7 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 13:44:17 +0200 Subject: [PATCH 51/92] refactoring --- .../datadog_checks/sqlserver/activity.py | 18 +------------- .../datadog_checks/sqlserver/deadlocks.py | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index e2e6c0906a227..a356509a7363f 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -222,7 +222,7 @@ def run_job(self): if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: self._last_deadlocks_collection_time = time.time() try: - self._deadlocks.collect_deadlocks_wrapper() + self._deadlocks.collect_deadlocks() except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. @@ -409,22 +409,6 @@ def _create_activity_event(self, active_sessions, active_connections): } return event - def _create_deadlock_event(self, deadlock_xmls): - event = { - "host": self._check.resolved_hostname, - "ddagentversion": datadog_agent.get_version(), - "ddsource": "sqlserver", - "dbm_type": "deadlocks", - "collection_interval": self._deadlocks_collection_interval, - "ddtags": self.tags, - "timestamp": time.time() * 1000, - 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), - 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), - "cloud_metadata": self._config.cloud_metadata, - "sqlserver_deadlocks": deadlock_xmls, - } - return event - @tracked_method(agent_check_getter=agent_check_getter) def collect_activity(self): """ diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 64c56332610da..35b28d29793d1 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -52,7 +52,7 @@ def obfuscate_xml(self, root): frame.text = self.obfuscate_no_except_wrapper(frame.text) return - def collect_deadlocks(self): + def _collect_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: self._log.debug("collecting sql server deadlocks") @@ -88,8 +88,8 @@ def collect_deadlocks(self): return converted_xmls @tracked_method(agent_check_getter=agent_check_getter) - def collect_deadlocks_wrapper(self): - deadlock_xmls_collected = self._deadlocks.collect_deadlocks() + def collect_deadlocks(self): + deadlock_xmls_collected = self._collect_deadlocks() deadlock_xmls = [] total_number_of_characters = 0 for i, deadlock in enumerate(deadlock_xmls_collected): @@ -111,3 +111,21 @@ def collect_deadlocks_wrapper(self): payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self.log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) + + def _create_deadlock_event(self, deadlock_xmls): + event = { + "host": self._check.resolved_hostname, + "ddagentversion": datadog_agent.get_version(), + "ddsource": "sqlserver", + "dbm_type": "deadlocks", + "collection_interval": self._deadlocks_collection_interval, + "ddtags": self.tags, + "timestamp": time.time() * 1000, + 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), + 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), + "cloud_metadata": self._config.cloud_metadata, + "sqlserver_deadlocks": deadlock_xmls, + } + return event + + From d843f58aa1523e4b56cdd7497679eb3ecf5c242e Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:42:57 +0200 Subject: [PATCH 52/92] deadlocks in separate job --- .../datadog_checks/sqlserver/activity.py | 68 +++---------------- .../datadog_checks/sqlserver/deadlocks.py | 27 +++++++- .../datadog_checks/sqlserver/sqlserver.py | 2 + 3 files changed, 37 insertions(+), 60 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/activity.py b/sqlserver/datadog_checks/sqlserver/activity.py index a356509a7363f..63186f680ccd1 100644 --- a/sqlserver/datadog_checks/sqlserver/activity.py +++ b/sqlserver/datadog_checks/sqlserver/activity.py @@ -14,7 +14,6 @@ from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION -from datadog_checks.sqlserver.deadlocks import Deadlocks from datadog_checks.sqlserver.utils import extract_sql_comments_and_procedure_name try: @@ -22,8 +21,7 @@ except ImportError: from ..stubs import datadog_agent -DEFAULT_ACTIVITY_COLLECTION_INTERVAL = 10 -DEFAULT_DEADLOCKS_COLLECTION_INTERVAL = 600 +DEFAULT_COLLECTION_INTERVAL = 10 MAX_PAYLOAD_BYTES = 19e6 CONNECTIONS_QUERY = """\ @@ -158,78 +156,32 @@ def __init__(self, check, config: SQLServerConfig): self.tags = [t for t in check.tags if not t.startswith('dd.internal')] self.log = check.log self._config = config - - self._last_deadlocks_collection_time = 0 - self._last_activity_collection_time = 0 - - self._deadlocks_collection_enabled = is_affirmative(config.deadlocks_config.get("enabled", True)) - self._deadlocks_collection_interval = config.deadlocks_config.get( - "collection_interval", DEFAULT_DEADLOCKS_COLLECTION_INTERVAL - ) - if self._deadlocks_collection_interval <= 0: - self._deadlocks_collection_interval = DEFAULT_DEADLOCKS_COLLECTION_INTERVAL - - self._activity_collection_enabled = is_affirmative(config.activity_config.get("enabled", True)) - self._activity_collection_interval = config.activity_config.get( - "collection_interval", DEFAULT_ACTIVITY_COLLECTION_INTERVAL + collection_interval = float( + self._config.activity_config.get('collection_interval', DEFAULT_COLLECTION_INTERVAL) ) - if self._activity_collection_enabled <= 0: - self._activity_collection_enabled = DEFAULT_ACTIVITY_COLLECTION_INTERVAL - - if self._deadlocks_collection_enabled and not self._activity_collection_enabled: - self.collection_interval = self._deadlocks_collection_interval - elif not self._deadlocks_collection_enabled and self._activity_collection_enabled: - self.collection_interval = self._activity_collection_interval - else: - self.collection_interval = min(self._deadlocks_collection_interval, self._activity_collection_interval) - - self.enabled = self._deadlocks_collection_enabled or self._activity_collection_enabled - + if collection_interval <= 0: + collection_interval = DEFAULT_COLLECTION_INTERVAL + self.collection_interval = collection_interval super(SqlserverActivity, self).__init__( check, run_sync=is_affirmative(self._config.activity_config.get('run_sync', False)), - enabled=self.enabled, + enabled=is_affirmative(self._config.activity_config.get('enabled', True)), expected_db_exceptions=(), min_collection_interval=self._config.min_collection_interval, dbms="sqlserver", - rate_limit=1 / float(self.collection_interval), + rate_limit=1 / float(collection_interval), job_name="query-activity", shutdown_callback=self._close_db_conn, ) self._conn_key_prefix = "dbm-activity-" self._activity_payload_max_bytes = MAX_PAYLOAD_BYTES self._exec_requests_cols_cached = None - self._deadlocks = Deadlocks(check, self._conn_key_prefix, self._config) - self._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES def _close_db_conn(self): pass def run_job(self): - elapsed_time_activity = time.time() - self._last_activity_collection_time - if self._activity_collection_enabled and elapsed_time_activity >= self._activity_collection_interval: - self._last_activity_collection_time = time.time() - try: - self.collect_activity() - except Exception as e: - self._log.error( - """An error occurred while collecting SQLServer activity. - This may be unavailable until the error is resolved. The error - {}""".format( - e - ) - ) - elapsed_time_deadlocks = time.time() - self._last_deadlocks_collection_time - if self._deadlocks_collection_enabled and elapsed_time_deadlocks >= self._deadlocks_collection_interval: - self._last_deadlocks_collection_time = time.time() - try: - self._deadlocks.collect_deadlocks() - except Exception as e: - self._log.error( - """An error occurred while collecting SQLServer deadlocks. - This may be unavailable until the error is resolved. The error - {}""".format( - e - ) - ) + self.collect_activity() @tracked_method(agent_check_getter=agent_check_getter) def _get_active_connections(self, cursor): @@ -398,7 +350,7 @@ def _create_activity_event(self, active_sessions, active_connections): "ddagentversion": datadog_agent.get_version(), "ddsource": "sqlserver", "dbm_type": "activity", - "collection_interval": self._activity_collection_interval, + "collection_interval": self.collection_interval, "ddtags": self.tags, "timestamp": time.time() * 1000, 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 35b28d29793d1..b132a410f874a 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -6,17 +6,24 @@ from datetime import datetime from time import time -from datadog_checks.base.utils.db.utils import obfuscate_sql_with_metadata +from datadog_checks.base import is_affirmative +from datadog_checks.base.utils.db.utils import DBMAsyncJob, obfuscate_sql_with_metadata from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY +try: + import datadog_agent +except ImportError: + from ..stubs import datadog_agent MAX_DEADLOCKS = 100 +MAX_PAYLOAD_BYTES = 19e6 +DEFAULT_COLLECTION_INTERVAL = 600 def agent_check_getter(self): return self._check -class Deadlocks: +class Deadlocks(DBMAsyncJob): def __init__(self, check, conn_prefix, config): self._check = check @@ -25,6 +32,22 @@ def __init__(self, check, conn_prefix, config): self._config = config self._last_deadlock_timestamp = time() self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) + self._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES + self.collection_interval = config.deadlocks_config.get( + "collection_interval", DEFAULT_COLLECTION_INTERVAL + ) + super(Deadlocks, self).__init__( + check, + run_sync=True, + enabled=self._config.deadlocks_config.get('enabled', True), + expected_db_exceptions=(), + min_collection_interval=self._config.min_collection_interval, + dbms="sqlserver", + rate_limit=1 / float(self.collection_interval), + job_name="deadlocks", + shutdown_callback=self._close_db_conn, + ) + def obfuscate_no_except_wrapper(self, sql_text): try: diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index e0927bcfd0405..8ca81a130ff3f 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -21,6 +21,7 @@ from datadog_checks.sqlserver.activity import SqlserverActivity from datadog_checks.sqlserver.agent_history import SqlserverAgentHistory from datadog_checks.sqlserver.config import SQLServerConfig +from datadog_checks.sqlserver.deadlocks import Deadlocks from datadog_checks.sqlserver.database_metrics import ( SqlserverAgentMetrics, SqlserverDatabaseBackupMetrics, @@ -139,6 +140,7 @@ def __init__(self, name, init_config, instances): self.sql_metadata = SqlserverMetadata(self, self._config) self.activity = SqlserverActivity(self, self._config) self.agent_history = SqlserverAgentHistory(self, self._config) + self.deadlocks = Deadlocks(self, self._config) self.static_info_cache = TTLCache( maxsize=100, From 7203f834194c2ab2ec6974c9ac31a76f27860fa0 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:49:06 +0200 Subject: [PATCH 53/92] separate deadlock tests --- sqlserver/tests/test_activity.py | 89 ++---------------------- sqlserver/tests/test_deadlocks.py | 110 ++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 83 deletions(-) create mode 100644 sqlserver/tests/test_deadlocks.py diff --git a/sqlserver/tests/test_activity.py b/sqlserver/tests/test_activity.py index 606dba76cde5e..7e8d73375afde 100644 --- a/sqlserver/tests/test_activity.py +++ b/sqlserver/tests/test_activity.py @@ -5,16 +5,14 @@ from __future__ import unicode_literals import concurrent -import copy import datetime import json -import logging import os import re import threading import time -import xml.etree.ElementTree as ET from concurrent.futures.thread import ThreadPoolExecutor +from copy import copy import mock import pytest @@ -26,7 +24,7 @@ from datadog_checks.sqlserver.activity import DM_EXEC_REQUESTS_COLS, _hash_to_hex from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION -from .utils import create_deadlock +from .conftest import DEFAULT_TIMEOUT try: import pyodbc @@ -56,7 +54,7 @@ def dbm_instance(instance_docker): instance_docker['query_metrics'] = {'enabled': False} instance_docker['procedure_metrics'] = {'enabled': False} instance_docker['collect_settings'] = {'enabled': False} - return copy.copy(instance_docker) + return copy(instance_docker) @pytest.mark.integration @@ -747,13 +745,13 @@ def _load_test_activity_json(filename): return json.load(f) -def _get_conn_for_user(instance_docker, user, timeout=1, _autocommit=False): +def _get_conn_for_user(instance_docker, user, _autocommit=False): # Make DB connection conn_str = 'DRIVER={};Server={};Database=master;UID={};PWD={};TrustServerCertificate=yes;'.format( instance_docker['driver'], instance_docker['host'], user, "Password12!" ) - conn = pyodbc.connect(conn_str, timeout=timeout, autocommit=_autocommit) - conn.timeout = timeout + conn = pyodbc.connect(conn_str, timeout=DEFAULT_TIMEOUT, autocommit=_autocommit) + conn.timeout = DEFAULT_TIMEOUT return conn @@ -908,78 +906,3 @@ def test_sanitize_activity_row(dbm_instance, row): row = check.activity._obfuscate_and_sanitize_row(row) assert isinstance(row['query_hash'], str) assert isinstance(row['query_plan_hash'], str) - -def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): - dd_run_check(check) - dbm_activity = aggregator.get_event_platform_events("dbm-activity") - matched = [] - for event in dbm_activity: - if "sqlserver_deadlocks" in event: - matched.append(event) - return matched - -@pytest.mark.integration -@pytest.mark.usefixtures('dd_environment') -def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): - dbm_instance['deadlocks_collection'] = { - 'enabled': True, - 'run_sync': True, - 'collection_interval': 0.1, - } - dbm_instance['query_activity']['enabled'] = False - - sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - - deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) - assert not deadlock_payloads, "shouldn't have sent an empty payload" - - created_deadlock = False - # Rarely instead of creating a deadlock one of the transactions time outs - for _ in range(0, 3): - bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) - fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) - created_deadlock = create_deadlock(bob_conn, fred_conn) - bob_conn.close() - fred_conn.close() - if created_deadlock: - break - try: - assert created_deadlock, "Couldn't create a deadlock, exiting" - except AssertionError as e: - raise e - - dbm_instance_no_dbm = copy.deepcopy(dbm_instance) - dbm_instance_no_dbm['dbm'] = False - sqlserver_check_no_dbm = SQLServer(CHECK_NAME, init_config, [dbm_instance_no_dbm]) - deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check_no_dbm, aggregator) - assert len(deadlock_payloads) == 0, "deadlock should be behind dbm" - - dbm_instance['dbm_enabled'] = True - deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) - try: - assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlock_payloads)) - except AssertionError as e: - raise e - assert isinstance(deadlock_payloads, dict), "Should have collected a dictionary" - #deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] - deadlocks = deadlock_payloads['sqlserver_deadlocks'] - found = 0 - for d in deadlocks: - assert not "ERROR" in d, "Shouldn't have generated an error" - try: - root = ET.fromstring(d) - except ET.ParseError as e: - logging.error("deadlock events: %s", str(deadlocks)) - raise e - process_list = root.find(".//process-list") - for process in process_list.findall('process'): - if ( - process.find('inputbuf').text - == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" - ): - found += 1 - try: - assert found == 1, "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) - except AssertionError as e: - logging.error("deadlock XML: %s", str(d)) - raise e diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py new file mode 100644 index 0000000000000..739c557b787f7 --- /dev/null +++ b/sqlserver/tests/test_deadlocks.py @@ -0,0 +1,110 @@ +# (C) Datadog, Inc. 2021-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from __future__ import unicode_literals + +import concurrent +import copy +import datetime +import json +import logging +import os +import re +import threading +import time +import xml.etree.ElementTree as ET +from concurrent.futures.thread import ThreadPoolExecutor + +import mock +import pytest +from dateutil import parser + +from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding +from datadog_checks.dev.ci import running_on_windows_ci +from datadog_checks.sqlserver import SQLServer +from datadog_checks.sqlserver.activity import DM_EXEC_REQUESTS_COLS, _hash_to_hex + +from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION +from .utils import create_deadlock + +try: + import pyodbc +except ImportError: + pyodbc = None + + +def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): + dd_run_check(check) + dbm_activity = aggregator.get_event_platform_events("dbm-activity") + matched = [] + for event in dbm_activity: + if "sqlserver_deadlocks" in event: + matched.append(event) + return matched + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): + dbm_instance['deadlocks_collection'] = { + 'enabled': True, + 'run_sync': True, + 'collection_interval': 0.1, + } + dbm_instance['query_activity']['enabled'] = False + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) + + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) + assert not deadlock_payloads, "shouldn't have sent an empty payload" + + created_deadlock = False + # Rarely instead of creating a deadlock one of the transactions time outs + for _ in range(0, 3): + bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) + fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) + created_deadlock = create_deadlock(bob_conn, fred_conn) + bob_conn.close() + fred_conn.close() + if created_deadlock: + break + try: + assert created_deadlock, "Couldn't create a deadlock, exiting" + except AssertionError as e: + raise e + + dbm_instance_no_dbm = copy.deepcopy(dbm_instance) + dbm_instance_no_dbm['dbm'] = False + sqlserver_check_no_dbm = SQLServer(CHECK_NAME, init_config, [dbm_instance_no_dbm]) + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check_no_dbm, aggregator) + assert len(deadlock_payloads) == 0, "deadlock should be behind dbm" + + dbm_instance['dbm_enabled'] = True + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) + try: + assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlock_payloads)) + except AssertionError as e: + raise e + assert isinstance(deadlock_payloads, dict), "Should have collected a dictionary" + #deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] + deadlocks = deadlock_payloads['sqlserver_deadlocks'] + found = 0 + for d in deadlocks: + assert not "ERROR" in d, "Shouldn't have generated an error" + try: + root = ET.fromstring(d) + except ET.ParseError as e: + logging.error("deadlock events: %s", str(deadlocks)) + raise e + process_list = root.find(".//process-list") + for process in process_list.findall('process'): + if ( + process.find('inputbuf').text + == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" + ): + found += 1 + try: + assert found == 1, "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) + except AssertionError as e: + logging.error("deadlock XML: %s", str(d)) + raise e From 5b04149491137a4a16da4fa9890ec5ccc11b15eb Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 14:50:04 +0200 Subject: [PATCH 54/92] linter --- .../datadog_checks/sqlserver/deadlocks.py | 18 ++++++------ sqlserver/datadog_checks/sqlserver/schemas.py | 9 +++--- sqlserver/tests/test_deadlocks.py | 28 ++++++++++--------- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index b132a410f874a..84dcc9fef68d3 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -20,11 +20,12 @@ MAX_PAYLOAD_BYTES = 19e6 DEFAULT_COLLECTION_INTERVAL = 600 + def agent_check_getter(self): return self._check -class Deadlocks(DBMAsyncJob): +class Deadlocks(DBMAsyncJob): def __init__(self, check, conn_prefix, config): self._check = check self._log = self._check.log @@ -33,9 +34,7 @@ def __init__(self, check, conn_prefix, config): self._last_deadlock_timestamp = time() self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) self._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES - self.collection_interval = config.deadlocks_config.get( - "collection_interval", DEFAULT_COLLECTION_INTERVAL - ) + self.collection_interval = config.deadlocks_config.get("collection_interval", DEFAULT_COLLECTION_INTERVAL) super(Deadlocks, self).__init__( check, run_sync=True, @@ -48,7 +47,6 @@ def __init__(self, check, conn_prefix, config): shutdown_callback=self._close_db_conn, ) - def obfuscate_no_except_wrapper(self, sql_text): try: sql_text = obfuscate_sql_with_metadata( @@ -85,7 +83,9 @@ def _collect_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute(DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) + cursor.execute( + DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) + ) results = cursor.fetchall() converted_xmls = [] for result in results: @@ -127,14 +127,14 @@ def collect_deadlocks(self): break else: deadlock_xmls.append(deadlock) - + # Send payload only if deadlocks found if deadlock_xmls: deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self.log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) - + def _create_deadlock_event(self, deadlock_xmls): event = { "host": self._check.resolved_hostname, @@ -150,5 +150,3 @@ def _create_deadlock_event(self, deadlock_xmls): "sqlserver_deadlocks": deadlock_xmls, } return event - - diff --git a/sqlserver/datadog_checks/sqlserver/schemas.py b/sqlserver/datadog_checks/sqlserver/schemas.py index 8d645796ef615..3bc009d206656 100644 --- a/sqlserver/datadog_checks/sqlserver/schemas.py +++ b/sqlserver/datadog_checks/sqlserver/schemas.py @@ -32,7 +32,6 @@ class SubmitData: - def __init__(self, submit_data_function, base_event, logger): self._submit_to_agent_queue = submit_data_function self._base_event = base_event @@ -88,10 +87,10 @@ def send_truncated_msg(self, db_name, time_spent): } db_info = self.db_info[db_name] event["metadata"] = [{**(db_info)}] - event["collection_errors"][0]["message"] = ( - "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( - self._total_columns_sent, time_spent, db_name - ) + event["collection_errors"][0][ + "message" + ] = "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( + self._total_columns_sent, time_spent, db_name ) json_event = json.dumps(event, default=default_json_event_encoding) self._log.debug("Reporting truncation of schema collection: {}".format(self.truncate(json_event))) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 739c557b787f7..d2731829413bb 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -43,6 +43,7 @@ def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): matched.append(event) return matched + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): @@ -52,12 +53,12 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): 'collection_interval': 0.1, } dbm_instance['query_activity']['enabled'] = False - + sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) - + deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) assert not deadlock_payloads, "shouldn't have sent an empty payload" - + created_deadlock = False # Rarely instead of creating a deadlock one of the transactions time outs for _ in range(0, 3): @@ -72,23 +73,25 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): assert created_deadlock, "Couldn't create a deadlock, exiting" except AssertionError as e: raise e - + dbm_instance_no_dbm = copy.deepcopy(dbm_instance) dbm_instance_no_dbm['dbm'] = False sqlserver_check_no_dbm = SQLServer(CHECK_NAME, init_config, [dbm_instance_no_dbm]) deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check_no_dbm, aggregator) assert len(deadlock_payloads) == 0, "deadlock should be behind dbm" - + dbm_instance['dbm_enabled'] = True deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) try: - assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format(len(deadlock_payloads)) + assert len(deadlock_payloads) == 1, "Should have collected one deadlock payload, but collected: {}.".format( + len(deadlock_payloads) + ) except AssertionError as e: raise e assert isinstance(deadlock_payloads, dict), "Should have collected a dictionary" - #deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] + # deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] deadlocks = deadlock_payloads['sqlserver_deadlocks'] - found = 0 + found = 0 for d in deadlocks: assert not "ERROR" in d, "Shouldn't have generated an error" try: @@ -98,13 +101,12 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): raise e process_list = root.find(".//process-list") for process in process_list.findall('process'): - if ( - process.find('inputbuf').text - == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;" - ): + if process.find('inputbuf').text == "UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;": found += 1 try: - assert found == 1, "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) + assert ( + found == 1 + ), "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) except AssertionError as e: logging.error("deadlock XML: %s", str(d)) raise e From e0fba54ae1d412a75cf3debe18768f7d5ebefba0 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:52:10 +0200 Subject: [PATCH 55/92] deadlocks as async job --- .../datadog_checks/sqlserver/deadlocks.py | 24 +++++++--- .../datadog_checks/sqlserver/sqlserver.py | 2 + sqlserver/tests/test_deadlocks.py | 48 ++++++++++--------- 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 84dcc9fef68d3..1c7dcb59ea41d 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -7,8 +7,11 @@ from time import time from datadog_checks.base import is_affirmative -from datadog_checks.base.utils.db.utils import DBMAsyncJob, obfuscate_sql_with_metadata +from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding, obfuscate_sql_with_metadata +from datadog_checks.base.utils.serialization import json from datadog_checks.base.utils.tracking import tracked_method +from datadog_checks.sqlserver.config import SQLServerConfig +from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY try: @@ -26,10 +29,10 @@ def agent_check_getter(self): class Deadlocks(DBMAsyncJob): - def __init__(self, check, conn_prefix, config): + def __init__(self, check, config: SQLServerConfig): + self.tags = [t for t in check.tags if not t.startswith('dd.internal')] self._check = check self._log = self._check.log - self._conn_key_prefix = conn_prefix self._config = config self._last_deadlock_timestamp = time() self._max_deadlocks = config.deadlocks_config.get("max_deadlocks", MAX_DEADLOCKS) @@ -46,6 +49,11 @@ def __init__(self, check, conn_prefix, config): job_name="deadlocks", shutdown_callback=self._close_db_conn, ) + self._conn_key_prefix = "dbm-deadlocks-" + + def _close_db_conn(self): + pass + def obfuscate_no_except_wrapper(self, sql_text): try: @@ -132,7 +140,7 @@ def collect_deadlocks(self): if deadlock_xmls: deadlocks_event = self._create_deadlock_event(deadlock_xmls) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - self.log.debug("Deadlocks payload: %s", str(payload)) + self._log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) def _create_deadlock_event(self, deadlock_xmls): @@ -141,12 +149,16 @@ def _create_deadlock_event(self, deadlock_xmls): "ddagentversion": datadog_agent.get_version(), "ddsource": "sqlserver", "dbm_type": "deadlocks", - "collection_interval": self._deadlocks_collection_interval, + "collection_interval": self.collection_interval, "ddtags": self.tags, - "timestamp": time.time() * 1000, + "timestamp": time() * 1000, 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, "sqlserver_deadlocks": deadlock_xmls, } return event + + def run_job(self): + self.collect_deadlocks() + diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 8ca81a130ff3f..6cbac5a76f8bf 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -177,6 +177,7 @@ def cancel(self): self.activity.cancel() self.sql_metadata.cancel() self._schemas.cancel() + self.deadlocks.cancel() def config_checks(self): if self._config.autodiscovery and self.instance.get("database"): @@ -791,6 +792,7 @@ def check(self, _): self.activity.run_job_loop(self.tags) self.sql_metadata.run_job_loop(self.tags) self._schemas.run_job_loop(self.tags) + self.deadlocks.run_job_loop(self.tags) else: self.log.debug("Skipping check") diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index d2731829413bb..3803c2cf858e6 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -4,28 +4,17 @@ from __future__ import unicode_literals -import concurrent -import copy -import datetime -import json import logging -import os -import re -import threading -import time import xml.etree.ElementTree as ET from concurrent.futures.thread import ThreadPoolExecutor -import mock import pytest -from dateutil import parser -from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding -from datadog_checks.dev.ci import running_on_windows_ci +from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.activity import DM_EXEC_REQUESTS_COLS, _hash_to_hex -from .common import CHECK_NAME, OPERATION_TIME_METRIC_NAME, SQLSERVER_MAJOR_VERSION +from .common import CHECK_NAME from .utils import create_deadlock try: @@ -33,6 +22,19 @@ except ImportError: pyodbc = None +@pytest.fixture +def dbm_instance(instance_docker): + instance_docker['dbm'] = True + # set a very small collection interval so the tests go fast + instance_docker['query_activity'] = { + 'enabled': False, + } + # do not need query_metrics for these tests + instance_docker['query_metrics'] = {'enabled': False} + instance_docker['procedure_metrics'] = {'enabled': False} + instance_docker['collect_settings'] = {'enabled': False} + instance_docker['deadlocks_collection'] = {'enabled': True, 'collection_interval': 0.1} + return copy(instance_docker) def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): dd_run_check(check) @@ -44,17 +46,19 @@ def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): return matched +def _get_conn_for_user(instance_docker, user, timeout=1, _autocommit=False): + # Make DB connection + conn_str = 'DRIVER={};Server={};Database=master;UID={};PWD={};TrustServerCertificate=yes;'.format( + instance_docker['driver'], instance_docker['host'], user, "Password12!" + ) + conn = pyodbc.connect(conn_str, timeout=timeout, autocommit=_autocommit) + conn.timeout = timeout + return conn + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): - dbm_instance['deadlocks_collection'] = { - 'enabled': True, - 'run_sync': True, - 'collection_interval': 0.1, - } - dbm_instance['query_activity']['enabled'] = False - - sqlserver_check = SQLServer(CHECK_NAME, init_config, [dbm_instance]) + sqlserver_check = SQLServer(CHECK_NAME, {}, [dbm_instance]) deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check, aggregator) assert not deadlock_payloads, "shouldn't have sent an empty payload" @@ -74,7 +78,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: raise e - dbm_instance_no_dbm = copy.deepcopy(dbm_instance) + dbm_instance_no_dbm = deepcopy(dbm_instance) dbm_instance_no_dbm['dbm'] = False sqlserver_check_no_dbm = SQLServer(CHECK_NAME, init_config, [dbm_instance_no_dbm]) deadlock_payloads = run_check_and_return_deadlock_payloads(dd_run_check, sqlserver_check_no_dbm, aggregator) From 7bf23cb46b27d03dbeffd88372e38d3002dec304 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:48:36 +0200 Subject: [PATCH 56/92] payload as a list of dicts --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 2 +- sqlserver/tests/test_deadlocks.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 1c7dcb59ea41d..6fde2df979d62 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -134,7 +134,7 @@ def collect_deadlocks(self): ) break else: - deadlock_xmls.append(deadlock) + deadlock_xmls.append({"xml": deadlock}) # Send payload only if deadlocks found if deadlock_xmls: diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 3803c2cf858e6..6a370eec50054 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -92,14 +92,13 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): ) except AssertionError as e: raise e - assert isinstance(deadlock_payloads, dict), "Should have collected a dictionary" - # deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] - deadlocks = deadlock_payloads['sqlserver_deadlocks'] + deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] found = 0 for d in deadlocks: assert not "ERROR" in d, "Shouldn't have generated an error" + assert isinstance(d, dict), "sqlserver_deadlocks should be a dictionary" try: - root = ET.fromstring(d) + root = ET.fromstring(d["xml"]) except ET.ParseError as e: logging.error("deadlock events: %s", str(deadlocks)) raise e From 1ddd032a0fe68ef27e517c5c4f3035e1baa3ff58 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:22:57 +0200 Subject: [PATCH 57/92] renamed query varaible --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 6 +++--- sqlserver/datadog_checks/sqlserver/queries.py | 2 +- sqlserver/tests/test_deadlocks.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 6fde2df979d62..4b9dd402426cf 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -12,7 +12,7 @@ from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION -from datadog_checks.sqlserver.queries import DETECT_DEADLOCK_QUERY +from datadog_checks.sqlserver.queries import DEADLOCK_QUERY try: import datadog_agent @@ -87,12 +87,12 @@ def _collect_deadlocks(self): self._log.debug("collecting sql server deadlocks") self._log.debug( "Running query [%s] with max deadlocks %s and timestamp %s", - DETECT_DEADLOCK_QUERY, + DEADLOCK_QUERY, self._max_deadlocks, self._last_deadlock_timestamp, ) cursor.execute( - DETECT_DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) + DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) ) results = cursor.fetchall() converted_xmls = [] diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 2ed895d4ff918..e00e188a68b31 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,7 +214,7 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ -DETECT_DEADLOCK_QUERY = """ +DEADLOCK_QUERY = """ SELECT TOP(?) xdr.value('@timestamp', 'datetime') AS [Date], xdr.query('.') AS [Event_Data] FROM (SELECT CAST([target_data] AS XML) AS Target_Data diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 6a370eec50054..63681970f54e7 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -12,7 +12,6 @@ from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer -from datadog_checks.sqlserver.activity import DM_EXEC_REQUESTS_COLS, _hash_to_hex from .common import CHECK_NAME from .utils import create_deadlock From c37ea6728e7e3e3c83b567c305a43458d42ba8d1 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:28:31 +0200 Subject: [PATCH 58/92] query signatures --- .../datadog_checks/sqlserver/deadlocks.py | 93 +++++++++++-------- sqlserver/tests/test_deadlocks.py | 26 +++++- sqlserver/tests/utils.py | 3 + 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 4b9dd402426cf..57809ac3ef31f 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -7,6 +7,7 @@ from time import time from datadog_checks.base import is_affirmative +from datadog_checks.base.utils.db.sql import compute_sql_signature from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding, obfuscate_sql_with_metadata from datadog_checks.base.utils.serialization import json from datadog_checks.base.utils.tracking import tracked_method @@ -62,26 +63,35 @@ def obfuscate_no_except_wrapper(self, sql_text): )['query'] except Exception as e: sql_text = "ERROR: failed to obfuscate" + error_text = "Failed to obfuscate sql text within a deadlock" if self._config.log_unobfuscated_queries: - self._log.warning("Failed to obfuscate sql text within a deadlock=[%s] | err=[%s]", sql_text, e) - else: - self._log.warning("Failed to obfuscate sql text within a deadlock | err=[%s]", e) + error_text += "=[%s]" % sql_text + error_text += " | err=[%s]" + self._log.error(error_text, e) return sql_text - def obfuscate_xml(self, root): + def _obfuscate_xml(self, root): process_list = root.find(".//process-list") if process_list is None: raise Exception("process-list element not found. The deadlock XML is in an unexpected format.") + query_signatures = dict() for process in process_list.findall('process'): for inputbuf in process.findall('.//inputbuf'): if inputbuf.text is not None: inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) + spid = process.get('spid') + if spid is not None: + if spid in query_signatures: + continue + query_signatures[spid] = compute_sql_signature(inputbuf.text) + else: + self._log.error("spid not found in process element. Skipping query signature computation.") for frame in process.findall('.//frame'): if frame.text is not None: frame.text = self.obfuscate_no_except_wrapper(frame.text) - return - - def _collect_deadlocks(self): + return query_signatures + + def _query_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: self._log.debug("collecting sql server deadlocks") @@ -94,51 +104,52 @@ def _collect_deadlocks(self): cursor.execute( DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) ) - results = cursor.fetchall() - converted_xmls = [] - for result in results: - try: - root = ET.fromstring(result[1]) - except Exception as e: - self._log.error( - """An error occurred while collecting SQLServer deadlocks. - One of the deadlock XMLs couldn't be parsed. The error: {}. XML: {}""".format( - e, result - ) - ) - continue - try: - self.obfuscate_xml(root) - except Exception as e: - error = "An error occurred while obfuscating SQLServer deadlocks. The error: {}".format(e) - self._log.error(error) - continue - - converted_xmls.append(ET.tostring(root, encoding='unicode')) - self._last_deadlock_timestamp = time() - return converted_xmls + return cursor.fetchall() + - @tracked_method(agent_check_getter=agent_check_getter) - def collect_deadlocks(self): - deadlock_xmls_collected = self._collect_deadlocks() - deadlock_xmls = [] + def _create_deadlock_rows(self): + results = self._query_deadlocks() + deadlock_events = [] total_number_of_characters = 0 - for i, deadlock in enumerate(deadlock_xmls_collected): - total_number_of_characters += len(deadlock) + for i, result in enumerate(results): + try: + root = ET.fromstring(result) + except Exception as e: + self._log.error( + """An error occurred while collecting SQLServer deadlocks. + One of the deadlock XMLs couldn't be parsed. The error: {}. XML: {}""".format( + e, result + ) + ) + continue + query_signatures = dict() + try: + query_signatures = self._obfuscate_xml(root) + except Exception as e: + error = "An error occurred while obfuscating SQLServer deadlocks. The error: {}".format(e) + self._log.error(error) + continue + + total_number_of_characters += len(result) + len(query_signatures) if total_number_of_characters > self._deadlock_payload_max_bytes: self._log.warning( """We've dropped {} deadlocks from a total of {} deadlocks as the max deadlock payload of {} bytes was exceeded.""".format( - len(deadlock_xmls) - i, len(deadlock_xmls), self._deadlock_payload_max_bytes + len(results) - i, len(results), self._deadlock_payload_max_bytes ) ) break - else: - deadlock_xmls.append({"xml": deadlock}) + deadlock_events.append({"xml": ET.tostring(root, encoding='unicode'), "query_signatures": query_signatures}) + self._last_deadlock_timestamp = time() + return deadlock_events + + @tracked_method(agent_check_getter=agent_check_getter) + def collect_deadlocks(self): + rows = self._create_deadlock_rows() # Send payload only if deadlocks found - if deadlock_xmls: - deadlocks_event = self._create_deadlock_event(deadlock_xmls) + if rows: + deadlocks_event = self._create_deadlock_event(rows) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) self._log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 63681970f54e7..cad561334c092 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -6,12 +6,13 @@ import logging import xml.etree.ElementTree as ET -from concurrent.futures.thread import ThreadPoolExecutor - +import os import pytest from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer +from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES +from mock import patch, MagicMock from .common import CHECK_NAME from .utils import create_deadlock @@ -112,3 +113,24 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): except AssertionError as e: logging.error("deadlock XML: %s", str(d)) raise e + +DEADLOCKS_PLAN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "deadlocks") + +def _load_test_deadlocks_xml(filename): + with open(os.path.join(DEADLOCKS_PLAN_DIR, filename), 'r') as f: + return f.read() + +def test__create_deadlock_rows(): + deadlocks_obj = None + with patch.object(Deadlocks, '__init__', return_value=None): + deadlocks_obj = Deadlocks(None, None) + deadlocks_obj._check = MagicMock() + deadlocks_obj._log = MagicMock() + deadlocks_obj._config = MagicMock() + deadlocks_obj._config.obfuscator_options = {} + deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES + xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") + with patch.object(Deadlocks, '_query_deadlocks', return_value=[xml]): + rows = deadlocks_obj._create_deadlock_rows() + assert len(rows) == 1, "Should have created one deadlock row" + assert len(rows[0]["query_signatures"]) == 2, "Should have two query signatures" \ No newline at end of file diff --git a/sqlserver/tests/utils.py b/sqlserver/tests/utils.py index 184ce1c267a15..74bba55da3d13 100644 --- a/sqlserver/tests/utils.py +++ b/sqlserver/tests/utils.py @@ -292,6 +292,7 @@ def create_deadlock(bob_conn, fred_conn): return "deadlock" in exception_1_text or "deadlock" in exception_2_text +<<<<<<< HEAD def deep_compare(obj1, obj2): if isinstance(obj1, dict) and isinstance(obj2, dict): if set(obj1.keys()) != set(obj2.keys()): @@ -303,3 +304,5 @@ def deep_compare(obj1, obj2): return all(any(deep_compare(item1, item2) for item2 in obj2) for item1 in obj1) else: return obj1 == obj2 +======= +>>>>>>> dc91830ca1 (query signatures) From 26b7574906d9127734aa10d7e9a13908aadfef9a Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:53:57 +0200 Subject: [PATCH 59/92] fixed test cases --- .../datadog_checks/sqlserver/deadlocks.py | 6 +- .../deadlocks/sqlserver_deadlock_event.xml | 140 ++++++++++++++++++ sqlserver/tests/test_deadlocks.py | 2 +- 3 files changed, 144 insertions(+), 4 deletions(-) create mode 100644 sqlserver/tests/deadlocks/sqlserver_deadlock_event.xml diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 57809ac3ef31f..5e3dcef1e12db 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -113,7 +113,7 @@ def _create_deadlock_rows(self): total_number_of_characters = 0 for i, result in enumerate(results): try: - root = ET.fromstring(result) + root = ET.fromstring(result[1]) except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. @@ -154,7 +154,7 @@ def collect_deadlocks(self): self._log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) - def _create_deadlock_event(self, deadlock_xmls): + def _create_deadlock_event(self, deadlock_rows): event = { "host": self._check.resolved_hostname, "ddagentversion": datadog_agent.get_version(), @@ -166,7 +166,7 @@ def _create_deadlock_event(self, deadlock_xmls): 'sqlserver_version': self._check.static_info_cache.get(STATIC_INFO_VERSION, ""), 'sqlserver_engine_edition': self._check.static_info_cache.get(STATIC_INFO_ENGINE_EDITION, ""), "cloud_metadata": self._config.cloud_metadata, - "sqlserver_deadlocks": deadlock_xmls, + "sqlserver_deadlocks": deadlock_rows, } return event diff --git a/sqlserver/tests/deadlocks/sqlserver_deadlock_event.xml b/sqlserver/tests/deadlocks/sqlserver_deadlock_event.xml new file mode 100644 index 0000000000000..0384345f9c6eb --- /dev/null +++ b/sqlserver/tests/deadlocks/sqlserver_deadlock_event.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + unknown + + + unknown + + + + update [datadog_test-1].[dbo].[t] set n=1 where n=1 + + rollback + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + unknown + + + unknown + + + + begin TRANSACTION + + update [datadog_test-1].[dbo].[t] set n=1 where n=1 + + update [datadog_test-1].[dbo].[t] set n=2 where n=2 + + rollback + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index cad561334c092..acac55c1a6d68 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -130,7 +130,7 @@ def test__create_deadlock_rows(): deadlocks_obj._config.obfuscator_options = {} deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") - with patch.object(Deadlocks, '_query_deadlocks', return_value=[xml]): + with patch.object(Deadlocks, '_query_deadlocks', return_value=[["date placeholder", xml]]): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" assert len(rows[0]["query_signatures"]) == 2, "Should have two query signatures" \ No newline at end of file From ceb1d88d041077cf3b0e254102c1110abe3022d7 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:03:45 +0200 Subject: [PATCH 60/92] aux functions from utils to deadlock_test --- sqlserver/tests/test_deadlocks.py | 51 +++++++++++++++++++++++++- sqlserver/tests/utils.py | 61 ------------------------------- 2 files changed, 49 insertions(+), 63 deletions(-) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index acac55c1a6d68..3564b04047aa6 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals +import concurrent import logging import xml.etree.ElementTree as ET import os @@ -13,9 +14,9 @@ from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES from mock import patch, MagicMock +from threading import Event from .common import CHECK_NAME -from .utils import create_deadlock try: import pyodbc @@ -55,6 +56,52 @@ def _get_conn_for_user(instance_docker, user, timeout=1, _autocommit=False): conn.timeout = timeout return conn +def _run_first_deadlock_query(conn, event1, event2): + exception_text = "" + try: + conn.cursor().execute("BEGIN TRAN foo;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 1;") + event1.set() + event2.wait() + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") + except Exception as e: + # Exception is expected due to a deadlock + exception_text = str(e) + pass + conn.commit() + return exception_text + + +def _run_second_deadlock_query(conn, event1, event2): + exception_text = "" + try: + event1.wait() + conn.cursor().execute("BEGIN TRAN bar;") + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 2;") + event2.set() + conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") + except Exception as e: + # Exception is expected due to a deadlock + exception_text = str(e) + pass + conn.commit() + return exception_text + + +def _create_deadlock(bob_conn, fred_conn): + executor = concurrent.futures.thread.ThreadPoolExecutor(2) + event1 = Event() + event2 = Event() + + futures_first_query = executor.submit(_run_first_deadlock_query, bob_conn, event1, event2) + futures_second_query = executor.submit(_run_second_deadlock_query, fred_conn, event1, event2) + exception_1_text = futures_first_query.result() + exception_2_text = futures_second_query.result() + executor.shutdown() + return "deadlock" in exception_1_text or "deadlock" in exception_2_text + + + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): @@ -68,7 +115,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): for _ in range(0, 3): bob_conn = _get_conn_for_user(dbm_instance, 'bob', 3) fred_conn = _get_conn_for_user(dbm_instance, 'fred', 3) - created_deadlock = create_deadlock(bob_conn, fred_conn) + created_deadlock = _create_deadlock(bob_conn, fred_conn) bob_conn.close() fred_conn.close() if created_deadlock: diff --git a/sqlserver/tests/utils.py b/sqlserver/tests/utils.py index 74bba55da3d13..c5ad7278bf29a 100644 --- a/sqlserver/tests/utils.py +++ b/sqlserver/tests/utils.py @@ -1,13 +1,11 @@ # (C) Datadog, Inc. 2019-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) -import concurrent import os import string import threading from copy import copy from random import choice, randint, shuffle -from threading import Event import pyodbc import pytest @@ -247,62 +245,3 @@ def normalize_indexes_columns(actual_payload): index['column_names'] = ','.join(sorted_columns) -def run_first_deadlock_query(conn, event1, event2): - exception_text = "" - try: - conn.cursor().execute("BEGIN TRAN foo;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 1;") - event1.set() - event2.wait() - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2;") - except Exception as e: - # Exception is expected due to a deadlock - exception_text = str(e) - pass - conn.commit() - return exception_text - - -def run_second_deadlock_query(conn, event1, event2): - exception_text = "" - try: - event1.wait() - conn.cursor().execute("BEGIN TRAN bar;") - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 10 WHERE a = 2;") - event2.set() - conn.cursor().execute("UPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1;") - except Exception as e: - # Exception is expected due to a deadlock - exception_text = str(e) - pass - conn.commit() - return exception_text - - -def create_deadlock(bob_conn, fred_conn): - executor = concurrent.futures.thread.ThreadPoolExecutor(2) - event1 = Event() - event2 = Event() - - futures_first_query = executor.submit(run_first_deadlock_query, bob_conn, event1, event2) - futures_second_query = executor.submit(run_second_deadlock_query, fred_conn, event1, event2) - exception_1_text = futures_first_query.result() - exception_2_text = futures_second_query.result() - executor.shutdown() - return "deadlock" in exception_1_text or "deadlock" in exception_2_text - - -<<<<<<< HEAD -def deep_compare(obj1, obj2): - if isinstance(obj1, dict) and isinstance(obj2, dict): - if set(obj1.keys()) != set(obj2.keys()): - return False - return all(deep_compare(obj1[key], obj2[key]) for key in obj1) - elif isinstance(obj1, list) and isinstance(obj2, list): - if len(obj1) != len(obj2): - return False - return all(any(deep_compare(item1, item2) for item2 in obj2) for item1 in obj1) - else: - return obj1 == obj2 -======= ->>>>>>> dc91830ca1 (query signatures) From 4a59ec05b5b6d0cf19f0e888e5883f0aa474dfcf Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:00:42 +0200 Subject: [PATCH 61/92] change query signature struct --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 4 ++-- sqlserver/tests/test_deadlocks.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 5e3dcef1e12db..4e3cb395be560 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -74,7 +74,7 @@ def _obfuscate_xml(self, root): process_list = root.find(".//process-list") if process_list is None: raise Exception("process-list element not found. The deadlock XML is in an unexpected format.") - query_signatures = dict() + query_signatures = [] for process in process_list.findall('process'): for inputbuf in process.findall('.//inputbuf'): if inputbuf.text is not None: @@ -83,7 +83,7 @@ def _obfuscate_xml(self, root): if spid is not None: if spid in query_signatures: continue - query_signatures[spid] = compute_sql_signature(inputbuf.text) + query_signatures.append({"spid": spid, "signature": compute_sql_signature(inputbuf.text)}) else: self._log.error("spid not found in process element. Skipping query signature computation.") for frame in process.findall('.//frame'): diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 3564b04047aa6..a42d642d4f9e0 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -180,4 +180,7 @@ def test__create_deadlock_rows(): with patch.object(Deadlocks, '_query_deadlocks', return_value=[["date placeholder", xml]]): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" - assert len(rows[0]["query_signatures"]) == 2, "Should have two query signatures" \ No newline at end of file + row = rows[0] + query_signatures = row["query_signatures"] + assert len(query_signatures) == 2, "Should have two query signatures" + assert "spid" in query_signatures[0], "Should have spid in query signatures" \ No newline at end of file From 561b66fea3e0c5da9abd40ad8227c3686c712eaa Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Thu, 19 Sep 2024 17:54:31 +0200 Subject: [PATCH 62/92] spid as int --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 5 +++++ sqlserver/tests/test_deadlocks.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 4e3cb395be560..2ff30aced2665 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -81,6 +81,11 @@ def _obfuscate_xml(self, root): inputbuf.text = self.obfuscate_no_except_wrapper(inputbuf.text) spid = process.get('spid') if spid is not None: + try: + spid = int(spid) + except ValueError: + self._log.error("spid not an integer. Skipping query signature computation.") + continue if spid in query_signatures: continue query_signatures.append({"spid": spid, "signature": compute_sql_signature(inputbuf.text)}) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index a42d642d4f9e0..746faa6047857 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -183,4 +183,6 @@ def test__create_deadlock_rows(): row = rows[0] query_signatures = row["query_signatures"] assert len(query_signatures) == 2, "Should have two query signatures" - assert "spid" in query_signatures[0], "Should have spid in query signatures" \ No newline at end of file + first_mapping = query_signatures[0] + assert "spid" in first_mapping, "Should have spid in query signatures" + assert isinstance(first_mapping["spid"], int), "spid should be an int" \ No newline at end of file From 19f689dee606b0ee1a8507688f7c9bdc76945431 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Fri, 20 Sep 2024 12:36:51 +0200 Subject: [PATCH 63/92] db rows in dict --- .../datadog_checks/sqlserver/deadlocks.py | 20 +++++++++---------- sqlserver/datadog_checks/sqlserver/queries.py | 5 +++-- sqlserver/tests/test_deadlocks.py | 3 ++- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 2ff30aced2665..04b4967870da3 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -13,17 +13,16 @@ from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION -from datadog_checks.sqlserver.queries import DEADLOCK_QUERY +from datadog_checks.sqlserver.queries import DEADLOCK_QUERY, DEADLOCK_XML_COL try: import datadog_agent except ImportError: from ..stubs import datadog_agent +DEFAULT_COLLECTION_INTERVAL = 600 MAX_DEADLOCKS = 100 MAX_PAYLOAD_BYTES = 19e6 -DEFAULT_COLLECTION_INTERVAL = 600 - def agent_check_getter(self): return self._check @@ -109,21 +108,22 @@ def _query_deadlocks(self): cursor.execute( DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) ) - return cursor.fetchall() + columns = [column[0] for column in cursor.description] + return [dict(zip(columns, row)) for row in cursor.fetchall()] def _create_deadlock_rows(self): - results = self._query_deadlocks() + db_rows = self._query_deadlocks() deadlock_events = [] total_number_of_characters = 0 - for i, result in enumerate(results): + for i, row in enumerate(db_rows): try: - root = ET.fromstring(result[1]) + root = ET.fromstring(row[DEADLOCK_XML_COL]) except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. One of the deadlock XMLs couldn't be parsed. The error: {}. XML: {}""".format( - e, result + e, row ) ) continue @@ -135,12 +135,12 @@ def _create_deadlock_rows(self): self._log.error(error) continue - total_number_of_characters += len(result) + len(query_signatures) + total_number_of_characters += len(row) + len(query_signatures) if total_number_of_characters > self._deadlock_payload_max_bytes: self._log.warning( """We've dropped {} deadlocks from a total of {} deadlocks as the max deadlock payload of {} bytes was exceeded.""".format( - len(results) - i, len(results), self._deadlock_payload_max_bytes + len(db_rows) - i, len(db_rows), self._deadlock_payload_max_bytes ) ) break diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index e00e188a68b31..8e06619ab0eaf 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,9 +214,10 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ +DEADLOCK_XML_COL = "event_xml" DEADLOCK_QUERY = """ SELECT TOP(?) xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [Event_Data] + xdr.query('.') AS [%s] FROM (SELECT CAST([target_data] AS XML) AS Target_Data FROM sys.dm_xe_session_targets AS xt INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address @@ -225,7 +226,7 @@ ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE xdr.value('@timestamp', 'datetime') >= DATEADD(SECOND, ?, GETDATE()) -ORDER BY [Date] DESC;""" +ORDER BY [Date] DESC;""" % DEADLOCK_XML_COL def get_query_ao_availability_groups(sqlserver_major_version): diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 746faa6047857..ec8dc10ea05d4 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -13,6 +13,7 @@ from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES +from datadog_checks.sqlserver.queries import DEADLOCK_XML_COL from mock import patch, MagicMock from threading import Event @@ -177,7 +178,7 @@ def test__create_deadlock_rows(): deadlocks_obj._config.obfuscator_options = {} deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") - with patch.object(Deadlocks, '_query_deadlocks', return_value=[["date placeholder", xml]]): + with patch.object(Deadlocks, '_query_deadlocks', return_value=[{ DEADLOCK_XML_COL: xml }]): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" row = rows[0] From 2c21aaaf6c3dc2c32aab97107e091261b48c7dcf Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:55:13 +0200 Subject: [PATCH 64/92] deadlock timestamp --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 14 +++++++++++--- sqlserver/datadog_checks/sqlserver/queries.py | 9 +++++---- sqlserver/tests/test_deadlocks.py | 9 +++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 04b4967870da3..7e1da376f3e45 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -13,7 +13,7 @@ from datadog_checks.base.utils.tracking import tracked_method from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION -from datadog_checks.sqlserver.queries import DEADLOCK_QUERY, DEADLOCK_XML_COL +from datadog_checks.sqlserver.queries import DEADLOCK_QUERY, DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS try: import datadog_agent @@ -24,6 +24,10 @@ MAX_DEADLOCKS = 100 MAX_PAYLOAD_BYTES = 19e6 +PAYLOAD_TIMESTAMP = "deadlock_timestamp" +PAYLOAD_QUERY_SIGNATURE = "query_signatures" +PAYLOAD_XML = "xml" + def agent_check_getter(self): return self._check @@ -118,7 +122,7 @@ def _create_deadlock_rows(self): total_number_of_characters = 0 for i, row in enumerate(db_rows): try: - root = ET.fromstring(row[DEADLOCK_XML_COL]) + root = ET.fromstring(row[DEADLOCK_XML_ALIAS]) except Exception as e: self._log.error( """An error occurred while collecting SQLServer deadlocks. @@ -145,7 +149,11 @@ def _create_deadlock_rows(self): ) break - deadlock_events.append({"xml": ET.tostring(root, encoding='unicode'), "query_signatures": query_signatures}) + deadlock_events.append({ + PAYLOAD_TIMESTAMP: row[DEADLOCK_TIMESTAMP_ALIAS], + PAYLOAD_XML: ET.tostring(root, encoding='unicode'), + PAYLOAD_QUERY_SIGNATURE: query_signatures + }) self._last_deadlock_timestamp = time() return deadlock_events diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 8e06619ab0eaf..cbcf580665b9a 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -214,10 +214,11 @@ FK.name, FK.parent_object_id, FK.referenced_object_id; """ -DEADLOCK_XML_COL = "event_xml" +DEADLOCK_TIMESTAMP_ALIAS = "timestamp" +DEADLOCK_XML_ALIAS = "event_xml" DEADLOCK_QUERY = """ -SELECT TOP(?) xdr.value('@timestamp', 'datetime') AS [Date], - xdr.query('.') AS [%s] +SELECT TOP(?) xdr.value('@timestamp', 'datetime') AS [{timestamp}], + xdr.query('.') AS [{xml}] FROM (SELECT CAST([target_data] AS XML) AS Target_Data FROM sys.dm_xe_session_targets AS xt INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address @@ -226,7 +227,7 @@ ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE xdr.value('@timestamp', 'datetime') >= DATEADD(SECOND, ?, GETDATE()) -ORDER BY [Date] DESC;""" % DEADLOCK_XML_COL +;""".format(**{"timestamp": DEADLOCK_TIMESTAMP_ALIAS, "xml": DEADLOCK_XML_ALIAS}) def get_query_ao_availability_groups(sqlserver_major_version): diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index ec8dc10ea05d4..5f3d303f7076c 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -12,8 +12,8 @@ from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer -from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES -from datadog_checks.sqlserver.queries import DEADLOCK_XML_COL +from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES, PAYLOAD_QUERY_SIGNATURE, PAYLOAD_TIMESTAMP, PAYLOAD_XML +from datadog_checks.sqlserver.queries import DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS from mock import patch, MagicMock from threading import Event @@ -178,11 +178,12 @@ def test__create_deadlock_rows(): deadlocks_obj._config.obfuscator_options = {} deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") - with patch.object(Deadlocks, '_query_deadlocks', return_value=[{ DEADLOCK_XML_COL: xml }]): + with patch.object(Deadlocks, '_query_deadlocks', return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024", DEADLOCK_XML_ALIAS: xml}]): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" row = rows[0] - query_signatures = row["query_signatures"] + assert row[PAYLOAD_TIMESTAMP], "Should have a timestamp" + query_signatures = row[PAYLOAD_QUERY_SIGNATURE] assert len(query_signatures) == 2, "Should have two query signatures" first_mapping = query_signatures[0] assert "spid" in first_mapping, "Should have spid in query signatures" From f93748cb82c2a4ac6b8da08938d3607247472bac Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:10:33 +0200 Subject: [PATCH 65/92] read date in test case --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 1 + sqlserver/tests/test_deadlocks.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 7e1da376f3e45..3dd67c14fded8 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -164,6 +164,7 @@ def collect_deadlocks(self): if rows: deadlocks_event = self._create_deadlock_event(rows) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) + breakpoint() self._log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 5f3d303f7076c..91b52e79ec8dc 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -178,7 +178,7 @@ def test__create_deadlock_rows(): deadlocks_obj._config.obfuscator_options = {} deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") - with patch.object(Deadlocks, '_query_deadlocks', return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024", DEADLOCK_XML_ALIAS: xml}]): + with patch.object(Deadlocks, '_query_deadlocks', return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024-09-20T12:07:16.647000", DEADLOCK_XML_ALIAS: xml}]): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" row = rows[0] From 843907742c218b3c2c2e7b661f0f9d5a1d85b9ac Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:27:53 +0200 Subject: [PATCH 66/92] fixed test_deadlock_xml_bad_format --- .../datadog_checks/sqlserver/deadlocks.py | 1 - sqlserver/tests/test_deadlocks.py | 43 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 3dd67c14fded8..7e1da376f3e45 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -164,7 +164,6 @@ def collect_deadlocks(self): if rows: deadlocks_event = self._create_deadlock_event(rows) payload = json.dumps(deadlocks_event, default=default_json_event_encoding) - breakpoint() self._log.debug("Deadlocks payload: %s", str(payload)) self._check.database_monitoring_query_activity(payload) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 91b52e79ec8dc..32ba3f9e485c4 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -167,6 +167,21 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): def _load_test_deadlocks_xml(filename): with open(os.path.join(DEADLOCKS_PLAN_DIR, filename), 'r') as f: return f.read() + +@pytest.fixture +def deadlocks_collection_instance(instance_docker): + instance_docker['dbm'] = True + instance_docker['deadlocks_collection'] = { + 'enabled': True, + 'collection_interval': 1.0, + } + instance_docker['min_collection_interval'] = 1 + # do not need other dbm metrics + instance_docker['query_activity'] = {'enabled': False} + instance_docker['query_metrics'] = {'enabled': False} + instance_docker['procedure_metrics'] = {'enabled': False} + instance_docker['collect_settings'] = {'enabled': False} + return copy(instance_docker) def test__create_deadlock_rows(): deadlocks_obj = None @@ -187,4 +202,30 @@ def test__create_deadlock_rows(): assert len(query_signatures) == 2, "Should have two query signatures" first_mapping = query_signatures[0] assert "spid" in first_mapping, "Should have spid in query signatures" - assert isinstance(first_mapping["spid"], int), "spid should be an int" \ No newline at end of file + assert isinstance(first_mapping["spid"], int), "spid should be an int" + +def test_deadlock_xml_bad_format(deadlocks_collection_instance): + test_xml = """ + + + + + + + + + + + + + """ + check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) + deadlocks = check.deadlocks + root = ET.fromstring(test_xml) + try: + deadlocks._obfuscate_xml(root) + except Exception as e: + result = str(e) + assert result == "process-list element not found. The deadlock XML is in an unexpected format." + else: + assert False, "Should have raised an exception for bad XML format" From f37d3a42624efbc084aa690a57aadad3da421c13 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:29:52 +0200 Subject: [PATCH 67/92] refactored test__create_deadlock_rows --- sqlserver/tests/test_deadlocks.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 32ba3f9e485c4..f7a0da9a63eff 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -183,15 +183,9 @@ def deadlocks_collection_instance(instance_docker): instance_docker['collect_settings'] = {'enabled': False} return copy(instance_docker) -def test__create_deadlock_rows(): - deadlocks_obj = None - with patch.object(Deadlocks, '__init__', return_value=None): - deadlocks_obj = Deadlocks(None, None) - deadlocks_obj._check = MagicMock() - deadlocks_obj._log = MagicMock() - deadlocks_obj._config = MagicMock() - deadlocks_obj._config.obfuscator_options = {} - deadlocks_obj._deadlock_payload_max_bytes = MAX_PAYLOAD_BYTES +def test__create_deadlock_rows(deadlocks_collection_instance): + check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) + deadlocks_obj = check.deadlocks xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") with patch.object(Deadlocks, '_query_deadlocks', return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024-09-20T12:07:16.647000", DEADLOCK_XML_ALIAS: xml}]): rows = deadlocks_obj._create_deadlock_rows() @@ -220,10 +214,10 @@ def test_deadlock_xml_bad_format(deadlocks_collection_instance): """ check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) - deadlocks = check.deadlocks + deadlocks_obj = check.deadlocks root = ET.fromstring(test_xml) try: - deadlocks._obfuscate_xml(root) + deadlocks_obj._obfuscate_xml(root) except Exception as e: result = str(e) assert result == "process-list element not found. The deadlock XML is in an unexpected format." From 86f180305fbe31dfde60a9a78e1c1757c945183d Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:24:21 +0200 Subject: [PATCH 68/92] fixed test_deadlock_calls_obfuscator --- sqlserver/tests/test_deadlocks.py | 78 +++++++++++++++++++++++++++++++ sqlserver/tests/test_unit.py | 1 - 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index f7a0da9a63eff..448f2bac7ab59 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -9,6 +9,7 @@ import xml.etree.ElementTree as ET import os import pytest +import re from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer @@ -223,3 +224,80 @@ def test_deadlock_xml_bad_format(deadlocks_collection_instance): assert result == "process-list element not found. The deadlock XML is in an unexpected format." else: assert False, "Should have raised an exception for bad XML format" + + +def test_deadlock_calls_obfuscator(deadlocks_collection_instance): + test_xml = """ + + + + + + + + + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 100 WHERE a = 2; + + + + \nunknown + \nunknown + + \nUPDATE [datadog_test-1].dbo.deadlocks SET b = b + 20 WHERE a = 1; + + + + + + + """ + + expected_xml_string = ( + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + "obfuscated " + "obfuscated " + " " + "obfuscated " + " " + " " + " " + "obfuscated " + "obfuscated " + " " + "obfuscated " + " " + " " + " " + " " + " " + "" + ) + + with patch( + 'datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated" + ): + check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) + deadlocks_obj = check.deadlocks + root = ET.fromstring(test_xml) + deadlocks_obj._obfuscate_xml(root) + result_string = ET.tostring(root, encoding='unicode') + result_string = result_string.replace('\t', '').replace('\n', '') + result_string = re.sub(r'\s{2,}', ' ', result_string) + assert expected_xml_string == result_string + diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 813f8a739c293..bca846b6a434c 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -872,4 +872,3 @@ def test_exception_handling_by_do_for_dbs(instance_docker): 'datadog_checks.sqlserver.utils.is_azure_sql_database', return_value={} ): schemas._fetch_for_databases() - From d0783aedb0c1d92e5d9391e31a56e84362b81e4a Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:25:11 +0200 Subject: [PATCH 69/92] linster --- .../datadog_checks/sqlserver/deadlocks.py | 30 ++++++++--------- sqlserver/datadog_checks/sqlserver/queries.py | 4 ++- sqlserver/tests/test_deadlocks.py | 32 +++++++++++++------ 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 7e1da376f3e45..2360ce41c4c31 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -28,6 +28,7 @@ PAYLOAD_QUERY_SIGNATURE = "query_signatures" PAYLOAD_XML = "xml" + def agent_check_getter(self): return self._check @@ -54,11 +55,10 @@ def __init__(self, check, config: SQLServerConfig): shutdown_callback=self._close_db_conn, ) self._conn_key_prefix = "dbm-deadlocks-" - + def _close_db_conn(self): pass - def obfuscate_no_except_wrapper(self, sql_text): try: sql_text = obfuscate_sql_with_metadata( @@ -93,12 +93,12 @@ def _obfuscate_xml(self, root): continue query_signatures.append({"spid": spid, "signature": compute_sql_signature(inputbuf.text)}) else: - self._log.error("spid not found in process element. Skipping query signature computation.") + self._log.error("spid not found in process element. Skipping query signature computation.") for frame in process.findall('.//frame'): if frame.text is not None: frame.text = self.obfuscate_no_except_wrapper(frame.text) return query_signatures - + def _query_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): with self._check.connection.get_managed_cursor(key_prefix=self._conn_key_prefix) as cursor: @@ -109,12 +109,9 @@ def _query_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute( - DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time())) - ) + cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] - def _create_deadlock_rows(self): db_rows = self._query_deadlocks() @@ -138,7 +135,7 @@ def _create_deadlock_rows(self): error = "An error occurred while obfuscating SQLServer deadlocks. The error: {}".format(e) self._log.error(error) continue - + total_number_of_characters += len(row) + len(query_signatures) if total_number_of_characters > self._deadlock_payload_max_bytes: self._log.warning( @@ -149,11 +146,13 @@ def _create_deadlock_rows(self): ) break - deadlock_events.append({ - PAYLOAD_TIMESTAMP: row[DEADLOCK_TIMESTAMP_ALIAS], - PAYLOAD_XML: ET.tostring(root, encoding='unicode'), - PAYLOAD_QUERY_SIGNATURE: query_signatures - }) + deadlock_events.append( + { + PAYLOAD_TIMESTAMP: row[DEADLOCK_TIMESTAMP_ALIAS], + PAYLOAD_XML: ET.tostring(root, encoding='unicode'), + PAYLOAD_QUERY_SIGNATURE: query_signatures, + } + ) self._last_deadlock_timestamp = time() return deadlock_events @@ -182,7 +181,6 @@ def _create_deadlock_event(self, deadlock_rows): "sqlserver_deadlocks": deadlock_rows, } return event - + def run_job(self): self.collect_deadlocks() - diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index cbcf580665b9a..48b27d812e87f 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -227,7 +227,9 @@ ) AS XML_Data CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE xdr.value('@timestamp', 'datetime') >= DATEADD(SECOND, ?, GETDATE()) -;""".format(**{"timestamp": DEADLOCK_TIMESTAMP_ALIAS, "xml": DEADLOCK_XML_ALIAS}) +;""".format( + **{"timestamp": DEADLOCK_TIMESTAMP_ALIAS, "xml": DEADLOCK_XML_ALIAS} +) def get_query_ao_availability_groups(sqlserver_major_version): diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 448f2bac7ab59..5be07acc3e95c 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -13,7 +13,13 @@ from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer -from datadog_checks.sqlserver.deadlocks import Deadlocks, MAX_PAYLOAD_BYTES, PAYLOAD_QUERY_SIGNATURE, PAYLOAD_TIMESTAMP, PAYLOAD_XML +from datadog_checks.sqlserver.deadlocks import ( + Deadlocks, + MAX_PAYLOAD_BYTES, + PAYLOAD_QUERY_SIGNATURE, + PAYLOAD_TIMESTAMP, + PAYLOAD_XML, +) from datadog_checks.sqlserver.queries import DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS from mock import patch, MagicMock from threading import Event @@ -25,6 +31,7 @@ except ImportError: pyodbc = None + @pytest.fixture def dbm_instance(instance_docker): instance_docker['dbm'] = True @@ -39,6 +46,7 @@ def dbm_instance(instance_docker): instance_docker['deadlocks_collection'] = {'enabled': True, 'collection_interval': 0.1} return copy(instance_docker) + def run_check_and_return_deadlock_payloads(dd_run_check, check, aggregator): dd_run_check(check) dbm_activity = aggregator.get_event_platform_events("dbm-activity") @@ -58,6 +66,7 @@ def _get_conn_for_user(instance_docker, user, timeout=1, _autocommit=False): conn.timeout = timeout return conn + def _run_first_deadlock_query(conn, event1, event2): exception_text = "" try: @@ -103,7 +112,6 @@ def _create_deadlock(bob_conn, fred_conn): return "deadlock" in exception_1_text or "deadlock" in exception_2_text - @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): @@ -163,12 +171,15 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): logging.error("deadlock XML: %s", str(d)) raise e + DEADLOCKS_PLAN_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "deadlocks") + def _load_test_deadlocks_xml(filename): with open(os.path.join(DEADLOCKS_PLAN_DIR, filename), 'r') as f: return f.read() - + + @pytest.fixture def deadlocks_collection_instance(instance_docker): instance_docker['dbm'] = True @@ -184,11 +195,16 @@ def deadlocks_collection_instance(instance_docker): instance_docker['collect_settings'] = {'enabled': False} return copy(instance_docker) + def test__create_deadlock_rows(deadlocks_collection_instance): check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) deadlocks_obj = check.deadlocks xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") - with patch.object(Deadlocks, '_query_deadlocks', return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024-09-20T12:07:16.647000", DEADLOCK_XML_ALIAS: xml}]): + with patch.object( + Deadlocks, + '_query_deadlocks', + return_value=[{DEADLOCK_TIMESTAMP_ALIAS: "2024-09-20T12:07:16.647000", DEADLOCK_XML_ALIAS: xml}], + ): rows = deadlocks_obj._create_deadlock_rows() assert len(rows) == 1, "Should have created one deadlock row" row = rows[0] @@ -198,7 +214,8 @@ def test__create_deadlock_rows(deadlocks_collection_instance): first_mapping = query_signatures[0] assert "spid" in first_mapping, "Should have spid in query signatures" assert isinstance(first_mapping["spid"], int), "spid should be an int" - + + def test_deadlock_xml_bad_format(deadlocks_collection_instance): test_xml = """ @@ -289,9 +306,7 @@ def test_deadlock_calls_obfuscator(deadlocks_collection_instance): "" ) - with patch( - 'datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated" - ): + with patch('datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated"): check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) deadlocks_obj = check.deadlocks root = ET.fromstring(test_xml) @@ -300,4 +315,3 @@ def test_deadlock_calls_obfuscator(deadlocks_collection_instance): result_string = result_string.replace('\t', '').replace('\n', '') result_string = re.sub(r'\s{2,}', ' ', result_string) assert expected_xml_string == result_string - From aa93f88b8571aea1cd4a6065c620c5e63ba91668 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 14:31:49 +0200 Subject: [PATCH 70/92] disabled by default --- sqlserver/assets/configuration/spec.yaml | 2 +- sqlserver/datadog_checks/sqlserver/deadlocks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index cd587763ebe13..046ba2526935b 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -767,7 +767,7 @@ files: options: - name: enabled description: | - Enable the collection of deadlock data. Requires `dbm: true`. Enabled by default. + Enable the collection of deadlock data. Requires `dbm: true`. Disabled by default. value: type: boolean example: false diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 2360ce41c4c31..0269b572ee8b6 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -46,7 +46,7 @@ def __init__(self, check, config: SQLServerConfig): super(Deadlocks, self).__init__( check, run_sync=True, - enabled=self._config.deadlocks_config.get('enabled', True), + enabled=self._config.deadlocks_config.get('enabled', False), expected_db_exceptions=(), min_collection_interval=self._config.min_collection_interval, dbms="sqlserver", From 7d747ba490d5a6e7417d885b03aa85028cc6040b Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:08:06 +0200 Subject: [PATCH 71/92] fixing rebasing errors --- sqlserver/tests/test_unit.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index bca846b6a434c..268eb17d27ca9 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -10,25 +10,24 @@ import mock import pytest -from deepdiff import DeepDiff - from datadog_checks.dev import EnvVars from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.connection import split_sqlserver_host_port -from datadog_checks.sqlserver.deadlocks import Deadlocks from datadog_checks.sqlserver.metrics import SqlFractionMetric, SqlMasterDatabaseFileStats from datadog_checks.sqlserver.schemas import Schemas, SubmitData from datadog_checks.sqlserver.sqlserver import SQLConnectionError from datadog_checks.sqlserver.utils import ( Database, extract_sql_comments_and_procedure_name, + get_unixodbc_sysconfig, + is_non_empty_file, parse_sqlserver_major_version, set_default_driver_conf, ) from .common import CHECK_NAME, DOCKER_SERVER, assert_metrics -from .utils import windows_ci +from .utils import deep_compare, not_windows_ci, windows_ci try: @@ -438,17 +437,22 @@ def test_set_default_driver_conf(): with EnvVars({'DOCKER_DD_AGENT': 'true'}, ignore=['ODBCSYSINI']): set_default_driver_conf() assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) + + with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): + with EnvVars({}, ignore=['ODBCSYSINI']): + set_default_driver_conf() + assert 'ODBCSYSINI' in os.environ, "ODBCSYSINI should be set" + assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) - # `set_default_driver_conf` have no effect on the cases below - with EnvVars({'ODBCSYSINI': 'ABC', 'DOCKER_DD_AGENT': 'true'}): - set_default_driver_conf() - assert os.environ['ODBCSYSINI'] == 'ABC' + with EnvVars({}, ignore=['ODBCSYSINI']): + with mock.patch("os.path.exists", return_value=True): + # odbcinst.ini or odbc.ini exists in agent embedded directory + set_default_driver_conf() + assert 'ODBCSYSINI' not in os.environ - with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): - with EnvVars({}): + with EnvVars({'ODBCSYSINI': 'ABC'}): set_default_driver_conf() - assert 'ODBCSYSINI' in os.environ - assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) + assert os.environ['ODBCSYSINI'] == 'ABC' with EnvVars({}, ignore=['ODBCSYSINI']): with mock.patch("os.path.exists", return_value=True): From 60e1827ab822f40ca62eff1b0293c6dd3deb74d5 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:10:34 +0200 Subject: [PATCH 72/92] fixing rebasing errors --- sqlserver/tests/test_unit.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 268eb17d27ca9..36ad9a11054ed 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -437,19 +437,23 @@ def test_set_default_driver_conf(): with EnvVars({'DOCKER_DD_AGENT': 'true'}, ignore=['ODBCSYSINI']): set_default_driver_conf() assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) - + with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): with EnvVars({}, ignore=['ODBCSYSINI']): set_default_driver_conf() assert 'ODBCSYSINI' in os.environ, "ODBCSYSINI should be set" assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) - with EnvVars({}, ignore=['ODBCSYSINI']): - with mock.patch("os.path.exists", return_value=True): - # odbcinst.ini or odbc.ini exists in agent embedded directory - set_default_driver_conf() - assert 'ODBCSYSINI' not in os.environ + # `set_default_driver_conf` have no effect on the cases below + with EnvVars({'ODBCSYSINI': 'ABC', 'DOCKER_DD_AGENT': 'true'}): + set_default_driver_conf() + assert os.environ['ODBCSYSINI'] == 'ABC' + with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): + with EnvVars({}): + set_default_driver_conf() + assert 'ODBCSYSINI' in os.environ + assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) with EnvVars({'ODBCSYSINI': 'ABC'}): set_default_driver_conf() assert os.environ['ODBCSYSINI'] == 'ABC' From 3d35945e5acdfa51708a78beb81fb4c7acd0ed43 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:13:23 +0200 Subject: [PATCH 73/92] restore test_unit from master --- sqlserver/tests/test_unit.py | 47 ++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 36ad9a11054ed..4aadfa462af2f 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -29,7 +29,6 @@ from .common import CHECK_NAME, DOCKER_SERVER, assert_metrics from .utils import deep_compare, not_windows_ci, windows_ci - try: import pyodbc except ImportError: @@ -454,25 +453,26 @@ def test_set_default_driver_conf(): set_default_driver_conf() assert 'ODBCSYSINI' in os.environ assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) + with EnvVars({'ODBCSYSINI': 'ABC'}): set_default_driver_conf() assert os.environ['ODBCSYSINI'] == 'ABC' - with EnvVars({}, ignore=['ODBCSYSINI']): - with mock.patch("os.path.exists", return_value=True): - # odbcinst.ini or odbc.ini exists in agent embedded directory - set_default_driver_conf() - assert 'ODBCSYSINI' not in os.environ +@not_windows_ci +def test_set_default_driver_conf_linux(): + odbc_config_dir = os.path.expanduser('~') + with mock.patch("datadog_checks.sqlserver.utils.get_unixodbc_sysconfig", return_value=odbc_config_dir): with EnvVars({}, ignore=['ODBCSYSINI']): + odbc_inst = os.path.join(odbc_config_dir, "odbcinst.ini") + odbc_ini = os.path.join(odbc_config_dir, "odbc.ini") + for file in [odbc_inst, odbc_ini]: + if os.path.exists(file): + os.remove(file) + with open(odbc_ini, "x") as file: + file.write("dummy-content") set_default_driver_conf() - assert 'ODBCSYSINI' in os.environ # ODBCSYSINI is set by the integration - if pyodbc is not None: - assert pyodbc.drivers() is not None - - with EnvVars({'ODBCSYSINI': 'ABC'}): - set_default_driver_conf() - assert os.environ['ODBCSYSINI'] == 'ABC' + assert is_non_empty_file(odbc_inst), "odbc_inst should have been created when a non empty odbc.ini exists" @windows_ci @@ -830,12 +830,10 @@ def test_submit_data(): {"id": 3, "name": "test_db1", "schemas": [{"id": "1", "tables": [1, 2]}, {"id": "2", "tables": [1, 2]}]}, {"id": 4, "name": "test_db2", "schemas": [{"id": "3", "tables": [1, 2]}]}, ], - "timestamp": 1.1, } - difference = DeepDiff( - json.loads(submitted_data[0]), expected_data, exclude_paths="root['timestamp']", ignore_order=True - ) - assert len(difference) == 0 + data = json.loads(submitted_data[0]) + data.pop("timestamp") + assert deep_compare(data, expected_data) def test_fetch_throws(instance_docker): @@ -880,3 +878,16 @@ def test_exception_handling_by_do_for_dbs(instance_docker): 'datadog_checks.sqlserver.utils.is_azure_sql_database', return_value={} ): schemas._fetch_for_databases() + + +def test_get_unixodbc_sysconfig(): + etc_dir = os.path.sep + for dir in ["opt", "datadog-agent", "embedded", "bin", "python"]: + etc_dir = os.path.join(etc_dir, dir) + assert get_unixodbc_sysconfig(etc_dir).split(os.path.sep) == [ + "", + "opt", + "datadog-agent", + "embedded", + "etc", + ], "incorrect unix odbc config dir" From dd8d56e44b43e22980b66a1f96660fc3be3649f1 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:18:35 +0200 Subject: [PATCH 74/92] restore tests/utils from master --- sqlserver/tests/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sqlserver/tests/utils.py b/sqlserver/tests/utils.py index c5ad7278bf29a..17b6f03fbc887 100644 --- a/sqlserver/tests/utils.py +++ b/sqlserver/tests/utils.py @@ -245,3 +245,14 @@ def normalize_indexes_columns(actual_payload): index['column_names'] = ','.join(sorted_columns) +def deep_compare(obj1, obj2): + if isinstance(obj1, dict) and isinstance(obj2, dict): + if set(obj1.keys()) != set(obj2.keys()): + return False + return all(deep_compare(obj1[key], obj2[key]) for key in obj1) + elif isinstance(obj1, list) and isinstance(obj2, list): + if len(obj1) != len(obj2): + return False + return all(any(deep_compare(item1, item2) for item2 in obj2) for item1 in obj1) + else: + return obj1 == obj2 From 77cfb130f7aa3512c25a24a84fe0c4d7372b8761 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:40:47 +0200 Subject: [PATCH 75/92] linter --- sqlserver/datadog_checks/sqlserver/schemas.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/schemas.py b/sqlserver/datadog_checks/sqlserver/schemas.py index 3bc009d206656..74ce453a9278a 100644 --- a/sqlserver/datadog_checks/sqlserver/schemas.py +++ b/sqlserver/datadog_checks/sqlserver/schemas.py @@ -87,11 +87,11 @@ def send_truncated_msg(self, db_name, time_spent): } db_info = self.db_info[db_name] event["metadata"] = [{**(db_info)}] - event["collection_errors"][0][ - "message" - ] = "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( - self._total_columns_sent, time_spent, db_name - ) + event["collection_errors"][0]["message"] = ( + "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( + self._total_columns_sent, time_spent, db_name + ) + ) json_event = json.dumps(event, default=default_json_event_encoding) self._log.debug("Reporting truncation of schema collection: {}".format(self.truncate(json_event))) self._submit_to_agent_queue(json_event) From 4a44928ef707f19b2a65744ecc7ae7ec15950828 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:49:15 +0200 Subject: [PATCH 76/92] linter --- sqlserver/datadog_checks/sqlserver/schemas.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/schemas.py b/sqlserver/datadog_checks/sqlserver/schemas.py index 74ce453a9278a..6da6c81e87fdf 100644 --- a/sqlserver/datadog_checks/sqlserver/schemas.py +++ b/sqlserver/datadog_checks/sqlserver/schemas.py @@ -87,11 +87,9 @@ def send_truncated_msg(self, db_name, time_spent): } db_info = self.db_info[db_name] event["metadata"] = [{**(db_info)}] - event["collection_errors"][0]["message"] = ( - "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( - self._total_columns_sent, time_spent, db_name - ) - ) + event["collection_errors"][0]["message"] = "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( + self._total_columns_sent, time_spent, db_name + ) json_event = json.dumps(event, default=default_json_event_encoding) self._log.debug("Reporting truncation of schema collection: {}".format(self.truncate(json_event))) self._submit_to_agent_queue(json_event) From b7c161febd65b9b252bd104fa6d3937b830682d8 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:52:47 +0200 Subject: [PATCH 77/92] linter --- sqlserver/datadog_checks/sqlserver/schemas.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/schemas.py b/sqlserver/datadog_checks/sqlserver/schemas.py index 6da6c81e87fdf..b40dd59415fa8 100644 --- a/sqlserver/datadog_checks/sqlserver/schemas.py +++ b/sqlserver/datadog_checks/sqlserver/schemas.py @@ -87,8 +87,10 @@ def send_truncated_msg(self, db_name, time_spent): } db_info = self.db_info[db_name] event["metadata"] = [{**(db_info)}] - event["collection_errors"][0]["message"] = "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( - self._total_columns_sent, time_spent, db_name + event["collection_errors"][0]["message"] = ( + "Truncated after fetching {} columns, elapsed time is {}s, database is {}".format( + self._total_columns_sent, time_spent, db_name + ) ) json_event = json.dumps(event, default=default_json_event_encoding) self._log.debug("Reporting truncation of schema collection: {}".format(self.truncate(json_event))) From ff3213aaf8f5132a846ee552d4f275deb32007b7 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:03:20 +0200 Subject: [PATCH 78/92] linter --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 6 ++---- sqlserver/datadog_checks/sqlserver/sqlserver.py | 3 ++- sqlserver/tests/test_deadlocks.py | 8 +++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 0269b572ee8b6..c79d99230939c 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -3,10 +3,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import xml.etree.ElementTree as ET -from datetime import datetime -from time import time -from datadog_checks.base import is_affirmative from datadog_checks.base.utils.db.sql import compute_sql_signature from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding, obfuscate_sql_with_metadata from datadog_checks.base.utils.serialization import json @@ -14,6 +11,7 @@ from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.queries import DEADLOCK_QUERY, DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS +from time import time try: import datadog_agent @@ -128,7 +126,7 @@ def _create_deadlock_rows(self): ) ) continue - query_signatures = dict() + query_signatures = {} try: query_signatures = self._obfuscate_xml(root) except Exception as e: diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 6cbac5a76f8bf..242655866d699 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -1,12 +1,13 @@ # (C) Datadog, Inc. 2018-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) + from __future__ import division import copy import time -from collections import defaultdict +from collections import defaultdict from cachetools import TTLCache from datadog_checks.base import AgentCheck diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 5be07acc3e95c..468fbfdffbab0 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -15,13 +15,11 @@ from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.deadlocks import ( Deadlocks, - MAX_PAYLOAD_BYTES, PAYLOAD_QUERY_SIGNATURE, PAYLOAD_TIMESTAMP, - PAYLOAD_XML, ) from datadog_checks.sqlserver.queries import DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS -from mock import patch, MagicMock +from mock import patch from threading import Event from .common import CHECK_NAME @@ -152,7 +150,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): deadlocks = deadlock_payloads[0]['sqlserver_deadlocks'] found = 0 for d in deadlocks: - assert not "ERROR" in d, "Shouldn't have generated an error" + assert "ERROR" not in d, "Shouldn't have generated an error" assert isinstance(d, dict), "sqlserver_deadlocks should be a dictionary" try: root = ET.fromstring(d["xml"]) @@ -240,7 +238,7 @@ def test_deadlock_xml_bad_format(deadlocks_collection_instance): result = str(e) assert result == "process-list element not found. The deadlock XML is in an unexpected format." else: - assert False, "Should have raised an exception for bad XML format" + AssertionError("Should have raised an exception for bad XML format") def test_deadlock_calls_obfuscator(deadlocks_collection_instance): From 9a6acc7a32b975c7dc822923ec86782735c60bc7 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:06:17 +0200 Subject: [PATCH 79/92] licence --- sqlserver/tests/test_deadlocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 468fbfdffbab0..6c88f6e8b41cb 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -1,4 +1,4 @@ -# (C) Datadog, Inc. 2021-present +# (C) Datadog, Inc. 2024-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) From be2ab45fe8f8173bd08123e78badbea6ba110e45 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:10:26 +0200 Subject: [PATCH 80/92] instance fix --- .../datadog_checks/sqlserver/config_models/instance.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/config_models/instance.py b/sqlserver/datadog_checks/sqlserver/config_models/instance.py index daa899025fdee..ada48128cd5cc 100644 --- a/sqlserver/datadog_checks/sqlserver/config_models/instance.py +++ b/sqlserver/datadog_checks/sqlserver/config_models/instance.py @@ -61,14 +61,12 @@ class CustomQuery(BaseModel): arbitrary_types_allowed=True, frozen=True, ) - collection_interval: Optional[int] = None columns: Optional[tuple[MappingProxyType[str, Any], ...]] = None - metric_prefix: Optional[str] = None query: Optional[str] = None tags: Optional[tuple[str, ...]] = None -class Deadlocks(BaseModel): +class DeadlocksCollection(BaseModel): model_config = ConfigDict( arbitrary_types_allowed=True, frozen=True, @@ -195,7 +193,7 @@ class InstanceConfig(BaseModel): database_instance_collection_interval: Optional[float] = None db_fragmentation_object_names: Optional[tuple[str, ...]] = None dbm: Optional[bool] = None - deadlocks_collection: Optional[Deadlocks] = None + deadlocks_collection: Optional[DeadlocksCollection] = None disable_generic_tags: Optional[bool] = None driver: Optional[str] = None dsn: Optional[str] = None From 09b2e6402a8311abaffada9c1083b30c92d1001b Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:27:55 +0200 Subject: [PATCH 81/92] import order --- sqlserver/datadog_checks/sqlserver/sqlserver.py | 2 +- sqlserver/tests/test_deadlocks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 242655866d699..8754eb19f9602 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -7,8 +7,8 @@ import copy import time -from collections import defaultdict from cachetools import TTLCache +from collections import defaultdict from datadog_checks.base import AgentCheck from datadog_checks.base.config import is_affirmative diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 6c88f6e8b41cb..4548406e34170 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -6,10 +6,10 @@ import concurrent import logging -import xml.etree.ElementTree as ET import os import pytest import re +import xml.etree.ElementTree as ET from copy import copy, deepcopy from datadog_checks.sqlserver import SQLServer From 324fce1fc5a18529debfa4d84a63200dbab0b03b Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:22:26 +0200 Subject: [PATCH 82/92] linter: import order --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 2 +- sqlserver/datadog_checks/sqlserver/sqlserver.py | 4 ++-- sqlserver/tests/test_deadlocks.py | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index c79d99230939c..eb6b301bfd97f 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -3,6 +3,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import xml.etree.ElementTree as ET +from time import time from datadog_checks.base.utils.db.sql import compute_sql_signature from datadog_checks.base.utils.db.utils import DBMAsyncJob, default_json_event_encoding, obfuscate_sql_with_metadata @@ -11,7 +12,6 @@ from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.const import STATIC_INFO_ENGINE_EDITION, STATIC_INFO_VERSION from datadog_checks.sqlserver.queries import DEADLOCK_QUERY, DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS -from time import time try: import datadog_agent diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 8754eb19f9602..427aab153a4e5 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -6,9 +6,9 @@ import copy import time +from collections import defaultdict from cachetools import TTLCache -from collections import defaultdict from datadog_checks.base import AgentCheck from datadog_checks.base.config import is_affirmative @@ -22,13 +22,13 @@ from datadog_checks.sqlserver.activity import SqlserverActivity from datadog_checks.sqlserver.agent_history import SqlserverAgentHistory from datadog_checks.sqlserver.config import SQLServerConfig -from datadog_checks.sqlserver.deadlocks import Deadlocks from datadog_checks.sqlserver.database_metrics import ( SqlserverAgentMetrics, SqlserverDatabaseBackupMetrics, SqlserverDBFragmentationMetrics, SqlserverIndexUsageMetrics, ) +from datadog_checks.sqlserver.deadlocks import Deadlocks from datadog_checks.sqlserver.metadata import SqlserverMetadata from datadog_checks.sqlserver.schemas import Schemas from datadog_checks.sqlserver.statements import SqlserverStatementMetrics diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 4548406e34170..32ef503eb0294 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -7,20 +7,21 @@ import concurrent import logging import os -import pytest import re import xml.etree.ElementTree as ET - from copy import copy, deepcopy +from threading import Event + +import pytest +from mock import patch + from datadog_checks.sqlserver import SQLServer from datadog_checks.sqlserver.deadlocks import ( - Deadlocks, PAYLOAD_QUERY_SIGNATURE, PAYLOAD_TIMESTAMP, + Deadlocks, ) from datadog_checks.sqlserver.queries import DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS -from mock import patch -from threading import Event from .common import CHECK_NAME From 4e1d9687b3dcd9a3e628295996442147e6b572c0 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:28:58 +0000 Subject: [PATCH 83/92] asset validation --- sqlserver/datadog_checks/sqlserver/config_models/instance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sqlserver/datadog_checks/sqlserver/config_models/instance.py b/sqlserver/datadog_checks/sqlserver/config_models/instance.py index ada48128cd5cc..ef0200cb763a7 100644 --- a/sqlserver/datadog_checks/sqlserver/config_models/instance.py +++ b/sqlserver/datadog_checks/sqlserver/config_models/instance.py @@ -61,7 +61,9 @@ class CustomQuery(BaseModel): arbitrary_types_allowed=True, frozen=True, ) + collection_interval: Optional[int] = None columns: Optional[tuple[MappingProxyType[str, Any], ...]] = None + metric_prefix: Optional[str] = None query: Optional[str] = None tags: Optional[tuple[str, ...]] = None From 28927efd892c62b4e0646a2def07130220746ecb Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 09:17:11 +0200 Subject: [PATCH 84/92] get_deadlock_obj --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 5 ++++- sqlserver/tests/test_deadlocks.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index eb6b301bfd97f..1f121033dfd2f 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -96,6 +96,9 @@ def _obfuscate_xml(self, root): if frame.text is not None: frame.text = self.obfuscate_no_except_wrapper(frame.text) return query_signatures + + def _get_lookback_seconds(self): + return self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()) def _query_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): @@ -107,7 +110,7 @@ def _query_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()))) + cursor.execute(DEADLOCK_QUERY, (self._get_lookback_seconds())) columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 32ef503eb0294..5db6b03cf45f3 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -195,9 +195,13 @@ def deadlocks_collection_instance(instance_docker): return copy(instance_docker) -def test__create_deadlock_rows(deadlocks_collection_instance): +def get_deadlock_obj(deadlocks_collection_instance): check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) - deadlocks_obj = check.deadlocks + return check.deadlocks + + +def test__create_deadlock_rows(deadlocks_collection_instance): + deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) xml = _load_test_deadlocks_xml("sqlserver_deadlock_event.xml") with patch.object( Deadlocks, @@ -230,8 +234,7 @@ def test_deadlock_xml_bad_format(deadlocks_collection_instance): """ - check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) - deadlocks_obj = check.deadlocks + deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) root = ET.fromstring(test_xml) try: deadlocks_obj._obfuscate_xml(root) @@ -306,11 +309,11 @@ def test_deadlock_calls_obfuscator(deadlocks_collection_instance): ) with patch('datadog_checks.sqlserver.deadlocks.Deadlocks.obfuscate_no_except_wrapper', return_value="obfuscated"): - check = SQLServer(CHECK_NAME, {}, [deadlocks_collection_instance]) - deadlocks_obj = check.deadlocks + deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) root = ET.fromstring(test_xml) deadlocks_obj._obfuscate_xml(root) result_string = ET.tostring(root, encoding='unicode') result_string = result_string.replace('\t', '').replace('\n', '') result_string = re.sub(r'\s{2,}', ' ', result_string) assert expected_xml_string == result_string + From 8d87d54d34b312b3e761d30a2f4eabcc5694d7c6 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:40:32 +0200 Subject: [PATCH 85/92] lookback --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 5 +++-- sqlserver/tests/test_deadlocks.py | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 1f121033dfd2f..ec6c4a9812444 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -98,7 +98,7 @@ def _obfuscate_xml(self, root): return query_signatures def _get_lookback_seconds(self): - return self._max_deadlocks, min(-60, self._last_deadlock_timestamp - time()) + return min(-60, self._last_deadlock_timestamp - time()) def _query_deadlocks(self): with self._check.connection.open_managed_default_connection(key_prefix=self._conn_key_prefix): @@ -110,12 +110,13 @@ def _query_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute(DEADLOCK_QUERY, (self._get_lookback_seconds())) + cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, self._get_lookback_seconds())) columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] def _create_deadlock_rows(self): db_rows = self._query_deadlocks() + breakpoint() deadlock_events = [] total_number_of_characters = 0 for i, row in enumerate(db_rows): diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index 5db6b03cf45f3..d8c37e6ac69e2 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -317,3 +317,10 @@ def test_deadlock_calls_obfuscator(deadlocks_collection_instance): result_string = re.sub(r'\s{2,}', ' ', result_string) assert expected_xml_string == result_string + +def test__get_lookback_seconds(deadlocks_collection_instance): + deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) + deadlocks_obj._last_deadlock_timestamp = 100 + lookback_seconds = deadlocks_obj._get_lookback_seconds() + assert isinstance(lookback_seconds, float), "Should return a float" + From 5b92ce9abe91d5ec0383b2d0c3dc2bbdad83c2e8 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:04:38 +0200 Subject: [PATCH 86/92] key error --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index ec6c4a9812444..5da7d60596aa9 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -110,13 +110,18 @@ def _query_deadlocks(self): self._max_deadlocks, self._last_deadlock_timestamp, ) - cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, self._get_lookback_seconds())) + try: + cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, self._get_lookback_seconds())) + except KeyError as e: + raise KeyError(f"{str(e)} | cursor.description: {cursor.description}") + except Exception as e: + raise e + columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] def _create_deadlock_rows(self): db_rows = self._query_deadlocks() - breakpoint() deadlock_events = [] total_number_of_characters = 0 for i, row in enumerate(db_rows): From 6cc08e206839060ae5c8db9cd3e04289f3d07ce1 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:14:32 +0000 Subject: [PATCH 87/92] linter --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 4 ++-- sqlserver/tests/test_deadlocks.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index 5da7d60596aa9..b728a526b22b6 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -96,7 +96,7 @@ def _obfuscate_xml(self, root): if frame.text is not None: frame.text = self.obfuscate_no_except_wrapper(frame.text) return query_signatures - + def _get_lookback_seconds(self): return min(-60, self._last_deadlock_timestamp - time()) @@ -116,7 +116,7 @@ def _query_deadlocks(self): raise KeyError(f"{str(e)} | cursor.description: {cursor.description}") except Exception as e: raise e - + columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index d8c37e6ac69e2..a2790e7a58a11 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -234,7 +234,7 @@ def test_deadlock_xml_bad_format(deadlocks_collection_instance): """ - deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) + deadlocks_obj = get_deadlock_obj(deadlocks_collection_instance) root = ET.fromstring(test_xml) try: deadlocks_obj._obfuscate_xml(root) @@ -323,4 +323,3 @@ def test__get_lookback_seconds(deadlocks_collection_instance): deadlocks_obj._last_deadlock_timestamp = 100 lookback_seconds = deadlocks_obj._get_lookback_seconds() assert isinstance(lookback_seconds, float), "Should return a float" - From 6f39ba332ed9a1aa6c2186396efb543c8a07b183 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 12:51:06 +0000 Subject: [PATCH 88/92] exception handling --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index b728a526b22b6..db7d10e72e397 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -112,10 +112,8 @@ def _query_deadlocks(self): ) try: cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, self._get_lookback_seconds())) - except KeyError as e: - raise KeyError(f"{str(e)} | cursor.description: {cursor.description}") except Exception as e: - raise e + raise Exception(f"{str(e)} | cursor.description: {cursor.description}") columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] From e7bf8f8a7f83ebdfd0e9f73d91321652cd1bc091 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:38:14 +0000 Subject: [PATCH 89/92] enrich exception only on code error --- sqlserver/datadog_checks/sqlserver/deadlocks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sqlserver/datadog_checks/sqlserver/deadlocks.py b/sqlserver/datadog_checks/sqlserver/deadlocks.py index db7d10e72e397..b623760188330 100644 --- a/sqlserver/datadog_checks/sqlserver/deadlocks.py +++ b/sqlserver/datadog_checks/sqlserver/deadlocks.py @@ -113,7 +113,9 @@ def _query_deadlocks(self): try: cursor.execute(DEADLOCK_QUERY, (self._max_deadlocks, self._get_lookback_seconds())) except Exception as e: - raise Exception(f"{str(e)} | cursor.description: {cursor.description}") + if "Data column of Unknown ADO type" in str(e): + raise Exception(f"{str(e)} | cursor.description: {cursor.description}") + raise e columns = [column[0] for column in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] From 370f64c0b444921e5a5b8d9c94684f8ee8d170c9 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Tue, 24 Sep 2024 16:27:36 +0200 Subject: [PATCH 90/92] remove sqlncli from ci --- .../ci/scripts/sqlserver/windows/41_install_native_client.bat | 3 --- sqlserver/hatch.toml | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 .ddev/ci/scripts/sqlserver/windows/41_install_native_client.bat diff --git a/.ddev/ci/scripts/sqlserver/windows/41_install_native_client.bat b/.ddev/ci/scripts/sqlserver/windows/41_install_native_client.bat deleted file mode 100644 index 0fbd0e0b47cdd..0000000000000 --- a/.ddev/ci/scripts/sqlserver/windows/41_install_native_client.bat +++ /dev/null @@ -1,3 +0,0 @@ -powershell -Command "Invoke-WebRequest https://download.microsoft.com/download/F/3/C/F3C64941-22A0-47E9-BC9B-1A19B4CA3E88/ENU/x64/sqlncli.msi -OutFile sqlncli.msi" -msiexec /quiet /passive /qn /i sqlncli.msi IACCEPTSQLNCLILICENSETERMS=YES -del sqlncli.msi diff --git a/sqlserver/hatch.toml b/sqlserver/hatch.toml index 843ea38127536..b996a56b88920 100644 --- a/sqlserver/hatch.toml +++ b/sqlserver/hatch.toml @@ -14,7 +14,7 @@ setup = ["single", "ha"] [[envs.default.matrix]] python = ["3.12"] os = ["windows"] -driver = ["SQLOLEDB", "SQLNCLI11", "MSOLEDBSQL", "odbc"] +driver = ["SQLOLEDB", "MSOLEDBSQL", "odbc"] version = ["2019", "2022"] setup = ["single"] From 9a1695d4d2d776a98ff6cf97ef8e09d4b2158be4 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Wed, 25 Sep 2024 09:13:18 +0200 Subject: [PATCH 91/92] test diag change --- sqlserver/tests/test_deadlocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index a2790e7a58a11..c4674f6c28fd9 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -167,7 +167,7 @@ def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance): found == 1 ), "Should have collected the UPDATE statement in deadlock exactly once, but collected: {}.".format(found) except AssertionError as e: - logging.error("deadlock XML: %s", str(d)) + logging.error("deadlock payload: %s", str(deadlocks)) raise e From 33ce2573049907fae32d824de3b57ecdda6bc607 Mon Sep 17 00:00:00 2001 From: Nenad Noveljic <18366081+nenadnoveljic@users.noreply.github.com> Date: Wed, 25 Sep 2024 15:34:35 +0200 Subject: [PATCH 92/92] supported only with odbc --- sqlserver/assets/configuration/spec.yaml | 2 +- sqlserver/tests/test_deadlocks.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlserver/assets/configuration/spec.yaml b/sqlserver/assets/configuration/spec.yaml index 046ba2526935b..a78b914ccbfc4 100644 --- a/sqlserver/assets/configuration/spec.yaml +++ b/sqlserver/assets/configuration/spec.yaml @@ -763,7 +763,7 @@ files: - name: deadlocks_collection hidden: True description: | - Configure the collection of deadlock data. + Configure the collection of deadlock data. The feature is supported for odbc connector only. options: - name: enabled description: | diff --git a/sqlserver/tests/test_deadlocks.py b/sqlserver/tests/test_deadlocks.py index c4674f6c28fd9..1099a0dfcd1f6 100644 --- a/sqlserver/tests/test_deadlocks.py +++ b/sqlserver/tests/test_deadlocks.py @@ -24,6 +24,7 @@ from datadog_checks.sqlserver.queries import DEADLOCK_TIMESTAMP_ALIAS, DEADLOCK_XML_ALIAS from .common import CHECK_NAME +from .utils import not_windows_ado try: import pyodbc @@ -111,6 +112,8 @@ def _create_deadlock(bob_conn, fred_conn): return "deadlock" in exception_1_text or "deadlock" in exception_2_text +# TODO: remove @not_windows_ado when the functionality is supported for MSOLEDBSQL +@not_windows_ado @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') def test_deadlocks(aggregator, dd_run_check, init_config, dbm_instance):