*: Implement collector interface for registry perfdata (#1670)

This commit is contained in:
Jan-Otto Kröpke
2024-10-05 21:33:40 +02:00
committed by GitHub
parent 2a9a11bd01
commit 5952c51a39
53 changed files with 661 additions and 1267 deletions

View File

@@ -0,0 +1,40 @@
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{"*"}
)
//nolint:ireturn
func NewCollector(engine Engine, object string, instances []string, counters []string) (Collector, error) {
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,6 +1,4 @@
//go:build windows
package perfdata
package perftypes
import "github.com/prometheus/client_golang/prometheus"
@@ -57,7 +55,7 @@ const (
PERF_COUNTER_HISTOGRAM_TYPE = 0x80000000
)
var supportedCounterTypes = map[uint32]prometheus.ValueType{
var SupportedCounterTypes = map[uint32]prometheus.ValueType{
PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue,
PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue,

View File

@@ -0,0 +1,11 @@
package perftypes
import "github.com/prometheus/client_golang/prometheus"
const EmptyInstance = "------"
type CounterValues struct {
Type prometheus.ValueType
FirstValue float64
SecondValue float64
}

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,98 @@
package v1
import (
"fmt"
"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)
if err != nil {
return nil, fmt.Errorf("QueryPerformanceData: %w", err)
}
data := make(map[string]map[string]perftypes.CounterValues, len(perfObjects[0].Instances))
for _, perfObject := range perfObjects {
for _, perfInstance := range perfObject.Instances {
instanceName := perfInstance.Name
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 _, 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

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

@@ -0,0 +1,470 @@
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"
"unsafe"
"golang.org/x/sys/windows"
)
// TODO: There's a LittleEndian field in the PERF header - we ought to check it.
var bo = binary.LittleEndian
const averageCount64Type = 1073874176
// 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)
if errors.Is(err, error(windows.ERROR_MORE_DATA)) {
newBuffer := make([]byte, len(buffer)+16384)
copy(newBuffer, buffer)
buffer = newBuffer
continue
} else if 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) ([]*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)
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
}
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: obj.LookupName(),
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 == averageCount64Type,
}
}
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
}
}
// Next perfObjectType
objOffset += int64(obj.TotalByteLength)
}
return objects, 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 v1
import (
"testing"
)
func BenchmarkQueryPerformanceData(b *testing.B) {
for n := 0; n < b.N; n++ {
_, _ = QueryPerformanceData("Global")
}
}

View File

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

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

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

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

@@ -0,0 +1,136 @@
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)
}
})
}
}

View File

