process: Use registry collector for V1 data (#1814)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2024-12-21 22:58:47 +01:00
committed by GitHub
parent 39c929eefe
commit a9f8b3b722
158 changed files with 7793 additions and 7748 deletions

459
internal/pdh/collector.go Normal file
View File

@@ -0,0 +1,459 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh
import (
"errors"
"fmt"
"reflect"
"slices"
"strings"
"sync"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
//nolint:gochecknoglobals
var (
InstancesAll = []string{"*"}
InstancesTotal = []string{InstanceTotal}
)
type CounterValues = map[string]map[string]CounterValue
type Collector struct {
object string
counters map[string]Counter
handle pdhQueryHandle
totalCounterRequested bool
mu sync.RWMutex
nameIndexValue int
metricsTypeIndexValue int
collectCh chan any
errorCh chan error
}
type Counter struct {
Name string
Desc string
MetricType prometheus.ValueType
Instances map[string]pdhCounterHandle
Type uint32
Frequency int64
FieldIndexValue int
FieldIndexSecondValue int
}
func NewCollector[T any](object string, instances []string) (*Collector, error) {
valueType := reflect.TypeFor[T]()
return NewCollectorWithReflection(object, instances, valueType)
}
func NewCollectorWithReflection(object string, instances []string, valueType reflect.Type) (*Collector, error) {
var handle pdhQueryHandle
if ret := OpenQuery(0, 0, &handle); ret != ErrorSuccess {
return nil, NewPdhError(ret)
}
if len(instances) == 0 {
instances = []string{InstanceEmpty}
}
collector := &Collector{
object: object,
counters: make(map[string]Counter, valueType.NumField()),
handle: handle,
totalCounterRequested: slices.Contains(instances, InstanceTotal),
mu: sync.RWMutex{},
nameIndexValue: -1,
metricsTypeIndexValue: -1,
}
errs := make([]error, 0, valueType.NumField())
if f, ok := valueType.FieldByName("Name"); ok {
if f.Type.Kind() == reflect.String {
collector.nameIndexValue = f.Index[0]
}
}
if f, ok := valueType.FieldByName("MetricType"); ok {
if f.Type.Kind() == reflect.TypeOf(prometheus.ValueType(0)).Kind() {
collector.metricsTypeIndexValue = f.Index[0]
}
}
for _, f := range reflect.VisibleFields(valueType) {
counterName, ok := f.Tag.Lookup("perfdata")
if !ok {
continue
}
if f.Type.Kind() != reflect.Float64 {
errs = append(errs, fmt.Errorf("field %s must be a float64", f.Name))
continue
}
var counter Counter
if counter, ok = collector.counters[counterName]; !ok {
counter = Counter{
Name: counterName,
Instances: make(map[string]pdhCounterHandle, len(instances)),
FieldIndexSecondValue: -1,
FieldIndexValue: -1,
}
}
if strings.HasSuffix(counterName, ",secondvalue") {
counterName = strings.TrimSuffix(counterName, ",secondvalue")
counter.FieldIndexSecondValue = f.Index[0]
} else {
counter.FieldIndexValue = f.Index[0]
}
if len(counter.Instances) != 0 {
collector.counters[counterName] = counter
continue
}
var counterPath string
for _, instance := range instances {
counterPath = formatCounterPath(object, instance, counterName)
var counterHandle pdhCounterHandle
if ret := AddEnglishCounter(handle, counterPath, 0, &counterHandle); ret != ErrorSuccess {
errs = append(errs, fmt.Errorf("failed to add counter %s: %w", counterPath, NewPdhError(ret)))
continue
}
counter.Instances[instance] = counterHandle
if counter.Type != 0 {
continue
}
// Get the info with the current buffer size
bufLen := uint32(0)
if ret := GetCounterInfo(counterHandle, 0, &bufLen, nil); ret != MoreData {
errs = append(errs, fmt.Errorf("GetCounterInfo: %w", NewPdhError(ret)))
continue
}
buf := make([]byte, bufLen)
if ret := GetCounterInfo(counterHandle, 0, &bufLen, &buf[0]); ret != ErrorSuccess {
errs = append(errs, fmt.Errorf("GetCounterInfo: %w", NewPdhError(ret)))
continue
}
ci := (*CounterInfo)(unsafe.Pointer(&buf[0]))
counter.Type = ci.DwType
counter.Desc = windows.UTF16PtrToString(ci.SzExplainText)
counter.Desc = windows.UTF16PtrToString(ci.SzExplainText)
if val, ok := SupportedCounterTypes[counter.Type]; ok {
counter.MetricType = val
} else {
counter.MetricType = prometheus.GaugeValue
}
if counter.Type == PERF_ELAPSED_TIME {
if ret := GetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess {
errs = append(errs, fmt.Errorf("GetCounterTimeBase: %w", NewPdhError(ret)))
continue
}
}
}
collector.counters[counterName] = counter
}
if err := errors.Join(errs...); err != nil {
return collector, fmt.Errorf("failed to initialize collector: %w", err)
}
if len(collector.counters) == 0 {
return nil, errors.New("no counters configured")
}
collector.collectCh = make(chan any)
collector.errorCh = make(chan error)
go collector.collectRoutine()
// Collect initial data because some counters need to be read twice to get the correct value.
collectValues := reflect.New(reflect.SliceOf(valueType)).Elem()
if err := collector.Collect(collectValues.Addr().Interface()); err != nil && !errors.Is(err, ErrNoData) {
return collector, fmt.Errorf("failed to collect initial data: %w", err)
}
return collector, nil
}
func (c *Collector) Describe() map[string]string {
if c == nil {
return map[string]string{}
}
c.mu.RLock()
defer c.mu.RUnlock()
desc := make(map[string]string, len(c.counters))
for _, counter := range c.counters {
desc[counter.Name] = counter.Desc
}
return desc
}
func (c *Collector) Collect(dst any) error {
if c == nil {
return ErrPerformanceCounterNotInitialized
}
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.counters) == 0 || c.handle == 0 || c.collectCh == nil || c.errorCh == nil {
return ErrPerformanceCounterNotInitialized
}
c.collectCh <- dst
return <-c.errorCh
}
func (c *Collector) collectRoutine() {
var (
err error
itemCount uint32
items []RawCounterItem
bytesNeeded uint32
)
buf := make([]byte, 1)
for data := range c.collectCh {
err = (func() error {
if ret := CollectQueryData(c.handle); ret != ErrorSuccess {
return fmt.Errorf("failed to collect query data: %w", NewPdhError(ret))
}
dv := reflect.ValueOf(data)
if dv.Kind() != reflect.Ptr || dv.IsNil() {
return fmt.Errorf("expected a pointer, got %s: %w", dv.Kind(), mi.ErrInvalidEntityType)
}
dv = dv.Elem()
if dv.Kind() != reflect.Slice {
return fmt.Errorf("expected a pointer to a slice, got %s: %w", dv.Kind(), mi.ErrInvalidEntityType)
}
elemType := dv.Type().Elem()
if elemType.Kind() != reflect.Struct {
return fmt.Errorf("expected a pointer to a slice of structs, got a slice of %s: %w", elemType.Kind(), mi.ErrInvalidEntityType)
}
if dv.Len() != 0 {
dv.Set(reflect.MakeSlice(dv.Type(), 0, 0))
}
dv.Clear()
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
indexMap := map[string]int{}
stringMap := map[*uint16]string{}
for _, counter := range c.counters {
for _, instance := range counter.Instances {
// Get the info with the current buffer size
bytesNeeded = uint32(cap(buf))
for {
ret := GetRawCounterArray(instance, &bytesNeeded, &itemCount, &buf[0])
if ret == ErrorSuccess {
break
}
if err := NewPdhError(ret); ret != MoreData && !isKnownCounterDataError(err) {
return fmt.Errorf("GetRawCounterArray: %w", err)
}
if bytesNeeded <= uint32(cap(buf)) {
return fmt.Errorf("GetRawCounterArray reports buffer too small (%d), but buffer is large enough (%d): %w", uint32(cap(buf)), bytesNeeded, NewPdhError(ret))
}
buf = make([]byte, bytesNeeded)
}
items = unsafe.Slice((*RawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
var (
instanceName string
ok bool
)
for _, item := range items {
if item.RawValue.CStatus != CstatusValidData && item.RawValue.CStatus != CstatusNewData {
continue
}
if instanceName, ok = stringMap[item.SzName]; !ok {
instanceName = windows.UTF16PtrToString(item.SzName)
stringMap[item.SzName] = instanceName
}
if strings.HasSuffix(instanceName, InstanceTotal) && !c.totalCounterRequested {
continue
}
if instanceName == "" || instanceName == "*" {
instanceName = InstanceEmpty
}
var (
index int
ok bool
)
if index, ok = indexMap[instanceName]; !ok {
index = dv.Len()
indexMap[instanceName] = index
if c.nameIndexValue != -1 {
elemValue.Field(c.nameIndexValue).SetString(instanceName)
}
if c.metricsTypeIndexValue != -1 {
var metricsType prometheus.ValueType
if metricsType, ok = SupportedCounterTypes[counter.Type]; !ok {
metricsType = prometheus.GaugeValue
}
elemValue.Field(c.metricsTypeIndexValue).Set(reflect.ValueOf(metricsType))
}
dv.Set(reflect.Append(dv, elemValue))
}
// 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
// Ref: https://learn.microsoft.com/en-us/windows/win32/perfctrs/calculating-counter-values
switch counter.Type {
case PERF_ELAPSED_TIME:
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64((item.RawValue.FirstValue - WindowsEpoch) / counter.Frequency))
case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER:
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64(item.RawValue.FirstValue) * TicksToSecondScaleFactor)
default:
if counter.FieldIndexSecondValue != -1 {
dv.Index(index).
Field(counter.FieldIndexSecondValue).
SetFloat(float64(item.RawValue.SecondValue))
}
if counter.FieldIndexValue != -1 {
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64(item.RawValue.FirstValue))
}
}
}
}
}
if dv.Len() == 0 {
return ErrNoData
}
return nil
})()
c.errorCh <- err
}
}
func (c *Collector) Close() {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
CloseQuery(c.handle)
c.handle = 0
if c.collectCh != nil {
close(c.collectCh)
}
if c.errorCh != nil {
close(c.errorCh)
}
c.collectCh = nil
c.errorCh = nil
}
func formatCounterPath(object, instance, counterName string) string {
var counterPath string
if instance == InstanceEmpty {
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 == InvalidData ||
pdhErr.ErrorCode == CalcNegativeDenominator ||
pdhErr.ErrorCode == CalcNegativeValue ||
pdhErr.ErrorCode == CstatusInvalidData ||
pdhErr.ErrorCode == CstatusNoInstance ||
pdhErr.ErrorCode == NoData)
}

