Files
windows_exporter/internal/perfdata/collector.go
Jan-Otto Kröpke c8eeb595c0 feat: Support OpenMetrics (#1772)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
2024-11-26 19:43:52 +01:00

346 lines
8.9 KiB
Go

// 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 perfdata
import (
"errors"
"fmt"
"slices"
"strings"
"sync"
"unsafe"
"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
collectCh chan struct{}
counterValuesCh chan CounterValues
errorCh chan error
}
type Counter struct {
Name string
Desc string
Instances map[string]pdhCounterHandle
Type uint32
Frequency int64
}
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{InstanceEmpty}
}
collector := &Collector{
object: object,
counters: make(map[string]Counter, len(counters)),
handle: handle,
totalCounterRequested: slices.Contains(instances, InstanceTotal),
mu: sync.RWMutex{},
}
errs := make([]error, 0, len(counters))
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 {
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 := PdhGetCounterInfo(counterHandle, 0, &bufLen, nil); ret != PdhMoreData {
errs = append(errs, fmt.Errorf("PdhGetCounterInfo: %w", NewPdhError(ret)))
continue
}
buf := make([]byte, bufLen)
if ret := PdhGetCounterInfo(counterHandle, 0, &bufLen, &buf[0]); ret != ErrorSuccess {
errs = append(errs, fmt.Errorf("PdhGetCounterInfo: %w", NewPdhError(ret)))
continue
}
ci := (*PdhCounterInfo)(unsafe.Pointer(&buf[0]))
counter.Type = ci.DwType
counter.Desc = windows.UTF16PtrToString(ci.SzExplainText)
if counter.Type == PERF_ELAPSED_TIME {
if ret := PdhGetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess {
errs = append(errs, fmt.Errorf("PdhGetCounterTimeBase: %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 struct{})
collector.counterValuesCh = make(chan CounterValues)
collector.errorCh = make(chan error)
go collector.collectRoutine()
if _, err := collector.Collect(); 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() (CounterValues, error) {
if c == nil {
return CounterValues{}, ErrPerformanceCounterNotInitialized
}
c.mu.RLock()
defer c.mu.RUnlock()
if len(c.counters) == 0 || c.handle == 0 || c.collectCh == nil || c.counterValuesCh == nil || c.errorCh == nil {
return nil, ErrPerformanceCounterNotInitialized
}
c.collectCh <- struct{}{}
return <-c.counterValuesCh, <-c.errorCh
}
func (c *Collector) collectRoutine() {
var (
itemCount uint32
bytesNeeded uint32
)
buf := make([]byte, 1)
for range c.collectCh {
if ret := PdhCollectQueryData(c.handle); ret != ErrorSuccess {
c.counterValuesCh <- nil
c.errorCh <- fmt.Errorf("failed to collect query data: %w", NewPdhError(ret))
continue
}
counterValues, err := (func() (CounterValues, error) {
var data CounterValues
for _, counter := range c.counters {
for _, instance := range counter.Instances {
// Get the info with the current buffer size
bytesNeeded = uint32(cap(buf))
for {
ret := PdhGetRawCounterArray(instance, &bytesNeeded, &itemCount, &buf[0])
if ret == ErrorSuccess {
break
}
if err := NewPdhError(ret); ret != PdhMoreData && !isKnownCounterDataError(err) {
return nil, fmt.Errorf("PdhGetRawCounterArray: %w", err)
}
if bytesNeeded <= uint32(cap(buf)) {
return nil, fmt.Errorf("PdhGetRawCounterArray 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((*PdhRawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
if data == nil {
data = make(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, InstanceTotal) && !c.totalCounterRequested {
continue
}
if instanceName == "" || instanceName == "*" {
instanceName = InstanceEmpty
}
if _, ok := data[instanceName]; !ok {
data[instanceName] = make(map[string]CounterValue, len(c.counters))
}
values := CounterValue{
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
// Ref: https://learn.microsoft.com/en-us/windows/win32/perfctrs/calculating-counter-values
switch counter.Type {
case PERF_ELAPSED_TIME:
values.FirstValue = float64((item.RawValue.FirstValue - WindowsEpoch) / counter.Frequency)
case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER:
values.FirstValue = float64(item.RawValue.FirstValue) * TicksToSecondScaleFactor
case PERF_AVERAGE_BULK, PERF_RAW_FRACTION:
values.FirstValue = float64(item.RawValue.FirstValue)
values.SecondValue = float64(item.RawValue.SecondValue)
default:
values.FirstValue = float64(item.RawValue.FirstValue)
}
data[instanceName][counter.Name] = values
}
}
}
}
return data, nil
})()
if err == nil && len(counterValues) == 0 {
err = ErrNoData
}
c.counterValuesCh <- counterValues
c.errorCh <- err
}
}
func (c *Collector) Close() {
if c == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
PdhCloseQuery(c.handle)
c.handle = 0
close(c.collectCh)
close(c.counterValuesCh)
close(c.errorCh)
c.counterValuesCh = nil
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 == PdhInvalidData ||
pdhErr.ErrorCode == PdhCalcNegativeDenominator ||
pdhErr.ErrorCode == PdhCalcNegativeValue ||
pdhErr.ErrorCode == PdhCstatusInvalidData ||
pdhErr.ErrorCode == PdhCstatusNoInstance ||
pdhErr.ErrorCode == PdhNoData)
}