mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-20 19:49:03 +02:00
pdh: convert all raw time counter types to seconds (#2776)
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
a9d5bbef48
commit
da7bebc6b3
+31
-12
@@ -31,6 +31,7 @@ import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/headers/kernel32"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sys/windows"
|
||||
@@ -487,10 +488,10 @@ func newCollector[T any](logger *slog.Logger, query *Query, resultType CounterTy
|
||||
continue
|
||||
}
|
||||
case PERF_AVERAGE_TIMER:
|
||||
// Only [Collector.Frequency] uses the time base of an average
|
||||
// timer, so a missing one is not an error. Collect retries it.
|
||||
if ret := GetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess {
|
||||
counter.Frequency = 0
|
||||
// The time base of an average timer is the performance counter
|
||||
// frequency, so use that if PDH has none.
|
||||
if ret := GetCounterTimeBase(counterHandle, &counter.Frequency); ret != ErrorSuccess || counter.Frequency <= 0 {
|
||||
counter.Frequency, _ = kernel32.QueryPerformanceFrequency()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,11 +570,11 @@ func (c *Collector[T]) MetricType(counterName string) (prometheus.ValueType, boo
|
||||
}
|
||||
|
||||
// Frequency returns the time base in ticks per second of the raw timer counter
|
||||
// named counterName. The raw value of a PERF_AVERAGE_TIMER counter is the sum
|
||||
// of the measured durations in these ticks; divide it by the frequency to get
|
||||
// seconds. It returns false if the counter is unknown, is not such a timer or
|
||||
// its time base is not known yet. [Collector.Collect] reads a missing time
|
||||
// base again.
|
||||
// named counterName. [Collector.Collect] already converts the values of these
|
||||
// counters to seconds; a collector needs the time base only to work on the
|
||||
// ticks, for example to extend a 32-bit counter past its wrap. It returns
|
||||
// false if the counter is unknown, is not such a timer or its time base is not
|
||||
// known yet.
|
||||
func (c *Collector[T]) Frequency(counterName string) (int64, bool) {
|
||||
if c == nil {
|
||||
return 0, false
|
||||
@@ -724,7 +725,8 @@ func hasTimeBase(counterType uint32) bool {
|
||||
// the collector was created. Early after a reboot, PDH can report no data
|
||||
// for it, e.g. for APP_POOL_WAS while WAS is still starting (#2313). Without a
|
||||
// time base, an elapsed time counter has no valid value and every instance is
|
||||
// omitted.
|
||||
// omitted. An average timer falls back to QueryPerformanceFrequency at
|
||||
// creation, so it only needs this if that failed too.
|
||||
func (c *Collector[T]) loadTimeBase(counter *Counter) {
|
||||
if c.resultType != CounterTypeRaw || !hasTimeBase(counter.Type) || counter.Frequency > 0 {
|
||||
return
|
||||
@@ -810,9 +812,26 @@ func (c *Collector[T]) setRawValue(row *T, counter *Counter, value RawCounter) b
|
||||
if counter.FieldIndexValue != -1 {
|
||||
c.rows.setValue(row, counter.FieldIndexValue, float64(value.SecondValue-value.FirstValue)/float64(counter.Frequency))
|
||||
}
|
||||
case PERF_100NSEC_TIMER, PERF_PRECISION_100NS_TIMER:
|
||||
case PERF_100NSEC_TIMER, PERF_100NSEC_TIMER_INV, PERF_100NSEC_MULTI_TIMER, PERF_100NSEC_MULTI_TIMER_INV, PERF_PRECISION_100NS_TIMER,
|
||||
PERF_COUNTER_100NS_QUEUELEN_TYPE:
|
||||
// The queue length type sums the queue length times the elapsed 100 ns
|
||||
// ticks, so in seconds its rate is the average queue length.
|
||||
if counter.FieldIndexValue != -1 {
|
||||
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)*TicksToSecondScaleFactor)
|
||||
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)/TicksPerSecond)
|
||||
}
|
||||
case PERF_AVERAGE_TIMER:
|
||||
// The first value sums the measured durations in ticks of the counter's
|
||||
// time base, the second value counts the operations.
|
||||
if counter.Frequency <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if counter.FieldIndexSecondValue != -1 {
|
||||
c.rows.setValue(row, counter.FieldIndexSecondValue, float64(value.SecondValue))
|
||||
}
|
||||
|
||||
if counter.FieldIndexValue != -1 {
|
||||
c.rows.setValue(row, counter.FieldIndexValue, float64(value.FirstValue)/float64(counter.Frequency))
|
||||
}
|
||||
default:
|
||||
if counter.FieldIndexSecondValue != -1 {
|
||||
|
||||
@@ -385,6 +385,32 @@ func TestSetRawValueElapsedTime(t *testing.T) {
|
||||
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 2}))
|
||||
}
|
||||
|
||||
// TestSetRawValue100nsTimers checks that every 100 ns timer type, and the
|
||||
// 100 ns queue length type, is exported in seconds. 115960642 * (1 / 1e7) is
|
||||
// 11.596064199999999, so the test also catches a multiplication by the
|
||||
// reciprocal.
|
||||
func TestSetRawValue100nsTimers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := newRowSetTestCollector(false)
|
||||
|
||||
for _, counterType := range []uint32{
|
||||
PERF_100NSEC_TIMER,
|
||||
PERF_100NSEC_TIMER_INV,
|
||||
PERF_100NSEC_MULTI_TIMER,
|
||||
PERF_100NSEC_MULTI_TIMER_INV,
|
||||
PERF_PRECISION_100NS_TIMER,
|
||||
PERF_COUNTER_100NS_QUEUELEN_TYPE,
|
||||
} {
|
||||
var values rowSetValues
|
||||
|
||||
counter := &Counter{Type: counterType, FieldIndexValue: int(unsafe.Offsetof(values.A)), FieldIndexSecondValue: -1}
|
||||
|
||||
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 115_960_642, SecondValue: 1}))
|
||||
require.InDelta(t, 11.5960642, values.A, 0, "counter type %#x", counterType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectLoadsMissingTimeBase checks that an elapsed time counter whose time
|
||||
// base was not available when the collector was created gets it on a later
|
||||
// sample, instead of omitting every instance until a restart (#2313).
|
||||
@@ -419,17 +445,15 @@ func TestCollectLoadsMissingTimeBase(t *testing.T) {
|
||||
require.Positive(t, dst[0].SystemUpTime)
|
||||
}
|
||||
|
||||
// TestFrequencyOfAverageTimer checks that the time base of a raw average timer
|
||||
// is available, also when it was missing at startup, and that other counters
|
||||
// have none.
|
||||
func TestFrequencyOfAverageTimer(t *testing.T) {
|
||||
// TestAverageTimerInSeconds checks that the raw value of an average timer is
|
||||
// converted to seconds with the counter's time base, also when the time base
|
||||
// was missing at startup, and that the second value keeps the operation count.
|
||||
func TestAverageTimerInSeconds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type physicalDisk struct {
|
||||
AvgDiskSecPerRead float64 `perfdata:"Avg. Disk sec/Read"`
|
||||
AvgDiskSecPerReadBase float64 `perfdata:"Avg. Disk sec/Read,secondvalue"`
|
||||
DiskReadsPerSec float64 `perfdata:"Disk Reads/sec"`
|
||||
CurrentDiskQueueLength float64 `perfdata:"Current Disk Queue Length"`
|
||||
AvgDiskSecPerRead float64 `perfdata:"Avg. Disk sec/Read"`
|
||||
AvgDiskSecPerReadBase float64 `perfdata:"Avg. Disk sec/Read,secondvalue"`
|
||||
}
|
||||
|
||||
collector, err := NewCollector[physicalDisk](slog.New(slog.DiscardHandler), CounterTypeRaw, "PhysicalDisk", []string{"_Total"})
|
||||
@@ -437,36 +461,45 @@ func TestFrequencyOfAverageTimer(t *testing.T) {
|
||||
|
||||
t.Cleanup(collector.Close)
|
||||
|
||||
frequency, ok := collector.Frequency("Avg. Disk sec/Read")
|
||||
require.True(t, ok)
|
||||
require.Positive(t, frequency)
|
||||
|
||||
for _, name := range []string{"Disk Reads/sec", "Current Disk Queue Length", "unknown"} {
|
||||
_, ok = collector.Frequency(name)
|
||||
require.False(t, ok, name)
|
||||
}
|
||||
|
||||
// As if GetCounterTimeBase failed at startup.
|
||||
idx := slices.IndexFunc(collector.counters, func(counter Counter) bool { return counter.Name == "Avg. Disk sec/Read" })
|
||||
require.NotEqual(t, -1, idx)
|
||||
|
||||
collector.counters[idx].Frequency = 0
|
||||
frequency := collector.counters[idx].Frequency
|
||||
require.Positive(t, frequency)
|
||||
|
||||
_, ok = collector.Frequency("Avg. Disk sec/Read")
|
||||
require.False(t, ok)
|
||||
// As if GetCounterTimeBase failed at startup.
|
||||
collector.counters[idx].Frequency = 0
|
||||
|
||||
var dst []physicalDisk
|
||||
|
||||
require.NoError(t, collector.Collect(&dst))
|
||||
require.Equal(t, frequency, collector.counters[idx].Frequency)
|
||||
require.Len(t, dst, 1)
|
||||
require.GreaterOrEqual(t, dst[0].AvgDiskSecPerRead, 0.0)
|
||||
}
|
||||
|
||||
reloaded, ok := collector.Frequency("Avg. Disk sec/Read")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, frequency, reloaded)
|
||||
// TestSetRawValueAverageTimer checks the conversion of an average timer and
|
||||
// that a counter without a time base has no valid value.
|
||||
func TestSetRawValueAverageTimer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var nilCollector *Collector[physicalDisk]
|
||||
c := newRowSetTestCollector(false)
|
||||
|
||||
_, ok = nilCollector.Frequency("Avg. Disk sec/Read")
|
||||
require.False(t, ok)
|
||||
var values rowSetValues
|
||||
|
||||
counter := &Counter{
|
||||
Type: PERF_AVERAGE_TIMER,
|
||||
Frequency: 2_500_000,
|
||||
FieldIndexValue: int(unsafe.Offsetof(values.B)),
|
||||
FieldIndexSecondValue: int(unsafe.Offsetof(values.BSecond)),
|
||||
}
|
||||
|
||||
require.True(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 5_000_000, SecondValue: 7}))
|
||||
require.InDelta(t, 2.0, values.B, 0)
|
||||
require.InDelta(t, 7.0, values.BSecond, 0)
|
||||
|
||||
counter.Frequency = 0
|
||||
require.False(t, c.setRawValue(&values, counter, RawCounter{FirstValue: 1, SecondValue: 1}))
|
||||
}
|
||||
|
||||
func TestMetricTypeForCounterType(t *testing.T) {
|
||||
@@ -474,6 +507,10 @@ func TestMetricTypeForCounterType(t *testing.T) {
|
||||
|
||||
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, PERF_COUNTER_RAWCOUNT))
|
||||
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_COUNTER_BULK_COUNT))
|
||||
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_100NSEC_MULTI_TIMER))
|
||||
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_100NSEC_MULTI_TIMER_INV))
|
||||
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_AVERAGE_TIMER))
|
||||
require.Equal(t, prometheus.CounterValue, metricType(CounterTypeRaw, PERF_COUNTER_100NS_QUEUELEN_TYPE))
|
||||
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeRaw, 0xFFFFFFFF))
|
||||
require.Equal(t, prometheus.GaugeValue, metricType(CounterTypeFormatted, PERF_COUNTER_BULK_COUNT))
|
||||
}
|
||||
|
||||
+26
-19
@@ -21,8 +21,11 @@ import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
// Conversion factors.
|
||||
const (
|
||||
TicksToSecondScaleFactor = 1 / 1e7
|
||||
WindowsEpoch int64 = 116444736000000000
|
||||
// TicksPerSecond is the number of 100 ns ticks per second. Divide by it
|
||||
// rather than multiply by its reciprocal: the quotient is correctly rounded,
|
||||
// the product is not.
|
||||
TicksPerSecond = 1e7
|
||||
WindowsEpoch int64 = 116444736000000000
|
||||
)
|
||||
|
||||
// Based on https://github.com/leoluk/perflib_exporter/blob/master/collector/mapper.go
|
||||
@@ -74,21 +77,25 @@ const (
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var SupportedCounterTypes = map[uint32]prometheus.ValueType{
|
||||
PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue,
|
||||
PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue,
|
||||
PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue,
|
||||
PERF_COUNTER_LARGE_RAWCOUNT: prometheus.GaugeValue,
|
||||
PERF_COUNTER_DELTA: prometheus.CounterValue,
|
||||
PERF_COUNTER_COUNTER: prometheus.CounterValue,
|
||||
PERF_COUNTER_BULK_COUNT: prometheus.CounterValue,
|
||||
PERF_RAW_FRACTION: prometheus.GaugeValue,
|
||||
PERF_LARGE_RAW_FRACTION: prometheus.GaugeValue,
|
||||
PERF_100NSEC_TIMER: prometheus.CounterValue,
|
||||
PERF_PRECISION_100NS_TIMER: prometheus.CounterValue,
|
||||
PERF_SAMPLE_FRACTION: prometheus.GaugeValue,
|
||||
PERF_100NSEC_TIMER_INV: prometheus.CounterValue,
|
||||
PERF_ELAPSED_TIME: prometheus.GaugeValue,
|
||||
PERF_SAMPLE_BASE: prometheus.GaugeValue,
|
||||
PERF_RAW_BASE: prometheus.GaugeValue,
|
||||
PERF_LARGE_RAW_BASE: prometheus.GaugeValue,
|
||||
PERF_COUNTER_RAWCOUNT_HEX: prometheus.GaugeValue,
|
||||
PERF_COUNTER_LARGE_RAWCOUNT_HEX: prometheus.GaugeValue,
|
||||
PERF_COUNTER_RAWCOUNT: prometheus.GaugeValue,
|
||||
PERF_COUNTER_LARGE_RAWCOUNT: prometheus.GaugeValue,
|
||||
PERF_COUNTER_DELTA: prometheus.CounterValue,
|
||||
PERF_COUNTER_COUNTER: prometheus.CounterValue,
|
||||
PERF_COUNTER_BULK_COUNT: prometheus.CounterValue,
|
||||
PERF_RAW_FRACTION: prometheus.GaugeValue,
|
||||
PERF_LARGE_RAW_FRACTION: prometheus.GaugeValue,
|
||||
PERF_100NSEC_TIMER: prometheus.CounterValue,
|
||||
PERF_PRECISION_100NS_TIMER: prometheus.CounterValue,
|
||||
PERF_SAMPLE_FRACTION: prometheus.GaugeValue,
|
||||
PERF_100NSEC_TIMER_INV: prometheus.CounterValue,
|
||||
PERF_100NSEC_MULTI_TIMER: prometheus.CounterValue,
|
||||
PERF_100NSEC_MULTI_TIMER_INV: prometheus.CounterValue,
|
||||
PERF_ELAPSED_TIME: prometheus.GaugeValue,
|
||||
PERF_AVERAGE_TIMER: prometheus.CounterValue,
|
||||
PERF_COUNTER_100NS_QUEUELEN_TYPE: prometheus.CounterValue,
|
||||
PERF_SAMPLE_BASE: prometheus.GaugeValue,
|
||||
PERF_RAW_BASE: prometheus.GaugeValue,
|
||||
PERF_LARGE_RAW_BASE: prometheus.GaugeValue,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user