View File

@@ -0,0 +1,71 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh_test
import (
"testing"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/stretchr/testify/require"
)
type processFull struct {
Name string
ProcessorTime float64 `pdh:"% Processor Time"`
PrivilegedTime float64 `pdh:"% Privileged Time"`
UserTime float64 `pdh:"% User Time"`
CreatingProcessID float64 `pdh:"Creating Process ID"`
ElapsedTime float64 `pdh:"Elapsed Time"`
HandleCount float64 `pdh:"Handle Count"`
IDProcess float64 `pdh:"ID Process"`
IODataBytesSec float64 `pdh:"IO Data Bytes/sec"`
IODataOperationsSec float64 `pdh:"IO Data Operations/sec"`
IOOtherBytesSec float64 `pdh:"IO Other Bytes/sec"`
IOOtherOperationsSec float64 `pdh:"IO Other Operations/sec"`
IOReadBytesSec float64 `pdh:"IO Read Bytes/sec"`
IOReadOperationsSec float64 `pdh:"IO Read Operations/sec"`
IOWriteBytesSec float64 `pdh:"IO Write Bytes/sec"`
IOWriteOperationsSec float64 `pdh:"IO Write Operations/sec"`
PageFaultsSec float64 `pdh:"Page Faults/sec"`
PageFileBytesPeak float64 `pdh:"Page File Bytes Peak"`
PageFileBytes float64 `pdh:"Page File Bytes"`
PoolNonpagedBytes float64 `pdh:"Pool Nonpaged Bytes"`
PoolPagedBytes float64 `pdh:"Pool Paged Bytes"`
PriorityBase float64 `pdh:"Priority Base"`
PrivateBytes float64 `pdh:"Private Bytes"`
ThreadCount float64 `pdh:"Thread Count"`
VirtualBytesPeak float64 `pdh:"Virtual Bytes Peak"`
VirtualBytes float64 `pdh:"Virtual Bytes"`
WorkingSetPrivate float64 `pdh:"Working Set - Private"`
WorkingSetPeak float64 `pdh:"Working Set Peak"`
WorkingSet float64 `pdh:"Working Set"`
}
func BenchmarkTestCollector(b *testing.B) {
performanceData, err := pdh.NewCollector[processFull]("Process", []string{"*"})
require.NoError(b, err)
var data []processFull
for i := 0; i < b.N; i++ {
_ = performanceData.Collect(&data)
}
performanceData.Close()
b.ReportAllocs()
}

View File

@@ -0,0 +1,71 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh_test
import (
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type process struct {
Name string
ThreadCount float64 `perfdata:"Thread Count"`
}
func TestCollector(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
object string
instances []string
}{
{
object: "Process",
instances: []string{"*"},
},
} {
t.Run(tc.object, func(t *testing.T) {
t.Parallel()
performanceData, err := pdh.NewCollector[process](tc.object, tc.instances)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)
var data []process
err = performanceData.Collect(&data)
require.NoError(t, err)
require.NotEmpty(t, data)
err = performanceData.Collect(&data)
require.NoError(t, err)
require.NotEmpty(t, data)
for _, instance := range data {
if instance.Name == "Idle" || instance.Name == "Secure System" {
continue
}
assert.NotZerof(t, instance.ThreadCount, "object: %s, instance: %s, counter: %s", tc.object, instance, instance.ThreadCount)
}
})
}
}

92
internal/pdh/const.go Normal file
View File

@@ -0,0 +1,92 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh
import "github.com/prometheus/client_golang/prometheus"
// Conversion factors.
const (
TicksToSecondScaleFactor = 1 / 1e7
WindowsEpoch int64 = 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
)
//nolint:gochecknoglobals
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,
}

53
internal/pdh/error.go Normal file
View File

@@ -0,0 +1,53 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh
import "errors"
var (
ErrNoData = NewPdhError(NoData)
ErrPerformanceCounterNotInitialized = errors.New("performance counter not initialized")
)
// Error represents error returned from Performance Counters API.
type Error struct {
ErrorCode uint32
errorText string
}
func (m *Error) Is(err error) bool {
if err == nil {
return false
}
var e *Error
if errors.As(err, &e) {
return m.ErrorCode == e.ErrorCode
}
return false
}
func (m *Error) Error() string {
return m.errorText
}
func NewPdhError(code uint32) error {
return &Error{
ErrorCode: code,
errorText: FormatError(code),
}
}

633
internal/pdh/pdh.go Normal file
View File