@@ -1,22 +1,19 @@
//go:build windows
package perfdata
package v2
import (
"errors"
"fmt"
"strings"
"time"
"unsafe"
"github.com/prometheus-community/windows_exporter/internal/perfdata/perftypes"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
const EmptyInstance = "------"
type Collector struct {
time time.Time
object string
counters map[string]Counter
handle pdhQueryHandle
@@ -30,12 +27,6 @@ type Counter struct {
Frequency float64
}
type CounterValues struct {
Type prometheus.ValueType
FirstValue float64
SecondValue float64
}
func NewCollector(object string, instances []string, counters []string) (*Collector, error) {
var handle pdhQueryHandle
@@ -44,7 +35,7 @@ func NewCollector(object string, instances []string, counters []string) (*Collec
}
if len(instances) == 0 {
instances = []string{EmptyInstance}
instances = []string{perftypes.EmptyInstance}
}
collector := &Collector{
@@ -127,18 +118,16 @@ func (c *Collector) Describe() map[string]string {
return desc
}
func (c *Collector) Collect() (map[string]map[string]CounterValues, error) {
func (c *Collector) Collect() (map[string]map[string]perftypes.CounterValues, error) {
if len(c.counters) == 0 {
return map[string]map[string]CounterValues{}, nil
return map[string]map[string]perftypes.CounterValues{}, nil
}
if ret := PdhCollectQueryData(c.handle); ret != ErrorSuccess {
return nil, fmt.Errorf("failed to collect query data: %w", NewPdhError(ret))
}
c.time = time.Now()
var data map[string]map[string]CounterValues
var data map[string]map[string]perftypes.CounterValues
for _, counter := range c.counters {
for _, instance := range counter.Instances {
@@ -167,11 +156,11 @@ func (c *Collector) Collect() (map[string]map[string]CounterValues, error) {
items := (*[1 << 20]PdhRawCounterItem)(unsafe.Pointer(&buf[0]))[:itemCount]
if data == nil {
data = make(map[string]map[string]CounterValues, itemCount)
data = make(map[string]map[string]perftypes.CounterValues, itemCount)
}
var metricType prometheus.ValueType
if val, ok := supportedCounterTypes[counter.Type]; ok {
if val, ok := perftypes.SupportedCounterTypes[counter.Type]; ok {
metricType = val
} else {
metricType = prometheus.GaugeValue
@@ -184,15 +173,15 @@ func (c *Collector) Collect() (map[string]map[string]CounterValues, error) {
continue
}
if instanceName == "" {
instanceName = EmptyInstance
if instanceName == "" || instanceName == "*" {
instanceName = perftypes.EmptyInstance
}
if _, ok := data[instanceName]; !ok {
data[instanceName] = make(map[string]CounterValues, len(c.counters))
data[instanceName] = make(map[string]perftypes.CounterValues, len(c.counters))
}
values := CounterValues{
values := perftypes.CounterValues{
Type: metricType,
}
@@ -201,12 +190,12 @@ func (c *Collector) Collect() (map[string]map[string]CounterValues, error) {
// 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
values.SecondValue = float64(item.RawValue.SecondValue-WindowsEpoch) / counter.Frequency
case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER:
values.FirstValue = float64(item.RawValue.FirstValue) * TicksToSecondScaleFactor
values.SecondValue = float64(item.RawValue.SecondValue) * TicksToSecondScaleFactor
case perftypes.PERF_ELAPSED_TIME:
values.FirstValue = float64(item.RawValue.FirstValue-perftypes.WindowsEpoch) / counter.Frequency
values.SecondValue = float64(item.RawValue.SecondValue-perftypes.WindowsEpoch) / counter.Frequency
case perftypes.PERF_100NSEC_TIMER, perftypes.PERF_PRECISION_100NS_TIMER:
values.FirstValue = float64(item.RawValue.FirstValue) * perftypes.TicksToSecondScaleFactor
values.SecondValue = float64(item.RawValue.SecondValue) * perftypes.TicksToSecondScaleFactor
default:
values.FirstValue = float64(item.RawValue.FirstValue)
values.SecondValue = float64(item.RawValue.SecondValue)
@@ -228,7 +217,7 @@ func (c *Collector) Close() {
func formatCounterPath(object, instance, counterName string) string {
var counterPath string
if instance == EmptyInstance {
if instance == perftypes.EmptyInstance {
counterPath = fmt.Sprintf(`\%s\%s`, object, counterName)
} else {
counterPath = fmt.Sprintf(`\%s(%s)\%s`, object, instance, counterName)

View File

@@ -1,9 +1,9 @@
package perfdata_test
package v2_test
import (
"testing"
"github.com/prometheus-community/windows_exporter/internal/perfdata"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata/v2"
"github.com/stretchr/testify/require"
)
@@ -38,7 +38,7 @@ func BenchmarkTestCollector(b *testing.B) {
"Working Set Peak",
"Working Set",
}
performanceData, err := perfdata.NewCollector("Process", []string{"*"}, counters)
performanceData, err := v2.NewCollector("Process", []string{"*"}, counters)
require.NoError(b, err)
for i := 0; i < b.N; i++ {

View File

@@ -1,12 +1,12 @@
//go:build windows
package perfdata_test
package v2_test
import (
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/perfdata"
v2 "github.com/prometheus-community/windows_exporter/internal/perfdata/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -61,7 +61,7 @@ func TestCollector(t *testing.T) {
t.Run(tc.object, func(t *testing.T) {
t.Parallel()
performanceData, err := perfdata.NewCollector(tc.object, tc.instances, tc.counters)
performanceData, err := v2.NewCollector(tc.object, tc.instances, tc.counters)
require.NoError(t, err)
time.Sleep(100 * time.Millisecond)

View File

@@ -1,4 +1,4 @@
package perfdata
package v2
import "errors"

View File

@@ -31,7 +31,7 @@
//go:build windows
package perfdata
package v2
import (
"fmt"
@@ -44,10 +44,9 @@ import (
// Error codes.
const (
ErrorSuccess = 0
ErrorFailure = 1
ErrorInvalidFunction = 1
EpochDifferenceMicros int64 = 11644473600000000
ErrorSuccess = 0
ErrorFailure = 1
ErrorInvalidFunction = 1
)
type (
@@ -289,13 +288,13 @@ var (
// \\LogicalDisk(C:)\% Free Space
//
// To view all (internationalized...) counters on a system, there are three non-programmatic ways: perfmon utility,
// the typeperf command, and the registry editor. perfmon.exe is perhaps the easiest way, because it's basically a
// full implementation of the pdh.dll API, except with a GUI and all that. The registry setting also provides an
// 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 registry key contains several values as follows:
// This v1 key contains several values as follows:
//
// 1
// 1847
@@ -325,12 +324,6 @@ func PdhAddCounter(hQuery pdhQueryHandle, szFullCounterPath string, dwUserData u
return uint32(ret)
}
// PdhAddEnglishCounterSupported returns true if PdhAddEnglishCounterW Win API function was found in pdh.dll.
// PdhAddEnglishCounterW function is not supported on pre-Windows Vista systems.
func PdhAddEnglishCounterSupported() bool {
return pdhAddEnglishCounterW != nil
}
// PdhAddEnglishCounter adds the specified language-neutral counter to the query. See the PdhAddCounter function. This function only exists on
// Windows versions higher than Vista.
func PdhAddEnglishCounter(hQuery pdhQueryHandle, szFullCounterPath string, dwUserData uintptr, phCounter *pdhCounterHandle) uint32 {

View File

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

View File

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