diff --git a/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector.go b/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector.go index aca06fed..6180448f 100644 --- a/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector.go +++ b/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector.go @@ -54,7 +54,10 @@ var ( exposedToBroadIPAccessQueryKey = "exposed to broad ip access query key" exposedToBroadAccess = "exposed_to_broad_ip_access" - signals = []string{auditingEnabledQueryKey, allowUnencryptedConnQueryKey, exposedToBroadIPAccessQueryKey} + notProtectedByAutoFailoverQueryKey = "not protected by automatic failover query key" + notProtectedByAutoFailover = "not_protected_by_auto_failover" + + signals = []string{auditingEnabledQueryKey, allowUnencryptedConnQueryKey, exposedToBroadIPAccessQueryKey, notProtectedByAutoFailoverQueryKey} sqlMetrics = map[string]dbCenterSQLMetricsStruct{ versionQueryKey: { @@ -151,6 +154,59 @@ var ( }, resultProcessor: exposedToBroadIPAccess, }, + + notProtectedByAutoFailoverQueryKey: { + query: `WITH Protected_AGs AS ( + SELECT group_id + FROM sys.availability_replicas + WHERE availability_mode = 1 + AND failover_mode = 1 + GROUP BY group_id + HAVING COUNT(replica_id) >= 2 +), +DBStatuses AS ( + SELECT + CASE + WHEN SERVERPROPERTY('IsClustered') = 1 THEN 'PROTECTED_BY_FCI' + WHEN SERVERPROPERTY('IsHadrEnabled') = 1 + AND pag.group_id IS NOT NULL + AND ar_local.failover_mode = 1 + AND ar_local.availability_mode = 1 THEN 'PROTECTED_BY_AG' + WHEN dm.mirroring_guid IS NOT NULL + AND dm.mirroring_safety_level_desc = 'FULL' + AND dm.mirroring_witness_name IS NOT NULL + AND dm.mirroring_witness_name <> '' THEN 'PROTECTED_BY_MIRRORING' + ELSE 'UNPROTECTED' + END AS FailoverProtectionStatus + FROM master.sys.databases d + LEFT JOIN sys.dm_hadr_database_replica_states drs + ON d.database_id = drs.database_id AND drs.is_local = 1 + LEFT JOIN Protected_AGs pag + ON drs.group_id = pag.group_id + LEFT JOIN sys.availability_replicas ar_local + ON drs.replica_id = ar_local.replica_id + LEFT JOIN sys.database_mirroring dm + ON d.database_id = dm.database_id + WHERE d.name NOT IN ('master', 'model', 'msdb', 'tempdb') + AND d.state_desc = 'ONLINE' +) +SELECT + CASE + WHEN SUM(CASE WHEN FailoverProtectionStatus = 'UNPROTECTED' THEN 1 ELSE 0 END) > 0 THEN 1 + ELSE 0 + END AS not_protected_by_auto_failover +FROM DBStatuses;`, + fields: func(rows [][]any) []map[string]string { + var res []map[string]string + for _, row := range rows { + res = append(res, map[string]string{ + notProtectedByAutoFailover: handleNilInt(row[0]), + }) + } + return res + }, + resultProcessor: notProtectedByAutoFailoverProcessor, + }, } ) @@ -225,6 +281,14 @@ func exposedToBroadIPAccess(result, metrics map[string]string) { } } +func notProtectedByAutoFailoverProcessor(result, metrics map[string]string) { + if result[notProtectedByAutoFailover] == "1" { + metrics[databasecenter.NotProtectedByAutomaticFailoverKey] = strconv.FormatBool(true) + } else { + metrics[databasecenter.NotProtectedByAutomaticFailoverKey] = strconv.FormatBool(false) + } +} + // PopulateSignals populates the signals for the SQL Server in the metrics map. func PopulateSignals(ctx context.Context, c *V1, metrics map[string]string) { for _, signal := range signals { diff --git a/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector_test.go b/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector_test.go index 22b9a218..adf74afa 100644 --- a/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector_test.go +++ b/internal/sqlservermetrics/sqlcollector/dbcentersqlcollector_test.go @@ -133,10 +133,11 @@ func TestPopulateSignals(t *testing.T) { sqlmock.NewRows([]string{auditingEnabled}).AddRow(1), sqlmock.NewRows([]string{allowUnencryptedConn}).AddRow(0), sqlmock.NewRows([]string{exposedToBroadAccess}).AddRow(0), + sqlmock.NewRows([]string{notProtectedByAutoFailover}).AddRow(0), }, delay: 0, metrics: map[string]string{}, - wantMetrics: map[string]string{databasecenter.DatabaseAuditingDisabledKey: "false", databasecenter.UnencryptedConnectionsKey: "false", databasecenter.ExposedToPublicAccessKey: "false"}, + wantMetrics: map[string]string{databasecenter.DatabaseAuditingDisabledKey: "false", databasecenter.UnencryptedConnectionsKey: "false", databasecenter.ExposedToPublicAccessKey: "false", databasecenter.NotProtectedByAutomaticFailoverKey: "false"}, }, { name: "all signals are populated", @@ -145,10 +146,11 @@ func TestPopulateSignals(t *testing.T) { sqlmock.NewRows([]string{auditingEnabled}).AddRow(0), sqlmock.NewRows([]string{allowUnencryptedConn}).AddRow(1), sqlmock.NewRows([]string{exposedToBroadAccess}).AddRow(1), + sqlmock.NewRows([]string{notProtectedByAutoFailover}).AddRow(1), }, delay: 0, metrics: map[string]string{}, - wantMetrics: map[string]string{databasecenter.DatabaseAuditingDisabledKey: "true", databasecenter.UnencryptedConnectionsKey: "true", databasecenter.ExposedToPublicAccessKey: "true"}, + wantMetrics: map[string]string{databasecenter.DatabaseAuditingDisabledKey: "true", databasecenter.UnencryptedConnectionsKey: "true", databasecenter.ExposedToPublicAccessKey: "true", databasecenter.NotProtectedByAutomaticFailoverKey: "true"}, }, { name: "error caught when sql query returns error", @@ -166,6 +168,7 @@ func TestPopulateSignals(t *testing.T) { sqlmock.NewRows(nil), sqlmock.NewRows(nil), sqlmock.NewRows(nil), + sqlmock.NewRows(nil), }, metrics: map[string]string{}, wantMetrics: map[string]string{}, @@ -178,6 +181,7 @@ func TestPopulateSignals(t *testing.T) { sqlmock.NewRows([]string{auditingEnabled}).AddRow(1), sqlmock.NewRows([]string{allowUnencryptedConn}).AddRow(1), sqlmock.NewRows([]string{exposedToBroadAccess}).AddRow(1), + sqlmock.NewRows([]string{notProtectedByAutoFailover}).AddRow(0), }, metrics: map[string]string{}, wantMetrics: map[string]string{}, @@ -214,3 +218,34 @@ func TestPopulateSignals(t *testing.T) { }) } } + +func TestNotProtectedByAutoFailoverProcessor(t *testing.T) { + tests := []struct { + name string + result map[string]string + metrics map[string]string + wantMetrics map[string]string + }{ + { + name: "all databases protected", + result: map[string]string{notProtectedByAutoFailover: "0"}, + metrics: map[string]string{}, + wantMetrics: map[string]string{databasecenter.NotProtectedByAutomaticFailoverKey: "false"}, + }, + { + name: "at least one database unprotected raises signal", + result: map[string]string{notProtectedByAutoFailover: "1"}, + metrics: map[string]string{}, + wantMetrics: map[string]string{databasecenter.NotProtectedByAutomaticFailoverKey: "true"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + notProtectedByAutoFailoverProcessor(tc.result, tc.metrics) + if diff := cmp.Diff(tc.wantMetrics, tc.metrics); diff != "" { + t.Errorf("notProtectedByAutoFailoverProcessor() returned diff (-want +got):\n%s", diff) + } + }) + } +}