@@ -0,0 +1,633 @@
// 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 <an2048@googlemail.com>
// Joseph Watson <jtwatson@linux-consulting.us>
// Kevin Pors <krpors@gmail.com>
//go:build windows
package pdh
import (
"fmt"
"time"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
"golang.org/x/sys/windows"
)
// Error codes.
const (
ErrorSuccess = 0
ErrorFailure = 1
ErrorInvalidFunction = 1
)
type (
HANDLE uintptr
)
// PDH error codes, which can be returned by all Pdh* functions. Taken from mingw-w64 pdhmsg.h
const (
CstatusValidData uint32 = 0x00000000 // The returned data is valid.
CstatusNewData uint32 = 0x00000001 // The return data value is valid and different from the last sample.
CstatusNoMachine uint32 = 0x800007D0 // Unable to connect to the specified computer, or the computer is offline.
CstatusNoInstance uint32 = 0x800007D1
MoreData uint32 = 0x800007D2 // The PdhGetFormattedCounterArray* function can return this if there's 'more data to be displayed'.
CstatusItemNotValidated uint32 = 0x800007D3
Retry uint32 = 0x800007D4
NoData uint32 = 0x800007D5 // The query does not currently contain any counters (for example, limited access)
CalcNegativeDenominator uint32 = 0x800007D6
CalcNegativeTimebase uint32 = 0x800007D7
CalcNegativeValue uint32 = 0x800007D8
DialogCancelled uint32 = 0x800007D9
EndOfLogFile uint32 = 0x800007DA
AsyncQueryTimeout uint32 = 0x800007DB
CannotSetDefaultRealtimeDatasource uint32 = 0x800007DC
CstatusNoObject uint32 = 0xC0000BB8
CstatusNoCounter uint32 = 0xC0000BB9 // The specified counter could not be found.
CstatusInvalidData uint32 = 0xC0000BBA // The counter was successfully found, but the data returned is not valid.
MemoryAllocationFailure uint32 = 0xC0000BBB
InvalidHandle uint32 = 0xC0000BBC
InvalidArgument uint32 = 0xC0000BBD // Required argument is missing or incorrect.
FunctionNotFound uint32 = 0xC0000BBE
CstatusNoCountername uint32 = 0xC0000BBF
CstatusBadCountername uint32 = 0xC0000BC0 // Unable to parse the counter path. Check the format and syntax of the specified path.
InvalidBuffer uint32 = 0xC0000BC1
InsufficientBuffer uint32 = 0xC0000BC2
CannotConnectMachine uint32 = 0xC0000BC3
InvalidPath uint32 = 0xC0000BC4
InvalidInstance uint32 = 0xC0000BC5
InvalidData uint32 = 0xC0000BC6 // specified counter does not contain valid data or a successful status code.
NoDialogData uint32 = 0xC0000BC7
CannotReadNameStrings uint32 = 0xC0000BC8
LogFileCreateError uint32 = 0xC0000BC9
LogFileOpenError uint32 = 0xC0000BCA
LogTypeNotFound uint32 = 0xC0000BCB
NoMoreData uint32 = 0xC0000BCC
EntryNotInLogFile uint32 = 0xC0000BCD
DataSourceIsLogFile uint32 = 0xC0000BCE
DataSourceIsRealTime uint32 = 0xC0000BCF
UnableReadLogHeader uint32 = 0xC0000BD0
FileNotFound uint32 = 0xC0000BD1
FileAlreadyExists uint32 = 0xC0000BD2
NotImplemented uint32 = 0xC0000BD3
StringNotFound uint32 = 0xC0000BD4
UnableMapNameFiles uint32 = 0x80000BD5
UnknownLogFormat uint32 = 0xC0000BD6
UnknownLogsvcCommand uint32 = 0xC0000BD7
LogsvcQueryNotFound uint32 = 0xC0000BD8
LogsvcNotOpened uint32 = 0xC0000BD9
WbemError uint32 = 0xC0000BDA
AccessDenied uint32 = 0xC0000BDB
LogFileTooSmall uint32 = 0xC0000BDC
InvalidDatasource uint32 = 0xC0000BDD
InvalidSqldb uint32 = 0xC0000BDE
NoCounters uint32 = 0xC0000BDF
SQLAllocFailed uint32 = 0xC0000BE0
SQLAllocconFailed uint32 = 0xC0000BE1
SQLExecDirectFailed uint32 = 0xC0000BE2
SQLFetchFailed uint32 = 0xC0000BE3
SQLRowcountFailed uint32 = 0xC0000BE4
SQLMoreResultsFailed uint32 = 0xC0000BE5
SQLConnectFailed uint32 = 0xC0000BE6
SQLBindFailed uint32 = 0xC0000BE7
CannotConnectWmiServer uint32 = 0xC0000BE8
PlaCollectionAlreadyRunning uint32 = 0xC0000BE9
PlaErrorScheduleOverlap uint32 = 0xC0000BEA
PlaCollectionNotFound uint32 = 0xC0000BEB
PlaErrorScheduleElapsed uint32 = 0xC0000BEC
PlaErrorNostart uint32 = 0xC0000BED
PlaErrorAlreadyExists uint32 = 0xC0000BEE
PlaErrorTypeMismatch uint32 = 0xC0000BEF
PlaErrorFilepath uint32 = 0xC0000BF0
PlaServiceError uint32 = 0xC0000BF1
PlaValidationError uint32 = 0xC0000BF2
PlaValidationWarning uint32 = 0x80000BF3
PlaErrorNameTooLong uint32 = 0xC0000BF4
InvalidSQLLogFormat uint32 = 0xC0000BF5
CounterAlreadyInQuery uint32 = 0xC0000BF6
BinaryLogCorrupt uint32 = 0xC0000BF7
LogSampleTooSmall uint32 = 0xC0000BF8
OsLaterVersion uint32 = 0xC0000BF9
OsEarlierVersion uint32 = 0xC0000BFA
IncorrectAppendTime uint32 = 0xC0000BFB
UnmatchedAppendCounter uint32 = 0xC0000BFC
SQLAlterDetailFailed uint32 = 0xC0000BFD
QueryPerfDataTimeout uint32 = 0xC0000BFE
)
//nolint:gochecknoglobals
var Errors = map[uint32]string{
CstatusValidData: "PDH_CSTATUS_VALID_DATA",
CstatusNewData: "PDH_CSTATUS_NEW_DATA",
CstatusNoMachine: "PDH_CSTATUS_NO_MACHINE",
CstatusNoInstance: "PDH_CSTATUS_NO_INSTANCE",
MoreData: "PDH_MORE_DATA",
CstatusItemNotValidated: "PDH_CSTATUS_ITEM_NOT_VALIDATED",
Retry: "PDH_RETRY",
NoData: "PDH_NO_DATA",
CalcNegativeDenominator: "PDH_CALC_NEGATIVE_DENOMINATOR",
CalcNegativeTimebase: "PDH_CALC_NEGATIVE_TIMEBASE",
CalcNegativeValue: "PDH_CALC_NEGATIVE_VALUE",
DialogCancelled: "PDH_DIALOG_CANCELLED",
EndOfLogFile: "PDH_END_OF_LOG_FILE",
AsyncQueryTimeout: "PDH_ASYNC_QUERY_TIMEOUT",
CannotSetDefaultRealtimeDatasource: "PDH_CANNOT_SET_DEFAULT_REALTIME_DATASOURCE",
CstatusNoObject: "PDH_CSTATUS_NO_OBJECT",
CstatusNoCounter: "PDH_CSTATUS_NO_COUNTER",
CstatusInvalidData: "PDH_CSTATUS_INVALID_DATA",
MemoryAllocationFailure: "PDH_MEMORY_ALLOCATION_FAILURE",
InvalidHandle: "PDH_INVALID_HANDLE",
InvalidArgument: "PDH_INVALID_ARGUMENT",
FunctionNotFound: "PDH_FUNCTION_NOT_FOUND",
CstatusNoCountername: "PDH_CSTATUS_NO_COUNTERNAME",
CstatusBadCountername: "PDH_CSTATUS_BAD_COUNTERNAME",
InvalidBuffer: "PDH_INVALID_BUFFER",
InsufficientBuffer: "PDH_INSUFFICIENT_BUFFER",
CannotConnectMachine: "PDH_CANNOT_CONNECT_MACHINE",
InvalidPath: "PDH_INVALID_PATH",
InvalidInstance: "PDH_INVALID_INSTANCE",
InvalidData: "PDH_INVALID_DATA",
NoDialogData: "PDH_NO_DIALOG_DATA",
CannotReadNameStrings: "PDH_CANNOT_READ_NAME_STRINGS",
LogFileCreateError: "PDH_LOG_FILE_CREATE_ERROR",
LogFileOpenError: "PDH_LOG_FILE_OPEN_ERROR",
LogTypeNotFound: "PDH_LOG_TYPE_NOT_FOUND",
NoMoreData: "PDH_NO_MORE_DATA",
EntryNotInLogFile: "PDH_ENTRY_NOT_IN_LOG_FILE",
DataSourceIsLogFile: "PDH_DATA_SOURCE_IS_LOG_FILE",
DataSourceIsRealTime: "PDH_DATA_SOURCE_IS_REAL_TIME",
UnableReadLogHeader: "PDH_UNABLE_READ_LOG_HEADER",
FileNotFound: "PDH_FILE_NOT_FOUND",
FileAlreadyExists: "PDH_FILE_ALREADY_EXISTS",
NotImplemented: "PDH_NOT_IMPLEMENTED",
StringNotFound: "PDH_STRING_NOT_FOUND",
UnableMapNameFiles: "PDH_UNABLE_MAP_NAME_FILES",
UnknownLogFormat: "PDH_UNKNOWN_LOG_FORMAT",
UnknownLogsvcCommand: "PDH_UNKNOWN_LOGSVC_COMMAND",
LogsvcQueryNotFound: "PDH_LOGSVC_QUERY_NOT_FOUND",
LogsvcNotOpened: "PDH_LOGSVC_NOT_OPENED",
WbemError: "PDH_WBEM_ERROR",
AccessDenied: "PDH_ACCESS_DENIED",
LogFileTooSmall: "PDH_LOG_FILE_TOO_SMALL",
InvalidDatasource: "PDH_INVALID_DATASOURCE",
InvalidSqldb: "PDH_INVALID_SQLDB",
NoCounters: "PDH_NO_COUNTERS",
SQLAllocFailed: "PDH_SQL_ALLOC_FAILED",
SQLAllocconFailed: "PDH_SQL_ALLOCCON_FAILED",
SQLExecDirectFailed: "PDH_SQL_EXEC_DIRECT_FAILED",
SQLFetchFailed: "PDH_SQL_FETCH_FAILED",
SQLRowcountFailed: "PDH_SQL_ROWCOUNT_FAILED",
SQLMoreResultsFailed: "PDH_SQL_MORE_RESULTS_FAILED",
SQLConnectFailed: "PDH_SQL_CONNECT_FAILED",
SQLBindFailed: "PDH_SQL_BIND_FAILED",
CannotConnectWmiServer: "PDH_CANNOT_CONNECT_WMI_SERVER",
PlaCollectionAlreadyRunning: "PDH_PLA_COLLECTION_ALREADY_RUNNING",
PlaErrorScheduleOverlap: "PDH_PLA_ERROR_SCHEDULE_OVERLAP",
PlaCollectionNotFound: "PDH_PLA_COLLECTION_NOT_FOUND",
PlaErrorScheduleElapsed: "PDH_PLA_ERROR_SCHEDULE_ELAPSED",
PlaErrorNostart: "PDH_PLA_ERROR_NOSTART",
PlaErrorAlreadyExists: "PDH_PLA_ERROR_ALREADY_EXISTS",
PlaErrorTypeMismatch: "PDH_PLA_ERROR_TYPE_MISMATCH",
PlaErrorFilepath: "PDH_PLA_ERROR_FILEPATH",
PlaServiceError: "PDH_PLA_SERVICE_ERROR",
PlaValidationError: "PDH_PLA_VALIDATION_ERROR",
PlaValidationWarning: "PDH_PLA_VALIDATION_WARNING",
PlaErrorNameTooLong: "PDH_PLA_ERROR_NAME_TOO_LONG",
InvalidSQLLogFormat: "PDH_INVALID_SQL_LOG_FORMAT",
CounterAlreadyInQuery: "PDH_COUNTER_ALREADY_IN_QUERY",
BinaryLogCorrupt: "PDH_BINARY_LOG_CORRUPT",
LogSampleTooSmall: "PDH_LOG_SAMPLE_TOO_SMALL",
OsLaterVersion: "PDH_OS_LATER_VERSION",
OsEarlierVersion: "PDH_OS_EARLIER_VERSION",
IncorrectAppendTime: "PDH_INCORRECT_APPEND_TIME",
UnmatchedAppendCounter: "PDH_UNMATCHED_APPEND_COUNTER",
SQLAlterDetailFailed: "PDH_SQL_ALTER_DETAIL_FAILED",
QueryPerfDataTimeout: "PDH_QUERY_PERF_DATA_TIMEOUT",
}
// Formatting options for GetFormattedCounterValue().
//
//goland:noinspection GoUnusedConst
const (
FmtRaw = 0x00000010
FmtAnsi = 0x00000020
FmtUnicode = 0x00000040
FmtLong = 0x00000100 // Return data as a long int.
FmtDouble = 0x00000200 // Return data as a double precision floating point real.
FmtLarge = 0x00000400 // Return data as a 64 bit integer.
FmtNoscale = 0x00001000 // can be OR-ed: Do not apply the counter's default scaling factor.
Fmt1000 = 0x00002000 // can be OR-ed: multiply the actual value by 1,000.
FmtNodata = 0x00004000 // can be OR-ed: unknown what this is for, MSDN says nothing.
FmtNocap100 = 0x00008000 // can be OR-ed: do not cap values > 100.
PerfDetailCostly = 0x00010000
PerfDetailStandard = 0x0000FFFF
)
type (
pdhQueryHandle HANDLE // query handle
pdhCounterHandle HANDLE // counter handle
)
//nolint:gochecknoglobals
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")
)
// AddCounter adds the specified counter to the query. This is the internationalized version. Preferably, use the
// function AddEnglishCounter instead. hQuery is the query handle, which has been fetched by OpenQuery.
// 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 GetCounterInfo() and access dwQueryUserData of the CounterInfo 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 v1 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 v1 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 v1 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 AddCounter(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)
}
// AddEnglishCounter adds the specified language-neutral counter to the query. See the AddCounter function. This function only exists on
// Windows versions higher than Vista.
func AddEnglishCounter(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)
}
// CloseQuery closes all counters contained in the specified query, closes all handles related to the query,
// and frees all memory associated with the query.
func CloseQuery(hQuery pdhQueryHandle) uint32 {
ret, _, _ := pdhCloseQuery.Call(uintptr(hQuery))
return uint32(ret)
}
// CollectQueryData 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.OpenQuery(0, 0, &handle)
// ret = win.AddEnglishCounter(handle, "\\Processor(_Total)\\% Idle Time", 0, &counterHandle)
// var derp win.PDH_FMT_COUNTERVALUE_DOUBLE
//
// ret = win.CollectQueryData(handle)
// fmt.Printf("Collect return code is %x\n", ret) // return code will be PDH_CSTATUS_INVALID_DATA
// ret = win.GetFormattedCounterValueDouble(counterHandle, 0, &derp)
//
// ret = win.CollectQueryData(handle)
// fmt.Printf("Collect return code is %x\n", ret) // return code will be ERROR_SUCCESS
// ret = win.GetFormattedCounterValueDouble(counterHandle, 0, &derp)
//
// The CollectQueryData 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 CollectQueryData(hQuery pdhQueryHandle) uint32 {
ret, _, _ := pdhCollectQueryData.Call(uintptr(hQuery))
return uint32(ret)
}
// CollectQueryDataWithTime 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 CollectQueryDataWithTime(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()
}
// GetFormattedCounterValueDouble 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 GetFormattedCounterValueDouble(hCounter pdhCounterHandle, lpdwType *uint32, pValue *FmtCounterValueDouble) uint32 {
ret, _, _ := pdhGetFormattedCounterValue.Call(
uintptr(hCounter),
uintptr(FmtDouble|FmtNocap100),
uintptr(unsafe.Pointer(lpdwType)),
uintptr(unsafe.Pointer(pValue)))
return uint32(ret)
}
// GetFormattedCounterArrayDouble 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 FmtCounterValueItemDouble.
// 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.CollectQueryData(queryHandle)
// if ret == win.ERROR_SUCCESS {
// ret = win.GetFormattedCounterArrayDouble(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.GetFormattedCounterArrayDouble(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 GetFormattedCounterArrayDouble(hCounter pdhCounterHandle, lpdwBufferSize *uint32, lpdwBufferCount *uint32, itemBuffer *byte) uint32 {
ret, _, _ := pdhGetFormattedCounterArrayW.Call(
uintptr(hCounter),
uintptr(FmtDouble|FmtNocap100),
uintptr(unsafe.Pointer(lpdwBufferSize)),
uintptr(unsafe.Pointer(lpdwBufferCount)),
uintptr(unsafe.Pointer(itemBuffer)))
return uint32(ret)
}
// OpenQuery 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 GetCounterInfo and access dwQueryUserData of the CounterInfo 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 OpenQuery(szDataSource uintptr, dwUserData uintptr, phQuery *pdhQueryHandle) uint32 {
ret, _, _ := pdhOpenQuery.Call(
szDataSource,
dwUserData,
uintptr(unsafe.Pointer(phQuery)))
return uint32(ret)
}
// ExpandWildCardPath 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 ExpandWildCardPath(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)
}
// ValidatePath validates a path. Will return ErrorSuccess when ok, or PdhCstatusBadCountername when the path is erroneous.
func ValidatePath(path string) uint32 {
ptxt, _ := windows.UTF16PtrFromString(path)
ret, _, _ := pdhValidatePathW.Call(uintptr(unsafe.Pointer(ptxt)))
return uint32(ret)
}
func FormatError(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())
}
// GetCounterInfo 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 AddCounter 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 CounterInfo 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 GetCounterInfo(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)
}
// GetRawCounterValue 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 RawCounter structure will contain PdhCstatusNoInstance.
//
// hCounter [in]
// Handle of the counter from which to retrieve the current raw value. The AddCounter 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 RawCounter structure that receives the counter value.
func GetRawCounterValue(hCounter pdhCounterHandle, lpdwType *uint32, pValue *RawCounter) uint32 {
ret, _, _ := pdhGetRawCounterValue.Call(
uintptr(hCounter),
uintptr(unsafe.Pointer(lpdwType)),
uintptr(unsafe.Pointer(pValue)))
return uint32(ret)
}
// GetRawCounterArray 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 AddCounter 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 RawCounterItem structures; the structures contain the raw instance counter values.
// Set to NULL if lpdwBufferSize is zero.
func GetRawCounterArray(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)
}
// GetCounterTimeBase returns the time base of the specified counter.
// hCounter
// Handle of the counter for whose current raw instance values you want to retrieve. The AddCounter function returns this handle.
//
// lpdwItemCount
// Time base that specifies the number of performance values a counter samples per second.
func GetCounterTimeBase(hCounter pdhCounterHandle, pTimeBase *int64) uint32 {
ret, _, _ := pdhPdhGetCounterTimeBase.Call(
uintptr(hCounter),
uintptr(unsafe.Pointer(pTimeBase)))
return uint32(ret)
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Leopold Schabel / The perflib_exporter authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,178 @@
package registry
import (
"fmt"
"reflect"
"strings"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
)
type Collector struct {
object string
query string
counters map[string]Counter
nameIndexValue int
}
type Counter struct {
Name string
Desc string
Instances map[string]uint32
Type uint32
Frequency float64
FieldIndexValue int
FieldIndexSecondValue int
}
func NewCollector[T any](object string, _ []string) (*Collector, error) {
collector := &Collector{
object: object,
query: MapCounterToIndex(object),
nameIndexValue: -1,
counters: make(map[string]Counter),
}
var values [0]T
valueType := reflect.TypeOf(values).Elem()
if f, ok := valueType.FieldByName("Name"); ok {
if f.Type.Kind() == reflect.String {
collector.nameIndexValue = f.Index[0]
}
}
for _, f := range reflect.VisibleFields(valueType) {
counterName, ok := f.Tag.Lookup("perfdata")
if !ok {
continue
}
var counter Counter
if counter, ok = collector.counters[counterName]; !ok {
counter = Counter{
Name: counterName,
FieldIndexSecondValue: -1,
FieldIndexValue: -1,
}
}
if strings.HasSuffix(counterName, ",secondvalue") {
counterName = strings.TrimSuffix(counterName, ",secondvalue")
counter.FieldIndexSecondValue = f.Index[0]
} else {
counter.FieldIndexValue = f.Index[0]
}
collector.counters[counterName] = counter
}
var collectValues []T
if err := collector.Collect(&collectValues); err != nil {
return nil, fmt.Errorf("failed to collect initial data: %w", err)
}
return collector, nil
}
func (c *Collector) Describe() map[string]string {
return map[string]string{}
}
func (c *Collector) Collect(data any) error {
dv := reflect.ValueOf(data)
if dv.Kind() != reflect.Ptr || dv.IsNil() {
return mi.ErrInvalidEntityType
}
dv = dv.Elem()
elemType := dv.Type().Elem()
elemValue := reflect.ValueOf(reflect.New(elemType).Interface()).Elem()
if dv.Kind() != reflect.Slice || elemType.Kind() != reflect.Struct {
return mi.ErrInvalidEntityType
}
perfObjects, err := QueryPerformanceData(c.query, c.object)
if err != nil {
return fmt.Errorf("QueryPerformanceData: %w", err)
}
if len(perfObjects) == 0 || perfObjects[0] == nil || len(perfObjects[0].Instances) == 0 {
return nil
}
if dv.Len() != 0 {
dv.Set(reflect.MakeSlice(dv.Type(), 0, len(perfObjects[0].Instances)))
}
dv.Clear()
for _, perfObject := range perfObjects {
if perfObject.Name != c.object {
continue
}
for _, perfInstance := range perfObject.Instances {
instanceName := perfInstance.Name
if strings.HasSuffix(instanceName, "_Total") {
continue
}
if instanceName == "" || instanceName == "*" {
instanceName = pdh.InstanceEmpty
}
if c.nameIndexValue != -1 {
elemValue.Field(c.nameIndexValue).SetString(instanceName)
}
dv.Set(reflect.Append(dv, elemValue))
index := dv.Len() - 1
for _, perfCounter := range perfInstance.Counters {
if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter {
continue
}
counter, ok := c.counters[perfCounter.Def.Name]
if !ok {
continue
}
switch perfCounter.Def.CounterType {
case pdh.PERF_ELAPSED_TIME:
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64((perfCounter.Value - pdh.WindowsEpoch) / perfObject.Frequency))
case pdh.PERF_100NSEC_TIMER, pdh.PERF_PRECISION_100NS_TIMER:
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64(perfCounter.Value) * pdh.TicksToSecondScaleFactor)
default:
if counter.FieldIndexSecondValue != -1 {
dv.Index(index).
Field(counter.FieldIndexSecondValue).
SetFloat(float64(perfCounter.SecondValue))
}
if counter.FieldIndexValue != -1 {
dv.Index(index).
Field(counter.FieldIndexValue).
SetFloat(float64(perfCounter.Value))
}
}
}
}
}
return nil
}
func (c *Collector) Close() {}

