// 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 ( "context" "errors" "fmt" "log/slog" "maps" "slices" "strings" "time" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/mi" "github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus-community/windows_exporter/internal/utils/recovery" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sys/windows/registry" ) const ( Name = "mssql" subCollectorAccessMethods = "accessmethods" subCollectorAvailabilityReplica = "availreplica" subCollectorBufferManager = "bufman" subCollectorDatabases = "databases" subCollectorDatabaseReplica = "dbreplica" subCollectorGeneralStatistics = "genstats" subCollectorInfo = "info" subCollectorLocks = "locks" subCollectorMemoryManager = "memmgr" subCollectorSQLErrors = "sqlerrors" subCollectorSQLStats = "sqlstats" subCollectorTransactions = "transactions" subCollectorWaitStats = "waitstats" ) type Config struct { CollectorsEnabled []string `yaml:"enabled"` } //nolint:gochecknoglobals var ConfigDefaults = Config{ CollectorsEnabled: []string{ subCollectorAccessMethods, subCollectorAvailabilityReplica, subCollectorBufferManager, subCollectorDatabases, subCollectorDatabaseReplica, subCollectorGeneralStatistics, subCollectorInfo, subCollectorLocks, subCollectorMemoryManager, subCollectorSQLErrors, subCollectorSQLStats, subCollectorTransactions, subCollectorWaitStats, }, } // A Collector is a Prometheus Collector for various WMI Win32_PerfRawData_MSSQLSERVER_* metrics. type Collector struct { collectorAccessMethods collectorAvailabilityReplica collectorBufferManager collectorDatabaseReplica collectorDatabases collectorGeneralStatistics collectorInstance collectorLocks collectorMemoryManager collectorSQLErrors collectorSQLStats collectorTransactions collectorWaitStats config Config logger *slog.Logger mssqlInstances []mssqlInstance collectorFns []func(ch chan<- prometheus.Metric) error closeFns []func() // meta mssqlScrapeDurationDesc *prometheus.Desc mssqlScrapeSuccessDesc *prometheus.Desc } func New(config *Config) *Collector { if config == nil { config = &ConfigDefaults } if config.CollectorsEnabled == nil { config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled } c := &Collector{ config: *config, } return c } func NewWithFlags(app *kingpin.Application) *Collector { c := &Collector{ config: ConfigDefaults, } var collectorsEnabled string app.Flag( "collector.mssql.enabled", "Comma-separated list of collectors to use.", ).Default(strings.Join(c.config.CollectorsEnabled, ",")).StringVar(&collectorsEnabled) app.Action(func(*kingpin.ParseContext) error { c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",") return nil }) return c } func (c *Collector) GetName() string { return Name } func (c *Collector) Close() error { for _, fn := range c.closeFns { fn() } c.closeFns = nil return nil } func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { c.logger = logger.With(slog.String("collector", Name)) subCollectors := map[string]struct { build func() error collect func(ch chan<- prometheus.Metric) error close func() }{ subCollectorAccessMethods: { build: c.buildAccessMethods, collect: c.collectAccessMethods, close: c.closeAccessMethods, }, subCollectorAvailabilityReplica: { build: c.buildAvailabilityReplica, collect: c.collectAvailabilityReplica, close: c.closeAvailabilityReplica, }, subCollectorBufferManager: { build: c.buildBufferManager, collect: c.collectBufferManager, close: c.closeBufferManager, }, subCollectorDatabases: { build: c.buildDatabases, collect: c.collectDatabases, close: c.closeDatabases, }, subCollectorDatabaseReplica: { build: c.buildDatabaseReplica, collect: c.collectDatabaseReplica, close: c.closeDatabaseReplica, }, subCollectorGeneralStatistics: { build: c.buildGeneralStatistics, collect: c.collectGeneralStatistics, close: c.closeGeneralStatistics, }, subCollectorInfo: { build: c.buildInstance, collect: c.collectInstance, close: c.closeInstance, }, subCollectorLocks: { build: c.buildLocks, collect: c.collectLocks, close: c.closeLocks, }, subCollectorMemoryManager: { build: c.buildMemoryManager, collect: c.collectMemoryManager, close: c.closeMemoryManager, }, subCollectorSQLErrors: { build: c.buildSQLErrors, collect: c.collectSQLErrors, close: c.closeSQLErrors, }, subCollectorSQLStats: { build: c.buildSQLStats, collect: c.collectSQLStats, close: c.closeSQLStats, }, subCollectorTransactions: { build: c.buildTransactions, collect: c.collectTransactions, close: c.closeTransactions, }, subCollectorWaitStats: { build: c.buildWaitStats, collect: c.collectWaitStats, close: c.closeWaitStats, }, } // Sort a copy, to not modify the slice of the caller or ConfigDefaults. // Result must order, to prevent test failures. collectorsEnabled := slices.Compact(slices.Sorted(slices.Values(c.config.CollectorsEnabled))) for _, name := range collectorsEnabled { if _, ok := subCollectors[name]; !ok { return fmt.Errorf("unknown sub collector: %s. Possible values: %s", name, strings.Join(slices.Sorted(maps.Keys(subCollectors)), ", "), ) } } instances, err := c.getMSSQLInstances() if err != nil { return fmt.Errorf("couldn't get SQL instances: %w", err) } c.mssqlInstances = instances c.collectorFns = make([]func(ch chan<- prometheus.Metric) error, 0, len(collectorsEnabled)) c.closeFns = make([]func(), 0, len(collectorsEnabled)) errs := make([]error, 0, len(collectorsEnabled)) for _, name := range collectorsEnabled { // Register close before build, so that Close also releases a partially built sub collector. c.closeFns = append(c.closeFns, subCollectors[name].close) if err := subCollectors[name].build(); err != nil { errs = append(errs, fmt.Errorf("failed to build %s collector: %w", name, err)) } c.collectorFns = append(c.collectorFns, subCollectors[name].collect) } c.mssqlScrapeDurationDesc = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "collector_duration_seconds"), "windows_exporter: Duration of an mssql child collection.", []string{"collector", "mssql_instance"}, nil, ) c.mssqlScrapeSuccessDesc = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "collector_success"), "windows_exporter: Whether a mssql child collector was successful.", []string{"collector", "mssql_instance"}, nil, ) return errors.Join(errs...) } // Collect sends the metric values for each metric // to the provided prometheus Metric channel. func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error { if len(c.mssqlInstances) == 0 { return fmt.Errorf("no SQL instances found: %w", pdh.ErrNoData) } var g recovery.Group for _, fn := range c.collectorFns { g.Go(func() error { return fn(ch) }) } return g.Wait() } func (c *Collector) getMSSQLInstances() ([]mssqlInstance, error) { regKey := `Software\Microsoft\Microsoft SQL Server\Instance Names\SQL` k, err := registry.OpenKey(registry.LOCAL_MACHINE, regKey, registry.QUERY_VALUE) if err != nil { return nil, fmt.Errorf("couldn't open registry to determine SQL instances: %w", err) } defer func(key registry.Key) { if err := key.Close(); err != nil { c.logger.Warn("failed to close registry key", slog.Any("err", err), ) } }(k) instanceNames, err := k.ReadValueNames(0) if err != nil { return nil, fmt.Errorf("couldn't read subkey names: %w", err) } sqlInstances := make([]mssqlInstance, 0, len(instanceNames)) for _, instanceName := range instanceNames { instanceVersion, _, err := k.GetStringValue(instanceName) if err != nil { return nil, fmt.Errorf("couldn't get instance info: %w", err) } instance, err := newMssqlInstance(instanceName, instanceVersion) if err != nil { return nil, err } sqlInstances = append(sqlInstances, instance) } c.logger.Debug(fmt.Sprintf("detected MSSQL Instances: %#v\n", sqlInstances)) return sqlInstances, nil } // mssqlGetPerfObjectName returns the name of the Windows Performance // Counter object for the given SQL instance and Collector. func (c *Collector) mssqlGetPerfObjectName(sqlInstance mssqlInstance, collector string) string { sb := strings.Builder{} if sqlInstance.isFirstInstance { sb.WriteString("SQLServer:") } else { sb.WriteString("MSSQL$") sb.WriteString(sqlInstance.name) sb.WriteString(":") } sb.WriteString(collector) return sb.String() } // mssqlGetPerfObjectName returns the name of the Windows Performance // Counter object for the given SQL instance and Collector. func (c *Collector) collect[T any]( ch chan<- prometheus.Metric, collector string, perfDataCollectors map[mssqlInstance]*pdh.Collector[T], collectFn func(ch chan<- prometheus.Metric, sqlInstance mssqlInstance, perfDataCollector *pdh.Collector[T]) error, ) error { errs := make([]error, 0, len(perfDataCollectors)) ctx := context.Background() for sqlInstance, perfDataCollector := range perfDataCollectors { begin := time.Now() success := 1.0 err := collectFn(ch, sqlInstance, perfDataCollector) duration := time.Since(begin) if err != nil && !errors.Is(err, pdh.ErrNoData) { errs = append(errs, err) success = 0.0 c.logger.LogAttrs(ctx, slog.LevelDebug, fmt.Sprintf("mssql class collector %s for instance %s failed after %s", collector, sqlInstance.name, duration), slog.Any("err", err), ) } else { c.logger.LogAttrs(ctx, slog.LevelDebug, fmt.Sprintf("mssql class collector %s for instance %s succeeded after %s", collector, sqlInstance.name, duration)) } if collector == "" { continue } ch <- prometheus.MustNewConstMetric( c.mssqlScrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), collector, sqlInstance.name, ) ch <- prometheus.MustNewConstMetric( c.mssqlScrapeSuccessDesc, prometheus.GaugeValue, success, collector, sqlInstance.name, ) } return errors.Join(errs...) }