From f7432aad1a63c92af1d7087ab0fa1968e592a0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 6 Sep 2024 18:12:50 +0200 Subject: [PATCH] perfdata: add experimental generic collector (#1459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- .github/workflows/lint.yml | 4 + .golangci.yaml | 8 +- docs/collector.perfdata.md | 114 ++++ pkg/collector/collector.go | 2 + pkg/collector/config.go | 3 + pkg/collector/map.go | 2 + pkg/collector/os/os.go | 4 +- pkg/collector/perfdata/perfdata.go | 176 +++++ .../perfdata/perfdata_collector_test.go | 85 +++ pkg/collector/perfdata/perfdata_test.go | 18 + pkg/collector/perfdata/types.go | 18 + pkg/collector/process/process.go | 3 +- pkg/collector/tcp/const.go | 16 + pkg/collector/tcp/tcp.go | 99 +-- .../terminal_services/terminal_services.go | 4 +- pkg/collector/time/time.go | 4 +- pkg/headers/kernel32/kernel32.go | 14 +- pkg/headers/wtsapi32/wtsapi32.go | 17 +- pkg/log/eventlog/eventlog.go | 3 +- pkg/perfdata/collector.go | 234 +++++++ pkg/perfdata/collector_bench_test.go | 49 ++ pkg/perfdata/collector_test.go | 86 +++ pkg/perfdata/const.go | 78 +++ pkg/perfdata/error.go | 18 + pkg/perfdata/pdh.go | 634 ++++++++++++++++++ pkg/perfdata/pdh_amd64.go | 143 ++++ pkg/perfdata/pdh_arm64.go | 143 ++++ pkg/perflib/perflib.go | 11 +- pkg/perflib/raw_types.go | 5 +- pkg/perflib/utf16.go | 7 +- tools/e2e-output.txt | 8 + tools/end-to-end-test.ps1 | 8 +- 32 files changed, 1939 insertions(+), 79 deletions(-) create mode 100644 docs/collector.perfdata.md create mode 100644 pkg/collector/perfdata/perfdata.go create mode 100644 pkg/collector/perfdata/perfdata_collector_test.go create mode 100644 pkg/collector/perfdata/perfdata_test.go create mode 100644 pkg/collector/perfdata/types.go create mode 100644 pkg/collector/tcp/const.go create mode 100644 pkg/perfdata/collector.go create mode 100644 pkg/perfdata/collector_bench_test.go create mode 100644 pkg/perfdata/collector_test.go create mode 100644 pkg/perfdata/const.go create mode 100644 pkg/perfdata/error.go create mode 100644 pkg/perfdata/pdh.go create mode 100644 pkg/perfdata/pdh_amd64.go create mode 100644 pkg/perfdata/pdh_arm64.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4cd52d1eb..b5adbbace 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,6 +12,8 @@ on: - "tools/e2e-output.txt" branches: - master + - next + - main pull_request: paths: - "go.mod" @@ -21,6 +23,8 @@ on: - "tools/e2e-output.txt" branches: - master + - next + - main env: PROMU_VER: '0.14.0' diff --git a/.golangci.yaml b/.golangci.yaml index 9aaffccfc..f8c93f2da 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -52,7 +52,13 @@ linters-settings: # Support string case: `camel`, `pascal`, `kebab`, `snake`, `upperSnake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower`, `header` json: camel yaml: snake - + forbidigo: + forbid: + - "^(fmt\\.Print(|f|ln)|print|println)$" + - p: "^syscall\\..*$" + msg: use golang.org/x/sys/windows instead of syscall + - p: "^windows\\.NewLazyDLL$" + msg: use NewLazySystemDLL instead NewLazyDLL issues: exclude: - don't use underscores in Go names diff --git a/docs/collector.perfdata.md b/docs/collector.perfdata.md new file mode 100644 index 000000000..ef1271cc1 --- /dev/null +++ b/docs/collector.perfdata.md @@ -0,0 +1,114 @@ +# Perfdata collector + +The perfdata collector exposes any configured metric. + +| | | +|---------------------|-------------------------| +| Metric name prefix | `perfdata` | +| Data source | Performance Data Helper | +| Enabled by default? | No | + +## Flags + + +### `--collector.perfdata.objects` + +Objects is a list of objects to collect metrics from. The value takes the form of a JSON array of strings. YAML is also supported. + +The collector supports only english named counter. Localized counter-names are not supported. + +#### Schema + +YAML: +```yaml +- object: "Processor Information" + instances: ["*"] + instance_label: "core" + counters: + "% Processor Time": {} +- object: "Memory" + counters: + "Cache Faults/sec": + type: "counter" +``` + +JSON: + +```json +[ + {"object":"Processor Information","instance_label": "core","instances":["*"],"counters": {"% Processor Time": {}}}, + {"object":"Memory","counters": {"Cache Faults/sec": {"type": "counter"}}} +] +``` + +#### name + +ObjectName is the Object to query for, like Processor, DirectoryServices, LogicalDisk or similar. + +The collector supports only english named counter. Localized counter-names are not supported. + +#### instances + +The instances key (this is an array) declares the instances of a counter you would like returned, it can be one or more values. + +Example: Instances = `["C:","D:","E:"]` + +This will return only for the instances C:, D: and E: where relevant. To get all instances of a Counter, use `["*"]` only. + +Some Objects like `Memory` do not have instances to select from at all. In this case, the `instances` key can be omitted. + +#### counters + +The Counters key (this is an object) declares the counters of the ObjectName you would like returned, it can also be one or more values. + +Example: Counters = `{"% Idle Time": {}, "% Disk Read Time": {}, "% Disk Write Time": {}}` + +This must be specified for every counter you want the results. Wildcards are not supported. + +#### counters Sub-Schema + +##### type + +This key is optional. It indicates the type of the counter. The value can be `counter` or `gauge`. +If not specified, the windows_exporter will try to determine the type based on the counter type. + +### Example + +``` +# HELP windows_perfdata_memory_cache_faults_sec +# TYPE windows_perfdata_memory_cache_faults_sec counter +windows_perfdata_memory_cache_faults_sec 2.369977e+07 +# HELP windows_perfdata_processor_information__processor_time +# TYPE windows_perfdata_processor_information__processor_time gauge +windows_perfdata_processor_information__processor_time{instance="0,0"} 1.7259640625e+11 +windows_perfdata_processor_information__processor_time{instance="0,1"} 1.7576796875e+11 +windows_perfdata_processor_information__processor_time{instance="0,10"} 2.2704234375e+11 +windows_perfdata_processor_information__processor_time{instance="0,11"} 2.3069296875e+11 +windows_perfdata_processor_information__processor_time{instance="0,12"} 2.3302265625e+11 +windows_perfdata_processor_information__processor_time{instance="0,13"} 2.32851875e+11 +windows_perfdata_processor_information__processor_time{instance="0,14"} 2.3282421875e+11 +windows_perfdata_processor_information__processor_time{instance="0,15"} 2.3271234375e+11 +windows_perfdata_processor_information__processor_time{instance="0,16"} 2.329590625e+11 +windows_perfdata_processor_information__processor_time{instance="0,17"} 2.32800625e+11 +windows_perfdata_processor_information__processor_time{instance="0,18"} 2.3194359375e+11 +windows_perfdata_processor_information__processor_time{instance="0,19"} 2.32380625e+11 +windows_perfdata_processor_information__processor_time{instance="0,2"} 1.954765625e+11 +windows_perfdata_processor_information__processor_time{instance="0,20"} 2.3259765625e+11 +windows_perfdata_processor_information__processor_time{instance="0,21"} 2.3268515625e+11 +windows_perfdata_processor_information__processor_time{instance="0,22"} 2.3301765625e+11 +windows_perfdata_processor_information__processor_time{instance="0,23"} 2.3264328125e+11 +windows_perfdata_processor_information__processor_time{instance="0,3"} 1.94745625e+11 +windows_perfdata_processor_information__processor_time{instance="0,4"} 2.2011453125e+11 +windows_perfdata_processor_information__processor_time{instance="0,5"} 2.27244375e+11 +windows_perfdata_processor_information__processor_time{instance="0,6"} 2.25501875e+11 +windows_perfdata_processor_information__processor_time{instance="0,7"} 2.2995265625e+11 +windows_perfdata_processor_information__processor_time{instance="0,8"} 2.2929890625e+11 +windows_perfdata_processor_information__processor_time{instance="0,9"} 2.313540625e+11 +windows_perfdata_processor_information__processor_time{instance="0,_Total"} 2.23009459635e+11 +``` + +## Metrics + +The perfdata collector returns metrics based on the user configuration. +The metrics are named based on the object name and the counter name. +The instance name is added as a label to the metric. diff --git a/pkg/collector/collector.go b/pkg/collector/collector.go index 9b51b8def..6ce3191b0 100644 --- a/pkg/collector/collector.go +++ b/pkg/collector/collector.go @@ -44,6 +44,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/netframework_clrsecurity" "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" + "github.com/prometheus-community/windows_exporter/pkg/collector/perfdata" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" @@ -117,6 +118,7 @@ func NewWithConfig(config Config) *Collectors { collectors[netframework_clrsecurity.Name] = netframework_clrsecurity.New(&config.NetframeworkClrsecurity) collectors[nps.Name] = nps.New(&config.Nps) collectors[os.Name] = os.New(&config.Os) + collectors[perfdata.Name] = perfdata.New(&config.PerfData) collectors[physical_disk.Name] = physical_disk.New(&config.PhysicalDisk) collectors[printer.Name] = printer.New(&config.Printer) collectors[process.Name] = process.New(&config.Process) diff --git a/pkg/collector/config.go b/pkg/collector/config.go index ea34bd78a..239f178d5 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -35,6 +35,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/netframework_clrsecurity" "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" + "github.com/prometheus-community/windows_exporter/pkg/collector/perfdata" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" @@ -90,6 +91,7 @@ type Config struct { NetframeworkClrsecurity netframework_clrsecurity.Config `yaml:"netframework_clrsecurity"` Nps nps.Config `yaml:"nps"` Os os.Config `yaml:"os"` + PerfData perfdata.Config `yaml:"perf_data"` PhysicalDisk physical_disk.Config `yaml:"physical_disk"` Printer printer.Config `yaml:"printer"` Process process.Config `yaml:"process"` @@ -148,6 +150,7 @@ var ConfigDefaults = Config{ NetframeworkClrsecurity: netframework_clrsecurity.ConfigDefaults, Nps: nps.ConfigDefaults, Os: os.ConfigDefaults, + PerfData: perfdata.ConfigDefaults, PhysicalDisk: physical_disk.ConfigDefaults, Printer: printer.ConfigDefaults, Process: process.ConfigDefaults, diff --git a/pkg/collector/map.go b/pkg/collector/map.go index 715a579d3..73f44cf47 100644 --- a/pkg/collector/map.go +++ b/pkg/collector/map.go @@ -36,6 +36,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/collector/netframework_clrsecurity" "github.com/prometheus-community/windows_exporter/pkg/collector/nps" "github.com/prometheus-community/windows_exporter/pkg/collector/os" + "github.com/prometheus-community/windows_exporter/pkg/collector/perfdata" "github.com/prometheus-community/windows_exporter/pkg/collector/physical_disk" "github.com/prometheus-community/windows_exporter/pkg/collector/printer" "github.com/prometheus-community/windows_exporter/pkg/collector/process" @@ -98,6 +99,7 @@ var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{ netframework_clrsecurity.Name: NewBuilderWithFlags(netframework_clrsecurity.NewWithFlags), nps.Name: NewBuilderWithFlags(nps.NewWithFlags), os.Name: NewBuilderWithFlags(os.NewWithFlags), + perfdata.Name: NewBuilderWithFlags(perfdata.NewWithFlags), physical_disk.Name: NewBuilderWithFlags(physical_disk.NewWithFlags), printer.Name: NewBuilderWithFlags(printer.NewWithFlags), process.Name: NewBuilderWithFlags(process.NewWithFlags), diff --git a/pkg/collector/os/os.go b/pkg/collector/os/os.go index 1b250d566..76067c8c8 100644 --- a/pkg/collector/os/os.go +++ b/pkg/collector/os/os.go @@ -8,7 +8,6 @@ import ( "os" "strconv" "strings" - "syscall" "time" "github.com/alecthomas/kingpin/v2" @@ -22,6 +21,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" + "golang.org/x/sys/windows" "golang.org/x/sys/windows/registry" ) @@ -309,7 +309,7 @@ func (c *Collector) collectTime(ch chan<- prometheus.Metric) error { } // timeZoneKeyName contains the english name of the timezone. - timezoneName := syscall.UTF16ToString(timeZoneInfo.TimeZoneKeyName[:]) + timezoneName := windows.UTF16ToString(timeZoneInfo.TimeZoneKeyName[:]) ch <- prometheus.MustNewConstMetric( c.time, diff --git a/pkg/collector/perfdata/perfdata.go b/pkg/collector/perfdata/perfdata.go new file mode 100644 index 000000000..ddbaf952c --- /dev/null +++ b/pkg/collector/perfdata/perfdata.go @@ -0,0 +1,176 @@ +//go:build windows + +package perfdata + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/alecthomas/kingpin/v2" + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus-community/windows_exporter/pkg/perfdata" + "github.com/prometheus-community/windows_exporter/pkg/types" + "github.com/prometheus/client_golang/prometheus" + "github.com/yusufpapurcu/wmi" + "golang.org/x/exp/maps" +) + +const ( + Name = "perfdata" +) + +type Config struct { + Objects []Object `yaml:"objects"` +} + +var ConfigDefaults = Config{ + Objects: make([]Object, 0), +} + +// A Collector is a Prometheus collector for perfdata metrics. +type Collector struct { + config Config +} + +func New(config *Config) *Collector { + if config == nil { + config = &ConfigDefaults + } + + if config.Objects == nil { + config.Objects = ConfigDefaults.Objects + } + + c := &Collector{ + config: *config, + } + + return c +} + +func NewWithFlags(app *kingpin.Application) *Collector { + c := &Collector{ + config: ConfigDefaults, + } + + var objects string + + app.Flag( + "collector.perfdata.objects", + "Objects of performance data to observe. See docs for more information on how to use this flag. By default, no objects are observed.", + ).Default("").StringVar(&objects) + + app.Action(func(*kingpin.ParseContext) error { + if objects == "" { + return nil + } + + if err := json.Unmarshal([]byte(objects), &c.config.Objects); err != nil { + return fmt.Errorf("failed to parse objects: %w", err) + } + + return nil + }) + + return c +} + +func (c *Collector) GetName() string { + return Name +} + +func (c *Collector) GetPerfCounter(_ log.Logger) ([]string, error) { + return []string{}, nil +} + +func (c *Collector) Close(_ log.Logger) error { + for _, object := range c.config.Objects { + object.collector.Close() + } + + return nil +} + +func (c *Collector) Build(logger log.Logger, _ *wmi.Client) error { + _ = level.Warn(logger).Log("msg", "The perfdata collector is in an experimental state! The configuration may change in future. Please report any issues.") + + for i, object := range c.config.Objects { + collector, err := perfdata.NewCollector(object.Object, object.Instances, maps.Keys(object.Counters)) + if err != nil { + return fmt.Errorf("failed to create pdh collector: %w", err) + } + + if object.InstanceLabel == "" { + c.config.Objects[i].InstanceLabel = "instance" + } + + c.config.Objects[i].collector = collector + } + + return nil +} + +// Collect sends the metric values for each metric +// to the provided prometheus Metric channel. +func (c *Collector) Collect(_ *types.ScrapeContext, logger log.Logger, ch chan<- prometheus.Metric) error { + if err := c.collect(ch); err != nil { + _ = level.Error(logger).Log("msg", "failed collecting performance data metrics", "err", err) + return err + } + return nil +} + +func (c *Collector) collect(ch chan<- prometheus.Metric) error { + for _, object := range c.config.Objects { + data, err := object.collector.Collect() + if err != nil { + return fmt.Errorf("failed to collect data: %w", err) + } + + for instance, counters := range data { + for counter, value := range counters { + var labels prometheus.Labels + if instance != perfdata.EmptyInstance { + labels = prometheus.Labels{object.InstanceLabel: instance} + } + + metricType := value.Type + if val, ok := object.Counters[counter]; ok { + switch val.Type { + case "counter": + metricType = prometheus.CounterValue + case "gauge": + metricType = prometheus.GaugeValue + } + } + + ch <- prometheus.MustNewConstMetric( + prometheus.NewDesc( + sanitizeMetricName(fmt.Sprintf("%s_perfdata_%s_%s", types.Namespace, object.Object, counter)), + fmt.Sprintf("Performance data for \\%s\\%s", object.Object, counter), + nil, + labels, + ), + metricType, + value.FirstValue, + ) + } + } + } + + return nil +} + +func sanitizeMetricName(name string) string { + replacer := strings.NewReplacer( + ".", "", + "%", "", + "/", "_", + " ", "_", + "-", "_", + ) + + return strings.Trim(replacer.Replace(strings.ToLower(name)), "_") +} diff --git a/pkg/collector/perfdata/perfdata_collector_test.go b/pkg/collector/perfdata/perfdata_collector_test.go new file mode 100644 index 000000000..973518ddf --- /dev/null +++ b/pkg/collector/perfdata/perfdata_collector_test.go @@ -0,0 +1,85 @@ +//go:build windows + +package perfdata_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "regexp" + "testing" + + "github.com/go-kit/log" + "github.com/prometheus-community/windows_exporter/pkg/collector/perfdata" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type collectorAdapter struct { + perfdata.Collector +} + +// Describe implements the prometheus.Collector interface. +func (a collectorAdapter) Describe(_ chan<- *prometheus.Desc) {} + +// Collect implements the prometheus.Collector interface. +func (a collectorAdapter) Collect(ch chan<- prometheus.Metric) { + if err := a.Collector.Collect(nil, log.NewLogfmtLogger(os.Stdout), ch); err != nil { + panic(fmt.Sprintf("failed to update collector: %v", err)) + } +} + +func TestCollector(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + object string + instances []string + counters map[string]perfdata.Counter + expectedMetrics *regexp.Regexp + }{ + { + object: "Memory", + instances: nil, + counters: map[string]perfdata.Counter{"Available Bytes": {Type: "gauge"}}, + expectedMetrics: regexp.MustCompile(`^# HELP windows_perfdata_memory_available_bytes Performance data for \\\\Memory\\\\Available Bytes\s*# TYPE windows_perfdata_memory_available_bytes gauge\s*windows_perfdata_memory_available_bytes \d`), + }, + { + object: "Process", + instances: []string{"*"}, + counters: map[string]perfdata.Counter{"Thread Count": {Type: "counter"}}, + expectedMetrics: regexp.MustCompile(`^# HELP windows_perfdata_process_thread_count Performance data for \\\\Process\\\\Thread Count\s*# TYPE windows_perfdata_process_thread_count counter\s*windows_perfdata_process_thread_count\{instance=".+"} \d`), + }, + } { + t.Run(tc.object, func(t *testing.T) { + t.Parallel() + + perfDataCollector := perfdata.New(&perfdata.Config{ + Objects: []perfdata.Object{ + { + Object: tc.object, + Instances: tc.instances, + Counters: tc.counters, + }, + }, + }) + + logger := log.NewLogfmtLogger(os.Stdout) + err := perfDataCollector.Build(logger, nil) + require.NoError(t, err) + + registry := prometheus.NewRegistry() + registry.MustRegister(collectorAdapter{*perfDataCollector}) + + rw := httptest.NewRecorder() + promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{}) + got := rw.Body.String() + + assert.NotEmpty(t, got) + assert.Regexp(t, tc.expectedMetrics, got) + }) + } +} diff --git a/pkg/collector/perfdata/perfdata_test.go b/pkg/collector/perfdata/perfdata_test.go new file mode 100644 index 000000000..17222815f --- /dev/null +++ b/pkg/collector/perfdata/perfdata_test.go @@ -0,0 +1,18 @@ +//go:build windows + +package perfdata_test + +import ( + "testing" + + "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/pkg/collector/perfdata" + "github.com/prometheus-community/windows_exporter/pkg/testutils" +) + +func BenchmarkCollector(b *testing.B) { + perfDataObjects := `[{"object":"Processor Information","instances":["*"],"counters":{"*": {}}}]` + kingpin.CommandLine.GetArg("collector.perfdata.objects").StringVar(&perfDataObjects) + + testutils.FuncBenchmarkCollector(b, perfdata.Name, perfdata.NewWithFlags) +} diff --git a/pkg/collector/perfdata/types.go b/pkg/collector/perfdata/types.go new file mode 100644 index 000000000..6b77d3dcf --- /dev/null +++ b/pkg/collector/perfdata/types.go @@ -0,0 +1,18 @@ +package perfdata + +import ( + "github.com/prometheus-community/windows_exporter/pkg/perfdata" +) + +type Object struct { + Object string `json:"object" yaml:"object"` + Instances []string `json:"instances" yaml:"instances"` + Counters map[string]Counter `json:"counters" yaml:"counters"` + InstanceLabel string `json:"instance_label" yaml:"instance_label"` //nolint:tagliatelle + + collector *perfdata.Collector +} + +type Counter struct { + Type string `json:"type" yaml:"type"` +} diff --git a/pkg/collector/process/process.go b/pkg/collector/process/process.go index 0fae09dd4..8b098fed3 100644 --- a/pkg/collector/process/process.go +++ b/pkg/collector/process/process.go @@ -8,7 +8,6 @@ import ( "regexp" "strconv" "strings" - "syscall" "unsafe" "github.com/alecthomas/kingpin/v2" @@ -620,7 +619,7 @@ func (c *Collector) openProcess(pid uint32) (windows.Handle, bool, error) { return 0, false, fmt.Errorf("failed to open process: %w", err) } - if errors.Is(err, syscall.Errno(0x57)) { // invalid parameter, for PIDs that don't exist + if errors.Is(err, windows.Errno(0x57)) { // invalid parameter, for PIDs that don't exist return 0, false, errors.New("process not found") } diff --git a/pkg/collector/tcp/const.go b/pkg/collector/tcp/const.go new file mode 100644 index 000000000..7de6573bd --- /dev/null +++ b/pkg/collector/tcp/const.go @@ -0,0 +1,16 @@ +package tcp + +// Win32_PerfRawData_Tcpip_TCPv4 docs +// - https://msdn.microsoft.com/en-us/library/aa394341(v=vs.85).aspx +// The TCPv6 performance object uses the same fields. +const ( + ConnectionFailures = "Connection Failures" + ConnectionsActive = "Connections Active" + ConnectionsEstablished = "Connections Established" + ConnectionsPassive = "Connections Passive" + ConnectionsReset = "Connections Reset" + SegmentsPersec = "Segments/sec" + SegmentsReceivedPersec = "Segments Received/sec" + SegmentsRetransmittedPersec = "Segments Retransmitted/sec" + SegmentsSentPersec = "Segments Sent/sec" +) diff --git a/pkg/collector/tcp/tcp.go b/pkg/collector/tcp/tcp.go index c52b6b01b..dd1e2c0fe 100644 --- a/pkg/collector/tcp/tcp.go +++ b/pkg/collector/tcp/tcp.go @@ -3,10 +3,12 @@ package tcp import ( + "fmt" + "github.com/alecthomas/kingpin/v2" "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/prometheus-community/windows_exporter/pkg/perflib" + "github.com/prometheus-community/windows_exporter/pkg/perfdata" "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" @@ -22,6 +24,9 @@ var ConfigDefaults = Config{} type Collector struct { config Config + perfDataCollector4 *perfdata.Collector + perfDataCollector6 *perfdata.Collector + connectionFailures *prometheus.Desc connectionsActive *prometheus.Desc connectionsEstablished *prometheus.Desc @@ -54,7 +59,7 @@ func (c *Collector) GetName() string { } func (c *Collector) GetPerfCounter(_ log.Logger) ([]string, error) { - return []string{"TCPv4"}, nil + return []string{}, nil } func (c *Collector) Close(_ log.Logger) error { @@ -62,6 +67,30 @@ func (c *Collector) Close(_ log.Logger) error { } func (c *Collector) Build(_ log.Logger, _ *wmi.Client) error { + counters := []string{ + ConnectionFailures, + ConnectionsActive, + ConnectionsEstablished, + ConnectionsPassive, + ConnectionsReset, + SegmentsPersec, + SegmentsReceivedPersec, + SegmentsRetransmittedPersec, + SegmentsSentPersec, + } + + var err error + + c.perfDataCollector4, err = perfdata.NewCollector("TCPv4", nil, counters) + if err != nil { + return fmt.Errorf("failed to create TCPv4 collector: %w", err) + } + + c.perfDataCollector6, err = perfdata.NewCollector("TCPv6", nil, counters) + if err != nil { + return fmt.Errorf("failed to create TCPv6 collector: %w", err) + } + c.connectionFailures = prometheus.NewDesc( prometheus.BuildFQName(types.Namespace, Name, "connection_failures_total"), "(TCP.ConnectionFailures)", @@ -121,106 +150,86 @@ func (c *Collector) Build(_ log.Logger, _ *wmi.Client) error { // Collect sends the metric values for each metric // to the provided prometheus Metric channel. -func (c *Collector) Collect(ctx *types.ScrapeContext, logger log.Logger, ch chan<- prometheus.Metric) error { +func (c *Collector) Collect(_ *types.ScrapeContext, logger log.Logger, ch chan<- prometheus.Metric) error { logger = log.With(logger, "collector", Name) - if err := c.collect(ctx, logger, ch); err != nil { + if err := c.collect(ch); err != nil { _ = level.Error(logger).Log("msg", "failed collecting tcp metrics", "err", err) return err } return nil } -// Win32_PerfRawData_Tcpip_TCPv4 docs -// - https://msdn.microsoft.com/en-us/library/aa394341(v=vs.85).aspx -// The TCPv6 performance object uses the same fields. -type tcp struct { - ConnectionFailures float64 `perflib:"Connection Failures"` - ConnectionsActive float64 `perflib:"Connections Active"` - ConnectionsEstablished float64 `perflib:"Connections Established"` - ConnectionsPassive float64 `perflib:"Connections Passive"` - ConnectionsReset float64 `perflib:"Connections Reset"` - SegmentsPersec float64 `perflib:"Segments/sec"` - SegmentsReceivedPersec float64 `perflib:"Segments Received/sec"` - SegmentsRetransmittedPersec float64 `perflib:"Segments Retransmitted/sec"` - SegmentsSentPersec float64 `perflib:"Segments Sent/sec"` -} - -func writeTCPCounters(metrics tcp, labels []string, c *Collector, ch chan<- prometheus.Metric) { +func writeTCPCounters(metrics map[string]perfdata.CounterValues, labels []string, c *Collector, ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( c.connectionFailures, prometheus.CounterValue, - metrics.ConnectionFailures, + metrics[ConnectionFailures].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.connectionsActive, prometheus.CounterValue, - metrics.ConnectionsActive, + metrics[ConnectionsActive].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.connectionsEstablished, prometheus.GaugeValue, - metrics.ConnectionsEstablished, + metrics[ConnectionsEstablished].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.connectionsPassive, prometheus.CounterValue, - metrics.ConnectionsPassive, + metrics[ConnectionsPassive].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.connectionsReset, prometheus.CounterValue, - metrics.ConnectionsReset, + metrics[ConnectionsReset].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.segmentsTotal, prometheus.CounterValue, - metrics.SegmentsPersec, + metrics[SegmentsPersec].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.segmentsReceivedTotal, prometheus.CounterValue, - metrics.SegmentsReceivedPersec, + metrics[SegmentsReceivedPersec].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.segmentsRetransmittedTotal, prometheus.CounterValue, - metrics.SegmentsRetransmittedPersec, + metrics[SegmentsRetransmittedPersec].FirstValue, labels..., ) ch <- prometheus.MustNewConstMetric( c.segmentsSentTotal, prometheus.CounterValue, - metrics.SegmentsSentPersec, + metrics[SegmentsSentPersec].FirstValue, labels..., ) } -func (c *Collector) collect(ctx *types.ScrapeContext, logger log.Logger, ch chan<- prometheus.Metric) error { - logger = log.With(logger, "collector", Name) - var dst []tcp - - // TCPv4 counters - if err := perflib.UnmarshalObject(ctx.PerfObjects["TCPv4"], &dst, logger); err != nil { - return err - } - if len(dst) != 0 { - writeTCPCounters(dst[0], []string{"ipv4"}, c, ch) +func (c *Collector) collect(ch chan<- prometheus.Metric) error { + data, err := c.perfDataCollector4.Collect() + if err != nil { + return fmt.Errorf("failed to collect TCPv4 metrics: %w", err) } - // TCPv6 counters - if err := perflib.UnmarshalObject(ctx.PerfObjects["TCPv6"], &dst, logger); err != nil { - return err - } - if len(dst) != 0 { - writeTCPCounters(dst[0], []string{"ipv6"}, c, ch) + writeTCPCounters(data[perfdata.EmptyInstance], []string{"ipv4"}, c, ch) + + data, err = c.perfDataCollector6.Collect() + if err != nil { + return fmt.Errorf("failed to collect TCPv6 metrics: %w", err) } + writeTCPCounters(data[perfdata.EmptyInstance], []string{"ipv6"}, c, ch) + return nil } diff --git a/pkg/collector/terminal_services/terminal_services.go b/pkg/collector/terminal_services/terminal_services.go index 47e7a9483..ebdedc09b 100644 --- a/pkg/collector/terminal_services/terminal_services.go +++ b/pkg/collector/terminal_services/terminal_services.go @@ -7,7 +7,6 @@ import ( "fmt" "strconv" "strings" - "syscall" "github.com/alecthomas/kingpin/v2" "github.com/go-kit/log" @@ -17,6 +16,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/types" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" + "golang.org/x/sys/windows" ) const ( @@ -55,7 +55,7 @@ type Collector struct { connectionBrokerEnabled bool - hServer syscall.Handle + hServer windows.Handle sessionInfo *prometheus.Desc connectionBrokerPerformance *prometheus.Desc diff --git a/pkg/collector/time/time.go b/pkg/collector/time/time.go index 726e1597b..058a81a7d 100644 --- a/pkg/collector/time/time.go +++ b/pkg/collector/time/time.go @@ -4,7 +4,6 @@ package time import ( "errors" - "syscall" "time" "github.com/alecthomas/kingpin/v2" @@ -16,6 +15,7 @@ import ( "github.com/prometheus-community/windows_exporter/pkg/winversion" "github.com/prometheus/client_golang/prometheus" "github.com/yusufpapurcu/wmi" + "golang.org/x/sys/windows" ) const Name = "time" @@ -165,7 +165,7 @@ func (c *Collector) collectTime(ch chan<- prometheus.Metric) error { } // timeZoneKeyName contains the english name of the timezone. - timezoneName := syscall.UTF16ToString(timeZoneInfo.TimeZoneKeyName[:]) + timezoneName := windows.UTF16ToString(timeZoneInfo.TimeZoneKeyName[:]) ch <- prometheus.MustNewConstMetric( c.timezone, diff --git a/pkg/headers/kernel32/kernel32.go b/pkg/headers/kernel32/kernel32.go index f0b37f914..17f2b6278 100644 --- a/pkg/headers/kernel32/kernel32.go +++ b/pkg/headers/kernel32/kernel32.go @@ -1,14 +1,16 @@ package kernel32 import ( - "syscall" "unsafe" + + "golang.org/x/sys/windows" ) var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") + kernel32 = windows.NewLazySystemDLL("kernel32.dll") procGetDynamicTimeZoneInformationSys = kernel32.NewProc("GetDynamicTimeZoneInformation") + kernelLocalFileTimeToFileTime = kernel32.NewProc("LocalFileTimeToFileTime") ) // SYSTEMTIME contains a date and time. @@ -50,3 +52,11 @@ func GetDynamicTimeZoneInformation() (DynamicTimezoneInformation, error) { return tzi, nil } + +func LocalFileTimeToFileTime(localFileTime, utcFileTime *windows.Filetime) uint32 { + ret, _, _ := kernelLocalFileTimeToFileTime.Call( + uintptr(unsafe.Pointer(localFileTime)), + uintptr(unsafe.Pointer(utcFileTime))) + + return uint32(ret) +} diff --git a/pkg/headers/wtsapi32/wtsapi32.go b/pkg/headers/wtsapi32/wtsapi32.go index 87bde129d..437391d1d 100644 --- a/pkg/headers/wtsapi32/wtsapi32.go +++ b/pkg/headers/wtsapi32/wtsapi32.go @@ -2,7 +2,6 @@ package wtsapi32 import ( "fmt" - "syscall" "unsafe" "github.com/go-kit/log" @@ -105,30 +104,30 @@ var ( } ) -func WTSOpenServer(server string) (syscall.Handle, error) { +func WTSOpenServer(server string) (windows.Handle, error) { var ( err error serverName *uint16 ) if server != "" { - serverName, err = syscall.UTF16PtrFromString(server) + serverName, err = windows.UTF16PtrFromString(server) if err != nil { - return syscall.InvalidHandle, err + return windows.InvalidHandle, err } } r1, _, err := procWTSOpenServerEx.Call(uintptr(unsafe.Pointer(serverName))) - serverHandle := syscall.Handle(r1) + serverHandle := windows.Handle(r1) - if serverHandle == syscall.InvalidHandle { - return syscall.InvalidHandle, err + if serverHandle == windows.InvalidHandle { + return windows.InvalidHandle, err } return serverHandle, nil } -func WTSCloseServer(server syscall.Handle) error { +func WTSCloseServer(server windows.Handle) error { r1, _, err := procWTSCloseServer.Call(uintptr(server)) if r1 != 1 { @@ -152,7 +151,7 @@ func WTSFreeMemoryEx(class WTSTypeClass, pMemory uintptr, numberOfEntries uint32 return nil } -func WTSEnumerateSessionsEx(server syscall.Handle, logger log.Logger) ([]WTSSession, error) { +func WTSEnumerateSessionsEx(server windows.Handle, logger log.Logger) ([]WTSSession, error) { var sessionInfoPointer uintptr var count uint32 diff --git a/pkg/log/eventlog/eventlog.go b/pkg/log/eventlog/eventlog.go index 5df5a4fc8..8b7fd2138 100644 --- a/pkg/log/eventlog/eventlog.go +++ b/pkg/log/eventlog/eventlog.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "sync" - "syscall" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -68,7 +67,7 @@ func (l *eventlogLogger) Log(keyvals ...interface{}) error { // golang.org/x/sys/windows/svc/eventlog does not provide func which allows to send more than one string. // See: https://github.com/golang/go/issues/59780 - msg, err := syscall.UTF16PtrFromString(lb.buf.String()) + msg, err := windows.UTF16PtrFromString(lb.buf.String()) if err != nil { return fmt.Errorf("error convert string to UTF-16: %w", err) } diff --git a/pkg/perfdata/collector.go b/pkg/perfdata/collector.go new file mode 100644 index 000000000..efd3e5aa3 --- /dev/null +++ b/pkg/perfdata/collector.go @@ -0,0 +1,234 @@ +//go:build windows + +package perfdata + +import ( + "errors" + "fmt" + "strings" + "time" + "unsafe" + + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/sys/windows" +) + +const EmptyInstance = "------" + +type Collector struct { + time time.Time + object string + counters []Counter + handle pdhQueryHandle +} + +type Counter struct { + Name string + Instances map[string]pdhCounterHandle + Type uint32 + Frequency float64 +} + +type CounterValues struct { + Type prometheus.ValueType + FirstValue float64 + SecondValue float64 +} + +func NewCollector(object string, instances []string, counters []string) (*Collector, error) { + var handle pdhQueryHandle + + if ret := PdhOpenQuery(0, 0, &handle); ret != ErrorSuccess { + return nil, NewPdhError(ret) + } + + if len(instances) == 0 { + instances = []string{EmptyInstance} + } + + collector := &Collector{ + object: object, + counters: make([]Counter, 0, len(counters)), + handle: handle, + } + + for _, counterName := range counters { + if counterName == "*" { + return nil, errors.New("wildcard counters are not supported") + } + + counter := Counter{ + Name: counterName, + Instances: make(map[string]pdhCounterHandle, len(instances)), + } + + var counterPath string + + for _, instance := range instances { + counterPath = formatCounterPath(object, instance, counterName) + + var counterHandle pdhCounterHandle + + if ret := PdhAddEnglishCounter(handle, counterPath, 0, &counterHandle); ret != ErrorSuccess { + return nil, fmt.Errorf("failed to add counter %s: %w", counterPath, NewPdhError(ret)) + } + + counter.Instances[instance] = counterHandle + + if counter.Type == 0 { + // Get the info with the current buffer size + bufLen := uint32(0) + + if ret := PdhGetCounterInfo(counterHandle, 0, &bufLen, nil); ret != PdhMoreData { + return nil, fmt.Errorf("PdhGetCounterInfo: %w", NewPdhError(ret)) + } + + buf := make([]byte, bufLen) + if ret := PdhGetCounterInfo(counterHandle, 0, &bufLen, &buf[0]); ret != ErrorSuccess { + return nil, fmt.Errorf("PdhGetCounterInfo: %w", NewPdhError(ret)) + } + + ci := (*PdhCounterInfo)(unsafe.Pointer(&buf[0])) + counter.Type = ci.DwType + + frequency := float64(0) + + if ret := PdhGetCounterTimeBase(counterHandle, &frequency); ret != ErrorSuccess { + return nil, fmt.Errorf("PdhGetCounterTimeBase: %w", NewPdhError(ret)) + } + counter.Frequency = frequency + } + } + + collector.counters = append(collector.counters, counter) + } + + if len(collector.counters) == 0 { + return nil, errors.New("no counters configured") + } + + if _, err := collector.Collect(); err != nil { + return nil, fmt.Errorf("failed to collect initial data: %w", err) + } + + return collector, nil +} + +func (c *Collector) Collect() (map[string]map[string]CounterValues, error) { + if len(c.counters) == 0 { + return map[string]map[string]CounterValues{}, nil + } + + if ret := PdhCollectQueryData(c.handle); ret != ErrorSuccess { + return nil, fmt.Errorf("failed to collect query data: %w", NewPdhError(ret)) + } + + c.time = time.Now() + + var data map[string]map[string]CounterValues + + for _, counter := range c.counters { + for _, instance := range counter.Instances { + // Get the info with the current buffer size + var itemCount uint32 + + // Get the info with the current buffer size + bufLen := uint32(0) + ret := PdhGetRawCounterArray(instance, &bufLen, &itemCount, nil) + if ret != PdhMoreData { + return nil, fmt.Errorf("PdhGetRawCounterArray: %w", NewPdhError(ret)) + } + + buf := make([]byte, bufLen) + + ret = PdhGetRawCounterArray(instance, &bufLen, &itemCount, &buf[0]) + if ret != ErrorSuccess { + if err := NewPdhError(ret); !isKnownCounterDataError(err) { + return nil, fmt.Errorf("PdhGetRawCounterArray: %w", err) + } + + continue + } + + items := (*[1 << 20]PdhRawCounterItem)(unsafe.Pointer(&buf[0]))[:itemCount] + + if data == nil { + data = make(map[string]map[string]CounterValues, itemCount) + } + + var metricType prometheus.ValueType + if val, ok := supportedCounterTypes[counter.Type]; ok { + metricType = val + } else { + metricType = prometheus.GaugeValue + } + + for _, item := range items { + if item.RawValue.CStatus == PdhCstatusValidData || item.RawValue.CStatus == PdhCstatusNewData { + instanceName := windows.UTF16PtrToString(item.SzName) + if strings.HasSuffix(instanceName, "_Total") { + continue + } + + if instanceName == "" { + instanceName = EmptyInstance + } + + if _, ok := data[instanceName]; !ok { + data[instanceName] = make(map[string]CounterValues, len(c.counters)) + } + + values := CounterValues{ + Type: metricType, + } + + // This is a workaround for the issue with the elapsed time counter type. + // Source: https://github.com/prometheus-community/windows_exporter/pull/335/files#diff-d5d2528f559ba2648c2866aec34b1eaa5c094dedb52bd0ff22aa5eb83226bd8dR76-R83 + + switch counter.Type { + case PERF_ELAPSED_TIME: + values.FirstValue = float64(item.RawValue.FirstValue-WindowsEpoch) / counter.Frequency + values.SecondValue = float64(item.RawValue.SecondValue-WindowsEpoch) / counter.Frequency + case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER: + values.FirstValue = float64(item.RawValue.FirstValue) * TicksToSecondScaleFactor + values.SecondValue = float64(item.RawValue.SecondValue) * TicksToSecondScaleFactor + default: + values.FirstValue = float64(item.RawValue.FirstValue) + values.SecondValue = float64(item.RawValue.SecondValue) + } + + data[instanceName][counter.Name] = values + } + } + } + } + + return data, nil +} + +func (c *Collector) Close() { + PdhCloseQuery(c.handle) +} + +func formatCounterPath(object, instance, counterName string) string { + var counterPath string + + if instance == EmptyInstance { + counterPath = fmt.Sprintf(`\%s\%s`, object, counterName) + } else { + counterPath = fmt.Sprintf(`\%s(%s)\%s`, object, instance, counterName) + } + + return counterPath +} + +func isKnownCounterDataError(err error) bool { + var pdhErr *Error + + return errors.As(err, &pdhErr) && (pdhErr.ErrorCode == PdhInvalidData || + pdhErr.ErrorCode == PdhCalcNegativeDenominator || + pdhErr.ErrorCode == PdhCalcNegativeValue || + pdhErr.ErrorCode == PdhCstatusInvalidData || + pdhErr.ErrorCode == PdhCstatusNoInstance || + pdhErr.ErrorCode == PdhNoData) +} diff --git a/pkg/perfdata/collector_bench_test.go b/pkg/perfdata/collector_bench_test.go new file mode 100644 index 000000000..d0a4aeb22 --- /dev/null +++ b/pkg/perfdata/collector_bench_test.go @@ -0,0 +1,49 @@ +package perfdata_test + +import ( + "testing" + + "github.com/prometheus-community/windows_exporter/pkg/perfdata" + "github.com/stretchr/testify/require" +) + +func BenchmarkTestCollector(b *testing.B) { + counters := []string{ + "% Processor Time", + "% Privileged Time", + "% User Time", + "Creating Process ID", + "Elapsed Time", + "Handle Count", + "ID Process", + "IO Data Bytes/sec", + "IO Data Operations/sec", + "IO Other Bytes/sec", + "IO Other Operations/sec", + "IO Read Bytes/sec", + "IO Read Operations/sec", + "IO Write Bytes/sec", + "IO Write Operations/sec", + "Page Faults/sec", + "Page File Bytes Peak", + "Page File Bytes", + "Pool Nonpaged Bytes", + "Pool Paged Bytes", + "Priority Base", + "Private Bytes", + "Thread Count", + "Virtual Bytes Peak", + "Virtual Bytes", + "Working Set - Private", + "Working Set Peak", + "Working Set", + } + performanceData, err := perfdata.NewCollector("Process", []string{"*"}, counters) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, _ = performanceData.Collect() + } + + performanceData.Close() +} diff --git a/pkg/perfdata/collector_test.go b/pkg/perfdata/collector_test.go new file mode 100644 index 000000000..6accd501f --- /dev/null +++ b/pkg/perfdata/collector_test.go @@ -0,0 +1,86 @@ +//go:build windows + +package perfdata_test + +import ( + "testing" + "time" + + "github.com/prometheus-community/windows_exporter/pkg/perfdata" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCollector(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + object string + instances []string + counters []string + }{ + { + object: "Memory", + counters: []string{ + "Available Bytes", + "Available KBytes", + "Available MBytes", + "Cache Bytes", + "Cache Bytes Peak", + "Cache Faults/sec", + "Commit Limit", + "Committed Bytes", + "Demand Zero Faults/sec", + "Free & Zero Page List Bytes", + "Free System Page Table Entries", + "Modified Page List Bytes", + "Page Reads/sec", + }, + }, { + object: "TCPv4", + counters: []string{ + "Connection Failures", + "Connections Active", + "Connections Established", + "Connections Passive", + "Connections Reset", + "Segments/sec", + "Segments Received/sec", + "Segments Retransmitted/sec", + "Segments Sent/sec", + }, + }, { + object: "Process", + instances: []string{"*"}, + counters: []string{ + "Thread Count", + "ID Process", + }, + }, + } { + t.Run(tc.object, func(t *testing.T) { + t.Parallel() + + performanceData, err := perfdata.NewCollector(tc.object, tc.instances, tc.counters) + require.NoError(t, err) + + time.Sleep(100 * time.Millisecond) + + data, err := performanceData.Collect() + require.NoError(t, err) + require.NotEmpty(t, data) + + for instance, d := range data { + require.NotEmpty(t, d) + + if instance == "Idle" || instance == "Secure System" { + continue + } + + for _, c := range tc.counters { + assert.NotZerof(t, d[c].FirstValue, "object: %s, instance: %s, counter: %s", tc.object, instance, c) + } + } + }) + } +} diff --git a/pkg/perfdata/const.go b/pkg/perfdata/const.go new file mode 100644 index 000000000..c618fd559 --- /dev/null +++ b/pkg/perfdata/const.go @@ -0,0 +1,78 @@ +//go:build windows + +package perfdata + +import "github.com/prometheus/client_golang/prometheus" + +// Conversion factors. +const ( + TicksToSecondScaleFactor = 1 / 1e7 + WindowsEpoch = 116444736000000000 +) + +// Based on https://github.com/leoluk/perflib_exporter/blob/master/collector/mapper.go +// +//goland:noinspection GoUnusedConst +const ( + PERF_COUNTER_RAWCOUNT_HEX = 0x00000000 + PERF_COUNTER_LARGE_RAWCOUNT_HEX = 0x00000100 + PERF_COUNTER_TEXT = 0x00000b00 + PERF_COUNTER_RAWCOUNT = 0x00010000 + PERF_COUNTER_LARGE_RAWCOUNT = 0x00010100 + PERF_DOUBLE_RAW = 0x00012000 + PERF_COUNTER_DELTA = 0x00400400 + PERF_COUNTER_LARGE_DELTA = 0x00400500 + PERF_SAMPLE_COUNTER = 0x00410400 + PERF_COUNTER_QUEUELEN_TYPE = 0x00450400 + PERF_COUNTER_LARGE_QUEUELEN_TYPE = 0x00450500 + PERF_COUNTER_100NS_QUEUELEN_TYPE = 0x00550500 + PERF_COUNTER_OBJ_TIME_QUEUELEN_TYPE = 0x00650500 + PERF_COUNTER_COUNTER = 0x10410400 + PERF_COUNTER_BULK_COUNT = 0x10410500 + PERF_RAW_FRACTION = 0x20020400 + PERF_LARGE_RAW_FRACTION = 0x20020500 + PERF_COUNTER_TIMER = 0x20410500 + PERF_PRECISION_SYSTEM_TIMER = 0x20470500 + PERF_100NSEC_TIMER = 0x20510500 + PERF_PRECISION_100NS_TIMER = 0x20570500 + PERF_OBJ_TIME_TIMER = 0x20610500 + PERF_PRECISION_OBJECT_TIMER = 0x20670500 + PERF_SAMPLE_FRACTION = 0x20c20400 + PERF_COUNTER_TIMER_INV = 0x21410500 + PERF_100NSEC_TIMER_INV = 0x21510500 + PERF_COUNTER_MULTI_TIMER = 0x22410500 + PERF_100NSEC_MULTI_TIMER = 0x22510500 + PERF_COUNTER_MULTI_TIMER_INV = 0x23410500 + PERF_100NSEC_MULTI_TIMER_INV = 0x23510500 + PERF_AVERAGE_TIMER = 0x30020400 + PERF_ELAPSED_TIME = 0x30240500 + PERF_COUNTER_NODATA = 0x40000200 + PERF_AVERAGE_BULK = 0x40020500 + PERF_SAMPLE_BASE = 0x40030401 + PERF_AVERAGE_BASE = 0x40030402 + PERF_RAW_BASE = 0x40030403 + PERF_PRECISION_TIMESTAMP = 0x40030500 + PERF_LARGE_RAW_BASE = 0x40030503 + PERF_COUNTER_MULTI_BASE = 0x42030500 + PERF_COUNTER_HISTOGRAM_TYPE = 0x80000000 +) + +var supportedCounterTypes = map[uint32]prometheus.ValueType{ + PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue, + PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue, + PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue, + PERF_COUNTER_LARGE_RAWCOUNT: prometheus.GaugeValue, + PERF_COUNTER_DELTA: prometheus.CounterValue, + PERF_COUNTER_COUNTER: prometheus.CounterValue, + PERF_COUNTER_BULK_COUNT: prometheus.CounterValue, + PERF_RAW_FRACTION: prometheus.GaugeValue, + PERF_LARGE_RAW_FRACTION: prometheus.GaugeValue, + PERF_100NSEC_TIMER: prometheus.CounterValue, + PERF_PRECISION_100NS_TIMER: prometheus.CounterValue, + PERF_SAMPLE_FRACTION: prometheus.GaugeValue, + PERF_100NSEC_TIMER_INV: prometheus.CounterValue, + PERF_ELAPSED_TIME: prometheus.GaugeValue, + PERF_SAMPLE_BASE: prometheus.GaugeValue, + PERF_RAW_BASE: prometheus.GaugeValue, + PERF_LARGE_RAW_BASE: prometheus.GaugeValue, +} diff --git a/pkg/perfdata/error.go b/pkg/perfdata/error.go new file mode 100644 index 000000000..46fa4fb90 --- /dev/null +++ b/pkg/perfdata/error.go @@ -0,0 +1,18 @@ +package perfdata + +// Error represents error returned from Performance Counters API. +type Error struct { + ErrorCode uint32 + errorText string +} + +func (m *Error) Error() string { + return m.errorText +} + +func NewPdhError(code uint32) error { + return &Error{ + ErrorCode: code, + errorText: PdhFormatError(code), + } +} diff --git a/pkg/perfdata/pdh.go b/pkg/perfdata/pdh.go new file mode 100644 index 000000000..c00d94ff3 --- /dev/null +++ b/pkg/perfdata/pdh.go @@ -0,0 +1,634 @@ +// Copyright (c) 2010-2024 The win Authors. All rights reserved. +// Copyright (c) 2024 The prometheus-community Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The names of the authors may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This is the official list of 'win' authors for copyright purposes. +// +// Alexander Neumann +// Joseph Watson +// Kevin Pors + +//go:build windows + +package perfdata + +import ( + "fmt" + "time" + "unsafe" + + "github.com/prometheus-community/windows_exporter/pkg/headers/kernel32" + "golang.org/x/sys/windows" +) + +// Error codes. +const ( + ErrorSuccess = 0 + ErrorFailure = 1 + ErrorInvalidFunction = 1 + EpochDifferenceMicros int64 = 11644473600000000 +) + +type ( + HANDLE uintptr +) + +// PDH error codes, which can be returned by all Pdh* functions. Taken from mingw-w64 pdhmsg.h + +const ( + PdhCstatusValidData = 0x00000000 // The returned data is valid. + PdhCstatusNewData = 0x00000001 // The return data value is valid and different from the last sample. + PdhCstatusNoMachine = 0x800007D0 // Unable to connect to the specified computer, or the computer is offline. + PdhCstatusNoInstance = 0x800007D1 + PdhMoreData = 0x800007D2 // The PdhGetFormattedCounterArray* function can return this if there's 'more data to be displayed'. + PdhCstatusItemNotValidated = 0x800007D3 + PdhRetry = 0x800007D4 + PdhNoData = 0x800007D5 // The query does not currently contain any counters (for example, limited access) + PdhCalcNegativeDenominator = 0x800007D6 + PdhCalcNegativeTimebase = 0x800007D7 + PdhCalcNegativeValue = 0x800007D8 + PdhDialogCancelled = 0x800007D9 + PdhEndOfLogFile = 0x800007DA + PdhAsyncQueryTimeout = 0x800007DB + PdhCannotSetDefaultRealtimeDatasource = 0x800007DC + PdhCstatusNoObject = 0xC0000BB8 + PdhCstatusNoCounter = 0xC0000BB9 // The specified counter could not be found. + PdhCstatusInvalidData = 0xC0000BBA // The counter was successfully found, but the data returned is not valid. + PdhMemoryAllocationFailure = 0xC0000BBB + PdhInvalidHandle = 0xC0000BBC + PdhInvalidArgument = 0xC0000BBD // Required argument is missing or incorrect. + PdhFunctionNotFound = 0xC0000BBE + PdhCstatusNoCountername = 0xC0000BBF + PdhCstatusBadCountername = 0xC0000BC0 // Unable to parse the counter path. Check the format and syntax of the specified path. + PdhInvalidBuffer = 0xC0000BC1 + PdhInsufficientBuffer = 0xC0000BC2 + PdhCannotConnectMachine = 0xC0000BC3 + PdhInvalidPath = 0xC0000BC4 + PdhInvalidInstance = 0xC0000BC5 + PdhInvalidData = 0xC0000BC6 // specified counter does not contain valid data or a successful status code. + PdhNoDialogData = 0xC0000BC7 + PdhCannotReadNameStrings = 0xC0000BC8 + PdhLogFileCreateError = 0xC0000BC9 + PdhLogFileOpenError = 0xC0000BCA + PdhLogTypeNotFound = 0xC0000BCB + PdhNoMoreData = 0xC0000BCC + PdhEntryNotInLogFile = 0xC0000BCD + PdhDataSourceIsLogFile = 0xC0000BCE + PdhDataSourceIsRealTime = 0xC0000BCF + PdhUnableReadLogHeader = 0xC0000BD0 + PdhFileNotFound = 0xC0000BD1 + PdhFileAlreadyExists = 0xC0000BD2 + PdhNotImplemented = 0xC0000BD3 + PdhStringNotFound = 0xC0000BD4 + PdhUnableMapNameFiles = 0x80000BD5 + PdhUnknownLogFormat = 0xC0000BD6 + PdhUnknownLogsvcCommand = 0xC0000BD7 + PdhLogsvcQueryNotFound = 0xC0000BD8 + PdhLogsvcNotOpened = 0xC0000BD9 + PdhWbemError = 0xC0000BDA + PdhAccessDenied = 0xC0000BDB + PdhLogFileTooSmall = 0xC0000BDC + PdhInvalidDatasource = 0xC0000BDD + PdhInvalidSqldb = 0xC0000BDE + PdhNoCounters = 0xC0000BDF + PdhSQLAllocFailed = 0xC0000BE0 + PdhSQLAllocconFailed = 0xC0000BE1 + PdhSQLExecDirectFailed = 0xC0000BE2 + PdhSQLFetchFailed = 0xC0000BE3 + PdhSQLRowcountFailed = 0xC0000BE4 + PdhSQLMoreResultsFailed = 0xC0000BE5 + PdhSQLConnectFailed = 0xC0000BE6 + PdhSQLBindFailed = 0xC0000BE7 + PdhCannotConnectWmiServer = 0xC0000BE8 + PdhPlaCollectionAlreadyRunning = 0xC0000BE9 + PdhPlaErrorScheduleOverlap = 0xC0000BEA + PdhPlaCollectionNotFound = 0xC0000BEB + PdhPlaErrorScheduleElapsed = 0xC0000BEC + PdhPlaErrorNostart = 0xC0000BED + PdhPlaErrorAlreadyExists = 0xC0000BEE + PdhPlaErrorTypeMismatch = 0xC0000BEF + PdhPlaErrorFilepath = 0xC0000BF0 + PdhPlaServiceError = 0xC0000BF1 + PdhPlaValidationError = 0xC0000BF2 + PdhPlaValidationWarning = 0x80000BF3 + PdhPlaErrorNameTooLong = 0xC0000BF4 + PdhInvalidSQLLogFormat = 0xC0000BF5 + PdhCounterAlreadyInQuery = 0xC0000BF6 + PdhBinaryLogCorrupt = 0xC0000BF7 + PdhLogSampleTooSmall = 0xC0000BF8 + PdhOsLaterVersion = 0xC0000BF9 + PdhOsEarlierVersion = 0xC0000BFA + PdhIncorrectAppendTime = 0xC0000BFB + PdhUnmatchedAppendCounter = 0xC0000BFC + PdhSQLAlterDetailFailed = 0xC0000BFD + PdhQueryPerfDataTimeout = 0xC0000BFE +) + +var PDHErrors = map[uint32]string{ + PdhCstatusValidData: "PDH_CSTATUS_VALID_DATA", + PdhCstatusNewData: "PDH_CSTATUS_NEW_DATA", + PdhCstatusNoMachine: "PDH_CSTATUS_NO_MACHINE", + PdhCstatusNoInstance: "PDH_CSTATUS_NO_INSTANCE", + PdhMoreData: "PDH_MORE_DATA", + PdhCstatusItemNotValidated: "PDH_CSTATUS_ITEM_NOT_VALIDATED", + PdhRetry: "PDH_RETRY", + PdhNoData: "PDH_NO_DATA", + PdhCalcNegativeDenominator: "PDH_CALC_NEGATIVE_DENOMINATOR", + PdhCalcNegativeTimebase: "PDH_CALC_NEGATIVE_TIMEBASE", + PdhCalcNegativeValue: "PDH_CALC_NEGATIVE_VALUE", + PdhDialogCancelled: "PDH_DIALOG_CANCELLED", + PdhEndOfLogFile: "PDH_END_OF_LOG_FILE", + PdhAsyncQueryTimeout: "PDH_ASYNC_QUERY_TIMEOUT", + PdhCannotSetDefaultRealtimeDatasource: "PDH_CANNOT_SET_DEFAULT_REALTIME_DATASOURCE", + PdhCstatusNoObject: "PDH_CSTATUS_NO_OBJECT", + PdhCstatusNoCounter: "PDH_CSTATUS_NO_COUNTER", + PdhCstatusInvalidData: "PDH_CSTATUS_INVALID_DATA", + PdhMemoryAllocationFailure: "PDH_MEMORY_ALLOCATION_FAILURE", + PdhInvalidHandle: "PDH_INVALID_HANDLE", + PdhInvalidArgument: "PDH_INVALID_ARGUMENT", + PdhFunctionNotFound: "PDH_FUNCTION_NOT_FOUND", + PdhCstatusNoCountername: "PDH_CSTATUS_NO_COUNTERNAME", + PdhCstatusBadCountername: "PDH_CSTATUS_BAD_COUNTERNAME", + PdhInvalidBuffer: "PDH_INVALID_BUFFER", + PdhInsufficientBuffer: "PDH_INSUFFICIENT_BUFFER", + PdhCannotConnectMachine: "PDH_CANNOT_CONNECT_MACHINE", + PdhInvalidPath: "PDH_INVALID_PATH", + PdhInvalidInstance: "PDH_INVALID_INSTANCE", + PdhInvalidData: "PDH_INVALID_DATA", + PdhNoDialogData: "PDH_NO_DIALOG_DATA", + PdhCannotReadNameStrings: "PDH_CANNOT_READ_NAME_STRINGS", + PdhLogFileCreateError: "PDH_LOG_FILE_CREATE_ERROR", + PdhLogFileOpenError: "PDH_LOG_FILE_OPEN_ERROR", + PdhLogTypeNotFound: "PDH_LOG_TYPE_NOT_FOUND", + PdhNoMoreData: "PDH_NO_MORE_DATA", + PdhEntryNotInLogFile: "PDH_ENTRY_NOT_IN_LOG_FILE", + PdhDataSourceIsLogFile: "PDH_DATA_SOURCE_IS_LOG_FILE", + PdhDataSourceIsRealTime: "PDH_DATA_SOURCE_IS_REAL_TIME", + PdhUnableReadLogHeader: "PDH_UNABLE_READ_LOG_HEADER", + PdhFileNotFound: "PDH_FILE_NOT_FOUND", + PdhFileAlreadyExists: "PDH_FILE_ALREADY_EXISTS", + PdhNotImplemented: "PDH_NOT_IMPLEMENTED", + PdhStringNotFound: "PDH_STRING_NOT_FOUND", + PdhUnableMapNameFiles: "PDH_UNABLE_MAP_NAME_FILES", + PdhUnknownLogFormat: "PDH_UNKNOWN_LOG_FORMAT", + PdhUnknownLogsvcCommand: "PDH_UNKNOWN_LOGSVC_COMMAND", + PdhLogsvcQueryNotFound: "PDH_LOGSVC_QUERY_NOT_FOUND", + PdhLogsvcNotOpened: "PDH_LOGSVC_NOT_OPENED", + PdhWbemError: "PDH_WBEM_ERROR", + PdhAccessDenied: "PDH_ACCESS_DENIED", + PdhLogFileTooSmall: "PDH_LOG_FILE_TOO_SMALL", + PdhInvalidDatasource: "PDH_INVALID_DATASOURCE", + PdhInvalidSqldb: "PDH_INVALID_SQLDB", + PdhNoCounters: "PDH_NO_COUNTERS", + PdhSQLAllocFailed: "PDH_SQL_ALLOC_FAILED", + PdhSQLAllocconFailed: "PDH_SQL_ALLOCCON_FAILED", + PdhSQLExecDirectFailed: "PDH_SQL_EXEC_DIRECT_FAILED", + PdhSQLFetchFailed: "PDH_SQL_FETCH_FAILED", + PdhSQLRowcountFailed: "PDH_SQL_ROWCOUNT_FAILED", + PdhSQLMoreResultsFailed: "PDH_SQL_MORE_RESULTS_FAILED", + PdhSQLConnectFailed: "PDH_SQL_CONNECT_FAILED", + PdhSQLBindFailed: "PDH_SQL_BIND_FAILED", + PdhCannotConnectWmiServer: "PDH_CANNOT_CONNECT_WMI_SERVER", + PdhPlaCollectionAlreadyRunning: "PDH_PLA_COLLECTION_ALREADY_RUNNING", + PdhPlaErrorScheduleOverlap: "PDH_PLA_ERROR_SCHEDULE_OVERLAP", + PdhPlaCollectionNotFound: "PDH_PLA_COLLECTION_NOT_FOUND", + PdhPlaErrorScheduleElapsed: "PDH_PLA_ERROR_SCHEDULE_ELAPSED", + PdhPlaErrorNostart: "PDH_PLA_ERROR_NOSTART", + PdhPlaErrorAlreadyExists: "PDH_PLA_ERROR_ALREADY_EXISTS", + PdhPlaErrorTypeMismatch: "PDH_PLA_ERROR_TYPE_MISMATCH", + PdhPlaErrorFilepath: "PDH_PLA_ERROR_FILEPATH", + PdhPlaServiceError: "PDH_PLA_SERVICE_ERROR", + PdhPlaValidationError: "PDH_PLA_VALIDATION_ERROR", + PdhPlaValidationWarning: "PDH_PLA_VALIDATION_WARNING", + PdhPlaErrorNameTooLong: "PDH_PLA_ERROR_NAME_TOO_LONG", + PdhInvalidSQLLogFormat: "PDH_INVALID_SQL_LOG_FORMAT", + PdhCounterAlreadyInQuery: "PDH_COUNTER_ALREADY_IN_QUERY", + PdhBinaryLogCorrupt: "PDH_BINARY_LOG_CORRUPT", + PdhLogSampleTooSmall: "PDH_LOG_SAMPLE_TOO_SMALL", + PdhOsLaterVersion: "PDH_OS_LATER_VERSION", + PdhOsEarlierVersion: "PDH_OS_EARLIER_VERSION", + PdhIncorrectAppendTime: "PDH_INCORRECT_APPEND_TIME", + PdhUnmatchedAppendCounter: "PDH_UNMATCHED_APPEND_COUNTER", + PdhSQLAlterDetailFailed: "PDH_SQL_ALTER_DETAIL_FAILED", + PdhQueryPerfDataTimeout: "PDH_QUERY_PERF_DATA_TIMEOUT", +} + +// Formatting options for GetFormattedCounterValue(). +// +//goland:noinspection GoUnusedConst +const ( + PdhFmtRaw = 0x00000010 + PdhFmtAnsi = 0x00000020 + PdhFmtUnicode = 0x00000040 + PdhFmtLong = 0x00000100 // Return data as a long int. + PdhFmtDouble = 0x00000200 // Return data as a double precision floating point real. + PdhFmtLarge = 0x00000400 // Return data as a 64 bit integer. + PdhFmtNoscale = 0x00001000 // can be OR-ed: Do not apply the counter's default scaling factor. + PdhFmt1000 = 0x00002000 // can be OR-ed: multiply the actual value by 1,000. + PdhFmtNodata = 0x00004000 // can be OR-ed: unknown what this is for, MSDN says nothing. + PdhFmtNocap100 = 0x00008000 // can be OR-ed: do not cap values > 100. + PerfDetailCostly = 0x00010000 + PerfDetailStandard = 0x0000FFFF +) + +type ( + pdhQueryHandle HANDLE // query handle + pdhCounterHandle HANDLE // counter handle +) + +var ( + libPdhDll = windows.NewLazySystemDLL("pdh.dll") + + pdhAddCounterW = libPdhDll.NewProc("PdhAddCounterW") + pdhAddEnglishCounterW = libPdhDll.NewProc("PdhAddEnglishCounterW") + pdhCloseQuery = libPdhDll.NewProc("PdhCloseQuery") + pdhCollectQueryData = libPdhDll.NewProc("PdhCollectQueryData") + pdhCollectQueryDataWithTime = libPdhDll.NewProc("PdhCollectQueryDataWithTime") + pdhGetFormattedCounterValue = libPdhDll.NewProc("PdhGetFormattedCounterValue") + pdhGetFormattedCounterArrayW = libPdhDll.NewProc("PdhGetFormattedCounterArrayW") + pdhOpenQuery = libPdhDll.NewProc("PdhOpenQuery") + pdhValidatePathW = libPdhDll.NewProc("PdhValidatePathW") + pdhExpandWildCardPathW = libPdhDll.NewProc("PdhExpandWildCardPathW") + pdhGetCounterInfoW = libPdhDll.NewProc("PdhGetCounterInfoW") + pdhGetRawCounterValue = libPdhDll.NewProc("PdhGetRawCounterValue") + pdhGetRawCounterArrayW = libPdhDll.NewProc("PdhGetRawCounterArrayW") + pdhPdhGetCounterTimeBase = libPdhDll.NewProc("PdhGetCounterTimeBase") +) + +// PdhAddCounter adds the specified counter to the query. This is the internationalized version. Preferably, use the +// function PdhAddEnglishCounter instead. hQuery is the query handle, which has been fetched by PdhOpenQuery. +// szFullCounterPath is a full, internationalized counter path (this will differ per Windows language version). +// dwUserData is a 'user-defined value', which becomes part of the counter information. To retrieve this value +// later, call PdhGetCounterInfo() and access dwQueryUserData of the PdhCounterInfo structure. +// +// Examples of szFullCounterPath (in an English version of Windows): +// +// \\Processor(_Total)\\% Idle Time +// \\Processor(_Total)\\% Processor Time +// \\LogicalDisk(C:)\% Free Space +// +// To view all (internationalized...) counters on a system, there are three non-programmatic ways: perfmon utility, +// the typeperf command, and the registry editor. perfmon.exe is perhaps the easiest way, because it's basically a +// full implementation of the pdh.dll API, except with a GUI and all that. The registry setting also provides an +// interface to the available counters, and can be found at the following key: +// +// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\CurrentLanguage +// +// This registry key contains several values as follows: +// +// 1 +// 1847 +// 2 +// System +// 4 +// Memory +// 6 +// % Processor Time +// ... many, many more +// +// Somehow, these numeric values can be used as szFullCounterPath too: +// +// \2\6 will correspond to \\System\% Processor Time +// +// The typeperf command may also be pretty easy. To find all performance counters, simply execute: +// +// typeperf -qx +func PdhAddCounter(hQuery pdhQueryHandle, szFullCounterPath string, dwUserData uintptr, phCounter *pdhCounterHandle) uint32 { + ptxt, _ := windows.UTF16PtrFromString(szFullCounterPath) + ret, _, _ := pdhAddCounterW.Call( + uintptr(hQuery), + uintptr(unsafe.Pointer(ptxt)), + dwUserData, + uintptr(unsafe.Pointer(phCounter))) + + return uint32(ret) +} + +// PdhAddEnglishCounterSupported returns true if PdhAddEnglishCounterW Win API function was found in pdh.dll. +// PdhAddEnglishCounterW function is not supported on pre-Windows Vista systems. +func PdhAddEnglishCounterSupported() bool { + return pdhAddEnglishCounterW != nil +} + +// PdhAddEnglishCounter adds the specified language-neutral counter to the query. See the PdhAddCounter function. This function only exists on +// Windows versions higher than Vista. +func PdhAddEnglishCounter(hQuery pdhQueryHandle, szFullCounterPath string, dwUserData uintptr, phCounter *pdhCounterHandle) uint32 { + if pdhAddEnglishCounterW == nil { + return ErrorInvalidFunction + } + + ptxt, _ := windows.UTF16PtrFromString(szFullCounterPath) + ret, _, _ := pdhAddEnglishCounterW.Call( + uintptr(hQuery), + uintptr(unsafe.Pointer(ptxt)), + dwUserData, + uintptr(unsafe.Pointer(phCounter))) + + return uint32(ret) +} + +// PdhCloseQuery closes all counters contained in the specified query, closes all handles related to the query, +// and frees all memory associated with the query. +func PdhCloseQuery(hQuery pdhQueryHandle) uint32 { + ret, _, _ := pdhCloseQuery.Call(uintptr(hQuery)) + + return uint32(ret) +} + +// PdhCollectQueryData collects the current raw data value for all counters in the specified query and updates the status +// code of each counter. With some counters, this function needs to be repeatedly called before the value +// of the counter can be extracted with PdhGetFormattedCounterValue(). For example, the following code +// requires at least two calls: +// +// var handle win.PDH_HQUERY +// var counterHandle win.PDH_HCOUNTER +// ret := win.PdhOpenQuery(0, 0, &handle) +// ret = win.PdhAddEnglishCounter(handle, "\\Processor(_Total)\\% Idle Time", 0, &counterHandle) +// var derp win.PDH_FMT_COUNTERVALUE_DOUBLE +// +// ret = win.PdhCollectQueryData(handle) +// fmt.Printf("Collect return code is %x\n", ret) // return code will be PDH_CSTATUS_INVALID_DATA +// ret = win.PdhGetFormattedCounterValueDouble(counterHandle, 0, &derp) +// +// ret = win.PdhCollectQueryData(handle) +// fmt.Printf("Collect return code is %x\n", ret) // return code will be ERROR_SUCCESS +// ret = win.PdhGetFormattedCounterValueDouble(counterHandle, 0, &derp) +// +// The PdhCollectQueryData will return an error in the first call because it needs two values for +// displaying the correct data for the processor idle time. The second call will have a 0 return code. +func PdhCollectQueryData(hQuery pdhQueryHandle) uint32 { + ret, _, _ := pdhCollectQueryData.Call(uintptr(hQuery)) + + return uint32(ret) +} + +// PdhCollectQueryDataWithTime queries data from perfmon, retrieving the device/windows timestamp from the node it was collected on. +// Converts the filetime structure to a GO time class and returns the native time. +func PdhCollectQueryDataWithTime(hQuery pdhQueryHandle) (uint32, time.Time) { + var localFileTime windows.Filetime + + ret, _, _ := pdhCollectQueryDataWithTime.Call(uintptr(hQuery), uintptr(unsafe.Pointer(&localFileTime))) + + if ret == ErrorSuccess { + var utcFileTime windows.Filetime + + if ret := kernel32.LocalFileTimeToFileTime(&localFileTime, &utcFileTime); ret == 0 { + return uint32(ErrorFailure), time.Now() + } + + retTime := time.Unix(0, utcFileTime.Nanoseconds()) + + return uint32(ErrorSuccess), retTime + } + + return uint32(ret), time.Now() +} + +// PdhGetFormattedCounterValueDouble formats the given hCounter using a 'double'. The result is set into the specialized union struct pValue. +// This function does not directly translate to a Windows counterpart due to union specialization tricks. +func PdhGetFormattedCounterValueDouble(hCounter pdhCounterHandle, lpdwType *uint32, pValue *PdhFmtCountervalueDouble) uint32 { + ret, _, _ := pdhGetFormattedCounterValue.Call( + uintptr(hCounter), + uintptr(PdhFmtDouble|PdhFmtNocap100), + uintptr(unsafe.Pointer(lpdwType)), + uintptr(unsafe.Pointer(pValue))) + + return uint32(ret) +} + +// PdhGetFormattedCounterArrayDouble returns an array of formatted counter values. Use this function when you want to format the counter values of a +// counter that contains a wildcard character for the instance name. The itemBuffer must a slice of type PdhFmtCountervalueItemDouble. +// An example of how this function can be used: +// +// okPath := "\\Process(*)\\% Processor Time" // notice the wildcard * character +// +// // omitted all necessary stuff ... +// +// var bufSize uint32 +// var bufCount uint32 +// var size uint32 = uint32(unsafe.Sizeof(win.PDH_FMT_COUNTERVALUE_ITEM_DOUBLE{})) +// var emptyBuf [1]win.PDH_FMT_COUNTERVALUE_ITEM_DOUBLE // need at least 1 addressable null ptr. +// +// for { +// // collect +// ret := win.PdhCollectQueryData(queryHandle) +// if ret == win.ERROR_SUCCESS { +// ret = win.PdhGetFormattedCounterArrayDouble(counterHandle, &bufSize, &bufCount, &emptyBuf[0]) // uses null ptr here according to MSDN. +// if ret == win.PDH_MORE_DATA { +// filledBuf := make([]win.PDH_FMT_COUNTERVALUE_ITEM_DOUBLE, bufCount*size) +// ret = win.PdhGetFormattedCounterArrayDouble(counterHandle, &bufSize, &bufCount, &filledBuf[0]) +// for i := 0; i < int(bufCount); i++ { +// c := filledBuf[i] +// var s string = win.UTF16PtrToString(c.SzName) +// fmt.Printf("Index %d -> %s, value %v\n", i, s, c.FmtValue.DoubleValue) +// } +// +// filledBuf = nil +// // Need to at least set bufSize to zero, because if not, the function will not +// // return PDH_MORE_DATA and will not set the bufSize. +// bufCount = 0 +// bufSize = 0 +// } +// +// time.Sleep(2000 * time.Millisecond) +// } +// } +func PdhGetFormattedCounterArrayDouble(hCounter pdhCounterHandle, lpdwBufferSize *uint32, lpdwBufferCount *uint32, itemBuffer *byte) uint32 { + ret, _, _ := pdhGetFormattedCounterArrayW.Call( + uintptr(hCounter), + uintptr(PdhFmtDouble|PdhFmtNocap100), + uintptr(unsafe.Pointer(lpdwBufferSize)), + uintptr(unsafe.Pointer(lpdwBufferCount)), + uintptr(unsafe.Pointer(itemBuffer))) + + return uint32(ret) +} + +// PdhOpenQuery creates a new query that is used to manage the collection of performance data. +// szDataSource is a null terminated string that specifies the name of the log file from which to +// retrieve the performance data. If 0, performance data is collected from a real-time data source. +// dwUserData is a user-defined value to associate with this query. To retrieve the user data later, +// call PdhGetCounterInfo and access dwQueryUserData of the PdhCounterInfo structure. phQuery is +// the handle to the query, and must be used in subsequent calls. This function returns a PDH_ +// constant error code, or ErrorSuccess if the call succeeded. +func PdhOpenQuery(szDataSource uintptr, dwUserData uintptr, phQuery *pdhQueryHandle) uint32 { + ret, _, _ := pdhOpenQuery.Call( + szDataSource, + dwUserData, + uintptr(unsafe.Pointer(phQuery))) + + return uint32(ret) +} + +// PdhExpandWildCardPath examines the specified computer or log file and returns those counter paths that match the given counter path +// which contains wildcard characters. The general counter path format is as follows: +// +// \\computer\object(parent/instance#index)\counter +// +// The parent, instance, index, and counter components of the counter path may contain either a valid name or a wildcard character. +// The computer, parent, instance, and index components are not necessary for all counters. +// +// The following is a list of the possible formats: +// +// \\computer\object(parent/instance#index)\counter +// \\computer\object(parent/instance)\counter +// \\computer\object(instance#index)\counter +// \\computer\object(instance)\counter +// \\computer\object\counter +// \object(parent/instance#index)\counter +// \object(parent/instance)\counter +// \object(instance#index)\counter +// \object(instance)\counter +// \object\counter +// Use an asterisk (*) as the wildcard character, for example, \object(*)\counter. +// +// If a wildcard character is specified in the parent name, all instances of the specified object +// that match the specified instance and counter fields will be returned. +// For example, \object(*/instance)\counter. +// +// If a wildcard character is specified in the instance name, all instances of the specified object and parent object will be returned if all instance names +// corresponding to the specified index match the wildcard character. For example, \object(parent/*)\counter. +// If the object does not contain an instance, an error occurs. +// +// If a wildcard character is specified in the counter name, all counters of the specified object are returned. +// +// Partial counter path string matches (for example, "pro*") are supported. +func PdhExpandWildCardPath(szWildCardPath string, mszExpandedPathList *uint16, pcchPathListLength *uint32) uint32 { + ptxt, _ := windows.UTF16PtrFromString(szWildCardPath) + flags := uint32(0) // expand instances and counters + ret, _, _ := pdhExpandWildCardPathW.Call( + 0, // search counters on local computer + uintptr(unsafe.Pointer(ptxt)), + uintptr(unsafe.Pointer(mszExpandedPathList)), + uintptr(unsafe.Pointer(pcchPathListLength)), + uintptr(unsafe.Pointer(&flags))) + + return uint32(ret) +} + +// PdhValidatePath validates a path. Will return ErrorSuccess when ok, or PdhCstatusBadCountername when the path is erroneous. +func PdhValidatePath(path string) uint32 { + ptxt, _ := windows.UTF16PtrFromString(path) + ret, _, _ := pdhValidatePathW.Call(uintptr(unsafe.Pointer(ptxt))) + + return uint32(ret) +} + +func PdhFormatError(msgID uint32) string { + var flags uint32 = windows.FORMAT_MESSAGE_FROM_HMODULE | windows.FORMAT_MESSAGE_ARGUMENT_ARRAY | windows.FORMAT_MESSAGE_IGNORE_INSERTS + buf := make([]uint16, 300) + _, err := windows.FormatMessage(flags, libPdhDll.Handle(), msgID, 0, buf, nil) + if err == nil { + return windows.UTF16PtrToString(&buf[0]) + } + + return fmt.Sprintf("(pdhErr=%d) %s", msgID, err.Error()) +} + +// PdhGetCounterInfo retrieves information about a counter, such as data size, counter type, path, and user-supplied data values +// hCounter [in] +// Handle of the counter from which you want to retrieve information. The PdhAddCounter function returns this handle. +// +// bRetrieveExplainText [in] +// Determines whether explain text is retrieved. If you set this parameter to TRUE, the explain text for the counter is retrieved. +// If you set this parameter to FALSE, the field in the returned buffer is NULL. +// +// pdwBufferSize [in, out] +// Size of the lpBuffer buffer, in bytes. If zero on input, the function returns PdhMoreData and sets this parameter to the required buffer size. +// If the buffer is larger than the required size, the function sets this parameter to the actual size of the buffer that was used. +// If the specified size on input is greater than zero but less than the required size, you should not rely on the returned size to reallocate the buffer. +// +// lpBuffer [out] +// Caller-allocated buffer that receives a PdhCounterInfo structure. +// The structure is variable-length, because the string data is appended to the end of the fixed-format portion of the structure. +// This is done so that all data is returned in a single buffer allocated by the caller. Set to NULL if pdwBufferSize is zero. +func PdhGetCounterInfo(hCounter pdhCounterHandle, bRetrieveExplainText int, pdwBufferSize *uint32, lpBuffer *byte) uint32 { + ret, _, _ := pdhGetCounterInfoW.Call( + uintptr(hCounter), + uintptr(bRetrieveExplainText), + uintptr(unsafe.Pointer(pdwBufferSize)), + uintptr(unsafe.Pointer(lpBuffer))) + + return uint32(ret) +} + +// PdhGetRawCounterValue returns the current raw value of the counter. +// If the specified counter instance does not exist, this function will return ErrorSuccess +// and the CStatus member of the PdhRawCounter structure will contain PdhCstatusNoInstance. +// +// hCounter [in] +// Handle of the counter from which to retrieve the current raw value. The PdhAddCounter function returns this handle. +// +// lpdwType [out] +// Receives the counter type. For a list of counter types, see the Counter Types section of the Windows Server 2003 Deployment Kit. +// This parameter is optional. +// +// pValue [out] +// A PdhRawCounter structure that receives the counter value. +func PdhGetRawCounterValue(hCounter pdhCounterHandle, lpdwType *uint32, pValue *PdhRawCounter) uint32 { + ret, _, _ := pdhGetRawCounterValue.Call( + uintptr(hCounter), + uintptr(unsafe.Pointer(lpdwType)), + uintptr(unsafe.Pointer(pValue))) + + return uint32(ret) +} + +// PdhGetRawCounterArray returns an array of raw values from the specified counter. Use this function when you want to retrieve the raw counter values +// of a counter that contains a wildcard character for the instance name. +// hCounter +// Handle of the counter for whose current raw instance values you want to retrieve. The PdhAddCounter function returns this handle. +// +// lpdwBufferSize +// Size of the ItemBuffer buffer, in bytes. If zero on input, the function returns PdhMoreData and sets this parameter to the required buffer size. +// If the buffer is larger than the required size, the function sets this parameter to the actual size of the buffer that was used. +// If the specified size on input is greater than zero but less than the required size, you should not rely on the returned size to reallocate the buffer. +// +// lpdwItemCount +// Number of raw counter values in the ItemBuffer buffer. +// +// ItemBuffer +// Caller-allocated buffer that receives the array of PdhRawCounterItem structures; the structures contain the raw instance counter values. +// Set to NULL if lpdwBufferSize is zero. +func PdhGetRawCounterArray(hCounter pdhCounterHandle, lpdwBufferSize *uint32, lpdwBufferCount *uint32, itemBuffer *byte) uint32 { + ret, _, _ := pdhGetRawCounterArrayW.Call( + uintptr(hCounter), + uintptr(unsafe.Pointer(lpdwBufferSize)), + uintptr(unsafe.Pointer(lpdwBufferCount)), + uintptr(unsafe.Pointer(itemBuffer))) + return uint32(ret) +} + +// PdhGetCounterTimeBase returns the time base of the specified counter. +// hCounter +// Handle of the counter for whose current raw instance values you want to retrieve. The PdhAddCounter function returns this handle. +// +// lpdwItemCount +// Time base that specifies the number of performance values a counter samples per second. +func PdhGetCounterTimeBase(hCounter pdhCounterHandle, pTimeBase *float64) uint32 { + ret, _, _ := pdhPdhGetCounterTimeBase.Call( + uintptr(hCounter), + uintptr(unsafe.Pointer(pTimeBase))) + return uint32(ret) +} diff --git a/pkg/perfdata/pdh_amd64.go b/pkg/perfdata/pdh_amd64.go new file mode 100644 index 000000000..02ce5ec00 --- /dev/null +++ b/pkg/perfdata/pdh_amd64.go @@ -0,0 +1,143 @@ +// Copyright (c) 2010-2024 The win Authors. All rights reserved. +// Copyright (c) 2024 The prometheus-community Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The names of the authors may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This is the official list of 'win' authors for copyright purposes. +// +// Alexander Neumann +// Joseph Watson +// Kevin Pors + +//go:build windows + +package perfdata + +import "golang.org/x/sys/windows" + +// PdhFmtCountervalueDouble is a union specialization for double values. +type PdhFmtCountervalueDouble struct { + CStatus uint32 + DoubleValue float64 +} + +// PdhFmtCounterValueLarge is a union specialization for 64-bit integer values. +type PdhFmtCounterValueLarge struct { + CStatus uint32 + LargeValue int64 +} + +// PdhFmtCounterValueLong is a union specialization for long values. +type PdhFmtCounterValueLong struct { + CStatus uint32 + LongValue int32 + padding [4]byte //nolint:unused // Memory reservation +} + +// PdhFmtCountervalueItemDouble is a union specialization for double values, used by PdhGetFormattedCounterArrayDouble. +type PdhFmtCountervalueItemDouble struct { + SzName *uint16 + FmtValue PdhFmtCountervalueDouble +} + +// PdhFmtCounterValueItemLarge is a union specialization for 'large' values, used by PdhGetFormattedCounterArrayLarge(). +type PdhFmtCounterValueItemLarge struct { + SzName *uint16 // pointer to a string + FmtValue PdhFmtCounterValueLarge +} + +// PdhFmtCounterValueItemLong is a union specialization for long values, used by PdhGetFormattedCounterArrayLong(). +type PdhFmtCounterValueItemLong struct { + SzName *uint16 // pointer to a string + FmtValue PdhFmtCounterValueLong +} + +// PdhCounterInfo structure contains information describing the properties of a counter. This information also includes the counter path. +type PdhCounterInfo struct { + // Size of the structure, including the appended strings, in bytes. + DwLength uint32 + // Counter type. For a list of counter types, + // see the Counter Types section of the Windows Server 2003 Deployment Kit. + // The counter type constants are defined in Winperf.h. + DwType uint32 + // Counter version information. Not used. + CVersion uint32 + // Counter status that indicates if the counter value is valid. For a list of possible values, + // see Checking PDH Interface Return Values. + CStatus uint32 + // Scale factor to use when computing the displayable value of the counter. The scale factor is a power of ten. + // The valid range of this parameter is PDH_MIN_SCALE (–7) (the returned value is the actual value times 10–⁷) to + // Pdh_MAX_SCALE (+7) (the returned value is the actual value times 10⁺⁷). A value of zero will set the scale to one, so that the actual value is returned. + LScale int32 + // Default scale factor as suggested by the counter's provider. + LDefaultScale int32 + // The value passed in the dwUserData parameter when calling PdhAddCounter. + DwUserData *uint32 + // The value passed in the dwUserData parameter when calling PdhOpenQuery. + DwQueryUserData *uint32 + // Null-terminated string that specifies the full counter path. The string follows this structure in memory. + SzFullPath *uint16 // pointer to a string + // Null-terminated string that contains the name of the computer specified in the counter path. Is NULL, if the path does not specify a computer. + // The string follows this structure in memory. + SzMachineName *uint16 // pointer to a string + // Null-terminated string that contains the name of the performance object specified in the counter path. The string follows this structure in memory. + SzObjectName *uint16 // pointer to a string + // Null-terminated string that contains the name of the object instance specified in the counter path. Is NULL, if the path does not specify an instance. + // The string follows this structure in memory. + SzInstanceName *uint16 // pointer to a string + // Null-terminated string that contains the name of the parent instance specified in the counter path. + // Is NULL, if the path does not specify a parent instance. The string follows this structure in memory. + SzParentInstance *uint16 // pointer to a string + // Instance index specified in the counter path. Is 0, if the path does not specify an instance index. + DwInstanceIndex uint32 // pointer to a string + // Null-terminated string that contains the counter name. The string follows this structure in memory. + SzCounterName *uint16 // pointer to a string + // Help text that describes the counter. Is NULL if the source is a log file. + SzExplainText *uint16 // pointer to a string + // Start of the string data that is appended to the structure. + DataBuffer [1]uint32 // pointer to an extra space +} + +// The PdhRawCounter structure returns the data as it was collected from the counter provider. +// No translation, formatting, or other interpretation is performed on the data. +type PdhRawCounter struct { + // Counter status that indicates if the counter value is valid. Check this member before using the data in a calculation or displaying its value. + // For a list of possible values, see https://docs.microsoft.com/windows/desktop/PerfCtrs/checking-pdh-interface-return-values + CStatus uint32 + // Local time for when the data was collected + TimeStamp windows.Filetime + // First raw counter value. + FirstValue int64 + // Second raw counter value. Rate counters require two values in order to compute a displayable value. + SecondValue int64 + // If the counter type contains the PERF_MULTI_COUNTER flag, this member contains the additional counter data used in the calculation. + // For example, the PERF_100NSEC_MULTI_TIMER counter type contains the PERF_MULTI_COUNTER flag. + MultiCount uint32 +} + +type PdhRawCounterItem struct { + // Pointer to a null-terminated string that specifies the instance name of the counter. The string is appended to the end of this structure. + SzName *uint16 + // A PdhRawCounter structure that contains the raw counter value of the instance + RawValue PdhRawCounter +} diff --git a/pkg/perfdata/pdh_arm64.go b/pkg/perfdata/pdh_arm64.go new file mode 100644 index 000000000..0047e6acd --- /dev/null +++ b/pkg/perfdata/pdh_arm64.go @@ -0,0 +1,143 @@ +// Copyright (c) 2010-2024 The win Authors. All rights reserved. +// Copyright (c) 2024 The prometheus-community Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. The names of the authors may not be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// This is the official list of 'win' authors for copyright purposes. +// +// Alexander Neumann +// Joseph Watson +// Kevin Pors + +//go:build windows + +package perfdata + +import "golang.org/x/sys/windows" + +// PdhFmtCountervalueDouble is a union specialization for double values. +type PdhFmtCountervalueDouble struct { + CStatus uint32 + DoubleValue float64 +} + +// PdhFmtCounterValueLarge is a union specialization for 64-bit integer values. +type PdhFmtCountervalueLarge struct { + CStatus uint32 + LargeValue int64 +} + +// PdhFmtCounterValueLong is a union specialization for long values. +type PdhFmtCountervalueLong struct { + CStatus uint32 + LongValue int32 + padding [4]byte //nolint:unused // Memory reservation +} + +type PdhFmtCountervalueItemDouble struct { + SzName *uint16 + FmtValue PdhFmtCountervalueDouble +} + +// PdhFmtCounterValueItemLarge is a union specialization for 'large' values, used by PdhGetFormattedCounterArrayLarge(). +type PdhFmtCountervalueItemLarge struct { + SzName *uint16 // pointer to a string + FmtValue PdhFmtCountervalueLarge +} + +// PdhFmtCounterValueItemLong is a union specialization for long values, used by PdhGetFormattedCounterArrayLong(). +type PdhFmtCountervalueItemLong struct { + SzName *uint16 // pointer to a string + FmtValue PdhFmtCountervalueLong +} + +// PdhCounterInfo structure contains information describing the properties of a counter. This information also includes the counter path. +type PdhCounterInfo struct { + // Size of the structure, including the appended strings, in bytes. + DwLength uint32 + // Counter type. For a list of counter types, see the Counter Types section + // of the Windows Server 2003 Deployment Kit (http://go.microsoft.com/fwlink/p/?linkid=84422). + // The counter type constants are defined in Winperf.h. + DwType uint32 + // Counter version information. Not used. + CVersion uint32 + // Counter status that indicates if the counter value is valid. For a list of possible values, + // see Checking PDH Interface Return Values. + CStatus uint32 + // Scale factor to use when computing the displayable value of the counter. The scale factor is a power of ten. + // The valid range of this parameter is PDH_MIN_SCALE (–7) (the returned value is the actual value times 10–⁷) to + // Pdh_MAX_SCALE (+7) (the returned value is the actual value times 10⁺⁷). A value of zero will set the scale to one, so that the actual value is returned. + LScale int32 + // Default scale factor as suggested by the counter's provider. + LDefaultScale int32 + // The value passed in the dwUserData parameter when calling PdhAddCounter. + DwUserData *uint32 + // The value passed in the dwUserData parameter when calling PdhOpenQuery. + DwQueryUserData *uint32 + // Null-terminated string that specifies the full counter path. The string follows this structure in memory. + SzFullPath *uint16 // pointer to a string + // Null-terminated string that contains the name of the computer specified in the counter path. Is NULL, if the path does not specify a computer. + // The string follows this structure in memory. + SzMachineName *uint16 // pointer to a string + // Null-terminated string that contains the name of the performance object specified in the counter path. The string follows this structure in memory. + SzObjectName *uint16 // pointer to a string + // Null-terminated string that contains the name of the object instance specified in the counter path. Is NULL, if the path does not specify an instance. + // The string follows this structure in memory. + SzInstanceName *uint16 // pointer to a string + // Null-terminated string that contains the name of the parent instance specified in the counter path. + // Is NULL, if the path does not specify a parent instance. + // The string follows this structure in memory. + SzParentInstance *uint16 // pointer to a string + // Instance index specified in the counter path. Is 0, if the path does not specify an instance index. + DwInstanceIndex uint32 // pointer to a string + // Null-terminated string that contains the counter name. The string follows this structure in memory. + SzCounterName *uint16 // pointer to a string + // Help text that describes the counter. Is NULL if the source is a log file. + SzExplainText *uint16 // pointer to a string + // Start of the string data that is appended to the structure. + DataBuffer [1]uint32 // pointer to an extra space +} + +// The PdhRawCounter structure returns the data as it was collected from the counter provider. +// No translation, formatting, or other interpretation is performed on the data. +type PdhRawCounter struct { + // Counter status that indicates if the counter value is valid. Check this member before using the data in a calculation or displaying its value. + // For a list of possible values, see https://docs.microsoft.com/windows/desktop/PerfCtrs/checking-pdh-interface-return-values + CStatus uint32 + // Local time for when the data was collected + TimeStamp windows.Filetime + // First raw counter value. + FirstValue int64 + // Second raw counter value. Rate counters require two values in order to compute a displayable value. + SecondValue int64 + // If the counter type contains the PERF_MULTI_COUNTER flag, this member contains the additional counter data used in the calculation. + // For example, the PERF_100NSEC_MULTI_TIMER counter type contains the PERF_MULTI_COUNTER flag. + MultiCount uint32 +} + +type PdhRawCounterItem struct { + // Pointer to a null-terminated string that specifies the instance name of the counter. The string is appended to the end of this structure. + SzName *uint16 + // A PdhRawCounter structure that contains the raw counter value of the instance + RawValue PdhRawCounter +} diff --git a/pkg/perflib/perflib.go b/pkg/perflib/perflib.go index b2b7042fd..227905174 100644 --- a/pkg/perflib/perflib.go +++ b/pkg/perflib/perflib.go @@ -119,6 +119,8 @@ import ( "strings" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) // TODO: There's a LittleEndian field in the PERF header - we ought to check it. @@ -204,7 +206,7 @@ func queryRawData(query string) ([]byte, error) { buffer = make([]byte, bufLen) - name, err := syscall.UTF16PtrFromString(query) + name, err := windows.UTF16PtrFromString(query) if err != nil { return nil, fmt.Errorf("failed to encode query string: %w", err) } @@ -212,21 +214,22 @@ func queryRawData(query string) ([]byte, error) { for { bufLen := uint32(len(buffer)) + //nolint:forbidigo // Legacy Code err := syscall.RegQueryValueEx( - syscall.HKEY_PERFORMANCE_DATA, + windows.HKEY_PERFORMANCE_DATA, name, nil, &valType, (*byte)(unsafe.Pointer(&buffer[0])), &bufLen) - if errors.Is(err, error(syscall.ERROR_MORE_DATA)) { + if errors.Is(err, error(syscall.ERROR_MORE_DATA)) { //nolint:forbidigo // Legacy Code newBuffer := make([]byte, len(buffer)+16384) copy(newBuffer, buffer) buffer = newBuffer continue } else if err != nil { - var errNo syscall.Errno + var errNo syscall.Errno //nolint:forbidigo // Legacy Code if errors.As(err, &errNo) { return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo)) } diff --git a/pkg/perflib/raw_types.go b/pkg/perflib/raw_types.go index 5939b7da2..fbd509220 100644 --- a/pkg/perflib/raw_types.go +++ b/pkg/perflib/raw_types.go @@ -3,7 +3,8 @@ package perflib import ( "encoding/binary" "io" - "syscall" + + "golang.org/x/sys/windows" ) /* @@ -36,7 +37,7 @@ type perfDataBlock struct { HeaderLength uint32 NumObjectTypes uint32 DefaultObject int32 - SystemTime syscall.Systemtime + SystemTime windows.Systemtime _ uint32 // TODO PerfTime int64 PerfFreq int64 diff --git a/pkg/perflib/utf16.go b/pkg/perflib/utf16.go index 243199baa..e107a3402 100644 --- a/pkg/perflib/utf16.go +++ b/pkg/perflib/utf16.go @@ -3,7 +3,8 @@ package perflib import ( "encoding/binary" "io" - "syscall" + + "golang.org/x/sys/windows" ) // readUTF16StringAtPos Read an unterminated UTF16 string at a given position, specifying its length. @@ -19,7 +20,7 @@ func readUTF16StringAtPos(r io.ReadSeeker, absPos int64, length uint32) (string, return "", err } - return syscall.UTF16ToString(value), nil + return windows.UTF16ToString(value), nil } // readUTF16String Reads a null-terminated UTF16 string at the current offset. @@ -43,5 +44,5 @@ func readUTF16String(r io.Reader) (string, error) { return "", err } - return syscall.UTF16ToString(out), nil + return windows.UTF16ToString(out), nil } diff --git a/tools/e2e-output.txt b/tools/e2e-output.txt index e72380ccd..b69139807 100644 --- a/tools/e2e-output.txt +++ b/tools/e2e-output.txt @@ -60,6 +60,7 @@ windows_exporter_collector_success{collector="logical_disk"} 1 windows_exporter_collector_success{collector="memory"} 1 windows_exporter_collector_success{collector="net"} 1 windows_exporter_collector_success{collector="os"} 1 +windows_exporter_collector_success{collector="perfdata"} 1 windows_exporter_collector_success{collector="physical_disk"} 1 windows_exporter_collector_success{collector="process"} 1 windows_exporter_collector_success{collector="scheduled_task"} 1 @@ -75,6 +76,7 @@ windows_exporter_collector_timeout{collector="logical_disk"} 0 windows_exporter_collector_timeout{collector="memory"} 0 windows_exporter_collector_timeout{collector="net"} 0 windows_exporter_collector_timeout{collector="os"} 0 +windows_exporter_collector_timeout{collector="perfdata"} 0 windows_exporter_collector_timeout{collector="physical_disk"} 0 windows_exporter_collector_timeout{collector="process"} 0 windows_exporter_collector_timeout{collector="scheduled_task"} 0 @@ -241,6 +243,12 @@ windows_exporter_collector_timeout{collector="textfile"} 0 # TYPE windows_os_virtual_memory_free_bytes gauge # HELP windows_os_visible_memory_bytes Deprecated: Use `windows_memory_physical_total_bytes` instead. # TYPE windows_os_visible_memory_bytes gauge +# HELP windows_perfdata_memory_cache_faults_sec Performance data for \\Memory\\Cache Faults/sec +# TYPE windows_perfdata_memory_cache_faults_sec counter +# HELP windows_perfdata_processor_information__privileged_time Performance data for \\Processor Information\\% Privileged Time +# TYPE windows_perfdata_processor_information__privileged_time counter +# HELP windows_perfdata_processor_information__processor_time Performance data for \\Processor Information\\% Processor Time +# TYPE windows_perfdata_processor_information__processor_time counter # HELP windows_physical_disk_idle_seconds_total Seconds that the disk was idle (PhysicalDisk.PercentIdleTime) # TYPE windows_physical_disk_idle_seconds_total counter # HELP windows_physical_disk_read_bytes_total The number of bytes transferred from the disk during read operations (PhysicalDisk.DiskReadBytesPerSec) diff --git a/tools/end-to-end-test.ps1 b/tools/end-to-end-test.ps1 index 62b534156..344897be8 100644 --- a/tools/end-to-end-test.ps1 +++ b/tools/end-to-end-test.ps1 @@ -18,20 +18,22 @@ mkdir $textfile_dir | Out-Null Copy-Item 'e2e-textfile.prom' -Destination "$($textfile_dir)/e2e-textfile.prom" # Omit dynamic collector information that will change after each run -$skip_re = "^(go_|windows_exporter_build_info|windows_exporter_collector_duration_seconds|windows_exporter_perflib_snapshot_duration_seconds|process_|windows_textfile_mtime_seconds|windows_cpu|windows_cs|windows_logical_disk|windows_physical_disk|windows_memory|windows_net|windows_os|windows_process|windows_service_process|windows_system|windows_textfile_mtime_seconds)" +$skip_re = "^(go_|windows_exporter_build_info|windows_exporter_collector_duration_seconds|windows_exporter_perflib_snapshot_duration_seconds|process_|windows_textfile_mtime_seconds|windows_cpu|windows_cs|windows_logical_disk|windows_physical_disk|windows_memory|windows_net|windows_os|windows_process|windows_service_process|windows_system|windows_perfdata|windows_textfile_mtime_seconds)" # Start process in background, awaiting HTTP requests. # Use default collectors, port and address: http://localhost:9182/metrics $exporter_proc = Start-Process ` -PassThru ` -FilePath ..\windows_exporter.exe ` - -ArgumentList "--log.level=debug --web.disable-exporter-metrics --collectors.enabled=[defaults],cpu_info,textfile,process,scheduled_task --collector.process.include=explorer.exe --collector.scheduled_task.include=.*WinSAT --collector.service.include=Themes --collector.textfile.directories=$($textfile_dir)" ` + -ArgumentList "--log.level=debug","--web.disable-exporter-metrics","--collectors.enabled=[defaults],cpu_info,textfile,process,perfdata,scheduled_task","--collector.process.include=explorer.exe","--collector.scheduled_task.include=.*WinSAT","--collector.service.include=Themes","--collector.textfile.directories=$($textfile_dir)",@" +--collector.perfdata.objects="[{\"object\":\"Processor Information\",\"instance_label\":\"core\",\"instances\":[\"*\"],\"counters\":{\"% Processor Time\":{},\"% Privileged Time\":{}}},{\"object\":\"Memory\",\"counters\":{\"Cache Faults/sec\":{\"type\":\"counter\"}}}]" +"@ ` -WindowStyle Hidden ` -RedirectStandardOutput "$($temp_dir)/windows_exporter.log" ` -RedirectStandardError "$($temp_dir)/windows_exporter_error.log" # Exporter can take some time to start -for ($i=1; $i -le 5; $i++) { +for ($i=1; $i -le 1; $i++) { Start-Sleep 10 $netstat_output = netstat -anp tcp | Select-String 'listening'