Files
windows_exporter/internal/collector/mssql/counter_units_test.go
T
2026-10-16 10:48:26 +02:00

362 lines
15 KiB
Go

// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package mssql
import (
"log/slog"
"os"
"slices"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
"golang.org/x/sys/windows/registry"
)
// emitCollector is an unchecked collector that publishes the metrics of emit.
type emitCollector func(ch chan<- prometheus.Metric)
func (emitCollector) Describe(chan<- *prometheus.Desc) {}
func (e emitCollector) Collect(ch chan<- prometheus.Metric) { e(ch) }
type wantMetric struct {
metricType dto.MetricType
value float64
}
func gatherFirstSeries(t *testing.T, collector prometheus.Collector) map[string]*dto.Metric {
t.Helper()
registry := prometheus.NewPedanticRegistry()
require.NoError(t, registry.Register(collector))
families, err := registry.Gather()
require.NoError(t, err)
series := make(map[string]*dto.Metric, len(families))
for _, family := range families {
require.NotEmpty(t, family.GetMetric())
series[family.GetName()] = family.GetMetric()[0]
}
return series
}
func metricValue(m *dto.Metric) (dto.MetricType, float64) {
if m.GetCounter() != nil {
return dto.MetricType_COUNTER, m.GetCounter().GetValue()
}
return dto.MetricType_GAUGE, m.GetGauge().GetValue()
}
func TestCounterUnitConversions(t *testing.T) {
t.Parallel()
c := New(nil)
c.logger = slog.New(slog.DiscardHandler)
// No SQL instances are needed to build the metric descriptors.
require.NoError(t, c.buildBufferManager())
require.NoError(t, c.buildDatabases())
require.NoError(t, c.buildDatabaseReplica())
require.NoError(t, c.buildGeneralStatistics())
require.NoError(t, c.buildLocks())
require.NoError(t, c.buildTransactions())
require.NoError(t, c.buildWaitStats())
c.bufManPerfDataObject = []perfDataCounterValuesBufMan{{BufManReadaheadTimePerSec: 882}}
c.databasesPerfDataObject = []perfDataCounterValuesDatabases{{
Name: "db",
DatabasesLogFlushWaitTime: 4,
DatabasesLogFlushWriteTimeMS: 3,
DatabasesLogGrowths: 2,
DatabasesLogShrinks: 1,
DatabasesLogTruncations: 5,
DatabasesXTPControllerDLCLatencyPerFetch: 2500,
DatabasesXTPControllerDLCLatencyPerFetchBase: 5,
DatabasesXTPControllerDLCPeakLatency: 1000,
}}
c.dbReplicaPerfDataObject = []perfDataCounterValuesDBReplica{{
Name: "replica",
DbReplicaDatabaseFlowControlDelay: 1000,
DbReplicaGroupCommitTime: 1000,
DbReplicaTransactionDelay: 1500,
DbReplicaRecoveryQueue: 2,
DbReplicaRedoBytesRemaining: 3,
DbReplicaLogSendQueue: 4,
DbReplicaTotalLogRequiringUndo: 5,
DbReplicaLogRemainingForUndo: 6,
}}
c.genStatsPerfDataObject = []perfDataCounterValuesGenStats{{
GenStatsHTTPAuthenticatedRequests: 1,
GenStatsMarsDeadlocks: 2,
GenStatsSOAPEmptyRequests: 3,
GenStatsSOAPMethodInvocations: 4,
GenStatsSOAPSessionInitiateRequests: 5,
GenStatsSOAPSessionTerminateRequests: 6,
GenStatsSOAPSQLRequests: 7,
GenStatsSOAPWSDLRequests: 8,
GenStatsSQLTraceIOProviderLockWaits: 9,
GenStatsTempdbRecoveryUnitID: 10,
GenStatsTempdbRowsetID: 11,
}}
c.locksPerfDataObject = []perfDataCounterValuesLocks{{
Name: "locks",
LocksAverageWaitTimeMS: 499,
LocksLockWaitTimeMS: 499,
}}
c.transactionsPerfDataObject = []perfDataCounterValuesTransactions{{
TransactionsNonSnapshotVersionTransactions: 1,
TransactionsSnapshotTransactions: 2,
TransactionsUpdateSnapshotTransactions: 3,
TransactionsUpdateconflictratio: 4,
TransactionsUpdateconflictratioBase: 8,
TransactionsVersionCleanuprateKBPers: 5,
TransactionsVersionGenerationrateKBPers: 6,
TransactionsVersionStoreunitcount: 7,
}}
c.waitStatsPerfDataObject = []perfDataCounterValuesWaitStats{{
Name: "Waits started per second",
WaitStatsNetworkIOWaits: 2194,
}}
instance := mssqlInstance{name: "test"}
series := gatherFirstSeries(t, emitCollector(func(ch chan<- prometheus.Metric) {
c.emitBufferManagerMetrics(ch, instance)
c.emitDatabasesMetrics(ch, instance)
c.emitDatabaseReplicaMetrics(ch, instance)
c.emitGeneralStatisticsMetrics(ch, instance)
c.emitLocksMetrics(ch, instance)
c.emitTransactionsMetrics(ch, instance)
c.emitWaitStatsMetrics(ch, instance)
}))
counter := dto.MetricType_COUNTER
gauge := dto.MetricType_GAUGE
want := map[string]wantMetric{
"windows_mssql_bufman_read_ahead_issuing_seconds_total": {counter, 882e-6},
"windows_mssql_databases_log_flush_wait_seconds_total": {counter, 0.004},
"windows_mssql_databases_log_flush_write_seconds_total": {counter, 0.003},
"windows_mssql_databases_log_growths_total": {counter, 2},
"windows_mssql_databases_log_shrinks_total": {counter, 1},
"windows_mssql_databases_log_truncations_total": {counter, 5},
"windows_mssql_databases_xtp_controller_dlc_fetch_latency_seconds_total": {counter, 0.0025},
"windows_mssql_databases_xtp_controller_dlc_fetches_total": {counter, 5},
"windows_mssql_databases_xtp_controller_dlc_peak_latency_seconds": {gauge, 0.001},
"windows_mssql_dbreplica_database_flow_control_wait_seconds_total": {counter, 0.001},
"windows_mssql_dbreplica_group_commit_stall_seconds_total": {counter, 0.001},
"windows_mssql_dbreplica_transaction_delay_seconds_total": {counter, 1.5},
"windows_mssql_dbreplica_recovery_queue_bytes": {gauge, 2048},
"windows_mssql_dbreplica_redo_remaining_bytes": {gauge, 3072},
"windows_mssql_dbreplica_log_send_queue_bytes": {gauge, 4096},
"windows_mssql_dbreplica_total_log_requiring_undo_bytes": {gauge, 5120},
"windows_mssql_dbreplica_log_remaining_for_undo_bytes": {gauge, 6144},
"windows_mssql_genstats_http_authenticated_requests_total": {counter, 1},
"windows_mssql_genstats_mars_deadlocks_total": {counter, 2},
"windows_mssql_genstats_soap_empty_requests_total": {counter, 3},
"windows_mssql_genstats_soap_method_invocations_total": {counter, 4},
"windows_mssql_genstats_soap_session_initiate_requests_total": {counter, 5},
"windows_mssql_genstats_soap_session_terminate_requests_total": {counter, 6},
"windows_mssql_genstats_soapsql_requests_total": {counter, 7},
"windows_mssql_genstats_soapwsdl_requests_total": {counter, 8},
"windows_mssql_genstats_sql_trace_io_provider_lock_waits_total": {counter, 9},
"windows_mssql_genstats_tempdb_recovery_unit_ids_generated_total": {counter, 10},
"windows_mssql_genstats_tempdb_rowset_ids_generated_total": {counter, 11},
"windows_mssql_locks_wait_time_seconds_total": {counter, 0.499},
"windows_mssql_locks_lock_wait_seconds_total": {counter, 0.499},
"windows_mssql_transactions_nonsnapshot_version_active": {gauge, 1},
"windows_mssql_transactions_snapshot_active": {gauge, 2},
"windows_mssql_transactions_update_snapshot_active": {gauge, 3},
"windows_mssql_transactions_update_conflicts_total": {counter, 4},
"windows_mssql_transactions_update_snapshot_transactions_total": {counter, 8},
"windows_mssql_transactions_version_cleanup_bytes_total": {counter, 5120},
"windows_mssql_transactions_version_generation_bytes_total": {counter, 6144},
"windows_mssql_transactions_version_store_units": {gauge, 7},
"windows_mssql_waitstats_network_io_waits": {gauge, 2194},
"windows_mssql_waitstats_lock_waits": {gauge, 0},
}
for name, expected := range want {
metric, ok := series[name]
require.True(t, ok, "metric %s not emitted", name)
metricType, value := metricValue(metric)
require.Equal(t, expected.metricType, metricType, name)
require.InDelta(t, expected.value, value, 1e-9, name)
}
for _, removed := range []string{
"windows_mssql_databases_log_flush_wait_seconds",
"windows_mssql_databases_log_growths",
"windows_mssql_databases_xtp_controller_dlc_fetch_latency_seconds",
"windows_mssql_dbreplica_recovery_queue_records",
"windows_mssql_dbreplica_log_send_queue",
"windows_mssql_locks_count",
"windows_mssql_transactions_snapshot_active_total",
"windows_mssql_transactions_version_cleanup_rate_bytes",
} {
require.NotContains(t, series, removed)
}
}
// TestMetricTypesMatchCounterTypes checks the published types of the metrics
// against the PDH counter types of a local SQL Server instance.
func TestMetricTypesMatchCounterTypes(t *testing.T) {
t.Parallel()
// Collectors provisioned by CI must not be skipped, see docs/ci-testing.md.
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), Name)
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `Software\Microsoft\Microsoft SQL Server\Instance Names\SQL`, registry.QUERY_VALUE)
if err != nil {
require.False(t, required, "mssql is required but no SQL Server instance was found: %v", err)
t.Skipf("no SQL Server instance: %v", err)
}
_ = key.Close()
c := New(nil)
t.Cleanup(func() { require.NoError(t, c.Close()) })
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
if len(c.mssqlInstances) == 0 {
require.False(t, required, "mssql is required but no SQL Server instance was discovered")
t.Skip("no SQL Server instance")
}
instance := c.mssqlInstances[0]
type check struct {
metric string
counter string
// cumulative marks PERF_COUNTER_LARGE_RAWCOUNT counters whose help text
// says they are running totals, which PDH reports as gauges.
cumulative bool
metricType func(counter string) (prometheus.ValueType, bool)
}
bufMan := c.bufManPerfDataCollectors[instance].MetricType
databases := c.databasesPerfDataCollectors[instance].MetricType
dbReplica := c.dbReplicaPerfDataCollectors[instance].MetricType
genStats := c.genStatsPerfDataCollectors[instance].MetricType
locks := c.locksPerfDataCollectors[instance].MetricType
transactions := c.transactionsPerfDataCollectors[instance].MetricType
waitStats := c.waitStatsPerfDataCollectors[instance].MetricType
checks := []check{
{"windows_mssql_bufman_read_ahead_issuing_seconds_total", "Readahead time/sec", false, bufMan},
{"windows_mssql_databases_log_flush_wait_seconds_total", "Log Flush Wait Time", false, databases},
{"windows_mssql_databases_log_flush_write_seconds_total", "Log Flush Write Time (ms)", false, databases},
{"windows_mssql_databases_log_growths_total", "Log Growths", true, databases},
{"windows_mssql_databases_log_shrinks_total", "Log Shrinks", true, databases},
{"windows_mssql_databases_log_truncations_total", "Log Truncations", true, databases},
{"windows_mssql_dbreplica_database_flow_control_wait_seconds_total", "Database Flow Control Delay", false, dbReplica},
{"windows_mssql_dbreplica_group_commit_stall_seconds_total", "Group Commit Time", false, dbReplica},
{"windows_mssql_dbreplica_transaction_delay_seconds_total", "Transaction Delay", false, dbReplica},
{"windows_mssql_dbreplica_recovery_queue_bytes", "Recovery Queue", false, dbReplica},
{"windows_mssql_dbreplica_redo_remaining_bytes", "Redo Bytes Remaining", false, dbReplica},
{"windows_mssql_genstats_http_authenticated_requests_total", "HTTP Authenticated Requests", false, genStats},
{"windows_mssql_genstats_mars_deadlocks_total", "Mars Deadlocks", false, genStats},
{"windows_mssql_genstats_soap_empty_requests_total", "SOAP Empty Requests", false, genStats},
{"windows_mssql_genstats_soap_method_invocations_total", "SOAP Method Invocations", false, genStats},
{"windows_mssql_genstats_soap_session_initiate_requests_total", "SOAP Session Initiate Requests", false, genStats},
{"windows_mssql_genstats_soap_session_terminate_requests_total", "SOAP Session Terminate Requests", false, genStats},
{"windows_mssql_genstats_soapsql_requests_total", "SOAP SQL Requests", false, genStats},
{"windows_mssql_genstats_soapwsdl_requests_total", "SOAP WSDL Requests", false, genStats},
{"windows_mssql_genstats_sql_trace_io_provider_lock_waits_total", "SQL Trace IO Provider Lock Waits", false, genStats},
{"windows_mssql_genstats_tempdb_recovery_unit_ids_generated_total", "Tempdb recovery unit id", false, genStats},
{"windows_mssql_genstats_tempdb_rowset_ids_generated_total", "Tempdb rowset id", false, genStats},
{"windows_mssql_locks_lock_wait_seconds_total", "Lock Wait Time (ms)", false, locks},
{"windows_mssql_transactions_nonsnapshot_version_active", "NonSnapshot Version Transactions", false, transactions},
{"windows_mssql_transactions_snapshot_active", "Snapshot Transactions", false, transactions},
{"windows_mssql_transactions_update_snapshot_active", "Update Snapshot Transactions", false, transactions},
{"windows_mssql_transactions_version_cleanup_bytes_total", "Version Cleanup rate (KB/s)", false, transactions},
{"windows_mssql_transactions_version_generation_bytes_total", "Version Generation rate (KB/s)", false, transactions},
{"windows_mssql_transactions_version_store_units", "Version Store unit count", false, transactions},
{"windows_mssql_waitstats_lock_waits", "Lock waits", false, waitStats},
{"windows_mssql_waitstats_network_io_waits", "Network IO waits", false, waitStats},
}
metrics := make(chan prometheus.Metric)
collected := make([]prometheus.Metric, 0, 1024)
done := make(chan struct{})
go func() {
defer close(done)
for metric := range metrics {
collected = append(collected, metric)
}
}()
collectErr := c.Collect(metrics, 0)
close(metrics)
<-done
require.NoError(t, collectErr)
series := gatherFirstSeries(t, emitCollector(func(ch chan<- prometheus.Metric) {
for _, metric := range collected {
ch <- metric
}
}))
for _, check := range checks {
pdhType, ok := check.metricType(check.counter)
require.True(t, ok, "counter %s not found", check.counter)
if check.cumulative {
require.Equal(t, prometheus.GaugeValue, pdhType, check.counter)
pdhType = prometheus.CounterValue
}
want := dto.MetricType_GAUGE
if pdhType == prometheus.CounterValue {
want = dto.MetricType_COUNTER
}
metric, ok := series[check.metric]
if !ok && strings.HasPrefix(check.metric, "windows_mssql_dbreplica_") {
// Database Replica has only _Total without availability groups.
t.Logf("metric %s not emitted on this instance", check.metric)
continue
}
require.True(t, ok, "metric %s not emitted", check.metric)
metricType, _ := metricValue(metric)
require.Equal(t, want, metricType, check.metric)
}
}