View File

@@ -0,0 +1,86 @@
package registry
import (
"bytes"
"fmt"
"strconv"
"sync"
)
// CounterNameTable Initialize global name tables
// profiling, add option to disable name tables if necessary
// Not sure if we should resolve the names at all or just have the caller do it on demand
// (for many use cases the index is sufficient)
//
//nolint:gochecknoglobals
var CounterNameTable = *QueryNameTable("Counter 009")
func (p *perfObjectType) LookupName() string {
return CounterNameTable.LookupString(p.ObjectNameTitleIndex)
}
type NameTable struct {
once sync.Once
name string
table struct {
index map[uint32]string
string map[string]uint32
}
}
func (t *NameTable) LookupString(index uint32) string {
t.initialize()
return t.table.index[index]
}
func (t *NameTable) LookupIndex(str string) uint32 {
t.initialize()
return t.table.string[str]
}
// QueryNameTable Query a perflib name table from the v1. Specify the type and the language
// code (i.e. "Counter 009" or "Help 009") for English language.
func QueryNameTable(tableName string) *NameTable {
return &NameTable{
name: tableName,
}
}
func (t *NameTable) initialize() {
t.once.Do(func() {
t.table.index = make(map[uint32]string)
t.table.string = make(map[string]uint32)
buffer, err := queryRawData(t.name)
if err != nil {
panic(err)
}
r := bytes.NewReader(buffer)
for {
index, err := readUTF16String(r)
if err != nil {
break
}
desc, err := readUTF16String(r)
if err != nil {
break
}
if err != nil {
panic(fmt.Sprint("Invalid index ", index))
}
indexInt, _ := strconv.Atoi(index)
t.table.index[uint32(indexInt)] = desc
t.table.string[desc] = uint32(indexInt)
}
})
}

