chore: Remove registry based perfdata collector (#1742)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2024-11-17 21:51:12 +01:00
committed by GitHub
parent 6206b695c6
commit e6a15d4ec4
213 changed files with 8079 additions and 12405 deletions

View File

@@ -1,24 +1,30 @@
//go:build windows
package v2
package perfdata
import (
"errors"
"fmt"
"slices"
"strings"
"sync"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
var (
InstanceAll = []string{"*"}
InstanceTotal = []string{"_Total"}
)
type Collector struct {
object string
counters map[string]Counter
handle pdhQueryHandle
totalCounterRequested bool
mu sync.RWMutex
}
type Counter struct {
@@ -37,7 +43,7 @@ func NewCollector(object string, instances []string, counters []string) (*Collec
}
if len(instances) == 0 {
instances = []string{perftypes.EmptyInstance}
instances = []string{EmptyInstance}
}
collector := &Collector{
@@ -45,6 +51,7 @@ func NewCollector(object string, instances []string, counters []string) (*Collec
counters: make(map[string]Counter, len(counters)),
handle: handle,
totalCounterRequested: slices.Contains(instances, "_Total"),
mu: sync.RWMutex{},
}
for _, counterName := range counters {
@@ -90,7 +97,7 @@ func NewCollector(object string, instances []string, counters []string) (*Collec
counter.Type = ci.DwType
counter.Desc = windows.UTF16PtrToString(ci.SzExplainText)
if counter.Type == perftypes.PERF_ELAPSED_TIME {
if counter.Type == PERF_ELAPSED_TIME {
if ret := PdhGetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess {
return nil, fmt.Errorf("PdhGetCounterTimeBase: %w", NewPdhError(ret))
}
@@ -121,16 +128,23 @@ func (c *Collector) Describe() map[string]string {
return desc
}
func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, error) {
func (c *Collector) Collect() (map[string]map[string]CounterValues, error) {
if len(c.counters) == 0 {
return map[string]map[string]perftypes.CounterValues{}, nil
return map[string]map[string]CounterValues{}, nil
}
c.mu.RLock()
defer c.mu.RUnlock()
if c.handle == 0 {
return map[string]map[string]CounterValues{}, nil
}
if ret := PdhCollectQueryData(c.handle); ret != ErrorSuccess {
return nil, fmt.Errorf("failed to collect query data: %w", NewPdhError(ret))
}
var data map[string]map[string]perftypes.CounterValues
var data map[string]map[string]CounterValues
for _, counter := range c.counters {
for _, instance := range counter.Instances {
@@ -159,11 +173,11 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er
items := unsafe.Slice((*PdhRawCounterItem)(unsafe.Pointer(&buf[0])), itemCount)
if data == nil {
data = make(map[string]map[string]perftypes.CounterValues, itemCount)
data = make(map[string]map[string]CounterValues, itemCount)
}
var metricType prometheus.ValueType
if val, ok := perftypes.SupportedCounterTypes[counter.Type]; ok {
if val, ok := SupportedCounterTypes[counter.Type]; ok {
metricType = val
} else {
metricType = prometheus.GaugeValue
@@ -177,14 +191,14 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er
}
if instanceName == "" || instanceName == "*" {
instanceName = perftypes.EmptyInstance
instanceName = EmptyInstance
}
if _, ok := data[instanceName]; !ok {
data[instanceName] = make(map[string]perftypes.CounterValues, len(c.counters))
data[instanceName] = make(map[string]CounterValues, len(c.counters))
}
values := perftypes.CounterValues{
values := CounterValues{
Type: metricType,
}
@@ -193,11 +207,11 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er
// Ref: https://learn.microsoft.com/en-us/windows/win32/perfctrs/calculating-counter-values
switch counter.Type {
case perftypes.PERF_ELAPSED_TIME:
values.FirstValue = float64((item.RawValue.FirstValue - perftypes.WindowsEpoch) / counter.Frequency)
case perftypes.PERF_100NSEC_TIMER, perftypes.PERF_PRECISION_100NS_TIMER:
values.FirstValue = float64(item.RawValue.FirstValue) * perftypes.TicksToSecondScaleFactor
case perftypes.PERF_AVERAGE_BULK:
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:
values.FirstValue = float64(item.RawValue.FirstValue)
values.SecondValue = float64(item.RawValue.SecondValue)
default:
@@ -214,13 +228,18 @@ func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, er
}
func (c *Collector) Close() {
c.mu.Lock()
defer c.mu.Unlock()
PdhCloseQuery(c.handle)
c.handle = 0
}
func formatCounterPath(object, instance, counterName string) string {
var counterPath string
if instance == perftypes.EmptyInstance {
if instance == EmptyInstance {
counterPath = fmt.Sprintf(`\%s\%s`, object, counterName)
} else {
counterPath = fmt.Sprintf(`\%s(%s)\%s`, object, instance, counterName)

View File

@@ -1,9 +1,11 @@
package v2_test
//go:build windows
package perfdata_test
import (
"testing"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata/v2"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata"
"github.com/stretchr/testify/require"
)

View File

@@ -1,12 +1,12 @@
//go:build windows
package v2_test
package perfdata_test
import (
"testing"
"time"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata/v2"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@@ -1,4 +1,6 @@
package perftypes
//go:build windows
package perfdata
import "github.com/prometheus/client_golang/prometheus"

View File

@@ -1,4 +1,6 @@
package v2
//go:build windows
package perfdata
import "errors"

View File

@@ -31,7 +31,7 @@
//go:build windows
package v2
package perfdata
import (
"fmt"

View File

@@ -31,7 +31,7 @@
//go:build windows
package v2
package perfdata
import "golang.org/x/sys/windows"

View File

@@ -31,7 +31,7 @@
//go:build windows
package v2
package perfdata
import "golang.org/x/sys/windows"

View File

@@ -1,39 +0,0 @@
package perfdata
import (
"errors"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
v1 "github.com/prometheus-community/windows_exporter/internal/perfdata/v1"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata/v2"
)
type Collector interface {
Describe() map[string]string
Collect() (map[string]map[string]perftypes.CounterValues, error)
Close()
}
type Engine int
const (
_ Engine = iota
V1
V2
)
var (
ErrUnknownEngine = errors.New("unknown engine")
AllInstances = []string{"*"}
)
func NewCollector(engine Engine, object string, instances []string, counters []string) (Collector, error) { //nolint:ireturn
switch engine {
case V1:
return v1.NewCollector(object, instances, counters)
case V2:
return v2.NewCollector(object, instances, counters)
default:
return nil, ErrUnknownEngine
}
}

View File

@@ -1,11 +1,11 @@
package perftypes
//go:build windows
package perfdata
import "github.com/prometheus/client_golang/prometheus"
const EmptyInstance = "------"
var TotalInstance = []string{"_Total"}
type CounterValues struct {
Type prometheus.ValueType
FirstValue float64

View File

@@ -1,21 +0,0 @@
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

@@ -1,115 +0,0 @@
package v1
import (
"fmt"
"strings"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
"github.com/prometheus/client_golang/prometheus"
)
type Collector struct {
object string
query string
}
type Counter struct {
Name string
Desc string
Instances map[string]uint32
Type uint32
Frequency float64
}
func NewCollector(object string, _ []string, _ []string) (*Collector, error) {
collector := &Collector{
object: object,
query: MapCounterToIndex(object),
}
if _, err := collector.Collect(); 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() (map[string]map[string]perftypes.CounterValues, error) {
perfObjects, err := QueryPerformanceData(c.query, c.object)
if err != nil {
return nil, fmt.Errorf("QueryPerformanceData: %w", err)
}
if len(perfObjects) == 0 || perfObjects[0] == nil || len(perfObjects[0].Instances) == 0 {
return map[string]map[string]perftypes.CounterValues{}, nil
}
data := make(map[string]map[string]perftypes.CounterValues, len(perfObjects[0].Instances))
for _, perfObject := range perfObjects {
if perfObject.Name != c.object {
continue
}
for _, perfInstance := range perfObject.Instances {
instanceName := perfInstance.Name
if strings.HasSuffix(instanceName, "_Total") {
continue
}
if instanceName == "" || instanceName == "*" {
instanceName = perftypes.EmptyInstance
}
if _, ok := data[instanceName]; !ok {
data[instanceName] = make(map[string]perftypes.CounterValues, len(perfInstance.Counters))
}
for _, perfCounter := range perfInstance.Counters {
if perfCounter.Def.IsBaseValue && !perfCounter.Def.IsNanosecondCounter {
continue
}
if _, ok := data[instanceName][perfCounter.Def.Name]; !ok {
data[instanceName][perfCounter.Def.Name] = perftypes.CounterValues{
Type: prometheus.GaugeValue,
}
}
var metricType prometheus.ValueType
if val, ok := perftypes.SupportedCounterTypes[perfCounter.Def.CounterType]; ok {
metricType = val
} else {
metricType = prometheus.GaugeValue
}
values := perftypes.CounterValues{
Type: metricType,
}
switch perfCounter.Def.CounterType {
case perftypes.PERF_ELAPSED_TIME:
values.FirstValue = float64(perfCounter.Value-perftypes.WindowsEpoch) / float64(perfObject.Frequency)
values.SecondValue = float64(perfCounter.SecondValue-perftypes.WindowsEpoch) / float64(perfObject.Frequency)
case perftypes.PERF_100NSEC_TIMER, perftypes.PERF_PRECISION_100NS_TIMER:
values.FirstValue = float64(perfCounter.Value) * perftypes.TicksToSecondScaleFactor
values.SecondValue = float64(perfCounter.SecondValue) * perftypes.TicksToSecondScaleFactor
default:
values.FirstValue = float64(perfCounter.Value)
values.SecondValue = float64(perfCounter.SecondValue)
}
data[instanceName][perfCounter.Def.Name] = values
}
}
}
return data, nil
}
func (c *Collector) Close() {
}

View File

@@ -1,85 +0,0 @@
package v1
import (
"bytes"
"fmt"
"strconv"
"sync"
)
// Initialize global name tables
// TODO: 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)
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

@@ -1,490 +0,0 @@
package v1
/*
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/perfdata/perftypes"
"golang.org/x/sys/windows"
)
// TODO: There's a LittleEndian field in the PERF header - we ought to check it.
var bo = binary.LittleEndian
// 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
}
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:
// TODO: 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 == perftypes.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

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

View File

@@ -1,173 +0,0 @@
package v1
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 // TODO
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

@@ -1,117 +0,0 @@
package v1
import (
"errors"
"fmt"
"log/slog"
"reflect"
"strings"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
)
func UnmarshalObject(obj *PerfObject, vs interface{}, logger *slog.Logger) error {
if obj == nil {
return errors.New("counter not found")
}
rv := reflect.ValueOf(vs)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return fmt.Errorf("%v is nil or not a pointer to slice", reflect.TypeOf(vs))
}
ev := rv.Elem()
if ev.Kind() != reflect.Slice {
return fmt.Errorf("%v is not slice", reflect.TypeOf(vs))
}
// Ensure sufficient length
if ev.Cap() < len(obj.Instances) {
nvs := reflect.MakeSlice(ev.Type(), len(obj.Instances), len(obj.Instances))
ev.Set(nvs)
}
for idx, instance := range obj.Instances {
target := ev.Index(idx)
rt := target.Type()
counters := make(map[string]*PerfCounter, len(instance.Counters))
for _, ctr := range instance.Counters {
if ctr.Def.IsBaseValue && !ctr.Def.IsNanosecondCounter {
counters[ctr.Def.Name+"_Base"] = ctr
} else {
counters[ctr.Def.Name] = ctr
}
}
for i := range target.NumField() {
f := rt.Field(i)
tag := f.Tag.Get("perflib")
if tag == "" {
continue
}
secondValue := false
st := strings.Split(tag, ",")
tag = st[0]
for _, t := range st {
if t == "secondvalue" {
secondValue = true
}
}
ctr, found := counters[tag]
if !found {
logger.Debug(fmt.Sprintf("missing counter %q, have %v", tag, counterMapKeys(counters)))
continue
}
if !target.Field(i).CanSet() {
return fmt.Errorf("tagged field %v cannot be written to", f.Name)
}
if fieldType := target.Field(i).Type(); fieldType != reflect.TypeOf((*float64)(nil)).Elem() {
return fmt.Errorf("tagged field %v has wrong type %v, must be float64", f.Name, fieldType)
}
if secondValue {
if !ctr.Def.HasSecondValue {
return fmt.Errorf("tagged field %v expected a SecondValue, which was not present", f.Name)
}
target.Field(i).SetFloat(float64(ctr.SecondValue))
continue
}
switch ctr.Def.CounterType {
case perftypes.PERF_ELAPSED_TIME:
target.Field(i).SetFloat(float64(ctr.Value-perftypes.WindowsEpoch) / float64(obj.Frequency))
case perftypes.PERF_100NSEC_TIMER, perftypes.PERF_PRECISION_100NS_TIMER:
target.Field(i).SetFloat(float64(ctr.Value) * perftypes.TicksToSecondScaleFactor)
default:
target.Field(i).SetFloat(float64(ctr.Value))
}
}
if instance.Name != "" && target.FieldByName("Name").CanSet() {
target.FieldByName("Name").SetString(instance.Name)
}
}
return nil
}
func counterMapKeys(m map[string]*PerfCounter) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}

View File

@@ -1,49 +0,0 @@
package v1
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

@@ -1,23 +0,0 @@
package v1
import (
"strconv"
)
func MapCounterToIndex(name string) string {
return strconv.Itoa(int(CounterNameTable.LookupIndex(name)))
}
func GetPerflibSnapshot(objNames string) (map[string]*PerfObject, error) {
objects, err := QueryPerformanceData(objNames, "")
if err != nil {
return nil, err
}
indexed := make(map[string]*PerfObject)
for _, obj := range objects {
indexed[obj.Name] = obj
}
return indexed, nil
}

View File

@@ -1,136 +0,0 @@
package v1
import (
"io"
"log/slog"
"reflect"
"testing"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
)
type simple struct {
ValA float64 `perflib:"Something"`
ValB float64 `perflib:"Something Else"`
ValC float64 `perflib:"Something Else,secondvalue"`
}
func TestUnmarshalPerflib(t *testing.T) {
t.Parallel()
cases := []struct {
name string
obj *PerfObject
expectedOutput []simple
expectError bool
}{
{
name: "nil check",
obj: nil,
expectedOutput: []simple{},
expectError: true,
},
{
name: "Simple",
obj: &PerfObject{
Instances: []*PerfInstance{
{
Counters: []*PerfCounter{
{
Def: &PerfCounterDef{
Name: "Something",
CounterType: perftypes.PERF_COUNTER_COUNTER,
},
Value: 123,
},
},
},
},
},
expectedOutput: []simple{{ValA: 123}},
expectError: false,
},
{
name: "Multiple properties",
obj: &PerfObject{
Instances: []*PerfInstance{
{
Counters: []*PerfCounter{
{
Def: &PerfCounterDef{
Name: "Something",
CounterType: perftypes.PERF_COUNTER_COUNTER,
},
Value: 123,
},
{
Def: &PerfCounterDef{
Name: "Something Else",
CounterType: perftypes.PERF_COUNTER_COUNTER,
HasSecondValue: true,
},
Value: 256,
SecondValue: 222,
},
},
},
},
},
expectedOutput: []simple{{ValA: 123, ValB: 256, ValC: 222}},
expectError: false,
},
{
name: "Multiple instances",
obj: &PerfObject{
Instances: []*PerfInstance{
{
Counters: []*PerfCounter{
{
Def: &PerfCounterDef{
Name: "Something",
CounterType: perftypes.PERF_COUNTER_COUNTER,
},
Value: 321,
},
},
},
{
Counters: []*PerfCounter{
{
Def: &PerfCounterDef{
Name: "Something",
CounterType: perftypes.PERF_COUNTER_COUNTER,
},
Value: 231,
},
},
},
},
},
expectedOutput: []simple{{ValA: 321}, {ValA: 231}},
expectError: false,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
output := make([]simple, 0)
err := UnmarshalObject(c.obj, &output, logger)
if err != nil && !c.expectError {
t.Errorf("Did not expect error, got %q", err)
}
if err == nil && c.expectError {
t.Errorf("Expected an error, but got ok")
}
if err == nil && !reflect.DeepEqual(output, c.expectedOutput) {
t.Errorf("Output mismatch, expected %+v, got %+v", c.expectedOutput, output)
}
})
}
}