performancecounter: add a multiplier option to convert values to base units (#2773)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 21:33:47 +02:00
committed by GitHub
co-authored by Claude
parent db1ad936dc
commit dfc5653376
4 changed files with 92 additions and 6 deletions
+18 -1
View File
@@ -65,6 +65,7 @@ YAML:
- name: "Cache Faults/sec"
metric: windows_performancecounter_memory_cache_faults_per_second # optional
type: "gauge" # optional
multiplier: 1 # optional
```
</details>
@@ -106,7 +107,8 @@ YAML:
{
"name": "Cache Faults/sec",
"metric": "windows_performancecounter_memory_cache_faults_per_second",
"type": "gauge"
"type": "gauge",
"multiplier": 1
}
]
}
@@ -195,6 +197,21 @@ If not specified, the windows_exporter will try to determine the type based on t
This key is optional.
##### multiplier
A factor that the value is multiplied by before it is exported, to convert it to
the base unit the Prometheus [naming conventions](https://prometheus.io/docs/practices/naming/#base-units)
ask for. For example, `0.001` converts milliseconds to seconds, `0.01` percent to
a ratio and `3600` hours to seconds. It must be a positive number, so counters
keep counting up. If the multiplier is the reciprocal of an integer, like `0.001`,
the value is divided by that integer instead, which avoids rounding artifacts
such as `0.009000000000000001`. If not specified, the value is exported as is.
With the `raw` type, `PERF_100NSEC_TIMER_INV` counters such as `% Processor Time` are not converted to seconds
and arrive in 100 ns ticks; a multiplier of `1e-7` converts them.
This key is optional.
##### labels
Labels is a map of key-value pairs that will be added as labels to the metric.
@@ -31,6 +31,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/multiplier"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
@@ -175,6 +176,15 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
continue
}
m, err := multiplier.New(counter.Multiplier)
if err != nil {
errs = append(errs, fmt.Errorf("object %s: counter %s: %w", object.Name, counter.Name, err))
continue
}
object.Counters[j].multiplier = m
if _, ok := object.valueIndex[counter.Name]; ok {
errs = append(errs, fmt.Errorf("object %s: counter name %s is duplicated", object.Name, counter.Name))
@@ -328,7 +338,7 @@ func (c *Collector) collectObject(ch chan<- prometheus.Metric, perfDataObject Ob
labels,
),
metricTypes[j],
collectedCounterValue,
counter.multiplier.Apply(collectedCounterValue),
)
}
}
@@ -440,3 +440,57 @@ func TestCollectorMetricTypePerCounter(t *testing.T) {
require.Equal(t, dto.MetricType_COUNTER, types["windows_performancecounter_process_io_read_bytes_sec"])
}
}
// TestCollectorMultiplier checks that the multiplier scales the exported value.
// Commit Limit only changes when a page file is resized, so both objects read
// the same value.
func TestCollectorMultiplier(t *testing.T) {
t.Parallel()
c := performancecounter.New(&performancecounter.Config{
Objects: []performancecounter.Object{
{
Name: "bytes",
Object: "Memory",
Counters: []performancecounter.Counter{{Name: "Commit Limit", Metric: "windows_test_commit_limit_bytes"}},
},
{
Name: "kibibytes",
Object: "Memory",
Counters: []performancecounter.Counter{{Name: "Commit Limit", Metric: "windows_test_commit_limit_kibibytes", Multiplier: new(1.0 / 1024)}},
},
},
})
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
t.Cleanup(func() { require.NoError(t, c.Close()) })
registry := prometheus.NewRegistry()
registry.MustRegister(collectorAdapter{*c})
families, err := registry.Gather()
require.NoError(t, err)
values := make(map[string]float64, len(families))
for _, family := range families {
values[family.GetName()] = family.GetMetric()[0].GetGauge().GetValue()
}
require.Positive(t, values["windows_test_commit_limit_bytes"])
require.Equal(t, values["windows_test_commit_limit_bytes"]/1024, values["windows_test_commit_limit_kibibytes"]) //nolint:testifylint // The result must be exact.
}
func TestBuildInvalidMultiplier(t *testing.T) {
t.Parallel()
c := performancecounter.New(&performancecounter.Config{
Objects: []performancecounter.Object{{
Name: "memory",
Object: "Memory",
Counters: []performancecounter.Counter{{Name: "Commit Limit", Multiplier: new(-1.0)}},
}},
})
t.Cleanup(func() { require.NoError(t, c.Close()) })
require.ErrorContains(t, c.Build(slog.New(slog.DiscardHandler), nil), "object memory: counter Commit Limit: multiplier must be a positive finite number, got -1")
}
@@ -19,6 +19,7 @@ package performancecounter
import (
"github.com/prometheus-community/windows_exporter/internal/config/yamlstring"
"github.com/prometheus-community/windows_exporter/internal/multiplier"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"go.yaml.in/yaml/v3"
)
@@ -37,10 +38,14 @@ type Object struct {
}
type Counter struct {
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Metric string `json:"metric" yaml:"metric"`
Labels map[string]string `json:"labels" yaml:"labels"`
Name string `json:"name" yaml:"name"`
Type string `json:"type" yaml:"type"`
Metric string `json:"metric" yaml:"metric"`
Labels map[string]string `json:"labels" yaml:"labels"`
Multiplier *float64 `json:"multiplier" yaml:"multiplier"`
// Resolved at Build time.
multiplier multiplier.Multiplier
}
// https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/54691ebe11bb9ec32b4e35cd31fcb94a352de134/receiver/windowsperfcountersreceiver/README.md?plain=1#L150