View File

@@ -0,0 +1,493 @@
package registry
/*
Go bindings for the HKEY_PERFORMANCE_DATA perflib / Performance Counters interface.
# Overview
HKEY_PERFORMANCE_DATA is a low-level alternative to the higher-level PDH library and WMI.
It operates on blocks of counters and only returns raw values without calculating rates
or formatting them, which is exactly what you want for, say, a Prometheus exporter
(not so much for a GUI like Windows Performance Monitor).
Its overhead is much lower than the high-level libraries.
It operates on the same set of perflib providers as PDH and WMI. See this document
for more details on the relationship between the different libraries:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa371643(v=vs.85).aspx
Example C++ source code:
https://msdn.microsoft.com/de-de/library/windows/desktop/aa372138(v=vs.85).aspx
For now, the API is not stable and is probably going to change in future
perflib_exporter releases. If you want to use this library, send the author an email
so we can discuss your requirements and stabilize the API.
# Names
Counter names and help texts are resolved by looking up an index in a name table.
Since Microsoft loves internalization, both names and help texts can be requested
any locally available language.
The library automatically loads the name tables and resolves all identifiers
in English ("Name" and "HelpText" struct members). You can manually resolve
identifiers in a different language by using the NameTable API.
# Performance Counters intro
Windows has a system-wide performance counter mechanism. Most performance counters
are stored as actual counters, not gauges (with some exceptions).
There's additional metadata which defines how the counter should be presented to the user
(for example, as a calculated rate). This library disregards all of the display metadata.
At the top level, there's a number of performance counter objects.
Each object has counter definitions, which contain the metadata for a particular
counter, and either zero or multiple instances. We hide the fact that there are
objects with no instances, and simply return a single null instance.
There's one counter per counter definition and instance (or the object itself, if
there are no instances).
Behind the scenes, every perflib DLL provides one or more objects.
Perflib has a v1 where DLLs are dynamically registered and
unregistered. Some third party applications like VMWare provide their own counters,
but this is, sadly, a rare occurrence.
Different Windows releases have different numbers of counters.
Objects and counters are identified by well-known indices.
Here's an example object with one instance:
4320 WSMan Quota Statistics [7 counters, 1 instance(s)]
`-- "WinRMService"
`-- Total Requests/Second [4322] = 59
`-- User Quota Violations/Second [4324] = 0
`-- System Quota Violations/Second [4326] = 0
`-- Active Shells [4328] = 0
`-- Active Operations [4330] = 0
`-- Active Users [4332] = 0
`-- Process ID [4334] = 928
All "per second" metrics are counters, the rest are gauges.
Another example, with no instance:
4600 Network QoS Policy [6 counters, 1 instance(s)]
`-- (default)
`-- Packets transmitted [4602] = 1744
`-- Packets transmitted/sec [4604] = 4852
`-- Bytes transmitted [4606] = 4853
`-- Bytes transmitted/sec [4608] = 180388626632
`-- Packets dropped [4610] = 0
`-- Packets dropped/sec [4612] = 0
You can access the same values using PowerShell's Get-Counter cmdlet
or the Performance Monitor.
> Get-Counter '\WSMan Quota Statistics(WinRMService)\Process ID'
Timestamp CounterSamples
--------- --------------
1/28/2018 10:18:00 PM \\DEV\wsman quota statistics(winrmservice)\process id :
928
> (Get-Counter '\Process(Idle)\% Processor Time').CounterSamples[0] | Format-List *
[..detailed output...]
Data for some of the objects is also available through WMI:
> Get-CimInstance Win32_PerfRawData_Counters_WSManQuotaStatistics
Name : WinRMService
[...]
ActiveOperations : 0
ActiveShells : 0
ActiveUsers : 0
ProcessID : 928
SystemQuotaViolationsPerSecond : 0
TotalRequestsPerSecond : 59
UserQuotaViolationsPerSecond : 0
*/
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
"time"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"golang.org/x/sys/windows"
)
// There's a LittleEndian field in the PERF header - we ought to check it.
//
//nolint:gochecknoglobals
var bo = binary.LittleEndian
// PerfObject Top-level performance object (like "Process").
type PerfObject struct {
Name string
// NameIndex Same index you pass to QueryPerformanceData
NameIndex uint
Instances []*PerfInstance
CounterDefs []*PerfCounterDef
Frequency int64
rawData *perfObjectType
}
// PerfInstance Each object can have multiple instances. For example,
// In case the object has no instances, we return one single PerfInstance with an empty name.
type PerfInstance struct {
// *not* resolved using a name table
Name string
Counters []*PerfCounter
rawData *perfInstanceDefinition
rawCounterBlock *perfCounterBlock
}
type PerfCounterDef struct {
Name string
NameIndex uint
// For debugging - subject to removal. CounterType is a perflib
// implementation detail (see perflib.h) and should not be used outside
// of this package. We export it so we can show it on /dump.
CounterType uint32
// PERF_TYPE_COUNTER (otherwise, it's a gauge)
IsCounter bool
// PERF_COUNTER_BASE (base value of a multi-value fraction)
IsBaseValue bool
// PERF_TIMER_100NS
IsNanosecondCounter bool
HasSecondValue bool
rawData *perfCounterDefinition
}
type PerfCounter struct {
Value int64
Def *PerfCounterDef
SecondValue int64
}
//nolint:gochecknoglobals
var (
bufLenGlobal = uint32(400000)
bufLenCostly = uint32(2000000)
)
// queryRawData Queries the performance counter buffer using RegQueryValueEx, returning raw bytes. See:
// https://msdn.microsoft.com/de-de/library/windows/desktop/aa373219(v=vs.85).aspx
func queryRawData(query string) ([]byte, error) {
var (
valType uint32
buffer []byte
bufLen uint32
)
switch query {
case "Global":
bufLen = bufLenGlobal
case "Costly":
bufLen = bufLenCostly
default:
// depends on the number of values requested
// need make an educated guess
numCounters := len(strings.Split(query, " "))
bufLen = uint32(150000 * numCounters)
}
buffer = make([]byte, bufLen)
name, err := windows.UTF16PtrFromString(query)
if err != nil {
return nil, fmt.Errorf("failed to encode query string: %w", err)
}
for {
bufLen := uint32(len(buffer))
err := windows.RegQueryValueEx(
windows.HKEY_PERFORMANCE_DATA,
name,
nil,
&valType,
(*byte)(unsafe.Pointer(&buffer[0])),
&bufLen)
switch {
case errors.Is(err, error(windows.ERROR_MORE_DATA)):
newBuffer := make([]byte, len(buffer)+16384)
copy(newBuffer, buffer)
buffer = newBuffer
continue
case errors.Is(err, error(windows.ERROR_BUSY)):
time.Sleep(50 * time.Millisecond)
continue
case err != nil:
var errNo windows.Errno
if errors.As(err, &errNo) {
return nil, fmt.Errorf("ReqQueryValueEx failed: %w errno %d", err, uint(errNo))
}
return nil, err
}
buffer = buffer[:bufLen]
switch query {
case "Global":
if bufLen > bufLenGlobal {
bufLenGlobal = bufLen
}
case "Costly":
if bufLen > bufLenCostly {
bufLenCostly = bufLen
}
}
return buffer, nil
}
}
/*
QueryPerformanceData Query all performance counters that match a given query.
The query can be any of the following:
- "Global" (all performance counters except those Windows marked as costly)
- "Costly" (only the costly ones)
- One or more object indices, separated by spaces ("238 2 5")
Many objects have dependencies - if you query one of them, you often get back
more than you asked for.
*/
func QueryPerformanceData(query string, counterName string) ([]*PerfObject, error) {
buffer, err := queryRawData(query)
if err != nil {
return nil, err
}
r := bytes.NewReader(buffer)
// Read global header
header := new(perfDataBlock)
err = header.BinaryReadFrom(r)
if err != nil {
return nil, fmt.Errorf("failed to read performance data block for %q with: %w", query, err)
}
// Check for "PERF" signature
if header.Signature != [4]uint16{80, 69, 82, 70} {
panic("Invalid performance block header")
}
// Parse the performance data
numObjects := int(header.NumObjectTypes)
numFilteredObjects := 0
objects := make([]*PerfObject, numObjects)
objOffset := int64(header.HeaderLength)
for i := range numObjects {
_, err := r.Seek(objOffset, io.SeekStart)
if err != nil {
return nil, err
}
obj := new(perfObjectType)
err = obj.BinaryReadFrom(r)
if err != nil {
return nil, err
}
perfCounterName := obj.LookupName()
if counterName != "" && perfCounterName != counterName {
objOffset += int64(obj.TotalByteLength)
continue
}
numCounterDefs := int(obj.NumCounters)
numInstances := int(obj.NumInstances)
// Perf objects can have no instances. The perflib differentiates
// between objects with instances and without, but we just create
// an empty instance in order to simplify the interface.
if numInstances <= 0 {
numInstances = 1
}
instances := make([]*PerfInstance, numInstances)
counterDefs := make([]*PerfCounterDef, numCounterDefs)
objects[i] = &PerfObject{
Name: perfCounterName,
NameIndex: uint(obj.ObjectNameTitleIndex),
Instances: instances,
CounterDefs: counterDefs,
Frequency: obj.PerfFreq,
rawData: obj,
}
for i := range numCounterDefs {
def := new(perfCounterDefinition)
err := def.BinaryReadFrom(r)
if err != nil {
return nil, err
}
counterDefs[i] = &PerfCounterDef{
Name: def.LookupName(),
NameIndex: uint(def.CounterNameTitleIndex),
rawData: def,
CounterType: def.CounterType,
IsCounter: def.CounterType&0x400 == 0x400,
IsBaseValue: def.CounterType&0x00030000 == 0x00030000,
IsNanosecondCounter: def.CounterType&0x00100000 == 0x00100000,
HasSecondValue: def.CounterType == pdh.PERF_AVERAGE_BULK,
}
}
if obj.NumInstances <= 0 { //nolint:nestif
blockOffset := objOffset + int64(obj.DefinitionLength)
if _, err := r.Seek(blockOffset, io.SeekStart); err != nil {
return nil, err
}
_, counters, err := parseCounterBlock(buffer, r, blockOffset, counterDefs)
if err != nil {
return nil, err
}
instances[0] = &PerfInstance{
Name: "",
Counters: counters,
rawData: nil,
rawCounterBlock: nil,
}
} else {
instOffset := objOffset + int64(obj.DefinitionLength)
for i := range numInstances {
if _, err := r.Seek(instOffset, io.SeekStart); err != nil {
return nil, err
}
inst := new(perfInstanceDefinition)
if err = inst.BinaryReadFrom(r); err != nil {
return nil, err
}
name, _ := readUTF16StringAtPos(r, instOffset+int64(inst.NameOffset), inst.NameLength)
pos := instOffset + int64(inst.ByteLength)
offset, counters, err := parseCounterBlock(buffer, r, pos, counterDefs)
if err != nil {
return nil, err
}
instances[i] = &PerfInstance{
Name: name,
Counters: counters,
rawData: inst,
}
instOffset = pos + offset
}
}
if counterName != "" {
return objects[i : i+1], nil
}
// Next perfObjectType
objOffset += int64(obj.TotalByteLength)
numFilteredObjects++
}
return objects[:numFilteredObjects], nil
}
func parseCounterBlock(b []byte, r io.ReadSeeker, pos int64, defs []*PerfCounterDef) (int64, []*PerfCounter, error) {
_, err := r.Seek(pos, io.SeekStart)
if err != nil {
return 0, nil, err
}
block := new(perfCounterBlock)
err = block.BinaryReadFrom(r)
if err != nil {
return 0, nil, err
}
counters := make([]*PerfCounter, len(defs))
for i, def := range defs {
valueOffset := pos + int64(def.rawData.CounterOffset)
value := convertCounterValue(def.rawData, b, valueOffset)
secondValue := int64(0)
if def.HasSecondValue {
secondValue = convertCounterValue(def.rawData, b, valueOffset+8)
}
counters[i] = &PerfCounter{
Value: value,
Def: def,
SecondValue: secondValue,
}
}
return int64(block.ByteLength), counters, nil
}
func convertCounterValue(counterDef *perfCounterDefinition, buffer []byte, valueOffset int64) int64 {
/*
We can safely ignore the type since we're not interested in anything except the raw value.
We also ignore all of the other attributes (timestamp, presentation, multi counter values...)
See also: winperf.h.
Here's the most common value for CounterType:
65536 32bit counter
65792 64bit counter
272696320 32bit rate
272696576 64bit rate
*/
switch counterDef.CounterSize {
case 4:
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
case 8:
return int64(bo.Uint64(buffer[valueOffset:(valueOffset + 8)]))
default:
return int64(bo.Uint32(buffer[valueOffset:(valueOffset + 4)]))
}
}

