From db1ad936dcd9a5fe1ca4eb6811ee9f0b6de4e091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 16 Oct 2026 21:33:35 +0200 Subject: [PATCH] wmi: add a multiplier option to convert values to base units (#2772) Co-authored-by: Claude Signed-off-by: Claude --- docs/collector.wmi.md | 14 ++++++++++++++ internal/collector/wmi/types.go | 13 ++++++++----- internal/collector/wmi/wmi.go | 9 ++++++++- internal/collector/wmi/wmi_output_test.go | 23 ++++++++++++++++++++++- internal/collector/wmi/wmi_test.go | 10 ++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/docs/collector.wmi.md b/docs/collector.wmi.md index 9a5e65f6..37c0120a 100644 --- a/docs/collector.wmi.md +++ b/docs/collector.wmi.md @@ -79,6 +79,7 @@ YAML: metric: windows_wmi_logical_disk_free_bytes # optional help: Free space on the logical disk in bytes. # optional type: gauge # optional + multiplier: 1 # optional labels: # optional source: wmi ``` @@ -101,6 +102,7 @@ JSON: "metric": "windows_wmi_logical_disk_free_bytes", "help": "Free space on the logical disk in bytes.", "type": "gauge", + "multiplier": 1, "labels": { "source": "wmi" } } ] @@ -219,6 +221,18 @@ defaults to `gauge`. 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. + +This key is optional. + ##### labels Labels is a map of key-value pairs that will be added as constant labels to the diff --git a/internal/collector/wmi/types.go b/internal/collector/wmi/types.go index ec857d47..c8c4e39c 100644 --- a/internal/collector/wmi/types.go +++ b/internal/collector/wmi/types.go @@ -20,6 +20,7 @@ package wmi import ( "github.com/prometheus-community/windows_exporter/internal/config/yamlstring" "github.com/prometheus-community/windows_exporter/internal/mi" + "github.com/prometheus-community/windows_exporter/internal/multiplier" "github.com/prometheus/client_golang/prometheus" "go.yaml.in/yaml/v3" ) @@ -48,16 +49,18 @@ type LabelProperty struct { } type Property struct { - Name string `json:"name" yaml:"name"` - Metric string `json:"metric" yaml:"metric"` - Help string `json:"help" yaml:"help"` - Type string `json:"type" yaml:"type"` - Labels map[string]string `json:"labels" yaml:"labels"` + Name string `json:"name" yaml:"name"` + Metric string `json:"metric" yaml:"metric"` + Help string `json:"help" yaml:"help"` + Type string `json:"type" yaml:"type"` + Labels map[string]string `json:"labels" yaml:"labels"` + Multiplier *float64 `json:"multiplier" yaml:"multiplier"` // Resolved at Build time. elementName mi.ElementName desc *prometheus.Desc metricType prometheus.ValueType + multiplier multiplier.Multiplier } // UnmarshalYAML decodes the wmi block of the configuration file, where the diff --git a/internal/collector/wmi/wmi.go b/internal/collector/wmi/wmi.go index c59a0766..07db3890 100644 --- a/internal/collector/wmi/wmi.go +++ b/internal/collector/wmi/wmi.go @@ -31,6 +31,7 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/labelcache" "github.com/prometheus-community/windows_exporter/internal/mi" + "github.com/prometheus-community/windows_exporter/internal/multiplier" "github.com/prometheus-community/windows_exporter/internal/scrapecache" "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus/client_golang/prometheus" @@ -346,6 +347,12 @@ func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error { continue } + if property.multiplier, err = multiplier.New(property.Multiplier); err != nil { + errs = append(errs, fmt.Errorf("query %s: property %q: %w", query.Name, property.Name, err)) + + continue + } + if label, ok := firstCommonLabel(query.labelNames, property.Labels); ok { errs = append(errs, fmt.Errorf("query %s: property %q: constant label %q is already set by a label property", query.Name, property.Name, label)) @@ -588,7 +595,7 @@ func (c *Collector) collectQuery(ch chan<- prometheus.Metric, scrape *labelcache ch <- scrape.MustNewConstMetric( property.desc, property.metricType, - value, + property.multiplier.Apply(value), labelValues..., ) } diff --git a/internal/collector/wmi/wmi_output_test.go b/internal/collector/wmi/wmi_output_test.go index 37818ba8..1faa1df8 100644 --- a/internal/collector/wmi/wmi_output_test.go +++ b/internal/collector/wmi/wmi_output_test.go @@ -167,6 +167,21 @@ func TestCollectorMetrics(t *testing.T) { Class: "Win32_OperatingSystem", Properties: []wmi.Property{{Name: "Primary", Metric: "windows_wmi_test_shared", Labels: map[string]string{"query": "b"}}}, }, + { + // The multiplier converts units, e.g. milliseconds to seconds. + Name: "scaled_down", + Class: "Win32_OperatingSystem", + Properties: []wmi.Property{ + {Name: "Primary", Metric: "windows_wmi_test_scaled", Labels: map[string]string{"query": "down"}, Multiplier: new(0.001)}, + }, + }, + { + Name: "scaled_up", + Class: "Win32_OperatingSystem", + Properties: []wmi.Property{ + {Name: "Primary", Metric: "windows_wmi_test_scaled", Labels: map[string]string{"query": "up"}, Multiplier: new(3600.0)}, + }, + }, { Name: "missing_class", Class: "Win32_WindowsExporterDoesNotExist", @@ -205,6 +220,8 @@ windows_wmi_query_success{query="kernel"} 1 windows_wmi_query_success{query="missing_class"} 0 windows_wmi_query_success{query="os"} 1 windows_wmi_query_success{query="partial"} 0 +windows_wmi_query_success{query="scaled_down"} 1 +windows_wmi_query_success{query="scaled_up"} 1 windows_wmi_query_success{query="shared_a"} 1 windows_wmi_query_success{query="shared_b"} 1 windows_wmi_query_success{query="unsupported_label"} 0 @@ -212,6 +229,10 @@ windows_wmi_query_success{query="unsupported_label"} 0 # TYPE windows_wmi_test_parent_pid counter windows_wmi_test_parent_pid{foo="bar",handle="0",path="",pid="0",process="System Idle Process"} 0 windows_wmi_test_parent_pid{foo="bar",handle="4",path="",pid="4",process="System"} 0 +# HELP windows_wmi_test_scaled windows_exporter: custom WMI metric +# TYPE windows_wmi_test_scaled gauge +windows_wmi_test_scaled{query="down"} 0.001 +windows_wmi_test_scaled{query="up"} 3600 # HELP windows_wmi_test_shared windows_exporter: custom WMI metric # TYPE windows_wmi_test_shared gauge windows_wmi_test_shared{query="a"} 1 @@ -232,7 +253,7 @@ windows_wmi_test_shared{query="b"} 1 require.Equal(t, expected, strings.Join(lines, "\n")) - for _, name := range []string{"kernel", "missing_class", "os", "partial", "shared_a", "shared_b", "unsupported_label"} { + for _, name := range []string{"kernel", "missing_class", "os", "partial", "scaled_down", "scaled_up", "shared_a", "shared_b", "unsupported_label"} { require.Regexp(t, `(?m)^windows_wmi_query_duration_seconds\{query="`+name+`"\} \d`, got) } } diff --git a/internal/collector/wmi/wmi_test.go b/internal/collector/wmi/wmi_test.go index a2ebaf7f..f888287f 100644 --- a/internal/collector/wmi/wmi_test.go +++ b/internal/collector/wmi/wmi_test.go @@ -190,6 +190,16 @@ func TestCollectorBuildErrors(t *testing.T) { queries: valid(func(q *wmi.Query) { q.Properties[0].Type = "histogram" }), err: `invalid type "histogram"`, }, + { + name: "zero multiplier", + queries: valid(func(q *wmi.Query) { q.Properties[0].Multiplier = new(0.0) }), + err: `property "FreeSpace": multiplier must be a positive finite number, got 0`, + }, + { + name: "negative multiplier", + queries: valid(func(q *wmi.Query) { q.Properties[0].Multiplier = new(-1.0) }), + err: `multiplier must be a positive finite number, got -1`, + }, { name: "invalid label property name", queries: valid(func(q *wmi.Query) { q.LabelProperties = []wmi.LabelProperty{{Name: ""}} }),