From 2ef1a5fdf180662a4c2133f2b5c3d9b73d365722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Mon, 7 Oct 2024 00:15:54 +0200 Subject: [PATCH] logical_disk: Implement Perfdata collector (#1673) --- .golangci.yaml | 1 + internal/collector/cpu/cpu_test.go | 4 + internal/collector/cpu_info/cpu_info_test.go | 4 + internal/collector/dhcp/dhcp.go | 4 + internal/collector/filetime/filetime.go | 18 +- internal/collector/filetime/filetime_test.go | 6 + internal/collector/license/license_test.go | 4 + internal/collector/logical_disk/const.go | 43 ++++ .../collector/logical_disk/logical_disk.go | 228 ++++++++++++++++-- .../logical_disk/logical_disk_test.go | 7 + internal/collector/logon/logon_test.go | 4 + internal/collector/net/net_test.go | 14 ++ internal/collector/os/os_test.go | 6 + .../physical_disk/physical_disk_test.go | 6 + internal/collector/printer/printer_test.go | 4 + internal/collector/process/process_test.go | 53 +--- internal/perfdata/v1/collector.go | 17 ++ internal/perfdata/v1/perflib.go | 15 +- internal/testutils/testutils.go | 50 ++++ 19 files changed, 400 insertions(+), 88 deletions(-) create mode 100644 internal/collector/logical_disk/const.go create mode 100644 internal/collector/net/net_test.go diff --git a/.golangci.yaml b/.golangci.yaml index 8b54eba87..ca9ac8b63 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -16,6 +16,7 @@ linters: - goconst - gocyclo - gomnd + - paralleltest - lll - maintidx - mnd diff --git a/internal/collector/cpu/cpu_test.go b/internal/collector/cpu/cpu_test.go index 1cfdb8ed7..efa69e7f1 100644 --- a/internal/collector/cpu/cpu_test.go +++ b/internal/collector/cpu/cpu_test.go @@ -12,3 +12,7 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, cpu.Name, cpu.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, cpu.New, nil) +} diff --git a/internal/collector/cpu_info/cpu_info_test.go b/internal/collector/cpu_info/cpu_info_test.go index c1339d751..b59957a87 100644 --- a/internal/collector/cpu_info/cpu_info_test.go +++ b/internal/collector/cpu_info/cpu_info_test.go @@ -10,3 +10,7 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, cpu_info.Name, cpu_info.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, cpu_info.New, nil) +} diff --git a/internal/collector/dhcp/dhcp.go b/internal/collector/dhcp/dhcp.go index 2ad997e75..7a330873c 100644 --- a/internal/collector/dhcp/dhcp.go +++ b/internal/collector/dhcp/dhcp.go @@ -77,6 +77,10 @@ func (c *Collector) GetName() string { } func (c *Collector) GetPerfCounter(_ *slog.Logger) ([]string, error) { + if utils.PDHEnabled() { + return []string{}, nil + } + return []string{"DHCP Server"}, nil } diff --git a/internal/collector/filetime/filetime.go b/internal/collector/filetime/filetime.go index a2c6f771d..0dd8ccb76 100644 --- a/internal/collector/filetime/filetime.go +++ b/internal/collector/filetime/filetime.go @@ -20,11 +20,11 @@ import ( const Name = "filetime" type Config struct { - filePatterns []string + FilePatterns []string } var ConfigDefaults = Config{ - filePatterns: []string{}, + FilePatterns: []string{}, } // A Collector is a Prometheus Collector for collecting file times. @@ -39,8 +39,8 @@ func New(config *Config) *Collector { config = &ConfigDefaults } - if config.filePatterns == nil { - config.filePatterns = ConfigDefaults.filePatterns + if config.FilePatterns == nil { + config.FilePatterns = ConfigDefaults.FilePatterns } c := &Collector{ @@ -54,18 +54,18 @@ func NewWithFlags(app *kingpin.Application) *Collector { c := &Collector{ config: ConfigDefaults, } - c.config.filePatterns = make([]string, 0) + c.config.FilePatterns = make([]string, 0) var filePatterns string app.Flag( "collector.filetime.file-patterns", "Comma-separated list of file patterns. Each pattern is a glob pattern that can contain `*`, `?`, and `**` (recursive). See https://github.com/bmatcuk/doublestar#patterns", - ).Default(strings.Join(ConfigDefaults.filePatterns, ",")).StringVar(&filePatterns) + ).Default(strings.Join(ConfigDefaults.FilePatterns, ",")).StringVar(&filePatterns) app.Action(func(*kingpin.ParseContext) error { // doublestar.Glob() requires forward slashes - c.config.filePatterns = strings.Split(filepath.ToSlash(filePatterns), ",") + c.config.FilePatterns = strings.Split(filepath.ToSlash(filePatterns), ",") return nil }) @@ -97,7 +97,7 @@ func (c *Collector) Build(logger *slog.Logger, _ *wmi.Client) error { nil, ) - for _, filePattern := range c.config.filePatterns { + for _, filePattern := range c.config.FilePatterns { basePath, pattern := doublestar.SplitPattern(filePattern) _, err := doublestar.Glob(os.DirFS(basePath), pattern, doublestar.WithFilesOnly()) @@ -121,7 +121,7 @@ func (c *Collector) Collect(_ *types.ScrapeContext, logger *slog.Logger, ch chan func (c *Collector) collectGlob(logger *slog.Logger, ch chan<- prometheus.Metric) error { wg := sync.WaitGroup{} - for _, filePattern := range c.config.filePatterns { + for _, filePattern := range c.config.FilePatterns { wg.Add(1) go func(filePattern string) { diff --git a/internal/collector/filetime/filetime_test.go b/internal/collector/filetime/filetime_test.go index 4a111acb3..5da1d5603 100644 --- a/internal/collector/filetime/filetime_test.go +++ b/internal/collector/filetime/filetime_test.go @@ -10,3 +10,9 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, filetime.Name, filetime.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, filetime.New, &filetime.Config{ + FilePatterns: []string{"*.*"}, + }) +} diff --git a/internal/collector/license/license_test.go b/internal/collector/license/license_test.go index 459960ee8..ccaa19359 100644 --- a/internal/collector/license/license_test.go +++ b/internal/collector/license/license_test.go @@ -10,3 +10,7 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, license.Name, license.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, license.New, nil) +} diff --git a/internal/collector/logical_disk/const.go b/internal/collector/logical_disk/const.go new file mode 100644 index 000000000..15f700607 --- /dev/null +++ b/internal/collector/logical_disk/const.go @@ -0,0 +1,43 @@ +package logical_disk + +const ( + avgDiskReadQueueLength = "Avg. Disk Read Queue Length" + avgDiskSecPerRead = "Avg. Disk sec/Read" + avgDiskSecPerTransfer = "Avg. Disk sec/Transfer" + avgDiskSecPerWrite = "Avg. Disk sec/Write" + avgDiskWriteQueueLength = "Avg. Disk Write Queue Length" + currentDiskQueueLength = "Current Disk Queue Length" + freeSpace = "Free Megabytes" + diskReadBytesPerSec = "Disk Read Bytes/sec" + diskReadsPerSec = "Disk Reads/sec" + diskWriteBytesPerSec = "Disk Write Bytes/sec" + diskWritesPerSec = "Disk Writes/sec" + percentDiskReadTime = "% Disk Read Time" + percentDiskWriteTime = "% Disk Write Time" + percentFreeSpace = "% Free Space" + percentIdleTime = "% Idle Time" + SplitIOPerSec = "Split IO/Sec" +) + +// Win32_PerfRawData_PerfDisk_LogicalDisk docs: +// - https://msdn.microsoft.com/en-us/windows/hardware/aa394307(v=vs.71) - Win32_PerfRawData_PerfDisk_LogicalDisk class +// - https://msdn.microsoft.com/en-us/library/ms803973.aspx - LogicalDisk object reference. +type logicalDisk struct { + Name string + CurrentDiskQueueLength float64 `perflib:"Current Disk Queue Length"` + AvgDiskReadQueueLength float64 `perflib:"Avg. Disk Read Queue Length"` + AvgDiskWriteQueueLength float64 `perflib:"Avg. Disk Write Queue Length"` + DiskReadBytesPerSec float64 `perflib:"Disk Read Bytes/sec"` + DiskReadsPerSec float64 `perflib:"Disk Reads/sec"` + DiskWriteBytesPerSec float64 `perflib:"Disk Write Bytes/sec"` + DiskWritesPerSec float64 `perflib:"Disk Writes/sec"` + PercentDiskReadTime float64 `perflib:"% Disk Read Time"` + PercentDiskWriteTime float64 `perflib:"% Disk Write Time"` + PercentFreeSpace float64 `perflib:"% Free Space_Base"` + PercentFreeSpace_Base float64 `perflib:"Free Megabytes"` + PercentIdleTime float64 `perflib:"% Idle Time"` + SplitIOPerSec float64 `perflib:"Split IO/Sec"` + AvgDiskSecPerRead float64 `perflib:"Avg. Disk sec/Read"` + AvgDiskSecPerWrite float64 `perflib:"Avg. Disk sec/Write"` + AvgDiskSecPerTransfer float64 `perflib:"Avg. Disk sec/Transfer"` +} diff --git a/internal/collector/logical_disk/logical_disk.go b/internal/collector/logical_disk/logical_disk.go index 7bba3eaf4..a2ad3f56d 100644 --- a/internal/collector/logical_disk/logical_disk.go +++ b/internal/collector/logical_disk/logical_disk.go @@ -4,6 +4,7 @@ package logical_disk import ( "encoding/binary" + "errors" "fmt" "log/slog" "regexp" @@ -12,9 +13,11 @@ import ( "strings" "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/internal/perfdata" "github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes" v1 "github.com/prometheus-community/windows_exporter/internal/perfdata/v1" "github.com/prometheus-community/windows_exporter/internal/types" + "github.com/prometheus-community/windows_exporter/internal/utils" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows" @@ -36,6 +39,8 @@ var ConfigDefaults = Config{ type Collector struct { config Config + perfDataCollector perfdata.Collector + avgReadQueue *prometheus.Desc avgWriteQueue *prometheus.Desc freeSpace *prometheus.Desc @@ -125,6 +130,10 @@ func (c *Collector) GetName() string { } func (c *Collector) GetPerfCounter(_ *slog.Logger) ([]string, error) { + if utils.PDHEnabled() { + return []string{}, nil + } + return []string{"LogicalDisk"}, nil } @@ -133,6 +142,34 @@ func (c *Collector) Close(_ *slog.Logger) error { } func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { + if utils.PDHEnabled() { + counters := []string{ + currentDiskQueueLength, + avgDiskReadQueueLength, + avgDiskWriteQueueLength, + diskReadBytesPerSec, + diskReadsPerSec, + diskWriteBytesPerSec, + diskWritesPerSec, + percentDiskReadTime, + percentDiskWriteTime, + percentFreeSpace, + freeSpace, + percentIdleTime, + SplitIOPerSec, + avgDiskSecPerRead, + avgDiskSecPerWrite, + avgDiskSecPerTransfer, + } + + var err error + + c.perfDataCollector, err = perfdata.NewCollector(perfdata.V1, "LogicalDisk", perfdata.AllInstances, counters) + if err != nil { + return fmt.Errorf("failed to create LogicalDisk collector: %w", err) + } + } + c.information = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "info"), "A metric with a constant '1' value labeled with logical disk information", @@ -264,6 +301,11 @@ func (c *Collector) Build(_ *slog.Logger, _ *wmi.Client) error { // to the provided prometheus Metric channel. func (c *Collector) Collect(ctx *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { logger = logger.With(slog.String("collector", Name)) + + if utils.PDHEnabled() { + return c.collectPDH(logger, ch) + } + if err := c.collect(ctx, logger, ch); err != nil { logger.Error("failed collecting logical_disk metrics", slog.Any("err", err), @@ -275,32 +317,172 @@ func (c *Collector) Collect(ctx *types.ScrapeContext, logger *slog.Logger, ch ch return nil } -// Win32_PerfRawData_PerfDisk_LogicalDisk docs: -// - https://msdn.microsoft.com/en-us/windows/hardware/aa394307(v=vs.71) - Win32_PerfRawData_PerfDisk_LogicalDisk class -// - https://msdn.microsoft.com/en-us/library/ms803973.aspx - LogicalDisk object reference. -type logicalDisk struct { - Name string - CurrentDiskQueueLength float64 `perflib:"Current Disk Queue Length"` - AvgDiskReadQueueLength float64 `perflib:"Avg. Disk Read Queue Length"` - AvgDiskWriteQueueLength float64 `perflib:"Avg. Disk Write Queue Length"` - DiskReadBytesPerSec float64 `perflib:"Disk Read Bytes/sec"` - DiskReadsPerSec float64 `perflib:"Disk Reads/sec"` - DiskWriteBytesPerSec float64 `perflib:"Disk Write Bytes/sec"` - DiskWritesPerSec float64 `perflib:"Disk Writes/sec"` - PercentDiskReadTime float64 `perflib:"% Disk Read Time"` - PercentDiskWriteTime float64 `perflib:"% Disk Write Time"` - PercentFreeSpace float64 `perflib:"% Free Space_Base"` - PercentFreeSpace_Base float64 `perflib:"Free Megabytes"` - PercentIdleTime float64 `perflib:"% Idle Time"` - SplitIOPerSec float64 `perflib:"Split IO/Sec"` - AvgDiskSecPerRead float64 `perflib:"Avg. Disk sec/Read"` - AvgDiskSecPerWrite float64 `perflib:"Avg. Disk sec/Write"` - AvgDiskSecPerTransfer float64 `perflib:"Avg. Disk sec/Transfer"` +func (c *Collector) collectPDH(logger *slog.Logger, ch chan<- prometheus.Metric) error { + var ( + err error + diskID string + info volumeInfo + ) + + perfData, err := c.perfDataCollector.Collect() + if err != nil { + return fmt.Errorf("failed to collect LogicalDisk metrics: %w", err) + } + + if len(perfData) == 0 { + return errors.New("perflib query for LogicalDisk returned empty result set") + } + + for name, volume := range perfData { + if name == "_Total" || + c.config.VolumeExclude.MatchString(name) || + !c.config.VolumeInclude.MatchString(name) { + continue + } + + diskID, err = getDiskIDByVolume(name) + if err != nil { + logger.Warn("failed to get disk ID for "+name, + slog.Any("err", err), + ) + } + + info, err = getVolumeInfo(name) + if err != nil { + logger.Warn("failed to get volume information for "+name, + slog.Any("err", err), + ) + } + + ch <- prometheus.MustNewConstMetric( + c.information, + prometheus.GaugeValue, + 1, + diskID, + info.volumeType, + name, + info.label, + info.filesystem, + info.serialNumber, + ) + + ch <- prometheus.MustNewConstMetric( + c.requestsQueued, + prometheus.GaugeValue, + volume[currentDiskQueueLength].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.avgReadQueue, + prometheus.GaugeValue, + volume[avgDiskReadQueueLength].FirstValue*perftypes.TicksToSecondScaleFactor, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.avgWriteQueue, + prometheus.GaugeValue, + volume[avgDiskWriteQueueLength].FirstValue*perftypes.TicksToSecondScaleFactor, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.readBytesTotal, + prometheus.CounterValue, + volume[diskReadBytesPerSec].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.readsTotal, + prometheus.CounterValue, + volume[diskReadsPerSec].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.writeBytesTotal, + prometheus.CounterValue, + volume[diskWriteBytesPerSec].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.writesTotal, + prometheus.CounterValue, + volume[diskWritesPerSec].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.readTime, + prometheus.CounterValue, + volume[percentDiskReadTime].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.writeTime, + prometheus.CounterValue, + volume[percentDiskWriteTime].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.freeSpace, + prometheus.GaugeValue, + volume[freeSpace].FirstValue*1024*1024, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.totalSpace, + prometheus.GaugeValue, + volume[percentFreeSpace].FirstValue*1024*1024, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.idleTime, + prometheus.CounterValue, + volume[percentIdleTime].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.splitIOs, + prometheus.CounterValue, + volume[SplitIOPerSec].FirstValue, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.readLatency, + prometheus.CounterValue, + volume[avgDiskSecPerRead].FirstValue*perftypes.TicksToSecondScaleFactor, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.writeLatency, + prometheus.CounterValue, + volume[avgDiskSecPerWrite].FirstValue*perftypes.TicksToSecondScaleFactor, + name, + ) + + ch <- prometheus.MustNewConstMetric( + c.readWriteLatency, + prometheus.CounterValue, + volume[avgDiskSecPerTransfer].FirstValue*perftypes.TicksToSecondScaleFactor, + name, + ) + } + + return nil } func (c *Collector) collect(ctx *types.ScrapeContext, logger *slog.Logger, ch chan<- prometheus.Metric) error { - logger = logger.With(slog.String("collector", Name)) - var ( err error diskID string diff --git a/internal/collector/logical_disk/logical_disk_test.go b/internal/collector/logical_disk/logical_disk_test.go index 2bbeed790..a7197e7ed 100644 --- a/internal/collector/logical_disk/logical_disk_test.go +++ b/internal/collector/logical_disk/logical_disk_test.go @@ -6,6 +6,7 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/collector/logical_disk" "github.com/prometheus-community/windows_exporter/internal/testutils" + "github.com/prometheus-community/windows_exporter/internal/types" ) func BenchmarkCollector(b *testing.B) { @@ -14,3 +15,9 @@ func BenchmarkCollector(b *testing.B) { kingpin.CommandLine.GetArg("collector.logical_disk.volume-include").StringVar(&localVolumeInclude) testutils.FuncBenchmarkCollector(b, "logical_disk", logical_disk.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{ + VolumeInclude: types.RegExpAny, + }) +} diff --git a/internal/collector/logon/logon_test.go b/internal/collector/logon/logon_test.go index 91a1f6a11..b5c84f7ad 100644 --- a/internal/collector/logon/logon_test.go +++ b/internal/collector/logon/logon_test.go @@ -11,3 +11,7 @@ func BenchmarkCollector(b *testing.B) { // No context name required as Collector source is WMI testutils.FuncBenchmarkCollector(b, logon.Name, logon.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, logon.New, nil) +} diff --git a/internal/collector/net/net_test.go b/internal/collector/net/net_test.go new file mode 100644 index 000000000..c1e172a43 --- /dev/null +++ b/internal/collector/net/net_test.go @@ -0,0 +1,14 @@ +//go:build windows + +package net_test + +import ( + "testing" + + "github.com/prometheus-community/windows_exporter/internal/collector/net" + "github.com/prometheus-community/windows_exporter/internal/testutils" +) + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, net.New, nil) +} diff --git a/internal/collector/os/os_test.go b/internal/collector/os/os_test.go index c6676ddbb..ab0f28a5c 100644 --- a/internal/collector/os/os_test.go +++ b/internal/collector/os/os_test.go @@ -10,3 +10,9 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, os.Name, os.NewWithFlags) } + +func TestCollector(t *testing.T) { + t.Skip() + + testutils.TestCollector(t, os.New, nil) +} diff --git a/internal/collector/physical_disk/physical_disk_test.go b/internal/collector/physical_disk/physical_disk_test.go index 61ba88ce4..b09d027be 100644 --- a/internal/collector/physical_disk/physical_disk_test.go +++ b/internal/collector/physical_disk/physical_disk_test.go @@ -10,3 +10,9 @@ import ( func BenchmarkCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, physical_disk.Name, physical_disk.NewWithFlags) } + +func TestCollector(t *testing.T) { + t.Skip() + + testutils.TestCollector(t, physical_disk.New, nil) +} diff --git a/internal/collector/printer/printer_test.go b/internal/collector/printer/printer_test.go index 2bab8749a..628c6e406 100644 --- a/internal/collector/printer/printer_test.go +++ b/internal/collector/printer/printer_test.go @@ -14,3 +14,7 @@ func BenchmarkCollector(b *testing.B) { kingpin.CommandLine.GetArg("collector.printer.include").StringVar(&printersInclude) testutils.FuncBenchmarkCollector(b, "printer", printer.NewWithFlags) } + +func TestCollector(t *testing.T) { + testutils.TestCollector(t, printer.New, nil) +} diff --git a/internal/collector/process/process_test.go b/internal/collector/process/process_test.go index 4c213a2e5..3a751587a 100644 --- a/internal/collector/process/process_test.go +++ b/internal/collector/process/process_test.go @@ -1,18 +1,11 @@ package process_test import ( - "io" - "log/slog" - "sync" "testing" - "time" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/collector/process" "github.com/prometheus-community/windows_exporter/internal/testutils" - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/require" - "github.com/yusufpapurcu/wmi" ) func BenchmarkProcessCollector(b *testing.B) { @@ -23,48 +16,6 @@ func BenchmarkProcessCollector(b *testing.B) { testutils.FuncBenchmarkCollector(b, process.Name, process.NewWithFlags) } -func TestProcessCollector(t *testing.T) { - t.Setenv("WINDOWS_EXPORTER_PERF_COUNTERS_ENGINE", "pdh") - - var ( - metrics []prometheus.Metric - err error - ) - - logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - c := process.New(nil) - ch := make(chan prometheus.Metric, 10000) - - wmiClient := &wmi.Client{ - AllowMissingFields: true, - } - wmiClient.SWbemServicesClient, err = wmi.InitializeSWbemServices(wmiClient) - require.NoError(t, err) - - t.Cleanup(func() { - require.NoError(t, c.Close(logger)) - }) - - wg := sync.WaitGroup{} - wg.Add(1) - - go func() { - defer wg.Done() - - for metric := range ch { - metrics = append(metrics, metric) - } - }() - - require.NoError(t, c.Build(logger, wmiClient)) - - time.Sleep(1 * time.Second) - - require.NoError(t, c.Collect(nil, logger, ch)) - - close(ch) - - wg.Wait() - - require.NotEmpty(t, metrics) +func TestCollector(t *testing.T) { + testutils.TestCollector(t, process.New, nil) } diff --git a/internal/perfdata/v1/collector.go b/internal/perfdata/v1/collector.go index 846ad85ea..368522360 100644 --- a/internal/perfdata/v1/collector.go +++ b/internal/perfdata/v1/collector.go @@ -2,6 +2,7 @@ package v1 import ( "fmt" + "strings" "github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes" "github.com/prometheus/client_golang/prometheus" @@ -43,11 +44,23 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er return nil, fmt.Errorf("QueryPerformanceData: %w", err) } + if len(perfObjects) == 0 { + return map[string]map[string]perftypes.CounterValues{}, nil + } + data := make(map[string]map[string]perftypes.CounterValues, len(perfObjects[0].Instances)) for _, perfObject := range perfObjects { + if perfObject.Name != c.object { + continue + } + for _, perfInstance := range perfObject.Instances { instanceName := perfInstance.Name + if strings.HasSuffix(instanceName, "_Total") { + continue + } + if instanceName == "" || instanceName == "*" { instanceName = perftypes.EmptyInstance } @@ -57,6 +70,10 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er } for _, perfCounter := range perfInstance.Counters { + if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter { + continue + } + if _, ok := data[instanceName][perfCounter.Def.Name]; !ok { data[instanceName][perfCounter.Def.Name] = perftypes.CounterValues{ Type: prometheus.GaugeValue, diff --git a/internal/perfdata/v1/perflib.go b/internal/perfdata/v1/perflib.go index 746d6b874..6f3a322dd 100644 --- a/internal/perfdata/v1/perflib.go +++ b/internal/perfdata/v1/perflib.go @@ -117,16 +117,16 @@ import ( "fmt" "io" "strings" + "time" "unsafe" + "github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes" "golang.org/x/sys/windows" ) // TODO: There's a LittleEndian field in the PERF header - we ought to check it. var bo = binary.LittleEndian -const averageCount64Type = 1073874176 - // PerfObject Top-level performance object (like "Process"). type PerfObject struct { Name string @@ -221,13 +221,18 @@ func queryRawData(query string) ([]byte, error) { (*byte)(unsafe.Pointer(&buffer[0])), &bufLen) - if errors.Is(err, error(windows.ERROR_MORE_DATA)) { + switch { + case errors.Is(err, error(windows.ERROR_MORE_DATA)): newBuffer := make([]byte, len(buffer)+16384) copy(newBuffer, buffer) buffer = newBuffer continue - } else if err != nil { + case errors.Is(err, error(windows.ERROR_BUSY)): + time.Sleep(50 * time.Millisecond) + + continue + case err != nil: var errNo windows.Errno if errors.As(err, &errNo) { return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo)) @@ -349,7 +354,7 @@ func QueryPerformanceData(query string) ([]*PerfObject, error) { IsCounter: def.CounterType&0x400 == 0x400, IsBaseValue: def.CounterType&0x00030000 == 0x00030000, IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000, - HasSecondValue: def.CounterType == averageCount64Type, + HasSecondValue: def.CounterType == perftypes.PERF_AVERAGE_BULK, } } diff --git a/internal/testutils/testutils.go b/internal/testutils/testutils.go index d4a1fa5c6..a202d4636 100644 --- a/internal/testutils/testutils.go +++ b/internal/testutils/testutils.go @@ -5,12 +5,15 @@ package testutils import ( "io" "log/slog" + "sync" "testing" + "time" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/pkg/collector" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + "github.com/yusufpapurcu/wmi" ) func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, collectFunc collector.BuilderWithFlags[C]) { @@ -40,3 +43,50 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co require.NoError(b, c.Collect(scrapeContext, logger, metrics)) } } + +func TestCollector[C collector.Collector, V interface{}](t *testing.T, fn func(*V) C, conf *V) { + t.Helper() + t.Setenv("WINDOWS_EXPORTER_PERF_COUNTERS_ENGINE", "pdh") + + var ( + metrics []prometheus.Metric + err error + ) + + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + c := fn(conf) + ch := make(chan prometheus.Metric, 10000) + + wmiClient := &wmi.Client{ + AllowMissingFields: true, + } + wmiClient.SWbemServicesClient, err = wmi.InitializeSWbemServices(wmiClient) + require.NoError(t, err) + + t.Cleanup(func() { + require.NoError(t, c.Close(logger)) + }) + + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + + for metric := range ch { + metrics = append(metrics, metric) + } + }() + + require.NoError(t, c.Build(logger, wmiClient)) + + time.Sleep(1 * time.Second) + + require.NoError(t, c.Collect(nil, logger, ch)) + + close(ch) + + wg.Wait() + + require.NotEmpty(t, metrics) +}