View File

@@ -0,0 +1,11 @@
package registry
import (
"testing"
)
func BenchmarkQueryPerformanceData(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = QueryPerformanceData("Global", "")
}
}

View File

@@ -0,0 +1,173 @@
package registry
import (
"encoding/binary"
"io"
"golang.org/x/sys/windows"
)
/*
perfDataBlock
See: https://msdn.microsoft.com/de-de/library/windows/desktop/aa373157(v=vs.85).aspx
typedef struct _PERF_DATA_BLOCK {
WCHAR Signature[4];
DWORD LittleEndian;
DWORD Version;
DWORD Revision;
DWORD TotalByteLength;
DWORD HeaderLength;
DWORD NumObjectTypes;
DWORD DefaultObject;
SYSTEMTIME SystemTime;
LARGE_INTEGER PerfTime;
LARGE_INTEGER PerfFreq;
LARGE_INTEGER PerfTime100nSec;
DWORD SystemNameLength;
DWORD SystemNameOffset;
} PERF_DATA_BLOCK;
*/
type perfDataBlock struct {
Signature [4]uint16
LittleEndian uint32
Version uint32
Revision uint32
TotalByteLength uint32
HeaderLength uint32
NumObjectTypes uint32
DefaultObject int32
SystemTime windows.Systemtime
_ uint32 // unknown field
PerfTime int64
PerfFreq int64
PerfTime100nSec int64
SystemNameLength uint32
SystemNameOffset uint32
}
func (p *perfDataBlock) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
perfObjectType
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373160(v=vs.85).aspx
typedef struct _PERF_OBJECT_TYPE {
DWORD TotalByteLength;
DWORD DefinitionLength;
DWORD HeaderLength;
DWORD ObjectNameTitleIndex;
LPWSTR ObjectNameTitle;
DWORD ObjectHelpTitleIndex;
LPWSTR ObjectHelpTitle;
DWORD DetailLevel;
DWORD NumCounters;
DWORD DefaultCounter;
DWORD NumInstances;
DWORD CodePage;
LARGE_INTEGER PerfTime;
LARGE_INTEGER PerfFreq;
} PERF_OBJECT_TYPE;
*/
type perfObjectType struct {
TotalByteLength uint32
DefinitionLength uint32
HeaderLength uint32
ObjectNameTitleIndex uint32
ObjectNameTitle uint32
ObjectHelpTitleIndex uint32
ObjectHelpTitle uint32
DetailLevel uint32
NumCounters uint32
DefaultCounter int32
NumInstances int32
CodePage uint32
PerfTime int64
PerfFreq int64
}
func (p *perfObjectType) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
perfCounterDefinition
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373150(v=vs.85).aspx
typedef struct _PERF_COUNTER_DEFINITION {
DWORD ByteLength;
DWORD CounterNameTitleIndex;
LPWSTR CounterNameTitle;
DWORD CounterHelpTitleIndex;
LPWSTR CounterHelpTitle;
LONG DefaultScale;
DWORD DetailLevel;
DWORD CounterType;
DWORD CounterSize;
DWORD CounterOffset;
} PERF_COUNTER_DEFINITION;
*/
type perfCounterDefinition struct {
ByteLength uint32
CounterNameTitleIndex uint32
CounterNameTitle uint32
CounterHelpTitleIndex uint32
CounterHelpTitle uint32
DefaultScale int32
DetailLevel uint32
CounterType uint32
CounterSize uint32
CounterOffset uint32
}
func (p *perfCounterDefinition) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
func (p *perfCounterDefinition) LookupName() string {
return CounterNameTable.LookupString(p.CounterNameTitleIndex)
}
/*
perfCounterBlock
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373147(v=vs.85).aspx
typedef struct _PERF_COUNTER_BLOCK {
DWORD ByteLength;
} PERF_COUNTER_BLOCK;
*/
type perfCounterBlock struct {
ByteLength uint32
}
func (p *perfCounterBlock) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}
/*
perfInstanceDefinition
See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa373159(v=vs.85).aspx
typedef struct _PERF_INSTANCE_DEFINITION {
DWORD ByteLength;
DWORD ParentObjectTitleIndex;
DWORD ParentObjectInstance;
DWORD UniqueID;
DWORD NameOffset;
DWORD NameLength;
} PERF_INSTANCE_DEFINITION;
*/
type perfInstanceDefinition struct {
ByteLength uint32
ParentObjectTitleIndex uint32
ParentObjectInstance uint32
UniqueID uint32
NameOffset uint32
NameLength uint32
}
func (p *perfInstanceDefinition) BinaryReadFrom(r io.Reader) error {
return binary.Read(r, bo, p)
}

