diff --git a/sqlserver/changelog.d/18883.added b/sqlserver/changelog.d/18883.added new file mode 100644 index 0000000000000..f31a7c0e8c8e7 --- /dev/null +++ b/sqlserver/changelog.d/18883.added @@ -0,0 +1,20 @@ +Migrate following dynamic metrics to database_metrics for better maintainability and testability. +- SQLServer AlwaysOn metrics +- SQLServer FCI metrics +- SQLServer file stats metrics +- SQLServer primary log shipping metrics +- SQLServer secondary log shipping metrics +- SQLServer server state metrics +- SQLServer tempdb file space usage metrics +- SQLServer index usage metrics +- SQLServer database index fragmentation metrics +- SQLServer os tasks metrics +- SQLServer master files metrics +- SQLServer database files metrics +- SQLServer database stats metrics +- SQLServer database backup metrics +- SQLServer os schedulers metrics +- SQLServer database replication stats metrics +- SQLServer availability replicas metrics +- SQLServer availability groups metrics +Increase database backup metrics and index fragmentation metrics collection interval to 5 minutes. \ No newline at end of file diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/__init__.py b/sqlserver/datadog_checks/sqlserver/database_metrics/__init__.py index 9c5ad78e12fdb..6177697973710 100644 --- a/sqlserver/datadog_checks/sqlserver/database_metrics/__init__.py +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/__init__.py @@ -1,8 +1,23 @@ # (C) Datadog, Inc. 2024-present # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) +from .ao_metrics import SqlserverAoMetrics +from .availability_groups_metrics import SqlserverAvailabilityGroupsMetrics +from .availability_replicas_metrics import SqlserverAvailabilityReplicasMetrics +from .database_agent_metrics import SqlserverAgentMetrics from .database_backup_metrics import SqlserverDatabaseBackupMetrics +from .database_files_metrics import SqlserverDatabaseFilesMetrics +from .database_replication_stats_metrics import SqlserverDatabaseReplicationStatsMetrics +from .database_stats_metrics import SqlserverDatabaseStatsMetrics from .db_fragmentation_metrics import SqlserverDBFragmentationMetrics +from .fci_metrics import SqlserverFciMetrics +from .file_stats_metrics import SqlserverFileStatsMetrics from .index_usage_metrics import SqlserverIndexUsageMetrics -from .database_agent_metrics import SqlserverAgentMetrics +from .master_files_metrics import SqlserverMasterFilesMetrics +from .os_schedulers_metrics import SqlserverOsSchedulersMetrics +from .os_tasks_metrics import SqlserverOsTasksMetrics +from .primary_log_shipping_metrics import SqlserverPrimaryLogShippingMetrics +from .secondary_log_shipping_metrics import SqlserverSecondaryLogShippingMetrics +from .server_state_metrics import SqlserverServerStateMetrics +from .tempdb_file_space_usage_metrics import SqlserverTempDBFileSpaceUsageMetrics from .xe_session_metrics import SQLServerXESessionMetrics diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/ao_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/ao_metrics.py new file mode 100644 index 0000000000000..31b3238ba8752 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/ao_metrics.py @@ -0,0 +1,221 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from typing import List + +from datadog_checks.base.config import is_affirmative +from datadog_checks.sqlserver.utils import is_azure_database + +from .base import SqlserverDatabaseMetricsBase + +QUERY_AO_FAILOVER_CLUSTER = { + "name": "sys.dm_hadr_cluster", + "query": """ + SELECT + LOWER(quorum_type_desc) AS quorum_type_desc, + LOWER(quorum_state_desc) AS quorum_state_desc, + cluster_name, + 1, + 1 + FROM sys.dm_hadr_cluster + """.strip(), + "columns": [ + {"name": "quorum_type", "type": "tag"}, + {"name": "quorum_state", "type": "tag"}, + {"name": "failover_cluster", "type": "tag"}, + {"name": "ao.quorum_type", "type": "gauge"}, + {"name": "ao.quorum_state", "type": "gauge"}, + ], +} + +# sys.dm_hadr_cluster does not have a related column to join on, this cross join will add the +# cluster_name column to every row by multiplying all the rows in the left table against +# all the rows in the right table. Note, there will only be one row from sys.dm_hadr_cluster. +QUERY_AO_FAILOVER_CLUSTER_MEMBER = { + "name": "sys.dm_hadr_cluster_members", + "query": """ + SELECT + member_name, + LOWER(member_type_desc) AS member_type_desc, + LOWER(member_state_desc) AS member_state_desc, + FC.cluster_name, + 1, + 1, + number_of_quorum_votes + FROM sys.dm_hadr_cluster_members + CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC + """.strip(), + "columns": [ + {"name": "member_name", "type": "tag"}, + {"name": "member_type", "type": "tag"}, + {"name": "member_state", "type": "tag"}, + {"name": "failover_cluster", "type": "tag"}, + {"name": "ao.member.type", "type": "gauge"}, + {"name": "ao.member.state", "type": "gauge"}, + {"name": "ao.member.number_of_quorum_votes", "type": "gauge"}, + ], +} + + +class SqlserverAoMetrics(SqlserverDatabaseMetricsBase): + @property + def include_ao_metrics(self) -> bool: + return is_affirmative(self.instance_config.get('include_ao_metrics', False)) + + @property + def enabled(self) -> bool: + if not self.include_ao_metrics: + return False + if not self.major_version and not is_azure_database(self.engine_edition): + return False + if self.major_version > 2012 or is_azure_database(self.engine_edition): + return True + return False + + @property + def queries(self) -> List[dict]: + return [ + self.__get_query_ao_availability_groups(), + QUERY_AO_FAILOVER_CLUSTER, + QUERY_AO_FAILOVER_CLUSTER_MEMBER, + ] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"major_version={self.major_version}, " + f"engine_edition={self.engine_edition}, " + f"include_ao_metrics={self.include_ao_metrics})" + ) + + def __get_query_ao_availability_groups(self) -> dict: + """ + Construct the sys.availability_groups QueryExecutor configuration based on the SQL Server major version + + :params sqlserver_major_version: SQL Server major version (i.e. 2012, 2019, ...) + :return: a QueryExecutor query config object + """ + column_definitions_tags = { + # AG - sys.availability_groups + "AG.group_id AS availability_group": { + "name": "availability_group", + "type": "tag", + }, + "AG.name AS availability_group_name": { + "name": "availability_group_name", + "type": "tag", + }, + # AR - sys.availability_replicas + "AR.replica_server_name": {"name": "replica_server_name", "type": "tag"}, + "LOWER(AR.failover_mode_desc) AS failover_mode_desc": { + "name": "failover_mode", + "type": "tag", + }, + "LOWER(AR.availability_mode_desc) AS availability_mode_desc": { + "name": "availability_mode", + "type": "tag", + }, + # ADC - sys.availability_databases_cluster + "ADC.database_name": {"name": "database_name", "type": "tag"}, + # DRS - sys.dm_hadr_database_replica_states + "DRS.replica_id": {"name": "replica_id", "type": "tag"}, + "DRS.database_id": {"name": "database_id", "type": "tag"}, + "LOWER(DRS.database_state_desc) AS database_state_desc": { + "name": "database_state", + "type": "tag", + }, + "LOWER(DRS.synchronization_state_desc) AS synchronization_state_desc": { + "name": "synchronization_state", + "type": "tag", + }, + # FC - sys.dm_hadr_cluster + "FC.cluster_name": { + "name": "failover_cluster", + "type": "tag", + }, + } + column_definitions_metrics = { + "(DRS.log_send_queue_size * 1024) AS log_send_queue_size": { + "name": "ao.log_send_queue_size", + "type": "gauge", + }, + "(DRS.log_send_rate * 1024) AS log_send_rate": { + "name": "ao.log_send_rate", + "type": "gauge", + }, + "(DRS.redo_queue_size * 1024) AS redo_queue_size": { + "name": "ao.redo_queue_size", + "type": "gauge", + }, + "(DRS.redo_rate * 1024) AS redo_rate": { + "name": "ao.redo_rate", + "type": "gauge", + }, + "DRS.low_water_mark_for_ghosts": { + "name": "ao.low_water_mark_for_ghosts", + "type": "gauge", + }, + "(DRS.filestream_send_rate * 1024) AS filestream_send_rate": { + "name": "ao.filestream_send_rate", + "type": "gauge", + }, + # Other + "1 AS replica_sync_topology_indicator": { + "name": "ao.replica_status", + "type": "gauge", + }, + } + + # Include metrics based on version + if self.major_version >= 2016: + column_definitions_metrics["DRS.secondary_lag_seconds"] = { + "name": "ao.secondary_lag_seconds", + "type": "gauge", + } + if self.major_version >= 2014: + column_definitions_metrics["DRS.is_primary_replica"] = { + "name": "ao.is_primary_replica", + "type": "gauge", + } + column_definitions_tags[ + """ + CASE + WHEN DRS.is_primary_replica = 1 THEN 'primary' + WHEN DRS.is_primary_replica = 0 THEN 'secondary' + END AS replica_role_desc + """ + ] = {"name": "replica_role", "type": "tag"} + + # Sort columns to ensure a static column order + sql_columns = [] + metric_columns = [] + for column in sorted(column_definitions_tags.keys()): + sql_columns.append(column) + metric_columns.append(column_definitions_tags[column]) + for column in sorted(column_definitions_metrics.keys()): + sql_columns.append(column) + metric_columns.append(column_definitions_metrics[column]) + + return { + "name": "sys.availability_groups", + "query": """ + SELECT + {sql_columns} + FROM + sys.availability_groups AS AG + INNER JOIN sys.availability_replicas AS AR ON AG.group_id = AR.group_id + INNER JOIN sys.availability_databases_cluster AS ADC ON AG.group_id = ADC.group_id + INNER JOIN sys.dm_hadr_database_replica_states AS DRS ON AG.group_id = DRS.group_id + AND ADC.group_database_id = DRS.group_database_id + AND AR.replica_id = DRS.replica_id + -- `sys.dm_hadr_cluster` does not have a related column to join on, this cross join will add the + -- `cluster_name` column to every row by multiplying all the rows in the left table against + -- all the rows in the right table. Note, there will only be one row from `sys.dm_hadr_cluster`. + CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC + """.strip().format( + sql_columns=", ".join(sql_columns), + ), + "columns": metric_columns, + } diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/availability_groups_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/availability_groups_metrics.py new file mode 100644 index 0000000000000..c3c7937f65c2b --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/availability_groups_metrics.py @@ -0,0 +1,66 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +AVAILABILITY_GROUPS_METRICS_QUERY = { + "name": "sys.dm_hadr_availability_group_states", + "query": """SELECT + resource_group_id, + name, + synchronization_health_desc, + synchronization_health, + primary_recovery_health, + secondary_recovery_health + from sys.dm_hadr_availability_group_states as dhdrcs + inner join sys.availability_groups as ag + on ag.group_id = dhdrcs.group_id + """.strip(), + "columns": [ + {"name": "availability_group", "type": "tag"}, + {"name": "availability_group_name", "type": "tag"}, + {"name": "synchronization_health_desc", "type": "tag"}, + {"name": "ao.ag_sync_health", "type": "gauge"}, + {"name": "ao.primary_replica_health", "type": "gauge"}, + {"name": "ao.secondary_replica_health", "type": "gauge"}, + ], +} + + +class SqlserverAvailabilityGroupsMetrics(SqlserverDatabaseMetricsBase): + # sys.dm_hadr_availability_group_states + # Returns a row for each Always On availability group that possesses an availability replica on the local instance + # of SQL Server. Each row displays the states that define the health of a given availability group. + # + # https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-hadr-availability-group-states-transact-sql?view=sql-server-ver15 + @property + def include_ao_metrics(self) -> bool: + return is_affirmative(self.instance_config.get('include_ao_metrics', False)) + + @property + def availability_group(self): + return self.instance_config.get('availability_group') + + @property + def enabled(self): + if not self.include_ao_metrics: + return False + return True + + @property + def queries(self): + query = AVAILABILITY_GROUPS_METRICS_QUERY.copy() + if self.availability_group: + query['query'] += f" where resource_group_id = '{self.availability_group}'" + return [query] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_ao_metrics={self.include_ao_metrics}, " + f"availability_group={self.availability_group})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/availability_replicas_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/availability_replicas_metrics.py new file mode 100644 index 0000000000000..95b0c9041108c --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/availability_replicas_metrics.py @@ -0,0 +1,100 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +AVAILABILITY_REPLICAS_METRICS_QUERY = { + "name": "sys.availability_replicas", + "query": """SELECT + database_name, + resource_group_id, + name, + replica_server_name, + failover_mode_desc, + {is_primary_replica}, + failover_mode, + is_failover_ready + from sys.availability_replicas as ar + inner join sys.dm_hadr_database_replica_cluster_states as dhdrcs + on ar.replica_id = dhdrcs.replica_id + inner join sys.dm_hadr_database_replica_states as dhdrs + on ar.replica_id = dhdrs.replica_id + inner join sys.availability_groups as ag + on ag.group_id = ar.group_id + """.strip(), + "columns": [ + {"name": "db", "type": "tag"}, + {"name": "availability_group", "type": "tag"}, + {"name": "availability_group_name", "type": "tag"}, + {"name": "replica_server_name", "type": "tag"}, + {"name": "failover_mode_desc", "type": "tag"}, + {"name": "is_primary_replica", "type": "tag"}, + {"name": "ao.replica_failover_mode", "type": "gauge"}, + {"name": "ao.replica_failover_readiness", "type": "gauge"}, + ], +} + + +class SqlserverAvailabilityReplicasMetrics(SqlserverDatabaseMetricsBase): + # sys.availability_replicas (Transact-SQL) + # + # Returns a row for each of the availability replicas that belong to any Always On availability group in the WSFC + # failover cluster. If the local server instance is unable to talk to the WSFC failover cluster, for example because + # the cluster is down or quorum has been lost, only rows for local availability replicas are returned. + # These rows will contain only the columns of data that are cached locally in metadata. + # + # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-availability-replicas-transact-sql?view=sql-server-ver15 + @property + def include_ao_metrics(self) -> bool: + return is_affirmative(self.instance_config.get('include_ao_metrics', False)) + + @property + def availability_group(self): + return self.instance_config.get('availability_group') + + @property + def only_emit_local(self): + return is_affirmative(self.instance_config.get('only_emit_local', False)) + + @property + def ao_database(self): + return self.instance_config.get('ao_database') + + @property + def enabled(self): + if not self.include_ao_metrics: + return False + return True + + @property + def queries(self): + query = AVAILABILITY_REPLICAS_METRICS_QUERY.copy() + if self.availability_group or self.only_emit_local or self.ao_database: + where_clauses = [] + if self.availability_group: + where_clauses.append(f"resource_group_id = '{self.availability_group}'") + if self.only_emit_local: + where_clauses.append("is_local = 1") + if self.ao_database: + where_clauses.append(f"database_name = '{self.ao_database}'") + query['query'] += f" where {' and '.join(where_clauses)}" + if self.major_version >= 2014: + # This column only supported in SQL Server 2014 and later + is_primary_replica = "is_primary_replica" + else: + is_primary_replica = "'unknown' as is_primary_replica" + query['query'] = query['query'].format(is_primary_replica=is_primary_replica) + return [query] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_ao_metrics={self.include_ao_metrics}, " + f"availability_group={self.availability_group}, " + f"only_emit_local={self.only_emit_local}, " + f"ao_database={self.ao_database})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/database_files_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/database_files_metrics.py new file mode 100644 index 0000000000000..0786ebd5e999c --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/database_files_metrics.py @@ -0,0 +1,70 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import functools + +from .base import SqlserverDatabaseMetricsBase + +DATABASE_FILES_METRICS_QUERY = { + "name": "sys.database_files", + "query": """SELECT + file_id, + CASE type + WHEN 0 THEN 'data' + WHEN 1 THEN 'transaction_log' + WHEN 2 THEN 'filestream' + WHEN 3 THEN 'unknown' + WHEN 4 THEN 'full_text' + ELSE 'other' + END AS file_type, + physical_name, + name, + state_desc, + ISNULL(size, 0) as size, + ISNULL(CAST(FILEPROPERTY(name, 'SpaceUsed') as int), 0) as space_used, + state + FROM sys.database_files + """, + "columns": [ + {"name": "file_id", "type": "tag"}, + {"name": "file_type", "type": "tag"}, + {"name": "file_location", "type": "tag"}, + {"name": "file_name", "type": "tag"}, + {"name": "database_files_state_desc", "type": "tag"}, + {"name": "size", "type": "source"}, + {"name": "space_used", "type": "source"}, + {"name": "database.files.state", "type": "gauge"}, + ], + "extras": [ + # size/space_used are in pages, 1 page = 8 KB. Calculated after the query to avoid int overflow + {"name": "database.files.size", "expression": "size*8", "submit_type": "gauge"}, + {"name": "database.files.space_used", "expression": "space_used*8", "submit_type": "gauge"}, + ], +} + + +class SqlserverDatabaseFilesMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-files-transact-sql + @property + def enabled(self): + return True + + @property + def queries(self): + return [DATABASE_FILES_METRICS_QUERY] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(" f"enabled={self.enabled})" + + def _build_query_executors(self): + executors = [] + for database in self.databases: + executor = self.new_query_executor( + self.queries, + executor=functools.partial(self.execute_query_handler, db=database), + extra_tags=['db:{}'.format(database), 'database:{}'.format(database)], + ) + executor.compile_queries() + executors.append(executor) + return executors diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/database_replication_stats_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/database_replication_stats_metrics.py new file mode 100644 index 0000000000000..e1233e353cf83 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/database_replication_stats_metrics.py @@ -0,0 +1,72 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +DATABASE_REPLICATION_STATS_METRICS_QUERY = { + "name": "sys.dm_hadr_database_replica_states", + "query": """SELECT + resource_group_id, + name, + replica_server_name, + synchronization_state_desc, + synchronization_state + from sys.dm_hadr_database_replica_states as dhdrs + inner join sys.availability_groups as ag + on ag.group_id = dhdrs.group_id + inner join sys.availability_replicas as ar + on dhdrs.replica_id = ar.replica_id + """.strip(), + "columns": [ + {"name": "availability_group", "type": "tag"}, + {"name": "availability_group_name", "type": "tag"}, + {"name": "replica_server_name", "type": "tag"}, + {"name": "synchronization_state_desc", "type": "tag"}, + {"name": "ao.replica_sync_state", "type": "gauge"}, + ], +} + + +class SqlserverDatabaseReplicationStatsMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-hadr-database-replica-states-transact-sql?view=sql-server-ver15 + @property + def include_ao_metrics(self) -> bool: + return is_affirmative(self.instance_config.get('include_ao_metrics', False)) + + @property + def availability_group(self): + return self.instance_config.get('availability_group') + + @property + def only_emit_local(self): + return is_affirmative(self.instance_config.get('only_emit_local', False)) + + @property + def enabled(self): + if not self.include_ao_metrics: + return False + return True + + @property + def queries(self): + query = DATABASE_REPLICATION_STATS_METRICS_QUERY.copy() + if self.availability_group or self.only_emit_local: + where_clauses = [] + if self.availability_group: + where_clauses.append(f"resource_group_id = '{self.availability_group}'") + if self.only_emit_local: + where_clauses.append("is_local = 1") + query['query'] += f" where {' and '.join(where_clauses)}" + return [query] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_ao_metrics={self.include_ao_metrics}, " + f"availability_group={self.availability_group}, " + f"only_emit_local={self.only_emit_local})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/database_stats_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/database_stats_metrics.py new file mode 100644 index 0000000000000..d8024f4deb929 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/database_stats_metrics.py @@ -0,0 +1,45 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + + +from .base import SqlserverDatabaseMetricsBase + +DATABASE_STATS_METRICS_QUERY = { + "name": "sys.databases", + "query": """SELECT + name as db, + name as database_name, + state_desc, + recovery_model_desc, + state, + is_sync_with_backup, + is_in_standby, + is_read_only + from sys.databases + """, + "columns": [ + {"name": "db", "type": "tag"}, + {"name": "database", "type": "tag"}, + {"name": "database_state_desc", "type": "tag"}, + {"name": "database_recovery_model_desc", "type": "tag"}, + {"name": "database.state", "type": "gauge"}, + {"name": "database.is_sync_with_backup", "type": "gauge"}, + {"name": "database.is_in_standby", "type": "gauge"}, + {"name": "database.is_read_only", "type": "gauge"}, + ], +} + + +class SqlserverDatabaseStatsMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-ver15 + @property + def enabled(self): + return True + + @property + def queries(self): + return [DATABASE_STATS_METRICS_QUERY] + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(" f"enabled={self.enabled}" diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/fci_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/fci_metrics.py new file mode 100644 index 0000000000000..81bbf485509d3 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/fci_metrics.py @@ -0,0 +1,64 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative +from datadog_checks.sqlserver.const import ( + ENGINE_EDITION_AZURE_MANAGED_INSTANCE, +) +from datadog_checks.sqlserver.utils import is_azure_database + +from .base import SqlserverDatabaseMetricsBase + +QUERY_FAILOVER_CLUSTER_INSTANCE = { + "name": "sys.dm_os_cluster_nodes", + "query": """ + SELECT + NodeName AS node_name, + LOWER(status_description) AS status_description, + FC.cluster_name, + status, + is_current_owner + FROM sys.dm_os_cluster_nodes + -- `sys.dm_hadr_cluster` does not have a related column to join on, this cross join will add the + -- `cluster_name` column to every row by multiplying all the rows in the left table against + -- all the rows in the right table. Note, there will only be one row from `sys.dm_hadr_cluster`. + CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC + """.strip(), + "columns": [ + {"name": "node_name", "type": "tag"}, + {"name": "status", "type": "tag"}, + {"name": "failover_cluster", "type": "tag"}, + {"name": "fci.status", "type": "gauge"}, + {"name": "fci.is_current_owner", "type": "gauge"}, + ], +} + + +class SqlserverFciMetrics(SqlserverDatabaseMetricsBase): + @property + def include_fci_metrics(self): + return is_affirmative(self.instance_config.get('include_fci_metrics', False)) + + @property + def enabled(self): + if not self.include_fci_metrics: + return False + if not self.major_version and not is_azure_database(self.engine_edition): + return False + if self.major_version > 2012 or self.engine_edition == ENGINE_EDITION_AZURE_MANAGED_INSTANCE: + return True + return False + + @property + def queries(self): + return [QUERY_FAILOVER_CLUSTER_INSTANCE] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"major_version={self.major_version}, " + f"engine_edition={self.engine_edition}, " + f"include_fci_metrics={self.include_fci_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/file_stats_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/file_stats_metrics.py new file mode 100644 index 0000000000000..79e0877769d7a --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/file_stats_metrics.py @@ -0,0 +1,105 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.sqlserver.const import ENGINE_EDITION_SQL_DATABASE +from datadog_checks.sqlserver.utils import is_azure_database + +from .base import SqlserverDatabaseMetricsBase + + +class SqlserverFileStatsMetrics(SqlserverDatabaseMetricsBase): + @property + def enabled(self): + if not self.major_version and not is_azure_database(self.engine_edition): + return False + return True + + @property + def queries(self): + return [self.__get_query_file_stats()] + + def __get_query_file_stats(self) -> dict: + """ + Construct the dm_io_virtual_file_stats QueryExecutor configuration based on the SQL Server major version + :return: a QueryExecutor query config object + """ + + column_definitions = { + "size_on_disk_bytes": {"name": "files.size_on_disk", "type": "gauge"}, + "num_of_reads": {"name": "files.reads", "type": "monotonic_count"}, + "num_of_bytes_read": {"name": "files.read_bytes", "type": "monotonic_count"}, + "io_stall_read_ms": {"name": "files.read_io_stall", "type": "monotonic_count"}, + "io_stall_queued_read_ms": { + "name": "files.read_io_stall_queued", + "type": "monotonic_count", + }, + "num_of_writes": {"name": "files.writes", "type": "monotonic_count"}, + "num_of_bytes_written": { + "name": "files.written_bytes", + "type": "monotonic_count", + }, + "io_stall_write_ms": { + "name": "files.write_io_stall", + "type": "monotonic_count", + }, + "io_stall_queued_write_ms": { + "name": "files.write_io_stall_queued", + "type": "monotonic_count", + }, + "io_stall": {"name": "files.io_stall", "type": "monotonic_count"}, + } + + if self.major_version <= 2012 and not is_azure_database(self.engine_edition): + column_definitions.pop("io_stall_queued_read_ms") + column_definitions.pop("io_stall_queued_write_ms") + + # sort columns to ensure a static column order + sql_columns = [] + metric_columns = [] + for column in sorted(column_definitions.keys()): + sql_columns.append("fs.{}".format(column)) + metric_columns.append(column_definitions[column]) + + query_filter = "" + if self.major_version == 2022: + query_filter = "WHERE DB_NAME(fs.database_id) not like 'model_%'" + + query = """ + SELECT + DB_NAME(fs.database_id), + mf.state_desc, + mf.name, + mf.physical_name, + {sql_columns} + FROM sys.dm_io_virtual_file_stats(NULL, NULL) fs + LEFT JOIN sys.master_files mf + ON mf.database_id = fs.database_id + AND mf.file_id = fs.file_id {filter}; + """ + + if self.engine_edition == ENGINE_EDITION_SQL_DATABASE: + # Azure SQL DB does not have access to the sys.master_files view + query = """ + SELECT + DB_NAME(DB_ID()), + df.state_desc, + df.name, + df.physical_name, + {sql_columns} + FROM sys.dm_io_virtual_file_stats(DB_ID(), NULL) fs + LEFT JOIN sys.database_files df + ON df.file_id = fs.file_id; + """ + + return { + "name": "sys.dm_io_virtual_file_stats", + "query": query.strip().format(sql_columns=", ".join(sql_columns), filter=query_filter), + "columns": [ + {"name": "db", "type": "tag"}, + {"name": "state", "type": "tag"}, + {"name": "logical_name", "type": "tag"}, + {"name": "file_location", "type": "tag"}, + ] + + metric_columns, + } diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/master_files_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/master_files_metrics.py new file mode 100644 index 0000000000000..a3ebf5edd89ef --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/master_files_metrics.py @@ -0,0 +1,68 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +MASTER_FILES_METRICS_QUERY = { + "name": "sys.master_files", + "query": """SELECT + sys.databases.name as db, + sys.databases.name as database_name, + file_id, + CASE type + WHEN 0 THEN 'data' + WHEN 1 THEN 'transaction_log' + WHEN 2 THEN 'filestream' + WHEN 3 THEN 'unknown' + WHEN 4 THEN 'full_text' + ELSE 'other' + END AS file_type, + physical_name, + sys.master_files.state_desc as state_desc, + ISNULL(size, 0) as size, + sys.master_files.state as state + from sys.master_files + right outer join sys.databases on sys.master_files.database_id = sys.databases.database_id + """, + "columns": [ + {"name": "db", "type": "tag"}, + {"name": "database", "type": "tag"}, + {"name": "file_id", "type": "tag"}, + {"name": "file_type", "type": "tag"}, + {"name": "file_location", "type": "tag"}, + {"name": "database_files_state_desc", "type": "tag"}, + {"name": "size", "type": "source"}, + {"name": "database.master_files.state", "type": "gauge"}, + ], + "extras": [ + # size is in pages, 1 page = 8 KB. Calculated after the query to avoid int overflow + {"name": "database.master_files.size", "expression": "size*8", "submit_type": "gauge"}, + ], +} + + +class SqlserverMasterFilesMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-master-files-transact-sql + @property + def include_master_files_metrics(self): + return is_affirmative(self.instance_config.get('include_master_files_metrics', False)) + + @property + def enabled(self): + if not self.include_master_files_metrics: + return False + return True + + @property + def queries(self): + return [MASTER_FILES_METRICS_QUERY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_master_files_metrics={self.include_master_files_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/os_schedulers_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/os_schedulers_metrics.py new file mode 100644 index 0000000000000..c1dcfd8092652 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/os_schedulers_metrics.py @@ -0,0 +1,54 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +OS_SCHEDULERS_METRICS_QUERY = { + "name": "sys.dm_os_schedulers", + "query": """SELECT + scheduler_id, + parent_node_id, + current_tasks_count, + current_workers_count, + active_workers_count, + runnable_tasks_count, + work_queue_count + from sys.dm_os_schedulers + """, + "columns": [ + {"name": "scheduler_id", "type": "tag"}, + {"name": "parent_node_id", "type": "tag"}, + {"name": "scheduler.current_tasks_count", "type": "gauge"}, + {"name": "scheduler.current_workers_count", "type": "gauge"}, + {"name": "scheduler.active_workers_count", "type": "gauge"}, + {"name": "scheduler.runnable_tasks_count", "type": "gauge"}, + {"name": "scheduler.work_queue_count", "type": "gauge"}, + ], +} + + +class SqlserverOsSchedulersMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-schedulers-transact-sql + @property + def include_task_scheduler_metrics(self): + return is_affirmative(self.instance_config.get('include_task_scheduler_metrics', False)) + + @property + def enabled(self): + if not self.include_task_scheduler_metrics: + return False + return True + + @property + def queries(self): + return [OS_SCHEDULERS_METRICS_QUERY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_task_scheduler_metrics={self.include_task_scheduler_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/os_tasks_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/os_tasks_metrics.py new file mode 100644 index 0000000000000..036ef35e36bd8 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/os_tasks_metrics.py @@ -0,0 +1,50 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +OS_TASKS_METRICS_QUERY = { + "name": "sys.dm_os_tasks", + "query": """select + scheduler_id, + SUM(CAST(context_switches_count AS BIGINT)) as context_switches_count, + SUM(CAST(pending_io_count AS BIGINT)) as pending_io_count, + SUM(pending_io_byte_count) as pending_io_byte_count, + AVG(pending_io_byte_average) as pending_io_byte_average + from sys.dm_os_tasks group by scheduler_id + """, + "columns": [ + {"name": "scheduler_id", "type": "tag"}, + {"name": "task.context_switches_count", "type": "gauge"}, + {"name": "task.pending_io_count", "type": "gauge"}, + {"name": "task.pending_io_byte_count", "type": "gauge"}, + {"name": "task.pending_io_byte_average", "type": "gauge"}, + ], +} + + +class SqlserverOsTasksMetrics(SqlserverDatabaseMetricsBase): + # https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-tasks-transact-sql + @property + def include_task_scheduler_metrics(self): + return is_affirmative(self.instance_config.get('include_task_scheduler_metrics', False)) + + @property + def enabled(self): + if not self.include_task_scheduler_metrics: + return False + return True + + @property + def queries(self): + return [OS_TASKS_METRICS_QUERY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_task_scheduler_metrics={self.include_task_scheduler_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/primary_log_shipping_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/primary_log_shipping_metrics.py new file mode 100644 index 0000000000000..66724c1bacb09 --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/primary_log_shipping_metrics.py @@ -0,0 +1,50 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +QUERY_LOG_SHIPPING_PRIMARY = { + "name": "msdb.dbo.log_shipping_monitor_primary", + "query": """ + SELECT primary_id + ,primary_server + ,primary_database + ,DATEDIFF(SECOND, last_backup_date, GETDATE()) AS time_since_backup + ,backup_threshold*60 as backup_threshold + FROM msdb.dbo.log_shipping_monitor_primary + """.strip(), + "columns": [ + {"name": "primary_id", "type": "tag"}, + {"name": "primary_server", "type": "tag"}, + {"name": "primary_db", "type": "tag"}, + {"name": "log_shipping_primary.time_since_backup", "type": "gauge"}, + {"name": "log_shipping_primary.backup_threshold", "type": "gauge"}, + ], +} + + +class SqlserverPrimaryLogShippingMetrics(SqlserverDatabaseMetricsBase): + @property + def include_primary_log_shipping_metrics(self): + return is_affirmative(self.instance_config.get('include_primary_log_shipping_metrics', False)) + + @property + def enabled(self): + if not self.include_primary_log_shipping_metrics: + return False + return True + + @property + def queries(self): + return [QUERY_LOG_SHIPPING_PRIMARY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"include_primary_log_shipping_metrics={self.include_primary_log_shipping_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/secondary_log_shipping_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/secondary_log_shipping_metrics.py new file mode 100644 index 0000000000000..daa4476058b2d --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/secondary_log_shipping_metrics.py @@ -0,0 +1,58 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + + +from datadog_checks.base.config import is_affirmative + +from .base import SqlserverDatabaseMetricsBase + +QUERY_LOG_SHIPPING_SECONDARY = { + "name": "msdb.dbo.log_shipping_monitor_secondary", + "query": """ + SELECT secondary_server + ,secondary_database + ,secondary_id + ,primary_server + ,primary_database + ,DATEDIFF(SECOND, last_restored_date, GETDATE()) AS time_since_restore + ,DATEDIFF(SECOND, last_copied_date, GETDATE()) AS time_since_copy + ,last_restored_latency*60 as last_restored_latency + ,restore_threshold*60 as restore_threshold + FROM msdb.dbo.log_shipping_monitor_secondary + """.strip(), + "columns": [ + {"name": "secondary_server", "type": "tag"}, + {"name": "secondary_db", "type": "tag"}, + {"name": "secondary_id", "type": "tag"}, + {"name": "primary_server", "type": "tag"}, + {"name": "primary_db", "type": "tag"}, + {"name": "log_shipping_secondary.time_since_restore", "type": "gauge"}, + {"name": "log_shipping_secondary.time_since_copy", "type": "gauge"}, + {"name": "log_shipping_secondary.last_restored_latency", "type": "gauge"}, + {"name": "log_shipping_secondary.restore_threshold", "type": "gauge"}, + ], +} + + +class SqlserverSecondaryLogShippingMetrics(SqlserverDatabaseMetricsBase): + @property + def include_secondary_log_shipping_metrics(self): + return is_affirmative(self.instance_config.get('include_secondary_log_shipping_metrics', False)) + + @property + def enabled(self): + if not self.include_secondary_log_shipping_metrics: + return False + return True + + @property + def queries(self): + return [QUERY_LOG_SHIPPING_SECONDARY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}" + f"(enabled={self.enabled}, " + f"include_secondary_log_shipping_metrics={self.include_secondary_log_shipping_metrics})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/server_state_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/server_state_metrics.py new file mode 100644 index 0000000000000..536b67f0be4ae --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/server_state_metrics.py @@ -0,0 +1,51 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +from datadog_checks.sqlserver.const import ( + ENGINE_EDITION_SQL_DATABASE, +) + +from .base import SqlserverDatabaseMetricsBase + +QUERY_SERVER_STATIC_INFO = { + "name": "sys.dm_os_sys_info", + "query": """ + SELECT (os.ms_ticks/1000) AS [Server Uptime] + ,os.cpu_count AS [CPU Count] + ,(os.physical_memory_kb*1024) AS [Physical Memory Bytes] + ,os.virtual_memory_kb AS [Virtual Memory Bytes] + ,(os.committed_kb*1024) AS [Total Server Memory Bytes] + ,(os.committed_target_kb*1024) AS [Target Server Memory Bytes] + FROM sys.dm_os_sys_info os""".strip(), + "columns": [ + {"name": "server.uptime", "type": "gauge"}, + {"name": "server.cpu_count", "type": "gauge"}, + {"name": "server.physical_memory", "type": "gauge"}, + {"name": "server.virtual_memory", "type": "gauge"}, + {"name": "server.committed_memory", "type": "gauge"}, + {"name": "server.target_memory", "type": "gauge"}, + ], +} + + +class SqlserverServerStateMetrics(SqlserverDatabaseMetricsBase): + @property + def enabled(self): + # Server state queries require VIEW SERVER STATE permissions, which some managed database + # versions do not support. + if self.engine_edition in [ENGINE_EDITION_SQL_DATABASE]: + return False + return True + + @property + def queries(self): + return [QUERY_SERVER_STATIC_INFO] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"major_version={self.major_version}, " + f"engine_edition={self.engine_edition})" + ) diff --git a/sqlserver/datadog_checks/sqlserver/database_metrics/tempdb_file_space_usage_metrics.py b/sqlserver/datadog_checks/sqlserver/database_metrics/tempdb_file_space_usage_metrics.py new file mode 100644 index 0000000000000..6d926c0c2f74f --- /dev/null +++ b/sqlserver/datadog_checks/sqlserver/database_metrics/tempdb_file_space_usage_metrics.py @@ -0,0 +1,65 @@ +# (C) Datadog, Inc. 2024-present +# All rights reserved +# Licensed under a 3-clause BSD style license (see LICENSE) + +import functools + +from datadog_checks.base.config import is_affirmative +from datadog_checks.sqlserver.utils import is_azure_sql_database + +from .base import SqlserverDatabaseMetricsBase + +TEMPDB_SPACE_USAGE_QUERY = { + "name": "sys.dm_db_file_space_usage", + "query": """SELECT + database_id, + ISNULL(SUM(unallocated_extent_page_count)*1.0/128, 0) as free_space, + ISNULL(SUM(version_store_reserved_page_count)*1.0/128, 0) as used_space_by_version_store, + ISNULL(SUM(internal_object_reserved_page_count)*1.0/128, 0) as used_space_by_internal_object, + ISNULL(SUM(user_object_reserved_page_count)*1.0/128, 0) as used_space_by_user_object, + ISNULL(SUM(mixed_extent_page_count)*1.0/128, 0) as mixed_extent_space + FROM sys.dm_db_file_space_usage group by database_id""", + "columns": [ + {"name": "database_id", "type": "tag"}, + {"name": "tempdb.file_space_usage.free_space", "type": "gauge"}, + {"name": "tempdb.file_space_usage.version_store_space", "type": "gauge"}, + {"name": "tempdb.file_space_usage.internal_object_space", "type": "gauge"}, + {"name": "tempdb.file_space_usage.user_object_space", "type": "gauge"}, + {"name": "tempdb.file_space_usage.mixed_extent_space", "type": "gauge"}, + ], +} + + +class SqlserverTempDBFileSpaceUsageMetrics(SqlserverDatabaseMetricsBase): + @property + def include_tempdb_file_space_usage_metrics(self): + return is_affirmative(self.instance_config.get('include_tempdb_file_space_usage_metrics', True)) + + @property + def enabled(self): + if not self.include_tempdb_file_space_usage_metrics: + return False + if is_azure_sql_database(self.engine_edition): + return False + return True + + @property + def queries(self): + return [TEMPDB_SPACE_USAGE_QUERY] + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"enabled={self.enabled}, " + f"engine_edition={self.engine_edition}, " + f"include_tempdb_file_space_usage_metrics={self.include_tempdb_file_space_usage_metrics})" + ) + + def _build_query_executors(self): + executor = self.new_query_executor( + self.queries, + executor=functools.partial(self.execute_query_handler, db='tempdb'), + extra_tags=['db:tempdb', 'database:tempdb'], + ) + executor.compile_queries() + return [executor] diff --git a/sqlserver/datadog_checks/sqlserver/metrics.py b/sqlserver/datadog_checks/sqlserver/metrics.py index 8a66db57c3b06..9c9b410345ce5 100644 --- a/sqlserver/datadog_checks/sqlserver/metrics.py +++ b/sqlserver/datadog_checks/sqlserver/metrics.py @@ -2,6 +2,10 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) """ +DEPRECATED: +This module is considered deprecated and will be removed in a future release. +DO NOT add new metrics to this module. Instead, use the `database_metrics` module. + Collection of metric classes for specific SQL Server tables. """ from __future__ import division @@ -9,12 +13,6 @@ from collections import defaultdict from functools import partial -from datadog_checks.base import ensure_unicode -from datadog_checks.base.errors import CheckException -from datadog_checks.base.utils.time import get_precise_time - -from .utils import construct_use_statement, is_azure_sql_database - # Queries ALL_INSTANCES = 'ALL' @@ -373,673 +371,6 @@ def fetch_metric(self, rows, columns, values_cache=None): self.report_function(metric_name, column_val, tags=metric_tags) -# https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-schedulers-transact-sql -class SqlOsSchedulers(BaseSqlServerMetric): - TABLE = 'sys.dm_os_schedulers' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = "select * from {table}".format(table=TABLE) - OPERATION_NAME = 'os_schedulers_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - scheduler_index = columns.index("scheduler_id") - parent_node_index = columns.index("parent_node_id") - - for row in rows: - column_val = row[value_column_index] - scheduler_id = row[scheduler_index] - parent_node_id = row[parent_node_index] - - metric_tags = ['scheduler_id:{}'.format(str(scheduler_id)), 'parent_node_id:{}'.format(str(parent_node_id))] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-tasks-transact-sql -class SqlOsTasks(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.dm_os_tasks' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """ - select scheduler_id, - SUM(CAST(context_switches_count AS BIGINT)) as context_switches_count, - SUM(CAST(pending_io_count AS BIGINT)) as pending_io_count, - SUM(pending_io_byte_count) as pending_io_byte_count, - AVG(pending_io_byte_average) as pending_io_byte_average - from {table} group by scheduler_id; - """.format( - table=TABLE - ) - OPERATION_NAME = 'os_tasks_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - scheduler_id_column_index = columns.index("scheduler_id") - value_column_index = columns.index(self.column) - - for row in rows: - column_val = row[value_column_index] - scheduler_id = row[scheduler_id_column_index] - - metric_tags = ['scheduler_id:{}'.format(str(scheduler_id))] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-master-files-transact-sql -class SqlMasterDatabaseFileStats(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.master_files' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """ - select sys.databases.name as name, file_id, type, physical_name, size, max_size, - sys.master_files.state as state, sys.master_files.state_desc as state_desc from {table} - right outer join sys.databases on sys.master_files.database_id = sys.databases.database_id; - """.format( - table=TABLE - ) - DB_TYPE_MAP = {0: 'data', 1: 'transaction_log', 2: 'filestream', 3: 'unknown', 4: 'full_text'} - OPERATION_NAME = 'master_database_file_stats_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - db_name = columns.index("name") - file_id = columns.index("file_id") - file_type = columns.index("type") - file_location = columns.index("physical_name") - db_files_state_desc_index = columns.index("state_desc") - value_column_index = columns.index(self.column) - - for row in rows: - column_val = row[value_column_index] - if column_val is None: - continue - if self.column in ('size', 'max_size'): - column_val *= 8 # size reported in 8 KB pages - - fileid = row[file_id] - filetype = self.DB_TYPE_MAP[row[file_type]] - location = row[file_location] - db_files_state_desc = row[db_files_state_desc_index] - dbname = row[db_name] - - metric_tags = [ - 'database:{}'.format(str(dbname)), - 'db:{}'.format(str(dbname)), - 'file_id:{}'.format(str(fileid)), - 'file_type:{}'.format(str(filetype)), - 'file_location:{}'.format(str(location)), - 'database_files_state_desc:{}'.format(str(db_files_state_desc)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-database-files-transact-sql -class SqlDatabaseFileStats(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.database_files' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = "select *, CAST(FILEPROPERTY(name, 'SpaceUsed') as int) as space_used from {table}".format(table=TABLE) - OPERATION_NAME = 'database_file_stats_metrics' - - DB_TYPE_MAP = {0: 'data', 1: 'transaction_log', 2: 'filestream', 3: 'unknown', 4: 'full_text'} - - def __init__(self, cfg_instance, base_name, report_function, column, logger): - super(SqlDatabaseFileStats, self).__init__(cfg_instance, base_name, report_function, column, logger) - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - # special case since this table is specific to databases, need to run query for each database instance - rows = [] - columns = [] - - if databases is None: - databases = [] - - cursor.execute('select DB_NAME()') # This can return None in some implementations, so it cannot be chained - data = cursor.fetchall() - current_db = data[0][0] - logger.debug("%s: current db is %s", cls.__name__, current_db) - - for db in databases: - # use statements need to be executed separate from select queries - ctx = construct_use_statement(db) - try: - # Azure SQL DB does not allow running the USE command - if not is_azure_sql_database(engine_edition): - logger.debug("%s: changing cursor context via use statement: %s", cls.__name__, ctx) - cursor.execute(ctx) - logger.debug("%s: fetch_all executing query: %s", cls.__name__, cls.QUERY_BASE) - cursor.execute(cls.QUERY_BASE) - data = cursor.fetchall() - except Exception as e: - logger.warning("Error when trying to query db %s - skipping. Error: %s", db, e) - continue - - query_columns = ['database'] + [i[0] for i in cursor.description] - if columns: - if columns != query_columns: - raise CheckException('Assertion error: {} != {}'.format(columns, query_columns)) - else: - columns = query_columns - - results = [] - # insert database name as new column for each row - for row in data: - r = list(row) - r.insert(0, db) - results.append(r) - - rows.extend(results) - - logger.debug("%s: received %d rows and %d columns for db %s", cls.__name__, len(data), len(columns), db) - - # Azure SQL DB does not allow running the USE command - if not is_azure_sql_database(engine_edition): - # reset back to previous db - logger.debug("%s: reverting cursor context via use statement to %s", cls.__name__, current_db) - cursor.execute(construct_use_statement(current_db)) - - return rows, columns - - def fetch_metric(self, rows, columns, values_cache=None): - try: - db_name = columns.index('database') - file_id = columns.index("file_id") - file_type = columns.index("type") - file_location = columns.index("physical_name") - filename_idx = columns.index("name") - db_files_state_desc_index = columns.index("state_desc") - value_column_index = columns.index(self.column) - except ValueError as e: - raise CheckException( - "Could not fetch all required information from columns {}:\n\t{}".format(str(columns), str(e)) - ) - - for row in rows: - if row[db_name] != self.instance: - continue - column_val = row[value_column_index] - if self.column in ('size', 'max_size', 'space_used'): - column_val = (column_val or 0) * 8 # size reported in 8 KB pages - - fileid = row[file_id] - filetype = self.DB_TYPE_MAP[row[file_type]] - location = row[file_location] - filename = row[filename_idx] - db_files_state_desc = row[db_files_state_desc_index] - - metric_tags = [ - 'database:{}'.format(str(self.instance)), - 'db:{}'.format(str(self.instance)), - 'file_id:{}'.format(str(fileid)), - 'file_type:{}'.format(str(filetype)), - 'file_location:{}'.format(str(location)), - 'file_name:{}'.format(str(filename)), - 'database_files_state_desc:{}'.format(str(db_files_state_desc)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-ver15 -class SqlDatabaseStats(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.databases' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = "select * from {table}".format(table=TABLE) - OPERATION_NAME = 'database_stats_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - database_name = columns.index("name") - db_state_desc_index = columns.index("state_desc") - db_recovery_model_desc_index = columns.index("recovery_model_desc") - value_column_index = columns.index(self.column) - - for row in rows: - if row[database_name].lower() != self.instance.lower(): - continue - - column_val = row[value_column_index] - db_state_desc = row[db_state_desc_index] - db_recovery_model_desc = row[db_recovery_model_desc_index] - metric_tags = [ - 'database:{}'.format(str(self.instance)), - 'db:{}'.format(str(self.instance)), - 'database_state_desc:{}'.format(str(db_state_desc)), - 'database_recovery_model_desc:{}'.format(str(db_recovery_model_desc)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# msdb.dbo.backupset -# -# Contains a row for each backup set. A backup set -# contains the backup from a single, successful backup operation. -# https://docs.microsoft.com/en-us/sql/relational-databases/system-tables/backupset-transact-sql?view=sql-server-ver15 -class SqlDatabaseBackup(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'msdb.dbo.backupset' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """ - select sys.databases.name as database_name, count(backup_set_id) as backup_set_id_count - from {table} right outer join sys.databases - on sys.databases.name = msdb.dbo.backupset.database_name - group by sys.databases.name""".format( - table=TABLE - ) - OPERATION_NAME = 'database_backup_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - database_name = columns.index("database_name") - value_column_index = columns.index(self.column) - - for row in rows: - if row[database_name] != self.instance: - continue - - column_val = row[value_column_index] - metric_tags = [ - 'database:{}'.format(str(self.instance)), - 'db:{}'.format(str(self.instance)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - -# sys.dm_db_index_physical_stats -# -# Returns size and fragmentation information for the data and -# indexes of the specified table or view in SQL Server. -# -# There are reports of this query being very slow for large datasets, -# so debug query timing are included to help monitor it. -# https://dba.stackexchange.com/q/76374 -# -# https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-index-physical-stats-transact-sql?view=sql-server-ver15 -class SqlDbFragmentation(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.dm_db_index_physical_stats' - DEFAULT_METRIC_TYPE = 'gauge' - - QUERY_BASE = ( - "SELECT DB_NAME(DDIPS.database_id) as database_name, " - "OBJECT_NAME(DDIPS.object_id, DDIPS.database_id) as object_name, " - "DDIPS.index_id as index_id, DDIPS.fragment_count as fragment_count, " - "DDIPS.avg_fragment_size_in_pages as avg_fragment_size_in_pages, " - "DDIPS.page_count as page_count, " - "DDIPS.avg_fragmentation_in_percent as avg_fragmentation_in_percent, I.name as index_name " - "FROM {table} (DB_ID('{{db}}'),null,null,null,null) as DDIPS " - "INNER JOIN sys.indexes as I WITH (nolock) ON I.object_id = DDIPS.object_id " - "AND DDIPS.index_id = I.index_id " - "WHERE DDIPS.fragment_count is not null".format(table=TABLE) - ) - OPERATION_NAME = 'db_fragmentation_metrics' - - def __init__(self, cfg_instance, base_name, report_function, column, logger): - super(SqlDbFragmentation, self).__init__(cfg_instance, base_name, report_function, column, logger) - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - # special case to limit this query to specific databases and monitor performance - rows = [] - columns = [] - if databases is None: - databases = [] - - logger.debug("%s: gathering fragmentation metrics for these databases: %s", cls.__name__, databases) - - for db in databases: - ctx = construct_use_statement(db) - query = cls.QUERY_BASE.format(db=db) - start = get_precise_time() - try: - if not is_azure_sql_database(engine_edition): - logger.debug("%s: changing cursor context via use statement: %s", cls.__name__, ctx) - cursor.execute(ctx) - logger.debug("%s: fetch_all executing query: %s", cls.__name__, query) - cursor.execute(query) - data = cursor.fetchall() - except Exception as e: - logger.warning("Error when trying to query db %s - skipping. Error: %s", db, e) - continue - elapsed = get_precise_time() - start - - query_columns = [i[0] for i in cursor.description] - if columns: - if columns != query_columns: - raise CheckException('Assertion error: {} != {}'.format(columns, query_columns)) - else: - columns = query_columns - - rows.extend(data) - logger.debug("%s: received %d rows for db %s, elapsed time: %.4f sec", cls.__name__, len(data), db, elapsed) - - return rows, columns - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - database_name = columns.index("database_name") - object_name_index = columns.index("object_name") - index_id_index = columns.index("index_id") - index_name_index = columns.index("index_name") - - for row in rows: - if row[database_name] != self.instance: - continue - - column_val = row[value_column_index] - object_name = row[object_name_index] - index_id = row[index_id_index] - index_name = row[index_name_index] - - object_list = self.cfg_instance.get('db_fragmentation_object_names') - - if object_list and (object_name not in object_list): - continue - - metric_tags = [ - u'database_name:{}'.format(ensure_unicode(self.instance)), - u'db:{}'.format(ensure_unicode(self.instance)), - u'object_name:{}'.format(ensure_unicode(object_name)), - u'index_id:{}'.format(ensure_unicode(index_id)), - u'index_name:{}'.format(ensure_unicode(index_name)), - ] - - metric_tags.extend(self.tags) - self.report_function(self.metric_name, column_val, tags=metric_tags) - - -# https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-hadr-database-replica-states-transact-sql?view=sql-server-ver15 -class SqlDbReplicaStates(BaseSqlServerMetric): - TABLE = 'sys.dm_hadr_database_replica_states' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """select * from {table} as dhdrs - inner join sys.availability_groups as ag - on ag.group_id = dhdrs.group_id - inner join sys.availability_replicas as ar - on dhdrs.replica_id = ar.replica_id""".format( - table=TABLE - ) - OPERATION_NAME = 'db_replica_states_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - sync_state_desc_index = columns.index('synchronization_state_desc') - resource_group_id_index = columns.index('resource_group_id') - resource_group_name_index = columns.index('name') - replica_server_name_index = columns.index('replica_server_name') - is_local_index = columns.index('is_local') - - for row in rows: - is_local = row[is_local_index] - resource_group_id = row[resource_group_id_index] - resource_group_name = row[resource_group_name_index] - selected_ag = self.cfg_instance.get('availability_group') - - if self.cfg_instance.get('only_emit_local') and not is_local: - continue - - elif selected_ag and selected_ag != resource_group_id: - continue - - column_val = row[value_column_index] - sync_state_desc = row[sync_state_desc_index] - replica_server_name = row[replica_server_name_index] - - metric_tags = [ - 'synchronization_state_desc:{}'.format(str(sync_state_desc)), - 'replica_server_name:{}'.format(str(replica_server_name)), - 'availability_group:{}'.format(str(resource_group_id)), - 'availability_group_name:{}'.format(str(resource_group_name)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - - self.report_function(metric_name, column_val, tags=metric_tags) - - -# sys.dm_hadr_availability_group_states -# Returns a row for each Always On availability group that possesses an availability replica on the local instance of -# SQL Server. Each row displays the states that define the health of a given availability group. -# -# https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-hadr-availability-group-states-transact-sql?view=sql-server-ver15 -class SqlAvailabilityGroups(BaseSqlServerMetric): - TABLE = 'sys.dm_hadr_availability_group_states' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """select * from {table} as dhdrcs - inner join sys.availability_groups as ag - on ag.group_id = dhdrcs.group_id""".format( - table=TABLE - ) - OPERATION_NAME = 'availability_groups_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - - resource_group_id_index = columns.index('resource_group_id') - resource_group_name_index = columns.index('name') - sync_health_desc_index = columns.index('synchronization_health_desc') - - for row in rows: - resource_group_id = row[resource_group_id_index] - resource_group_name = row[resource_group_name_index] - selected_ag = self.cfg_instance.get('availability_group') - - if selected_ag and selected_ag != resource_group_id: - continue - - column_val = row[value_column_index] - sync_health_desc = row[sync_health_desc_index] - metric_tags = [ - 'availability_group:{}'.format(str(resource_group_id)), - 'availability_group_name:{}'.format(str(resource_group_name)), - 'synchronization_health_desc:{}'.format(str(sync_health_desc)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - - self.report_function(metric_name, column_val, tags=metric_tags) - - -# sys.availability_replicas (Transact-SQL) -# -# Returns a row for each of the availability replicas that belong to any Always On availability group in the WSFC -# failover cluster. If the local server instance is unable to talk to the WSFC failover cluster, for example because -# the cluster is down or quorum has been lost, only rows for local availability replicas are returned. -# These rows will contain only the columns of data that are cached locally in metadata. -# -# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-availability-replicas-transact-sql?view=sql-server-ver15 -class SqlAvailabilityReplicas(BaseSqlServerMetric): - TABLE = 'sys.availability_replicas' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """select * from {table} as ar - inner join sys.dm_hadr_database_replica_cluster_states as dhdrcs - on ar.replica_id = dhdrcs.replica_id - inner join sys.dm_hadr_database_replica_states as dhdrs - on ar.replica_id = dhdrs.replica_id and dhdrcs.group_database_id = dhdrs.group_database_id - inner join sys.availability_groups as ag - on ag.group_id = ar.group_id""".format( - table=TABLE - ) - OPERATION_NAME = 'availability_replicas_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - return cls._fetch_generic_values(cursor, None, logger) - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - - failover_mode_desc_index = columns.index('failover_mode_desc') - replica_server_name_index = columns.index('replica_server_name') - resource_group_id_index = columns.index('resource_group_id') - resource_group_name_index = columns.index('name') - is_local_index = columns.index('is_local') - database_name_index = columns.index('database_name') - try: - is_primary_replica_index = columns.index('is_primary_replica') - except ValueError: - # This column only supported in SQL Server 2014 and later - is_primary_replica_index = None - - for row in rows: - is_local = row[is_local_index] - resource_group_id = row[resource_group_id_index] - resource_group_name = row[resource_group_name_index] - database_name = row[database_name_index] - selected_ag = self.cfg_instance.get('availability_group') - selected_database = self.cfg_instance.get('ao_database') - if self.cfg_instance.get('only_emit_local') and not is_local: - continue - - elif selected_ag and selected_ag != resource_group_id: - continue - - elif selected_database and selected_database != database_name: - continue - - column_val = row[value_column_index] - failover_mode_desc = row[failover_mode_desc_index] - replica_server_name = row[replica_server_name_index] - resource_group_id = row[resource_group_id_index] - - metric_tags = [ - 'replica_server_name:{}'.format(str(replica_server_name)), - 'availability_group:{}'.format(str(resource_group_id)), - 'availability_group_name:{}'.format(str(resource_group_name)), - 'failover_mode_desc:{}'.format(str(failover_mode_desc)), - 'db:{}'.format(str(database_name)), - ] - if is_primary_replica_index is not None: - is_primary_replica = row[is_primary_replica_index] - metric_tags.append('is_primary_replica:{}'.format(str(is_primary_replica))) - else: - metric_tags.append('is_primary_replica:unknown') - - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - - self.report_function(metric_name, column_val, tags=metric_tags) - - -# https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-db-file-space-usage-transact-sql?view=sql-server-ver15 -class SqlDbFileSpaceUsage(BaseSqlServerMetric): - CUSTOM_QUERIES_AVAILABLE = False - TABLE = 'sys.dm_db_file_space_usage' - DEFAULT_METRIC_TYPE = 'gauge' - QUERY_BASE = """SELECT - database_id, - DB_NAME(database_id) as database_name, - ISNULL(SUM(unallocated_extent_page_count)*1.0/128, 0) as free_space, - ISNULL(SUM(version_store_reserved_page_count)*1.0/128, 0) as used_space_by_version_store, - ISNULL(SUM(internal_object_reserved_page_count)*1.0/128, 0) as used_space_by_internal_object, - ISNULL(SUM(user_object_reserved_page_count)*1.0/128, 0) as used_space_by_user_object, - ISNULL(SUM(mixed_extent_page_count)*1.0/128, 0) as mixed_extent_space - FROM {table} group by database_id""".format( - table=TABLE - ) - OPERATION_NAME = 'db_file_space_usage_metrics' - - @classmethod - def fetch_all_values(cls, cursor, counters_list, logger, databases=None, engine_edition=None): - rows = [] - columns = [] - - cursor.execute('select DB_NAME()') # This can return None in some implementations, so it cannot be chained - data = cursor.fetchall() - current_db = data[0][0] - logger.debug("%s: current db is %s", cls.__name__, current_db) - - logger.debug("%s: gathering db file space usage metrics for tempdb", cls.__name__) - db = 'tempdb' # we are only interested in tempdb - ctx = construct_use_statement(db) - start = get_precise_time() - try: - if not is_azure_sql_database(engine_edition): - logger.debug("%s: changing cursor context via use statement: %s", cls.__name__, ctx) - cursor.execute(ctx) - logger.debug("%s: fetch_all executing query: %s", cls.__name__, cls.QUERY_BASE) - cursor.execute(cls.QUERY_BASE) - data = cursor.fetchall() - except Exception as e: - logger.warning("Error when trying to query db %s - skipping. Error: %s", db, e) - elapsed = get_precise_time() - start - - query_columns = [i[0] for i in cursor.description] - if columns: - if columns != query_columns: - raise CheckException('Assertion error: {} != {}'.format(columns, query_columns)) - else: - columns = query_columns - - rows.extend(data) - logger.debug("%s: received %d rows for db %s, elapsed time: %.4f sec", cls.__name__, len(data), db, elapsed) - - # reset back to previous db - if current_db and not is_azure_sql_database(engine_edition): - logger.debug("%s: reverting cursor context via use statement to %s", cls.__name__, current_db) - cursor.execute(construct_use_statement(current_db)) - - return rows, columns - - def fetch_metric(self, rows, columns, values_cache=None): - value_column_index = columns.index(self.column) - database_id_index = columns.index('database_id') - database_name_index = columns.index('database_name') - - for row in rows: - database_id = row[database_id_index] - database_name = row[database_name_index] - column_val = row[value_column_index] - - if database_name != self.instance: - continue - - metric_tags = [ - 'database:{}'.format(str(database_name)), - 'db:{}'.format(str(database_name)), - 'database_id:{}'.format(str(database_id)), - ] - metric_tags.extend(self.tags) - metric_name = '{}'.format(self.metric_name) - self.report_function(metric_name, column_val, tags=metric_tags) - - DEFAULT_PERFORMANCE_TABLE = "sys.dm_os_performance_counters" VALID_TABLES = {cls.TABLE for cls in BaseSqlServerMetric.__subclasses__() if cls.CUSTOM_QUERIES_AVAILABLE} TABLE_MAPPING = { diff --git a/sqlserver/datadog_checks/sqlserver/queries.py b/sqlserver/datadog_checks/sqlserver/queries.py index 99f20e501e680..f479a4cd5ff3c 100644 --- a/sqlserver/datadog_checks/sqlserver/queries.py +++ b/sqlserver/datadog_checks/sqlserver/queries.py @@ -2,146 +2,6 @@ # All rights reserved # Licensed under a 3-clause BSD style license (see LICENSE) -from datadog_checks.sqlserver.const import ENGINE_EDITION_SQL_DATABASE -from datadog_checks.sqlserver.utils import is_azure_database - -QUERY_SERVER_STATIC_INFO = { - "name": "sys.dm_os_sys_info", - "query": """ - SELECT (os.ms_ticks/1000) AS [Server Uptime] - ,os.cpu_count AS [CPU Count] - ,(os.physical_memory_kb*1024) AS [Physical Memory Bytes] - ,os.virtual_memory_kb AS [Virtual Memory Bytes] - ,(os.committed_kb*1024) AS [Total Server Memory Bytes] - ,(os.committed_target_kb*1024) AS [Target Server Memory Bytes] - FROM sys.dm_os_sys_info os""".strip(), - "columns": [ - {"name": "server.uptime", "type": "gauge"}, - {"name": "server.cpu_count", "type": "gauge"}, - {"name": "server.physical_memory", "type": "gauge"}, - {"name": "server.virtual_memory", "type": "gauge"}, - {"name": "server.committed_memory", "type": "gauge"}, - {"name": "server.target_memory", "type": "gauge"}, - ], -} - -QUERY_AO_FAILOVER_CLUSTER = { - "name": "sys.dm_hadr_cluster", - "query": """ - SELECT - 1, - LOWER(quorum_type_desc) AS quorum_type_desc, - 1, - LOWER(quorum_state_desc) AS quorum_state_desc, - cluster_name - FROM sys.dm_hadr_cluster - """.strip(), - "columns": [ - {"name": "ao.quorum_type", "type": "gauge"}, - {"name": "quorum_type", "type": "tag"}, - {"name": "ao.quorum_state", "type": "gauge"}, - {"name": "quorum_state", "type": "tag"}, - {"name": "failover_cluster", "type": "tag"}, - ], -} - -QUERY_AO_FAILOVER_CLUSTER_MEMBER = { - "name": "sys.dm_hadr_cluster_members", - "query": """ - SELECT - member_name, - 1, - LOWER(member_type_desc) AS member_type_desc, - 1, - LOWER(member_state_desc) AS member_state_desc, - number_of_quorum_votes, - FC.cluster_name - FROM sys.dm_hadr_cluster_members - -- `sys.dm_hadr_cluster` does not have a related column to join on, this cross join will add the - -- `cluster_name` column to every row by multiplying all the rows in the left table against - -- all the rows in the right table. Note, there will only be one row from `sys.dm_hadr_cluster`. - CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC - """.strip(), - "columns": [ - {"name": "member_name", "type": "tag"}, - {"name": "ao.member.type", "type": "gauge"}, - {"name": "member_type", "type": "tag"}, - {"name": "ao.member.state", "type": "gauge"}, - {"name": "member_state", "type": "tag"}, - {"name": "ao.member.number_of_quorum_votes", "type": "gauge"}, - {"name": "failover_cluster", "type": "tag"}, - ], -} - -QUERY_FAILOVER_CLUSTER_INSTANCE = { - "name": "sys.dm_os_cluster_nodes", - "query": """ - SELECT - NodeName AS node_name, - status, - LOWER(status_description) AS status_description, - is_current_owner, - FC.cluster_name - FROM sys.dm_os_cluster_nodes - -- `sys.dm_hadr_cluster` does not have a related column to join on, this cross join will add the - -- `cluster_name` column to every row by multiplying all the rows in the left table against - -- all the rows in the right table. Note, there will only be one row from `sys.dm_hadr_cluster`. - CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC - """.strip(), - "columns": [ - {"name": "node_name", "type": "tag"}, - {"name": "fci.status", "type": "gauge"}, - {"name": "status", "type": "tag"}, - {"name": "fci.is_current_owner", "type": "gauge"}, - {"name": "failover_cluster", "type": "tag"}, - ], -} - -QUERY_LOG_SHIPPING_PRIMARY = { - "name": "msdb.dbo.log_shipping_monitor_primary", - "query": """ - SELECT primary_id - ,primary_server - ,primary_database - ,DATEDIFF(SECOND, last_backup_date, GETDATE()) AS time_since_backup - ,backup_threshold*60 as backup_threshold - FROM msdb.dbo.log_shipping_monitor_primary - """.strip(), - "columns": [ - {"name": "primary_id", "type": "tag"}, - {"name": "primary_server", "type": "tag"}, - {"name": "primary_db", "type": "tag"}, - {"name": "log_shipping_primary.time_since_backup", "type": "gauge"}, - {"name": "log_shipping_primary.backup_threshold", "type": "gauge"}, - ], -} - -QUERY_LOG_SHIPPING_SECONDARY = { - "name": "msdb.dbo.log_shipping_monitor_secondary", - "query": """ - SELECT secondary_server - ,secondary_database - ,secondary_id - ,primary_server - ,primary_database - ,DATEDIFF(SECOND, last_restored_date, GETDATE()) AS time_since_restore - ,DATEDIFF(SECOND, last_copied_date, GETDATE()) AS time_since_copy - ,last_restored_latency*60 as last_restored_latency - ,restore_threshold*60 as restore_threshold - FROM msdb.dbo.log_shipping_monitor_secondary - """.strip(), - "columns": [ - {"name": "secondary_server", "type": "tag"}, - {"name": "secondary_db", "type": "tag"}, - {"name": "secondary_id", "type": "tag"}, - {"name": "primary_server", "type": "tag"}, - {"name": "primary_db", "type": "tag"}, - {"name": "log_shipping_secondary.time_since_restore", "type": "gauge"}, - {"name": "log_shipping_secondary.time_since_copy", "type": "gauge"}, - {"name": "log_shipping_secondary.last_restored_latency", "type": "gauge"}, - {"name": "log_shipping_secondary.restore_threshold", "type": "gauge"}, - ], -} DB_QUERY = """ SELECT @@ -256,218 +116,3 @@ def get_deadlocks_query(convert_xml_to_str=False, xe_session_name="datadog"): CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr) WHERE xdr.value('@timestamp', 'datetime') >= DATEADD(SECOND, ?, GETDATE()) ;""" - - -def get_query_ao_availability_groups(sqlserver_major_version): - """ - Construct the sys.availability_groups QueryExecutor configuration based on the SQL Server major version - - :params sqlserver_major_version: SQL Server major version (i.e. 2012, 2019, ...) - :return: a QueryExecutor query config object - """ - column_definitions = { - # AG - sys.availability_groups - "AG.group_id AS availability_group": { - "name": "availability_group", - "type": "tag", - }, - "AG.name AS availability_group_name": { - "name": "availability_group_name", - "type": "tag", - }, - # AR - sys.availability_replicas - "AR.replica_server_name": {"name": "replica_server_name", "type": "tag"}, - "LOWER(AR.failover_mode_desc) AS failover_mode_desc": { - "name": "failover_mode", - "type": "tag", - }, - "LOWER(AR.availability_mode_desc) AS availability_mode_desc": { - "name": "availability_mode", - "type": "tag", - }, - # ADC - sys.availability_databases_cluster - "ADC.database_name": {"name": "database_name", "type": "tag"}, - # DRS - sys.dm_hadr_database_replica_states - "DRS.replica_id": {"name": "replica_id", "type": "tag"}, - "DRS.database_id": {"name": "database_id", "type": "tag"}, - "LOWER(DRS.database_state_desc) AS database_state_desc": { - "name": "database_state", - "type": "tag", - }, - "LOWER(DRS.synchronization_state_desc) AS synchronization_state_desc": { - "name": "synchronization_state", - "type": "tag", - }, - "(DRS.log_send_queue_size * 1024) AS log_send_queue_size": { - "name": "ao.log_send_queue_size", - "type": "gauge", - }, - "(DRS.log_send_rate * 1024) AS log_send_rate": { - "name": "ao.log_send_rate", - "type": "gauge", - }, - "(DRS.redo_queue_size * 1024) AS redo_queue_size": { - "name": "ao.redo_queue_size", - "type": "gauge", - }, - "(DRS.redo_rate * 1024) AS redo_rate": { - "name": "ao.redo_rate", - "type": "gauge", - }, - "DRS.low_water_mark_for_ghosts": { - "name": "ao.low_water_mark_for_ghosts", - "type": "gauge", - }, - "(DRS.filestream_send_rate * 1024) AS filestream_send_rate": { - "name": "ao.filestream_send_rate", - "type": "gauge", - }, - # FC - sys.dm_hadr_cluster - "FC.cluster_name": { - "name": "failover_cluster", - "type": "tag", - }, - # Other - "1 AS replica_sync_topology_indicator": { - "name": "ao.replica_status", - "type": "gauge", - }, - } - - # Include metrics based on version - if sqlserver_major_version >= 2016: - column_definitions["DRS.secondary_lag_seconds"] = { - "name": "ao.secondary_lag_seconds", - "type": "gauge", - } - if sqlserver_major_version >= 2014: - column_definitions["DRS.is_primary_replica"] = { - "name": "ao.is_primary_replica", - "type": "gauge", - } - column_definitions[ - """ - CASE - WHEN DRS.is_primary_replica = 1 THEN 'primary' - WHEN DRS.is_primary_replica = 0 THEN 'secondary' - END AS replica_role_desc - """ - ] = {"name": "replica_role", "type": "tag"} - - # Sort columns to ensure a static column order - sql_columns = [] - metric_columns = [] - for column in sorted(column_definitions.keys()): - sql_columns.append(column) - metric_columns.append(column_definitions[column]) - - return { - "name": "sys.availability_groups", - "query": """ - SELECT - {sql_columns} - FROM - sys.availability_groups AS AG - INNER JOIN sys.availability_replicas AS AR ON AG.group_id = AR.group_id - INNER JOIN sys.availability_databases_cluster AS ADC ON AG.group_id = ADC.group_id - INNER JOIN sys.dm_hadr_database_replica_states AS DRS ON AG.group_id = DRS.group_id - AND ADC.group_database_id = DRS.group_database_id - AND AR.replica_id = DRS.replica_id - -- `sys.dm_hadr_cluster` does not have a related column to join on, this cross join will add the - -- `cluster_name` column to every row by multiplying all the rows in the left table against - -- all the rows in the right table. Note, there will only be one row from `sys.dm_hadr_cluster`. - CROSS JOIN (SELECT TOP 1 cluster_name FROM sys.dm_hadr_cluster) AS FC - """.strip().format( - sql_columns=", ".join(sql_columns), - ), - "columns": metric_columns, - } - - -def get_query_file_stats(sqlserver_major_version, sqlserver_engine_edition): - """ - Construct the dm_io_virtual_file_stats QueryExecutor configuration based on the SQL Server major version - - :param sqlserver_engine_edition: The engine version (i.e. 5 for Azure SQL DB...) - :param sqlserver_major_version: SQL Server major version (i.e. 2012, 2019, ...) - :return: a QueryExecutor query config object - """ - - column_definitions = { - "size_on_disk_bytes": {"name": "files.size_on_disk", "type": "gauge"}, - "num_of_reads": {"name": "files.reads", "type": "monotonic_count"}, - "num_of_bytes_read": {"name": "files.read_bytes", "type": "monotonic_count"}, - "io_stall_read_ms": {"name": "files.read_io_stall", "type": "monotonic_count"}, - "io_stall_queued_read_ms": { - "name": "files.read_io_stall_queued", - "type": "monotonic_count", - }, - "num_of_writes": {"name": "files.writes", "type": "monotonic_count"}, - "num_of_bytes_written": { - "name": "files.written_bytes", - "type": "monotonic_count", - }, - "io_stall_write_ms": { - "name": "files.write_io_stall", - "type": "monotonic_count", - }, - "io_stall_queued_write_ms": { - "name": "files.write_io_stall_queued", - "type": "monotonic_count", - }, - "io_stall": {"name": "files.io_stall", "type": "monotonic_count"}, - } - - if sqlserver_major_version <= 2012 and not is_azure_database(sqlserver_engine_edition): - column_definitions.pop("io_stall_queued_read_ms") - column_definitions.pop("io_stall_queued_write_ms") - - # sort columns to ensure a static column order - sql_columns = [] - metric_columns = [] - for column in sorted(column_definitions.keys()): - sql_columns.append("fs.{}".format(column)) - metric_columns.append(column_definitions[column]) - - query_filter = "" - if sqlserver_major_version == 2022: - query_filter = "WHERE DB_NAME(fs.database_id) not like 'model_%'" - - query = """ - SELECT - DB_NAME(fs.database_id), - mf.state_desc, - mf.name, - mf.physical_name, - {sql_columns} - FROM sys.dm_io_virtual_file_stats(NULL, NULL) fs - LEFT JOIN sys.master_files mf - ON mf.database_id = fs.database_id - AND mf.file_id = fs.file_id {filter}; - """ - - if sqlserver_engine_edition == ENGINE_EDITION_SQL_DATABASE: - # Azure SQL DB does not have access to the sys.master_files view - query = """ - SELECT - DB_NAME(DB_ID()), - df.state_desc, - df.name, - df.physical_name, - {sql_columns} - FROM sys.dm_io_virtual_file_stats(DB_ID(), NULL) fs - LEFT JOIN sys.database_files df - ON df.file_id = fs.file_id; - """ - - return { - "name": "sys.dm_io_virtual_file_stats", - "query": query.strip().format(sql_columns=", ".join(sql_columns), filter=query_filter), - "columns": [ - {"name": "db", "type": "tag"}, - {"name": "state", "type": "tag"}, - {"name": "logical_name", "type": "tag"}, - {"name": "file_location", "type": "tag"}, - ] - + metric_columns, - } diff --git a/sqlserver/datadog_checks/sqlserver/sqlserver.py b/sqlserver/datadog_checks/sqlserver/sqlserver.py index 5eb81568c5320..10a7d5078d4a1 100644 --- a/sqlserver/datadog_checks/sqlserver/sqlserver.py +++ b/sqlserver/datadog_checks/sqlserver/sqlserver.py @@ -24,9 +24,24 @@ from datadog_checks.sqlserver.config import SQLServerConfig from datadog_checks.sqlserver.database_metrics import ( SqlserverAgentMetrics, + SqlserverAoMetrics, + SqlserverAvailabilityGroupsMetrics, + SqlserverAvailabilityReplicasMetrics, SqlserverDatabaseBackupMetrics, + SqlserverDatabaseFilesMetrics, + SqlserverDatabaseReplicationStatsMetrics, + SqlserverDatabaseStatsMetrics, SqlserverDBFragmentationMetrics, + SqlserverFciMetrics, + SqlserverFileStatsMetrics, SqlserverIndexUsageMetrics, + SqlserverMasterFilesMetrics, + SqlserverOsSchedulersMetrics, + SqlserverOsTasksMetrics, + SqlserverPrimaryLogShippingMetrics, + SqlserverSecondaryLogShippingMetrics, + SqlserverServerStateMetrics, + SqlserverTempDBFileSpaceUsageMetrics, SQLServerXESessionMetrics, ) from datadog_checks.sqlserver.deadlocks import Deadlocks @@ -45,20 +60,14 @@ from datadog_checks.sqlserver.__about__ import __version__ from datadog_checks.sqlserver.connection import Connection, SQLConnectionError, split_sqlserver_host_port from datadog_checks.sqlserver.const import ( - AO_METRICS, - AO_METRICS_PRIMARY, - AO_METRICS_SECONDARY, AUTODISCOVERY_QUERY, AWS_RDS_HOSTNAME_SUFFIX, AZURE_DEPLOYMENT_TYPE_TO_RESOURCE_TYPES, BASE_NAME_QUERY, COUNTER_TYPE_QUERY, - DATABASE_MASTER_FILES, - DATABASE_METRICS, DATABASE_SERVICE_CHECK_NAME, DATABASE_SERVICE_CHECK_QUERY, DBM_MIGRATED_METRICS, - ENGINE_EDITION_AZURE_MANAGED_INSTANCE, ENGINE_EDITION_SQL_DATABASE, INSTANCE_METRICS, INSTANCE_METRICS_DATABASE, @@ -74,24 +83,11 @@ STATIC_INFO_RDS, STATIC_INFO_VERSION, SWITCH_DB_STATEMENT, - TASK_SCHEDULER_METRICS, - TEMPDB_FILE_SPACE_USAGE_METRICS, VALID_METRIC_TYPES, expected_sys_databases_columns, ) from datadog_checks.sqlserver.metrics import DEFAULT_PERFORMANCE_TABLE, VALID_TABLES -from datadog_checks.sqlserver.queries import ( - QUERY_AO_FAILOVER_CLUSTER, - QUERY_AO_FAILOVER_CLUSTER_MEMBER, - QUERY_FAILOVER_CLUSTER_INSTANCE, - QUERY_LOG_SHIPPING_PRIMARY, - QUERY_LOG_SHIPPING_SECONDARY, - QUERY_SERVER_STATIC_INFO, - get_query_ao_availability_groups, - get_query_file_stats, -) from datadog_checks.sqlserver.utils import ( - is_azure_database, is_azure_sql_database, set_default_driver_conf, ) @@ -165,11 +161,8 @@ def __init__(self, name, init_config, instances): # Query declarations self._query_manager = None - self._dynamic_queries = None # DEPRECATED, new metrics should use database_metrics - self.server_state_queries = None - self.sqlserver_incr_fraction_metric_previous_values = {} - self._database_metrics = None + self.sqlserver_incr_fraction_metric_previous_values = {} self._schemas = Schemas(self, self._config) @@ -488,67 +481,6 @@ def _make_metric_list_to_collect(self, custom_metrics): physical_database_name=db.physical_db_name, ) - # Load database statistics - db_stats_to_collect = list(DATABASE_METRICS) - engine_edition = self.static_info_cache.get(STATIC_INFO_ENGINE_EDITION) - - for name, table, column in db_stats_to_collect: - # include database as a filter option - db_names = [d.name for d in self.databases] or [ - self.instance.get("database", self.connection.DEFAULT_DATABASE) - ] - for db_name in db_names: - cfg = { - "name": name, - "table": table, - "column": column, - "instance_name": db_name, - "tags": self.tags, - } - metrics_to_collect.append(self.typed_metric(cfg_inst=cfg, table=table, column=column)) - - # Load AlwaysOn metrics - if is_affirmative(self.instance.get("include_ao_metrics", False)): - for name, table, column in AO_METRICS + AO_METRICS_PRIMARY + AO_METRICS_SECONDARY: - db_name = "master" - cfg = { - "name": name, - "table": table, - "column": column, - "instance_name": db_name, - "tags": self.tags, - "ao_database": self.instance.get("ao_database", None), - "availability_group": self.instance.get("availability_group", None), - "only_emit_local": is_affirmative(self.instance.get("only_emit_local", False)), - } - metrics_to_collect.append(self.typed_metric(cfg_inst=cfg, table=table, column=column)) - - # Load metrics from scheduler and task tables, if enabled - if is_affirmative(self.instance.get("include_task_scheduler_metrics", False)): - for name, table, column in TASK_SCHEDULER_METRICS: - cfg = {"name": name, "table": table, "column": column, "tags": self.tags} - metrics_to_collect.append(self.typed_metric(cfg_inst=cfg, table=table, column=column)) - - # Load sys.master_files metrics - if is_affirmative(self.instance.get("include_master_files_metrics", False)): - for name, table, column in DATABASE_MASTER_FILES: - cfg = {"name": name, "table": table, "column": column, "tags": self.tags} - metrics_to_collect.append(self.typed_metric(cfg_inst=cfg, table=table, column=column)) - - # Load DB File Space Usage metrics - if is_affirmative( - self.instance.get("include_tempdb_file_space_usage_metrics", True) - ) and not is_azure_sql_database(engine_edition): - for name, table, column in TEMPDB_FILE_SPACE_USAGE_METRICS: - cfg = { - "name": name, - "table": table, - "column": column, - "instance_name": "tempdb", - "tags": self.tags, - } - metrics_to_collect.append(self.typed_metric(cfg_inst=cfg, table=table, column=column)) - # Load any custom metrics from conf.d/sqlserver.yaml for cfg in custom_metrics: sql_counter_type = None @@ -784,12 +716,6 @@ def check(self, _): self, self.execute_query_raw, tags=self.tags, hostname=self.resolved_hostname ) self._query_manager.compile_queries() - if self.server_state_queries is None: - self.server_state_queries = self._new_query_executor( - [QUERY_SERVER_STATIC_INFO], executor=self.execute_query_raw - ) - self.server_state_queries.compile_queries() - self._send_database_instance_metadata() if self._config.proc: self.do_stored_procedure_check() @@ -808,108 +734,67 @@ def check(self, _): else: self.log.debug("Skipping check") - @property - def dynamic_queries(self): - """ - Initializes dynamic queries which depend on static information loaded from the database - """ - if self._dynamic_queries: - return self._dynamic_queries - - major_version = self.static_info_cache.get(STATIC_INFO_MAJOR_VERSION) - engine_edition = self.static_info_cache.get(STATIC_INFO_ENGINE_EDITION) - # need either major_version or engine_edition to generate queries - if not major_version and not is_azure_database(engine_edition): - self.log.warning("missing major_version, cannot initialize dynamic queries") - return None - queries = [get_query_file_stats(major_version, engine_edition)] - - if is_affirmative(self.instance.get("include_ao_metrics", False)): - if major_version > 2012 or is_azure_database(engine_edition): - queries.extend( - [ - get_query_ao_availability_groups(major_version), - QUERY_AO_FAILOVER_CLUSTER, - QUERY_AO_FAILOVER_CLUSTER_MEMBER, - ] - ) - else: - self.log_missing_metric("AlwaysOn", major_version, engine_edition) - if is_affirmative(self.instance.get("include_fci_metrics", False)): - if major_version > 2012 or engine_edition == ENGINE_EDITION_AZURE_MANAGED_INSTANCE: - queries.extend([QUERY_FAILOVER_CLUSTER_INSTANCE]) - else: - self.log_missing_metric("Failover Cluster Instance", major_version, engine_edition) - - if is_affirmative(self.instance.get("include_primary_log_shipping_metrics", False)): - queries.extend([QUERY_LOG_SHIPPING_PRIMARY]) + def _new_database_metric_executor(self, database_metric_class, db_names=None): + return database_metric_class( + instance_config=self.instance, + new_query_executor=self._new_query_executor, + server_static_info=self.static_info_cache, + execute_query_handler=self.execute_query_raw, + databases=db_names, + ) - if is_affirmative(self.instance.get("include_secondary_log_shipping_metrics", False)): - queries.extend([QUERY_LOG_SHIPPING_SECONDARY]) + @property + def _instance_level_database_metrics(self): + # return the list of database metrics that are collected once at the instance level + return [ + SqlserverServerStateMetrics, + SqlserverFileStatsMetrics, + SqlserverAoMetrics, + SqlserverAvailabilityGroupsMetrics, + SqlserverAvailabilityReplicasMetrics, + SqlserverDatabaseReplicationStatsMetrics, + SqlserverFciMetrics, + SqlserverPrimaryLogShippingMetrics, + SqlserverSecondaryLogShippingMetrics, + SqlserverOsTasksMetrics, + SqlserverOsSchedulersMetrics, + SqlserverMasterFilesMetrics, + SqlserverDatabaseStatsMetrics, + SqlserverDatabaseBackupMetrics, + SqlserverAgentMetrics, + SQLServerXESessionMetrics, + ] - self._dynamic_queries = self._new_query_executor(queries, executor=self.execute_query_raw) - self._dynamic_queries.compile_queries() - self.log.debug("initialized dynamic queries") - return self._dynamic_queries + @property + def _database_level_database_metrics(self): + # return the list of database metrics that are collected for each database + return [ + SqlserverTempDBFileSpaceUsageMetrics, + SqlserverIndexUsageMetrics, + SqlserverDBFragmentationMetrics, + SqlserverDatabaseFilesMetrics, + ] @property def database_metrics(self): """ - Initializes database metrics which depend on static information loaded from the database + Initializes dynamic queries which depend on static information loaded from the database """ if self._database_metrics: return self._database_metrics + self._database_metrics = [] # list of database names to collect metrics for - db_names = [d.name for d in self.databases] or [self.instance.get("database", self.connection.DEFAULT_DATABASE)] + db_names = [d.name for d in self.databases] or [self.instance.get('database', self.connection.DEFAULT_DATABASE)] # instance level metrics - database_backup_metrics = SqlserverDatabaseBackupMetrics( - instance_config=self.instance, - new_query_executor=self._new_query_executor, - server_static_info=self.static_info_cache, - execute_query_handler=self.execute_query_raw, - ) - xe_session_metrics = SQLServerXESessionMetrics( - instance_config=self.instance, - new_query_executor=self._new_query_executor, - server_static_info=self.static_info_cache, - execute_query_handler=self.execute_query_raw, - ) + for database_metric_class in self._instance_level_database_metrics: + self._database_metrics.append(self._new_database_metric_executor(database_metric_class)) # database level metrics - index_usage_metrics = SqlserverIndexUsageMetrics( - instance_config=self.instance, - new_query_executor=self._new_query_executor, - server_static_info=self.static_info_cache, - execute_query_handler=self.execute_query_raw, - databases=db_names, - ) - db_fragmentation_metrics = SqlserverDBFragmentationMetrics( - instance_config=self.instance, - new_query_executor=self._new_query_executor, - server_static_info=self.static_info_cache, - execute_query_handler=self.execute_query_raw, - databases=db_names, - ) + for database_metric_class in self._database_level_database_metrics: + self._database_metrics.append(self._new_database_metric_executor(database_metric_class, db_names)) - database_agent_metrics = SqlserverAgentMetrics( - instance_config=self.instance, - new_query_executor=self._new_query_executor, - server_static_info=self.static_info_cache, - execute_query_handler=self.execute_query_raw, - ) - - # create a list of dynamic queries to execute - self._database_metrics = [ - # instance level metrics - database_backup_metrics, - database_agent_metrics, - xe_session_metrics, - # database level metrics - index_usage_metrics, - db_fragmentation_metrics, - ] self.log.debug("initialized dynamic queries") return self._database_metrics @@ -973,16 +858,6 @@ def collect_metrics(self): with self.connection.get_managed_cursor() as cursor: cursor.execute("SET NOCOUNT ON") try: - # Server state queries require VIEW SERVER STATE permissions, which some managed database - # versions do not support. - if self.static_info_cache.get(STATIC_INFO_ENGINE_EDITION) not in [ - ENGINE_EDITION_SQL_DATABASE, - ]: - self.server_state_queries.execute() - - if self.dynamic_queries: - self.dynamic_queries.execute() - # restore the current database after executing dynamic queries # this is to ensure the current database context is not changed with self.connection.restore_current_database_context(): diff --git a/sqlserver/tests/common.py b/sqlserver/tests/common.py index 611da0461bb4a..7dae6697a9af1 100644 --- a/sqlserver/tests/common.py +++ b/sqlserver/tests/common.py @@ -25,7 +25,6 @@ TASK_SCHEDULER_METRICS, TEMPDB_FILE_SPACE_USAGE_METRICS, ) -from datadog_checks.sqlserver.queries import get_query_file_stats from .utils import is_always_on @@ -64,12 +63,18 @@ def get_local_driver(): SQLSERVER_ENGINE_EDITION = int(os.environ.get('SQLSERVER_ENGINE_EDITION')) -def get_expected_file_stats_metrics(): - query_file_stats = get_query_file_stats(SQLSERVER_MAJOR_VERSION, SQLSERVER_ENGINE_EDITION) - return ["sqlserver." + c["name"] for c in query_file_stats["columns"] if c["type"] != "tag"] - - -EXPECTED_FILE_STATS_METRICS = get_expected_file_stats_metrics() +EXPECTED_FILE_STATS_METRICS = [ + 'sqlserver.files.io_stall', + 'sqlserver.files.read_io_stall_queued', + 'sqlserver.files.write_io_stall_queued', + 'sqlserver.files.read_io_stall', + 'sqlserver.files.write_io_stall', + 'sqlserver.files.read_bytes', + 'sqlserver.files.written_bytes', + 'sqlserver.files.reads', + 'sqlserver.files.writes', + 'sqlserver.files.size_on_disk', +] # SQL Server incremental sql fraction metrics require diffs in order to calculate # & report the metric, which means this requires a special unit/integration test coverage @@ -245,10 +250,7 @@ def get_expected_file_stats_metrics(): OPERATION_TIME_METRICS = [ 'simple_metrics', - 'database_stats_metrics', 'fraction_metrics', - 'db_file_space_usage_metrics', - 'database_file_stats_metrics', 'incr_fraction_metrics', ] @@ -318,11 +320,4 @@ def get_operation_time_metrics(instance): Return a list of all operation time metrics """ operation_time_metrics = deepcopy(OPERATION_TIME_METRICS) - if instance.get('include_task_scheduler_metrics', False): - operation_time_metrics.append('os_schedulers_metrics') - operation_time_metrics.append('os_tasks_metrics') - if instance.get('include_ao_metrics', False): - operation_time_metrics.append('availability_groups_metrics') - if instance.get('include_master_files_metrics', False): - operation_time_metrics.append('master_database_file_stats_metrics') return operation_time_metrics diff --git a/sqlserver/tests/test_database_metrics.py b/sqlserver/tests/test_database_metrics.py index f6c65c16c3ef6..c704c3596f4a0 100644 --- a/sqlserver/tests/test_database_metrics.py +++ b/sqlserver/tests/test_database_metrics.py @@ -4,6 +4,7 @@ from copy import deepcopy +from decimal import Decimal from unittest import mock import pytest @@ -14,9 +15,24 @@ STATIC_INFO_MAJOR_VERSION, ) from datadog_checks.sqlserver.database_metrics import ( + SqlserverAoMetrics, + SqlserverAvailabilityGroupsMetrics, + SqlserverAvailabilityReplicasMetrics, SqlserverDatabaseBackupMetrics, + SqlserverDatabaseFilesMetrics, + SqlserverDatabaseReplicationStatsMetrics, + SqlserverDatabaseStatsMetrics, SqlserverDBFragmentationMetrics, + SqlserverFciMetrics, + SqlserverFileStatsMetrics, SqlserverIndexUsageMetrics, + SqlserverMasterFilesMetrics, + SqlserverOsSchedulersMetrics, + SqlserverOsTasksMetrics, + SqlserverPrimaryLogShippingMetrics, + SqlserverSecondaryLogShippingMetrics, + SqlserverServerStateMetrics, + SqlserverTempDBFileSpaceUsageMetrics, ) from .common import ( @@ -25,6 +41,7 @@ SQLSERVER_MAJOR_VERSION, ) +INCR_FRACTION_METRICS = {'sqlserver.latches.latch_wait_time'} AUTODISCOVERY_DBS = ['master', 'msdb', 'datadog_test-1'] STATIC_SERVER_INFO = { @@ -33,6 +50,729 @@ } +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_sqlserver_file_stats_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + + mocked_results = [ + ('master', 'ONLINE', 'master', '/xx/master.mdf', 89, 0, 0, 73, 16, 3153920, 933888, 59, 98, 4194304), + ('master', 'ONLINE', 'mastlog', '/xx/mastlog.ldf', 22, 0, 0, 3, 19, 750592, 580608, 11, 97, 786432), + ('tempdb', 'ONLINE', 'tempdev', '/xx/tempdb.mdf', 3, 0, 0, 3, 0, 1728512, 32768, 29, 4, 8388608), + ('tempdb', 'ONLINE', 'templog', '/xx/templog.ldf', 1, 0, 0, 1, 0, 1007616, 16384, 7, 3, 8388608), + ('model', 'ONLINE', 'modeldev', '/xx/model.mdf', 22, 0, 0, 17, 5, 35151872, 409600, 59, 44, 8388608), + ('model', 'ONLINE', 'modellog', '/xx/modellog.ldf', 19, 0, 0, 12, 7, 1162752, 317440, 14, 48, 8388608), + ('msdb', 'ONLINE', 'MSDBData', '/xx/MSDBData.mdf', 34, 0, 0, 29, 5, 3891200, 196608, 62, 23, 14024704), + ('msdb', 'ONLINE', 'MSDBLog', '/xx/MSDBLog.ldf', 12, 0, 0, 3, 9, 1338368, 180736, 10, 30, 524288), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + file_stats_metrics = SqlserverFileStatsMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [file_stats_metrics] + + dd_run_check(sqlserver_check) + + tags = sqlserver_check._config.tags + for result in mocked_results: + db, state, logical_name, file_location, *metric_values = result + metrics = zip(file_stats_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'db:{db}', + f'state:{state}', + f'logical_name:{logical_name}', + f'file_location:{file_location}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_ao_metrics', [True, False]) +def test_sqlserver_ao_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_ao_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_ao_metrics'] = include_ao_metrics + + # Mocked results + mocked_ao_availability_groups = [ + ( + 'primary', # replica_role + 'master', # database_name + '0769C993-7AD1-4BA0-B319-5C8580B9A686', # availability_group + 'RDSAG0', # availability_group_name + 'EC2AMAZ-J78JTN1', # replica_server_name + '5', # database_id + '119CFD6A-C903-4E9E-B44A-D29CDB6633AA', # replica_id + 'rds_cluster', # failover_cluster + 'synchronous_commit', # availability_mode + 'automatic', # failover_mode + None, # database_state + 'synchronized', # synchronization_state + 1, # filestream_send_rate + 5, # log_send_queue_size + 1, # log_send_rate + 50, # redo_queue_size + 23, # redo_rate + 1, # replica_status + 1, # is_primary_replica + 300, # low_water_mark_for_ghosts + 1, # secondary_lag_seconds + ), + ] + mocked_ao_failover_cluster = [('node_majority', 'normal_quorum', '', 1, 1)] + mocked_ao_failover_cluster_member = [('08cd6223c153', 'cluster_node', 'up', '', 1, 1, 1)] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + execute_query_handler_mocked = mock.MagicMock() + execute_query_handler_mocked.side_effect = [ + mocked_ao_availability_groups, + mocked_ao_failover_cluster, + mocked_ao_failover_cluster_member, + ] + + ao_metrics = SqlserverAoMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [ao_metrics] + + dd_run_check(sqlserver_check) + + if not include_ao_metrics: + assert ao_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_ao_availability_groups: + ( + replica_role, + database_name, + availability_group, + availability_group_name, + replica_server_name, + database_id, + replica_id, + failover_cluster, + availability_mode, + failover_mode, + database_state, + synchronization_state, + *metric_values, + ) = result + metrics = zip( + ao_metrics.metric_names()[0], + [ + *metric_values, + ], + ) + expected_tags = [ + f'replica_role:{replica_role}', + f'database_name:{database_name}', + f'availability_group:{availability_group}', + f'availability_group_name:{availability_group_name}', + f'replica_server_name:{replica_server_name}', + f'database_id:{database_id}', + f'replica_id:{replica_id}', + f'failover_cluster:{failover_cluster}', + f'availability_mode:{availability_mode}', + f'failover_mode:{failover_mode}', + f'database_state:{database_state}', + f'synchronization_state:{synchronization_state}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + for result in mocked_ao_failover_cluster: + quorum_type, quorum_state, failover_cluster, *metric_values = result + metrics = zip(ao_metrics.metric_names()[1], metric_values) + expected_tags = [ + f'quorum_type:{quorum_type}', + f'quorum_state:{quorum_state}', + f'failover_cluster:{failover_cluster}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + for result in mocked_ao_failover_cluster_member: + member_name, member_type, member_state, failover_cluster, *metric_values = result + metrics = zip(ao_metrics.metric_names()[2], metric_values) + expected_tags = [ + f'member_name:{member_name}', + f'member_type:{member_type}', + f'member_state:{member_state}', + f'failover_cluster:{failover_cluster}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_ao_metrics', [True, False]) +@pytest.mark.parametrize( + 'availability_group,mocked_results', + [ + pytest.param( + None, + [('AG1', 'AG1', 'HEALTHY', 2, 1, None), ('AG2', 'AG2', 'HEALTHY', 2, 1, None)], + id='no availability_group', + ), + pytest.param('AG1', [('AG1', 'AG1', 'HEALTHY', 2, 1, None)], id='availability_group set'), + ], +) +def test_sqlserver_availability_groups_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_ao_metrics, + availability_group, + mocked_results, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_ao_metrics'] = include_ao_metrics + if availability_group: + instance_docker_metrics['availability_group'] = availability_group + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + availability_groups_metrics = SqlserverAvailabilityGroupsMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + if availability_group: + assert availability_groups_metrics.queries[0]['query'].endswith( + f" where resource_group_id = '{availability_group}'" + ) + + sqlserver_check._database_metrics = [availability_groups_metrics] + + dd_run_check(sqlserver_check) + + if not include_ao_metrics: + assert availability_groups_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + ag, availability_group_name, synchronization_health_desc, *metric_values = result + metrics = zip(availability_groups_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'availability_group:{ag}', + f'availability_group_name:{availability_group_name}', + f'synchronization_health_desc:{synchronization_health_desc}', + ] + tags + for metric_name, metric_value in metrics: + if metric_value is not None: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + if availability_group: + assert ag == availability_group + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_ao_metrics', [True, False]) +@pytest.mark.parametrize( + 'availability_group,only_emit_local,mocked_results', + [ + pytest.param( + None, + None, + [('AG1', 'AG1', 'aoag_secondary', 'SYNCHRONIZED', 2), ('AG1', 'AG1', 'aoag_primary', 'SYNCHRONIZED', 2)], + id='no availability_group, no only_emit_local', + ), + pytest.param( + 'AG1', + None, + [('AG1', 'AG1', 'aoag_secondary', 'SYNCHRONIZED', 2), ('AG1', 'AG1', 'aoag_primary', 'SYNCHRONIZED', 2)], + id='availability_group set, no only_emit_local', + ), + pytest.param( + None, + True, + [('AG1', 'AG1', 'aoag_primary', 'SYNCHRONIZED', 2)], + id='no availability_group, only_emit_local is True', + ), + pytest.param( + 'AG1', + True, + [('AG1', 'AG1', 'aoag_primary', 'SYNCHRONIZED', 2)], + id='availability_group set, only_emit_local is True', + ), + ], +) +def test_sqlserver_database_replication_stats_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_ao_metrics, + availability_group, + only_emit_local, + mocked_results, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_ao_metrics'] = include_ao_metrics + if availability_group: + instance_docker_metrics['availability_group'] = availability_group + if only_emit_local: + instance_docker_metrics['only_emit_local'] = only_emit_local + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + database_replication_stats_metrics = SqlserverDatabaseReplicationStatsMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + if availability_group: + assert f"resource_group_id = '{availability_group}'" in database_replication_stats_metrics.queries[0]['query'] + if only_emit_local: + assert "is_local = 1" in database_replication_stats_metrics.queries[0]['query'] + + sqlserver_check._database_metrics = [database_replication_stats_metrics] + + dd_run_check(sqlserver_check) + + if not include_ao_metrics: + assert database_replication_stats_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + ag, availability_group_name, replica_server_name, synchronization_state_desc, *metric_values = result + metrics = zip(database_replication_stats_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'availability_group:{ag}', + f'availability_group_name:{availability_group_name}', + f'replica_server_name:{replica_server_name}', + f'synchronization_state_desc:{synchronization_state_desc}', + ] + tags + for metric_name, metric_value in metrics: + if metric_value is not None: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + if availability_group: + assert ag == availability_group + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_ao_metrics', [True, False]) +@pytest.mark.parametrize( + 'availability_group,only_emit_local,ao_database,mocked_results', + [ + pytest.param( + None, + None, + None, + [ + ('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True), + ('datadog_test', 'AG1', 'AG1', 'aoag_secondary', 'MANUAL', False, 1, True), + ], + id='no availability_group, no only_emit_local, no ao_database', + ), + pytest.param( + 'AG1', + None, + None, + [ + ('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True), + ('datadog_test', 'AG1', 'AG1', 'aoag_secondary', 'MANUAL', False, 1, True), + ], + id='availability_group set, no only_emit_local, no ao_database', + ), + pytest.param( + None, + True, + None, + [('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True)], + id='no availability_group, only_emit_local is True, no ao_database', + ), + pytest.param( + 'AG1', + True, + None, + [('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True)], + id='availability_group set, only_emit_local is True, no ao_database', + ), + pytest.param( + None, + None, + 'my_db', + [], + id='no availability_group, no only_emit_local, ao_database set', + ), + pytest.param( + 'AG1', + None, + 'datadog_test', + [ + ('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True), + ('datadog_test', 'AG1', 'AG1', 'aoag_secondary', 'MANUAL', False, 1, True), + ], + id='availability_group set, no only_emit_local, ao_database set', + ), + pytest.param( + None, + True, + 'datadog_test', + [('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True)], + id='no availability_group, only_emit_local is True, ao_database set', + ), + pytest.param( + 'AG1', + True, + 'datadog_test', + [('datadog_test', 'AG1', 'AG1', 'aoag_primary', 'MANUAL', True, 1, True)], + id='availability_group set, only_emit_local is True, ao_database set', + ), + ], +) +def test_sqlserver_availability_replicas_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_ao_metrics, + availability_group, + only_emit_local, + ao_database, + mocked_results, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_ao_metrics'] = include_ao_metrics + if availability_group: + instance_docker_metrics['availability_group'] = availability_group + if only_emit_local: + instance_docker_metrics['only_emit_local'] = only_emit_local + if ao_database: + instance_docker_metrics['ao_database'] = ao_database + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + availability_replicas_metrics = SqlserverAvailabilityReplicasMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + if availability_group: + assert f"resource_group_id = '{availability_group}'" in availability_replicas_metrics.queries[0]['query'] + if only_emit_local: + assert "is_local = 1" in availability_replicas_metrics.queries[0]['query'] + if ao_database: + assert f"database_name = '{ao_database}'" in availability_replicas_metrics.queries[0]['query'] + + sqlserver_check._database_metrics = [availability_replicas_metrics] + + dd_run_check(sqlserver_check) + + if not include_ao_metrics: + assert availability_replicas_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + ( + database_name, + ag, + availability_group_name, + replica_server_name, + failover_mode_desc, + is_primary_replica, + *metric_values, + ) = result + metrics = zip(availability_replicas_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'db:{database_name}', + f'availability_group:{ag}', + f'availability_group_name:{availability_group_name}', + f'replica_server_name:{replica_server_name}', + f'failover_mode_desc:{failover_mode_desc}', + f'is_primary_replica:{is_primary_replica}', + ] + tags + for metric_name, metric_value in metrics: + if metric_value is not None: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + if availability_group: + assert ag == availability_group + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_fci_metrics', [True, False]) +def test_sqlserver_fci_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_fci_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_fci_metrics'] = include_fci_metrics + + mocked_results = [ + ('node1', 'up', 'cluster1', 0, 1), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + fci_metrics = SqlserverFciMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [fci_metrics] + + dd_run_check(sqlserver_check) + + if not include_fci_metrics: + assert fci_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + node_name, status, failover_cluster, *metric_values = result + metrics = zip(fci_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'node_name:{node_name}', + f'status:{status}', + f'failover_cluster:{failover_cluster}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_primary_log_shipping_metrics', [True, False]) +def test_sqlserver_primary_log_shipping_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_primary_log_shipping_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_primary_log_shipping_metrics'] = include_primary_log_shipping_metrics + + mocked_results = [('97E29D89-2FA0-44FF-9EF7-65DA75FE0E3E', 'EC2AMAZ-Q0NCNV5', 'MyDummyDB', 500, 3600)] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + primary_log_shipping_metrics = SqlserverPrimaryLogShippingMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [primary_log_shipping_metrics] + + dd_run_check(sqlserver_check) + + if not include_primary_log_shipping_metrics: + assert primary_log_shipping_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + primary_id, primary_server, primary_db, *metric_values = result + metrics = zip(primary_log_shipping_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'primary_id:{primary_id}', + f'primary_server:{primary_server}', + f'primary_db:{primary_db}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_secondary_log_shipping_metrics', [True, False]) +def test_sqlserver_secondary_log_shipping_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_secondary_log_shipping_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_secondary_log_shipping_metrics'] = include_secondary_log_shipping_metrics + + mocked_results = [ + ( + r'EC2AMAZ-Q0NCNV5\MYSECONDARY', + 'MyDummyDB', + '13269A43-4D79-4473-A8BE-300F0709FF49', + 'EC2AMAZ-Q0NCNV5', + 'MyDummyDB', + 800, + 13000000, + 125000, + 2700, + ) + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + primary_log_shipping_metrics = SqlserverSecondaryLogShippingMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [primary_log_shipping_metrics] + + dd_run_check(sqlserver_check) + + if not include_secondary_log_shipping_metrics: + assert primary_log_shipping_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + secondary_server, secondary_db, secondary_id, primary_server, primary_db, *metric_values = result + metrics = zip(primary_log_shipping_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'secondary_server:{secondary_server}', + f'secondary_db:{secondary_db}', + f'secondary_id:{secondary_id}', + f'primary_server:{primary_server}', + f'primary_db:{primary_db}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_sqlserver_server_state_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + + mocked_results = [(1000, 4, 8589934592, 17179869184, 4294967296, 8589934592)] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + server_state_metrics = SqlserverServerStateMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [server_state_metrics] + + dd_run_check(sqlserver_check) + + tags = sqlserver_check._config.tags + for result in mocked_results: + metrics = zip(server_state_metrics.metric_names()[0], result) + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_tempdb_file_space_usage_metrics', [True, False]) +def test_sqlserver_tempdb_file_space_usage_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_tempdb_file_space_usage_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_tempdb_file_space_usage_metrics'] = include_tempdb_file_space_usage_metrics + + mocked_results = [ + [(2, Decimal('5.375000'), Decimal('0.000000'), Decimal('0.000000'), Decimal('1.312500'), Decimal('1.312500'))] + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + tempdb_file_space_usage_metrics = SqlserverTempDBFileSpaceUsageMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [tempdb_file_space_usage_metrics] + + dd_run_check(sqlserver_check) + + if not include_tempdb_file_space_usage_metrics: + assert tempdb_file_space_usage_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + database_id, *metric_values = result + metrics = zip(tempdb_file_space_usage_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'database_id:{database_id}', + 'db:tempdb', + 'database:tempdb', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @pytest.mark.parametrize('include_index_usage_metrics', [True, False]) @@ -232,6 +972,323 @@ def test_sqlserver_db_fragmentation_metrics( aggregator.assert_metric(metric_name, count=0) +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_task_scheduler_metrics', [True, False]) +def test_sqlserver_os_schedulers_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_task_scheduler_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_task_scheduler_metrics'] = include_task_scheduler_metrics + + mocked_results = [ + (0, 0, 4, 6, 4, 0, 0), + (1, 0, 5, 7, 4, 0, 0), + (2, 0, 5, 6, 5, 0, 0), + (3, 0, 4, 6, 4, 0, 0), + (4, 0, 4, 7, 3, 0, 0), + (1048578, 0, 1, 1, 1, 0, 0), + (5, 1, 5, 7, 4, 0, 0), + (6, 1, 4, 6, 3, 0, 0), + (7, 1, 4, 7, 4, 0, 0), + (8, 1, 3, 5, 3, 0, 0), + (9, 1, 4, 7, 3, 0, 0), + (1048579, 1, 1, 1, 1, 0, 0), + (1048576, 64, 2, 3, 1, 0, 0), + (1048580, 0, 1, 1, 1, 0, 0), + (1048581, 0, 1, 1, 1, 0, 0), + (1048582, 0, 1, 1, 1, 0, 0), + (1048583, 0, 1, 1, 1, 0, 0), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + os_schedulers_metrics = SqlserverOsSchedulersMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [os_schedulers_metrics] + + dd_run_check(sqlserver_check) + + if not include_task_scheduler_metrics: + assert os_schedulers_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + scheduler_id, parent_node_id, *metric_values = result + metrics = zip(os_schedulers_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'scheduler_id:{scheduler_id}', + f'parent_node_id:{parent_node_id}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_task_scheduler_metrics', [True, False]) +def test_sqlserver_os_tasks_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_task_scheduler_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_task_scheduler_metrics'] = include_task_scheduler_metrics + + mocked_results = [ + (0, 40, 0, 0, 0), + (9, 46, 0, 0, 0), + (3, 17, 0, 0, 0), + (6, 14, 0, 0, 0), + (1048580, 427, 89, 0, 0), + (7, 353, 0, 0, 0), + (1, 201, 3, 0, 0), + (1048583, 4, 0, 0, 0), + (4, 734, 0, 0, 0), + (1048578, 5, 0, 0, 0), + (5, 152, 12, 0, 0), + (1048581, 429, 92, 0, 0), + (2, 1590, 223, 0, 0), + (1048582, 56, 0, 0, 0), + (1048579, 5, 0, 0, 0), + (1048576, 6, 0, 0, 0), + (8, 150, 43, 0, 0), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + os_tasks_metrics = SqlserverOsTasksMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [os_tasks_metrics] + + dd_run_check(sqlserver_check) + + if not include_task_scheduler_metrics: + assert os_tasks_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + scheduler_id, *metric_values = result + metrics = zip(os_tasks_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'scheduler_id:{scheduler_id}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +@pytest.mark.parametrize('include_master_files_metrics', [True, False]) +def test_sqlserver_master_files_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, + include_master_files_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + instance_docker_metrics['include_master_files_metrics'] = include_master_files_metrics + + mocked_results = [ + ('master', 'master', 1, 'data', '/var/opt/mssql/data/master.mdf', 'ONLINE', 4096, 0), + ('master', 'master', 2, 'transaction_log', '/var/opt/mssql/data/mastlog.ldf', 'ONLINE', 512, 0), + ('tempdb', 'tempdb', 1, 'data', '/var/opt/mssql/data/tempdb.mdf', 'ONLINE', 8192, 0), + ('tempdb', 'tempdb', 2, 'transaction_log', '/var/opt/mssql/data/templog.ldf', 'ONLINE', 8192, 0), + ('model', 'model', 1, 'data', '/var/opt/mssql/data/model.mdf', 'ONLINE', 8192, 0), + ('model', 'model', 2, 'transaction_log', '/var/opt/mssql/data/modellog.ldf', 'ONLINE', 8192, 0), + ('msdb', 'msdb', 1, 'data', '/var/opt/mssql/data/MSDBData.mdf', 'ONLINE', 13696, 0), + ('msdb', 'msdb', 2, 'transaction_log', '/var/opt/mssql/data/MSDBLog.ldf', 'ONLINE', 512, 0), + ('datadog_test', 'datadog_test', 1, 'data', '/var/opt/mssql/data/datadog_test.mdf', 'ONLINE', 8192, 0), + ( + 'datadog_test', + 'datadog_test', + 2, + 'transaction_log', + '/var/opt/mssql/data/datadog_test_log.ldf', + 'ONLINE', + 8192, + 0, + ), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + master_files_metrics = SqlserverMasterFilesMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [master_files_metrics] + + dd_run_check(sqlserver_check) + + if not include_master_files_metrics: + assert master_files_metrics.enabled is False + else: + tags = sqlserver_check._config.tags + for result in mocked_results: + db, database, file_id, file_type, file_location, database_files_state_desc, size, state = result + size *= 8 # size is in pages, 1 page = 8 KB + metrics = zip(master_files_metrics.metric_names()[0], [state, size]) + expected_tags = [ + f'db:{db}', + f'database:{database}', + f'file_id:{file_id}', + f'file_type:{file_type}', + f'file_location:{file_location}', + f'database_files_state_desc:{database_files_state_desc}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_sqlserver_database_files_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + + mocked_results = [ + [ + (1, 'data', '/var/opt/mssql/data/master.mdf', 'master', 'ONLINE', 4096, 0, 4096), + (2, 'transaction_log', '/var/opt/mssql/data/mastlog.ldf', 'mastlog', 'ONLINE', 768, 0, 424), + ], + [ + (1, 'data', '/var/opt/mssql/data/MSDBData.mdf', 'MSDBData', 'ONLINE', 13696, 0, 13696), + (2, 'transaction_log', '/var/opt/mssql/data/MSDBLog.ldf', 'MSDBLog', 'ONLINE', 512, 0, 432), + ], + [ + (1, 'data', '/var/opt/mssql/data/datadog_test.mdf', 'datadog_test', 'ONLINE', 8192, 0, 2624), + ( + 2, + 'transaction_log', + '/var/opt/mssql/data/datadog_test_log.ldf', + 'datadog_test_log', + 'ONLINE', + 8192, + 0, + 488, + ), + ], + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + execute_query_handler_mocked = mock.MagicMock() + execute_query_handler_mocked.side_effect = mocked_results + + database_files_metrics = SqlserverDatabaseFilesMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + databases=AUTODISCOVERY_DBS, + ) + + sqlserver_check._database_metrics = [database_files_metrics] + + dd_run_check(sqlserver_check) + + tags = sqlserver_check._config.tags + for db, result in zip(AUTODISCOVERY_DBS, mocked_results): + for row in result: + file_id, file_type, file_location, file_name, database_files_state_desc, size, space_used, state = row + size *= 8 # size is in pages, 1 page = 8 KB + space_used *= 8 # space_used is in pages, 1 page = 8 KB + metrics = zip(database_files_metrics.metric_names()[0], [state, size, space_used]) + expected_tags = [ + f'db:{db}', + f'database:{db}', + f'file_id:{file_id}', + f'file_type:{file_type}', + f'file_location:{file_location}', + f'file_name:{file_name}', + f'database_files_state_desc:{database_files_state_desc}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +def test_sqlserver_database_stats_metrics( + aggregator, + dd_run_check, + init_config, + instance_docker_metrics, +): + instance_docker_metrics['database_autodiscovery'] = True + + mocked_results = [ + ('master', 'master', 'ONLINE', 'SIMPLE', 0, False, False, False), + ('tempdb', 'tempdb', 'ONLINE', 'SIMPLE', 0, False, False, False), + ('model', 'model', 'ONLINE', 'FULL', 0, False, False, False), + ('msdb', 'msdb', 'ONLINE', 'SIMPLE', 0, False, False, False), + ('datadog_test', 'datadog_test', 'ONLINE', 'FULL', 0, False, False, False), + ] + + sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker_metrics]) + + def execute_query_handler_mocked(query, db=None): + return mocked_results + + database_stats_metrics = SqlserverDatabaseStatsMetrics( + instance_config=instance_docker_metrics, + new_query_executor=sqlserver_check._new_query_executor, + server_static_info=STATIC_SERVER_INFO, + execute_query_handler=execute_query_handler_mocked, + ) + + sqlserver_check._database_metrics = [database_stats_metrics] + + dd_run_check(sqlserver_check) + + tags = sqlserver_check._config.tags + for result in mocked_results: + db, database, database_state_desc, database_recovery_model_desc, *metric_values = result + metrics = zip(database_stats_metrics.metric_names()[0], metric_values) + expected_tags = [ + f'db:{db}', + f'database:{database}', + f'database_state_desc:{database_state_desc}', + f'database_recovery_model_desc:{database_recovery_model_desc}', + ] + tags + for metric_name, metric_value in metrics: + aggregator.assert_metric(metric_name, value=metric_value, tags=expected_tags) + + @pytest.mark.integration @pytest.mark.usefixtures('dd_environment') @pytest.mark.parametrize('database_backup_metrics_interval', [None, 600]) diff --git a/sqlserver/tests/test_integration.py b/sqlserver/tests/test_integration.py index 2ad9129c52b02..249f5e47b59a2 100644 --- a/sqlserver/tests/test_integration.py +++ b/sqlserver/tests/test_integration.py @@ -382,9 +382,7 @@ def test_autodiscovery_multiple_instances(aggregator, dd_run_check, instance_aut found_log = 0 for _, _, message in caplog.record_tuples: # make sure master and msdb is only queried once - if "SqlDatabaseFileStats: changing cursor context via use statement: use [master]" in message: - found_log += 1 - if "SqlDatabaseFileStats: changing cursor context via use statement: use [msdb]" in message: + if "Restoring the original database context master" in message: found_log += 1 assert found_log == 2 @@ -813,6 +811,21 @@ def execute_query(query, params): aggregator.assert_metric(m, tags=expected_tags, count=1) +@pytest.mark.integration +@pytest.mark.usefixtures('dd_environment') +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 = sqlserver_check._config.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( 'instance_propagate_agent_tags,init_config_propagate_agent_tags,should_propagate_agent_tags', [ diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 071c59c9db504..c3037e51c6a26 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -14,7 +14,7 @@ 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.metrics import SqlFractionMetric from datadog_checks.sqlserver.schemas import Schemas, SubmitData from datadog_checks.sqlserver.sqlserver import SQLConnectionError from datadog_checks.sqlserver.utils import ( @@ -262,42 +262,6 @@ def test_azure_autodiscovery_exclude_override(instance_autodiscovery): 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', [ @@ -524,19 +488,6 @@ def test_split_sqlserver_host(instance_host, split_host, split_port): 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 = sqlserver_check._config.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", [