mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-03-10 04:26:35 +00:00
process: Use registry collector for V1 data (#1814)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
459
internal/pdh/collector.go
Normal file
459
internal/pdh/collector.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user