View File

@@ -0,0 +1,49 @@
package registry
import (
"encoding/binary"
"io"
"golang.org/x/sys/windows"
)
// readUTF16StringAtPos Read an unterminated UTF16 string at a given position, specifying its length.
func readUTF16StringAtPos(r io.ReadSeeker, absPos int64, length uint32) (string, error) {
value := make([]uint16, length/2)
_, err := r.Seek(absPos, io.SeekStart)
if err != nil {
return "", err
}
err = binary.Read(r, bo, value)
if err != nil {
return "", err
}
return windows.UTF16ToString(value), nil
}
// readUTF16String Reads a null-terminated UTF16 string at the current offset.
func readUTF16String(r io.Reader) (string, error) {
var err error
b := make([]byte, 2)
out := make([]uint16, 0, 100)
for i := 0; err == nil; i += 2 {
_, err = r.Read(b)
if b[0] == 0 && b[1] == 0 {
break
}
out = append(out, bo.Uint16(b))
}
if err != nil {
return "", err
}
return windows.UTF16ToString(out), nil
}

View File

@@ -0,0 +1,9 @@
package registry
import (
"strconv"
)
func MapCounterToIndex(name string) string {
return strconv.Itoa(int(CounterNameTable.LookupIndex(name)))
}

139
internal/pdh/types.go Normal file
View File

