collector: reconcile advertised metric contracts (#2604)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-09 11:34:58 +02:00
committed by GitHub
parent 822d15f192
commit b743466112
11 changed files with 319 additions and 72 deletions
+4 -3
View File
@@ -13,11 +13,11 @@ Enabled by default? | Yes
### `--collector.net.nic-include`
If given, an interface name needs to match the include regexp in order for the corresponding metrics to be reported
If given, an interface name needs to match the include regular expression in order for the corresponding metrics to be reported
### `--collector.net.nic-exclude`
If given, an interface name needs to *not* match the exclude regexp in order for the corresponding metrics to be reported
If given, an interface name needs to *not* match the exclude regular expression in order for the corresponding metrics to be reported
### `--collector.net.enabled`
@@ -43,7 +43,8 @@ Comma-separated list of collectors to use. Defaults to all, if not specified. Su
| `windows_net_nic_address_info` | A metric with a constant '1' value labeled with the network interface's address information. | gauge | `nic`, `address`, `family` |
| `windows_net_nic_info` | A metric with a constant '1' value labeled with the network interface's general information. | gauge | `nic`, `friendly_name`, `mac` |
| `windows_net_nic_operation_status` | The operational status for the interface as defined in RFC 2863 as IfOperStatus. | gauge | `nic`, `status` |
| `windows_net_route_info` | A metric with a constant '1' value labeled with the network interface's route information. | gauge | `nic`, `src`, `dest`, `metric` |
Route information is not collected. The previously listed `windows_net_route_info` metric was never emitted and has been removed from the supported metric list.
### Example metric
Query the rate of transmitted network traffic
+65 -55
View File
@@ -91,60 +91,7 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
c.miQuery = miQuery
c.miSession = miSession
c.quotasCount = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "count"),
"Number of Quotas",
nil,
nil,
)
c.peakUsage = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "peak_usage_bytes"),
"The highest amount of disk space usage charged to this quota. (PeakUsage)",
[]string{"path", "template"},
nil,
)
c.size = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "size_bytes"),
"The size of the quota. (Size)",
[]string{"path", "template"},
nil,
)
c.usage = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "usage_bytes"),
"The current amount of disk space usage charged to this quota. (Usage)",
[]string{"path", "template"},
nil,
)
c.description = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "description"),
"Description of the quota (Description)",
[]string{"path", "template", "description"},
nil,
)
c.disabled = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "disabled"),
"If 1, the quota is disabled. The default value is 0. (Disabled)",
[]string{"path", "template"},
nil,
)
c.softLimit = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "softlimit"),
"If 1, the quota is a soft limit. If 0, the quota is a hard limit. The default value is 0. Optional (SoftLimit)",
[]string{"path", "template"},
nil,
)
c.template = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "template"),
"Quota template name. (Template)",
[]string{"path", "template"},
nil,
)
c.matchesTemplate = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "matchestemplate"),
"If 1, the property values of this quota match those values of the template from which it was derived. (MatchesTemplate)",
[]string{"path", "template"},
nil,
)
c.buildDescriptors()
var dst []msftFSRMQuota
if err := c.miSession.Query(&dst, mi.NamespaceRootWindowsFSRM, c.miQuery, 0); err != nil {
@@ -176,6 +123,12 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
return fmt.Errorf("WMI query failed: %w", err)
}
c.emitQuotas(ch, dst)
return nil
}
func (c *Collector) emitQuotas(ch chan<- prometheus.Metric, dst []msftFSRMQuota) {
var count int
for _, quota := range dst {
@@ -184,6 +137,8 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
template := quota.Template
Description := quota.Description
ch <- prometheus.MustNewConstMetric(c.template, prometheus.GaugeValue, 1, path, template)
ch <- prometheus.MustNewConstMetric(
c.peakUsage,
prometheus.GaugeValue,
@@ -245,6 +200,61 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
prometheus.GaugeValue,
float64(count),
)
}
return nil
func (c *Collector) buildDescriptors() {
c.quotasCount = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "count"),
"Number of Quotas",
nil,
nil,
)
c.peakUsage = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "peak_usage_bytes"),
"The highest amount of disk space usage charged to this quota. (PeakUsage)",
[]string{"path", "template"},
nil,
)
c.size = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "size_bytes"),
"The size of the quota. (Size)",
[]string{"path", "template"},
nil,
)
c.usage = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "usage_bytes"),
"The current amount of disk space usage charged to this quota. (Usage)",
[]string{"path", "template"},
nil,
)
c.description = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "description"),
"Description of the quota (Description)",
[]string{"path", "template", "description"},
nil,
)
c.disabled = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "disabled"),
"If 1, the quota is disabled. The default value is 0. (Disabled)",
[]string{"path", "template"},
nil,
)
c.softLimit = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "softlimit"),
"If 1, the quota is a soft limit. If 0, the quota is a hard limit. The default value is 0. Optional (SoftLimit)",
[]string{"path", "template"},
nil,
)
c.template = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "template"),
"Quota template name. (Template)",
[]string{"path", "template"},
nil,
)
c.matchesTemplate = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "matchestemplate"),
"If 1, the property values of this quota match those values of the template from which it was derived. (MatchesTemplate)",
[]string{"path", "template"},
nil,
)
}
@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package fsrmquota
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func TestQuotaTemplateMetric(t *testing.T) {
t.Parallel()
c := New(nil)
c.buildDescriptors()
metrics := make(chan prometheus.Metric, 20)
c.emitQuotas(metrics, []msftFSRMQuota{{Path: "C:\\data", Template: "Standard"}})
close(metrics)
found := false
for metric := range metrics {
if metric.Desc() != c.template {
continue
}
var value dto.Metric
require.NoError(t, metric.Write(&value))
require.InDelta(t, 1, value.GetGauge().GetValue(), 1e-9)
labels := make(map[string]string)
for _, label := range value.GetLabel() {
labels[label.GetName()] = label.GetValue()
}
require.Equal(t, map[string]string{"path": "C:\\data", "template": "Standard"}, labels)
found = true
}
require.True(t, found)
}
+5 -1
View File
@@ -211,7 +211,11 @@ func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
subCollectorHypervisorRootPartition: {
build: c.buildHypervisorRootPartition,
collect: c.collectHypervisorRootPartition,
close: func() { c.perfDataCollectorHypervisorRootPartition.Close() },
close: func() {
if c.perfDataCollectorHypervisorRootPartition != nil {
c.perfDataCollectorHypervisorRootPartition.Close()
}
},
},
subCollectorHypervisorRootVirtualProcessor: {
build: c.buildHypervisorRootVirtualProcessor,
@@ -21,13 +21,14 @@ import (
"fmt"
"github.com/prometheus-community/windows_exporter/internal/pdh"
pdhtypes "github.com/prometheus-community/windows_exporter/internal/pdh/types"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
)
// collectorHypervisorRootPartition Hyper-V Hypervisor Root Partition metrics
type collectorHypervisorRootPartition struct {
perfDataCollectorHypervisorRootPartition *pdh.Collector[perfDataCounterValuesHypervisorRootPartition]
perfDataCollectorHypervisorRootPartition pdhtypes.Collector[perfDataCounterValuesHypervisorRootPartition]
perfDataObjectHypervisorRootPartition []perfDataCounterValuesHypervisorRootPartition
hypervisorRootPartitionAddressSpaces *prometheus.Desc // \Hyper-V Hypervisor Root Partition(*)\Address Spaces
@@ -85,6 +86,12 @@ func (c *Collector) buildHypervisorRootPartition() error {
return fmt.Errorf("failed to create Hyper-V Hypervisor Root Partition collector: %w", err)
}
c.buildHypervisorRootPartitionDescriptors()
return nil
}
func (c *Collector) buildHypervisorRootPartitionDescriptors() {
c.hypervisorRootPartitionAddressSpaces = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "root_partition_address_spaces"),
"The number of address spaces in the virtual TLB of the partition",
@@ -211,8 +218,6 @@ func (c *Collector) buildHypervisorRootPartition() error {
nil,
nil,
)
return nil
}
func (c *Collector) collectHypervisorRootPartition(ch chan<- prometheus.Metric) error {
@@ -251,6 +256,12 @@ func (c *Collector) collectHypervisorRootPartition(ch chan<- prometheus.Metric)
c.perfDataObjectHypervisorRootPartition[0].HypervisorRootPartitionDeviceInterruptErrors,
)
ch <- prometheus.MustNewConstMetric(
c.hypervisorRootPartitionDeviceInterruptMappings,
prometheus.GaugeValue,
c.perfDataObjectHypervisorRootPartition[0].HypervisorRootPartitionDeviceInterruptMappings,
)
ch <- prometheus.MustNewConstMetric(
c.hypervisorRootPartitionDeviceInterruptThrottleEvents,
prometheus.GaugeValue,
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package hyperv
import (
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func TestRootPartitionInterruptMappings(t *testing.T) {
t.Parallel()
c := New(nil)
c.buildHypervisorRootPartitionDescriptors()
c.perfDataCollectorHypervisorRootPartition = interruptMappingFixture{}
metrics := make(chan prometheus.Metric, 100)
require.NoError(t, c.collectHypervisorRootPartition(metrics))
close(metrics)
found := false
for metric := range metrics {
if metric.Desc() != c.hypervisorRootPartitionDeviceInterruptMappings {
continue
}
var value dto.Metric
require.NoError(t, metric.Write(&value))
require.InDelta(t, 73, value.GetGauge().GetValue(), 1e-9)
found = true
}
require.True(t, found)
}
type interruptMappingFixture struct{}
func (interruptMappingFixture) Collect(dst *[]perfDataCounterValuesHypervisorRootPartition) error {
*dst = []perfDataCounterValuesHypervisorRootPartition{{HypervisorRootPartitionDeviceInterruptMappings: 73}}
return nil
}
func (interruptMappingFixture) Close() {}
@@ -34,6 +34,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
@@ -381,6 +382,10 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
)
}
if err == nil {
ch <- prometheus.MustNewConstMetric(c.readOnly, prometheus.GaugeValue, info.readonly, data.Name)
}
ch <- prometheus.MustNewConstMetric(
c.information,
prometheus.GaugeValue,
@@ -714,7 +719,7 @@ func getVolumeInfo(volumes map[string]string, rootDrive string) (volumeInfo, err
label: windows.UTF16PtrToString(&volBufLabel[0]),
filesystem: windows.UTF16PtrToString(&volBufType[0]),
serialNumber: fmt.Sprintf("%X", volSerialNum),
readonly: float64(fsFlags & windows.FILE_READ_ONLY_VOLUME),
readonly: utils.BoolToFloat(fsFlags&windows.FILE_READ_ONLY_VOLUME != 0),
}, nil
}
@@ -134,3 +134,20 @@ func TestCollectorVolumeFilters(t *testing.T) {
})
}
}
func TestCollectorReadOnlyMetric(t *testing.T) {
metrics := testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{
CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled,
VolumeInclude: types.RegExpAny,
})
family := metrics["windows_logical_disk_readonly"]
require.NotNil(t, family)
require.NotEmpty(t, family.GetMetric())
for _, metric := range family.GetMetric() {
value := metric.GetGauge().GetValue()
require.True(t, value == 0 || value == 1, "read-only flag must be normalized to a boolean")
require.Len(t, metric.GetLabel(), 1)
require.Equal(t, "volume", metric.GetLabel()[0].GetName())
}
}
-7
View File
@@ -83,7 +83,6 @@ type Collector struct {
nicIPAddressInfo *prometheus.Desc
nicOperStatus *prometheus.Desc
nicInfo *prometheus.Desc
routeInfo *prometheus.Desc
}
func New(config *Config) *Collector {
@@ -271,12 +270,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
[]string{"nic", "friendly_name", "mac"},
nil,
)
c.routeInfo = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "route_info"),
"A metric with a constant '1' value labeled with the network interface's route information.",
[]string{"nic", "src", "dest", "metric"},
nil,
)
var err error
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: Apache-2.0
//
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build windows
package smtp
import (
"log/slog"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
func TestGeneralFailureMetric(t *testing.T) {
t.Parallel()
c := New(nil)
// Descriptors are initialized before connecting to the optional SMTP provider.
_ = c.Build(slog.New(slog.DiscardHandler), nil)
if c.perfDataCollector != nil {
c.perfDataCollector.Close()
}
c.perfDataCollector = generalFailureFixture{}
metrics := make(chan prometheus.Metric, 100)
require.NoError(t, c.Collect(metrics, 0))
close(metrics)
found := false
for metric := range metrics {
if metric.Desc() != c.badMailedMessagesGeneralFailureTotal {
continue
}
var value dto.Metric
require.NoError(t, metric.Write(&value))
require.InDelta(t, 42, value.GetCounter().GetValue(), 1e-9)
require.Equal(t, "mail", value.GetLabel()[0].GetValue())
found = true
}
require.True(t, found)
}
type generalFailureFixture struct{}
func (generalFailureFixture) Collect(dst *[]perfDataCounterValues) error {
*dst = []perfDataCounterValues{{Name: "mail", BadmailedMessagesGeneralFailureTotal: 42}}
return nil
}
func (generalFailureFixture) Close() {}
func TestCloseBeforeProviderInitialization(t *testing.T) {
t.Parallel()
require.NoError(t, New(nil).Close())
}
+12 -2
View File
@@ -26,6 +26,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
pdhtypes "github.com/prometheus-community/windows_exporter/internal/pdh/types"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
)
@@ -46,7 +47,7 @@ var ConfigDefaults = Config{
type Collector struct {
config Config
perfDataCollector *pdh.Collector[perfDataCounterValues]
perfDataCollector pdhtypes.Collector[perfDataCounterValues]
perfDataObject []perfDataCounterValues
badMailedMessagesBadPickupFileTotal *prometheus.Desc
@@ -154,7 +155,9 @@ func (c *Collector) GetName() string {
}
func (c *Collector) Close() error {
c.perfDataCollector.Close()
if c.perfDataCollector != nil {
c.perfDataCollector.Close()
}
return nil
}
@@ -448,6 +451,13 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
data.Name,
)
ch <- prometheus.MustNewConstMetric(
c.badMailedMessagesGeneralFailureTotal,
prometheus.CounterValue,
data.BadmailedMessagesGeneralFailureTotal,
data.Name,
)
ch <- prometheus.MustNewConstMetric(
c.badMailedMessagesHopCountExceededTotal,
prometheus.CounterValue,