mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
pdh: report duplicate instances by occurrence instead of renaming them (#2742)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
@@ -19,7 +19,6 @@ package hyperv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/labelcache"
|
||||
"github.com/prometheus-community/windows_exporter/internal/osversion"
|
||||
@@ -47,7 +46,8 @@ type collectorDynamicMemoryVM struct {
|
||||
}
|
||||
|
||||
type perfDataCounterValuesDynamicMemoryVM struct {
|
||||
Name string
|
||||
Name string
|
||||
Occurrence int
|
||||
|
||||
// Hyper-V Dynamic Memory VM metrics
|
||||
VmMemoryAddedMemory float64 `perfdata:"Added Memory"`
|
||||
@@ -140,8 +140,10 @@ func (c *Collector) collectDynamicMemoryVM(ch chan<- prometheus.Metric, scrape *
|
||||
return fmt.Errorf("failed to collect Hyper-V Dynamic Memory VM metrics: %w", err)
|
||||
}
|
||||
|
||||
nameDynamicMemoryVMs(c.perfDataObjectDynamicMemoryVM)
|
||||
|
||||
for _, data := range c.perfDataObjectDynamicMemoryVM {
|
||||
vmName := dynamicMemoryVMName(data.Name)
|
||||
vmName := data.Name
|
||||
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
c.vmMemoryAddedMemory,
|
||||
@@ -223,19 +225,40 @@ func (c *Collector) collectDynamicMemoryVM(ch chan<- prometheus.Metric, scrape *
|
||||
// without a name.
|
||||
const vmUnknown = "(unknown)"
|
||||
|
||||
// dynamicMemoryVMName returns the vm label value for a Hyper-V Dynamic Memory VM
|
||||
// instance. Windows reports an empty instance name for VMs managed by the Host
|
||||
// Compute Service, e.g. WSL 2, which pdh exposes as
|
||||
// pdh.InstanceEmpty. The occurrence suffix of duplicates is kept, so the
|
||||
// series stay unique.
|
||||
func dynamicMemoryVMName(name string) string {
|
||||
if name == pdh.InstanceEmpty {
|
||||
return vmUnknown
|
||||
// nameDynamicMemoryVMs replaces the instance names of rows with their vm label
|
||||
// values. Windows reports an empty instance name for VMs managed by the Host
|
||||
// Compute Service, e.g. WSL 2, which pdh exposes as pdh.InstanceEmpty. Those
|
||||
// VMs are named vmUnknown before suffixDuplicateVMs keeps several of them, or
|
||||
// VMs with the same name, apart.
|
||||
func nameDynamicMemoryVMs(rows []perfDataCounterValuesDynamicMemoryVM) {
|
||||
// A VM can be named "(unknown)" itself. The unnamed VMs then count as
|
||||
// further occurrences of that name, so that they get a suffix.
|
||||
unknownVMs := 0
|
||||
|
||||
for _, row := range rows {
|
||||
if row.Name == vmUnknown {
|
||||
unknownVMs = max(unknownVMs, row.Occurrence+1)
|
||||
}
|
||||
}
|
||||
|
||||
if suffix, ok := strings.CutPrefix(name, pdh.InstanceEmpty+"#"); ok {
|
||||
return vmUnknown + "#" + suffix
|
||||
for i := range rows {
|
||||
if rows[i].Name == pdh.InstanceEmpty {
|
||||
rows[i].Name = vmUnknown
|
||||
rows[i].Occurrence += unknownVMs
|
||||
}
|
||||
}
|
||||
|
||||
return name
|
||||
suffixDuplicateVMs(rows,
|
||||
func(row *perfDataCounterValuesDynamicMemoryVM) *string { return &row.Name },
|
||||
func(row *perfDataCounterValuesDynamicMemoryVM) int { return row.Occurrence },
|
||||
)
|
||||
}
|
||||
|
||||
// suffixDuplicateVMs keeps VMs with the same name apart. Hyper-V allows several
|
||||
// VMs with the same name, and their counter instances then have the same name
|
||||
// too. The first keeps the name, later ones get the suffix "#1", "#2" and so on.
|
||||
// The suffix depends on the order of the sample and does not identify a VM
|
||||
// across scrapes or across counter objects.
|
||||
func suffixDuplicateVMs[T any](rows []T, name func(*T) *string, occurrence func(*T) int) {
|
||||
pdh.SuffixDuplicates(rows, name, occurrence)
|
||||
}
|
||||
|
||||
@@ -21,20 +21,107 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDynamicMemoryVMName(t *testing.T) {
|
||||
func TestNameDynamicMemoryVMs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for name, want := range map[string]string{
|
||||
pdh.InstanceEmpty: "(unknown)",
|
||||
pdh.InstanceEmpty + "#1": "(unknown)#1",
|
||||
pdh.InstanceEmpty + "#12": "(unknown)#12",
|
||||
"GitHubActions": "GitHubActions",
|
||||
"GitHubActions#1": "GitHubActions#1",
|
||||
"vm-------": "vm-------",
|
||||
} {
|
||||
require.Equal(t, want, dynamicMemoryVMName(name), name)
|
||||
rows := []perfDataCounterValuesDynamicMemoryVM{
|
||||
{Name: pdh.InstanceEmpty},
|
||||
{Name: pdh.InstanceEmpty, Occurrence: 1},
|
||||
{Name: pdh.InstanceEmpty, Occurrence: 2},
|
||||
{Name: "GitHubActions"},
|
||||
{Name: "GitHubActions", Occurrence: 1},
|
||||
// A VM named like a suffixed duplicate keeps its name.
|
||||
{Name: "dup#1"},
|
||||
{Name: "dup"},
|
||||
{Name: "dup", Occurrence: 1},
|
||||
{Name: "vm-------"},
|
||||
}
|
||||
|
||||
nameDynamicMemoryVMs(rows)
|
||||
|
||||
names := make([]string, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
names = append(names, row.Name)
|
||||
}
|
||||
|
||||
require.Equal(t, []string{
|
||||
"(unknown)", "(unknown)#1", "(unknown)#2",
|
||||
"GitHubActions", "GitHubActions#1",
|
||||
"dup#1", "dup", "dup#2",
|
||||
"vm-------",
|
||||
}, names)
|
||||
}
|
||||
|
||||
// TestNameDynamicMemoryVMsLiteralUnknown checks that a VM named "(unknown)"
|
||||
// and an unnamed VM are published apart.
|
||||
func TestNameDynamicMemoryVMsLiteralUnknown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rows := []perfDataCounterValuesDynamicMemoryVM{
|
||||
{Name: pdh.InstanceEmpty},
|
||||
{Name: vmUnknown},
|
||||
{Name: pdh.InstanceEmpty, Occurrence: 1},
|
||||
}
|
||||
|
||||
nameDynamicMemoryVMs(rows)
|
||||
|
||||
require.Equal(t, "(unknown)#1", rows[0].Name)
|
||||
require.Equal(t, "(unknown)", rows[1].Name)
|
||||
require.Equal(t, "(unknown)#2", rows[2].Name)
|
||||
}
|
||||
|
||||
// TestVirtualProcessorDuplicateVMs checks that the virtual processors of VMs
|
||||
// with the same name are published with a suffixed vm label and an unchanged
|
||||
// core label.
|
||||
func TestVirtualProcessorDuplicateVMs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := New(nil)
|
||||
c.buildHypervisorVirtualProcessorDescriptors()
|
||||
|
||||
c.perfDataObjectHypervisorVirtualProcessor = []perfDataCounterValuesHypervisorVirtualProcessor{
|
||||
{Name: "dup:Hv VP 0", HypervisorVirtualProcessorDispatches: 1},
|
||||
{Name: "dup:Hv VP 1", HypervisorVirtualProcessorDispatches: 2},
|
||||
{Name: "dup:Hv VP 0", Occurrence: 1, HypervisorVirtualProcessorDispatches: 3},
|
||||
{Name: "other:Hv VP 0", HypervisorVirtualProcessorDispatches: 4},
|
||||
// The first "web" VM has one core, the second two. pdh reports the
|
||||
// second VM's core 1 as the first occurrence of "web:Hv VP 1".
|
||||
{Name: "web:Hv VP 0", HypervisorVirtualProcessorDispatches: 5},
|
||||
{Name: "web:Hv VP 0", Occurrence: 1, HypervisorVirtualProcessorDispatches: 6},
|
||||
{Name: "web:Hv VP 1", HypervisorVirtualProcessorDispatches: 7},
|
||||
}
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
require.NoError(t, registry.Register(publishCollector(func(ch chan<- prometheus.Metric) error {
|
||||
return c.publishHypervisorVirtualProcessor(ch, nil)
|
||||
})))
|
||||
|
||||
families, err := registry.Gather()
|
||||
require.NoError(t, err)
|
||||
|
||||
got := map[string]float64{}
|
||||
|
||||
for _, family := range families {
|
||||
if family.GetName() != "windows_hyperv_hypervisor_virtual_processor_dispatches_total" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, metric := range family.GetMetric() {
|
||||
labels := map[string]string{}
|
||||
for _, label := range metric.GetLabel() {
|
||||
labels[label.GetName()] = label.GetValue()
|
||||
}
|
||||
|
||||
got[labels["vm"]+"/"+labels["core"]] = metric.GetCounter().GetValue()
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, map[string]float64{
|
||||
"dup/0": 1, "dup/1": 2, "dup#1/0": 3, "other/0": 4,
|
||||
"web/0": 5, "web#1/0": 6, "web#1/1": 7,
|
||||
}, got)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ type collectorHypervisorVirtualProcessor struct {
|
||||
}
|
||||
|
||||
type perfDataCounterValuesHypervisorVirtualProcessor struct {
|
||||
Name string
|
||||
Name string
|
||||
Occurrence int
|
||||
|
||||
// The run time counters and CPU Wait Time Per Dispatch are PERF_PRECISION_100NS_TIMER.
|
||||
// Their raw values are cumulative times, which pdh scales to seconds. Despite its name
|
||||
@@ -123,12 +124,34 @@ func (c *Collector) collectHypervisorVirtualProcessor(ch chan<- prometheus.Metri
|
||||
return c.publishHypervisorVirtualProcessor(ch, scrape)
|
||||
}
|
||||
|
||||
// assignVirtualProcessorVMs sets the Occurrence of each virtual processor row
|
||||
// to the index of its VM among the VMs with the same name, so that all cores
|
||||
// of one VM get the same vm label. pdh counts occurrences per instance name
|
||||
// ("<VM>:Hv VP <n>"), which would give the second core of the second VM the
|
||||
// occurrence of the first VM if the first VM has a single core. Windows lists
|
||||
// the virtual processors of a VM together, starting with core 0, in the order
|
||||
// that the rows keep, so each core 0 starts the next VM with that name.
|
||||
func assignVirtualProcessorVMs(rows []perfDataCounterValuesHypervisorVirtualProcessor, coreIDs []string) {
|
||||
vms := make(map[string]int, len(rows))
|
||||
|
||||
for i := range rows {
|
||||
if coreIDs[i] == "0" {
|
||||
vms[rows[i].Name]++
|
||||
}
|
||||
|
||||
rows[i].Occurrence = max(vms[rows[i].Name]-1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Collector) publishHypervisorVirtualProcessor(ch chan<- prometheus.Metric, scrape *labelcache.Scrape) error {
|
||||
for _, data := range c.perfDataObjectHypervisorVirtualProcessor {
|
||||
rows := c.perfDataObjectHypervisorVirtualProcessor
|
||||
coreIDs := make([]string, len(rows))
|
||||
|
||||
for i := range rows {
|
||||
// The name format is <VM Name>:Hv VP <vcore id>
|
||||
parts := strings.Split(data.Name, ":")
|
||||
parts := strings.Split(rows[i].Name, ":")
|
||||
if len(parts) != 2 {
|
||||
return fmt.Errorf("unexpected format of Name in Hyper-V Hypervisor Virtual Processor: %q, expected %q", data.Name, "<VM Name>:Hv VP <vcore id>")
|
||||
return fmt.Errorf("unexpected format of Name in Hyper-V Hypervisor Virtual Processor: %q, expected %q", rows[i].Name, "<VM Name>:Hv VP <vcore id>")
|
||||
}
|
||||
|
||||
coreParts := strings.Split(parts[1], " ")
|
||||
@@ -136,8 +159,21 @@ func (c *Collector) publishHypervisorVirtualProcessor(ch chan<- prometheus.Metri
|
||||
return fmt.Errorf("unexpected format of core identifier in Hyper-V Hypervisor Virtual Processor: %q, expected %q", parts[1], "Hv VP <vcore id>")
|
||||
}
|
||||
|
||||
vmName := parts[0]
|
||||
coreID := coreParts[2]
|
||||
rows[i].Name = parts[0]
|
||||
coreIDs[i] = coreParts[2]
|
||||
}
|
||||
|
||||
assignVirtualProcessorVMs(rows, coreIDs)
|
||||
|
||||
// VMs with the same name report the same instance names, see suffixDuplicateVMs.
|
||||
suffixDuplicateVMs(rows,
|
||||
func(row *perfDataCounterValuesHypervisorVirtualProcessor) *string { return &row.Name },
|
||||
func(row *perfDataCounterValuesHypervisorVirtualProcessor) int { return row.Occurrence },
|
||||
)
|
||||
|
||||
for i, data := range rows {
|
||||
vmName := data.Name
|
||||
coreID := coreIDs[i]
|
||||
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
c.hypervisorVirtualProcessorTimeTotal,
|
||||
|
||||
@@ -57,7 +57,8 @@ type collectorReplicaVM struct {
|
||||
}
|
||||
|
||||
type perfDataCounterValuesReplicaVM struct {
|
||||
Name string
|
||||
Name string
|
||||
Occurrence int
|
||||
|
||||
AverageReplicationLatency float64 `perfdata:"Average Replication Latency"`
|
||||
AverageReplicationSize float64 `perfdata:"Average Replication Size"`
|
||||
@@ -198,6 +199,11 @@ func (c *Collector) collectReplicaVMPerfData(ch chan<- prometheus.Metric, scrape
|
||||
return fmt.Errorf("failed to collect Hyper-V Replica VM metrics: %w", err)
|
||||
}
|
||||
|
||||
suffixDuplicateVMs(c.perfDataObjectReplicaVM,
|
||||
func(row *perfDataCounterValuesReplicaVM) *string { return &row.Name },
|
||||
func(row *perfDataCounterValuesReplicaVM) int { return row.Occurrence },
|
||||
)
|
||||
|
||||
for _, data := range c.perfDataObjectReplicaVM {
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
c.replicaVMAverageReplicationLatency,
|
||||
|
||||
@@ -37,7 +37,8 @@ type collectorVirtualMachineVidPartition struct {
|
||||
}
|
||||
|
||||
type perfDataCounterValuesVirtualMachineVidPartition struct {
|
||||
Name string
|
||||
Name string
|
||||
Occurrence int
|
||||
|
||||
PhysicalPagesAllocated float64 `perfdata:"Physical Pages Allocated"`
|
||||
PreferredNUMANodeIndex float64 `perfdata:"Preferred NUMA Node Index"`
|
||||
@@ -80,6 +81,11 @@ func (c *Collector) collectVirtualMachineVidPartition(ch chan<- prometheus.Metri
|
||||
return fmt.Errorf("failed to collect Hyper-V VM Vid Partition metrics: %w", err)
|
||||
}
|
||||
|
||||
suffixDuplicateVMs(c.perfDataObjectVirtualMachineVidPartition,
|
||||
func(row *perfDataCounterValuesVirtualMachineVidPartition) *string { return &row.Name },
|
||||
func(row *perfDataCounterValuesVirtualMachineVidPartition) int { return row.Occurrence },
|
||||
)
|
||||
|
||||
for _, data := range c.perfDataObjectVirtualMachineVidPartition {
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
c.physicalPagesAllocated,
|
||||
|
||||
@@ -55,7 +55,8 @@ type collectorVirtualSwitch struct {
|
||||
}
|
||||
|
||||
type perfDataCounterValuesVirtualSwitch struct {
|
||||
Name string
|
||||
Name string
|
||||
Occurrence int
|
||||
|
||||
VirtualSwitchBroadcastPacketsReceived float64 `perfdata:"Broadcast Packets Received/sec"`
|
||||
VirtualSwitchBroadcastPacketsSent float64 `perfdata:"Broadcast Packets Sent/sec"`
|
||||
@@ -224,6 +225,11 @@ func (c *Collector) collectVirtualSwitch(ch chan<- prometheus.Metric, scrape *la
|
||||
return fmt.Errorf("failed to collect Hyper-V Virtual Switch metrics: %w", err)
|
||||
}
|
||||
|
||||
suffixDuplicateVMs(c.perfDataObjectVirtualSwitch,
|
||||
func(row *perfDataCounterValuesVirtualSwitch) *string { return &row.Name },
|
||||
func(row *perfDataCounterValuesVirtualSwitch) int { return row.Occurrence },
|
||||
)
|
||||
|
||||
for _, data := range c.perfDataObjectVirtualSwitch {
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
c.virtualSwitchBroadcastPacketsReceived,
|
||||
|
||||
Reference in New Issue
Block a user