@@ -0,0 +1,139 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package pdh
import (
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
const (
InstanceEmpty = "------"
InstanceTotal = "_Total"
)
type CounterValue struct {
Type prometheus.ValueType
FirstValue float64
SecondValue float64
}
// FmtCounterValueDouble is a union specialization for double values.
type FmtCounterValueDouble struct {
CStatus uint32
DoubleValue float64
}
// FmtCounterValueLarge is a union specialization for 64-bit integer values.
type FmtCounterValueLarge struct {
CStatus uint32
LargeValue int64
}
// FmtCounterValueLong is a union specialization for long values.
type FmtCounterValueLong struct {
CStatus uint32
LongValue int32
padding [4]byte //nolint:unused // Memory reservation
}
// FmtCounterValueItemDouble is a union specialization for double values, used by GetFormattedCounterArrayDouble.
type FmtCounterValueItemDouble struct {
SzName *uint16
FmtValue FmtCounterValueDouble
}
// FmtCounterValueItemLarge is a union specialization for 'large' values, used by PdhGetFormattedCounterArrayLarge().
type FmtCounterValueItemLarge struct {
SzName *uint16 // pointer to a string
FmtValue FmtCounterValueLarge
}
// FmtCounterValueItemLong is a union specialization for long values, used by PdhGetFormattedCounterArrayLong().
type FmtCounterValueItemLong struct {
SzName *uint16 // pointer to a string
FmtValue FmtCounterValueLong
}
// CounterInfo structure contains information describing the properties of a counter. This information also includes the counter path.
type CounterInfo 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 <a "href=http://go.microsoft.com/fwlink/p/?linkid=84422">Windows Server 2003 Deployment Kit</a>.
// 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 <a href="https://msdn.microsoft.com/en-us/library/windows/desktop/aa371894(v=vs.85).aspx">Checking PDH Interface Return Values</a>.
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 AddCounter.
DwUserData *uint32
// The value passed in the dwUserData parameter when calling OpenQuery.
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 RawCounter structure returns the data as it was collected from the counter provider.
// No translation, formatting, or other interpretation is performed on the data.
type RawCounter 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 RawCounterItem 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 RawCounter structure that contains the raw counter value of the instance
RawValue RawCounter
}

View File

@@ -0,0 +1,6 @@
package types
type Collector interface {
Collect(dst any) error
Close()
}