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:
Jan-Otto Kröpke
2026-10-16 09:58:43 +02:00
committed by GitHub
parent d8dbf852d1
commit 43d77f6869
32 changed files with 832 additions and 266 deletions
@@ -42,6 +42,9 @@ type collectorADAccessProcesses struct {
type perfDataCounterValuesADAccessProcesses struct {
Name string
// Occurrence makes pdh return every process with the same name; the
// label logic in collectADAccessProcesses keeps them apart.
Occurrence int
LdapReadTime float64 `perfdata:"LDAP Read Time"`
LdapReadOperationsTotal float64 `perfdata:"LDAP Read Time,secondvalue"`
@@ -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,
+8 -1
View File
@@ -74,7 +74,14 @@ notes were recorded on Windows Server 2025 (build 26100) with only
exist only while a worker process runs; send one HTTP request to start one.
`HTTP Service Request Queues` instances are app pool names plus about 40
`---<n>` queues, which the collector skips. Recycled instances get a `#<n>`
suffix that the collector strips.
suffix that the collector strips. `internal/pdh` adds no suffix of its own.
The IIS rows have no `Occurrence` field, because no IIS object repeats an
instance name in one sample: IIS rejects duplicate site and application
pool names (`Web Service`, `APP_POOL_WAS`, `HTTP Service Request Queues`),
`W3SVC_W3WP` instances carry the worker process ID, and the overlapping
worker of a recycle has its own PID. A sample on this host had no
duplicates in these objects. If pdh logs dropped duplicates for an IIS
object, declare the field.
## Counter reference
+8
View File
@@ -33,6 +33,14 @@ Edition (`PatchLevel` 16.0.1000.6), named instance `NONDEFAULT`.
15 including `_Total`. `SQL Errors` instances are error classes
(`User Errors`, `Info Errors`, `Kill Connection Errors`,
`DB Offline Errors`); `_Total` equalled their sum (38 = 34 + 4).
- No row type declares `Occurrence` (see `internal/pdh`), because no object
repeats an instance name in one sample: database names are unique per
instance, a database belongs to at most one availability group
(`Database Replica`), `Availability Replica` instances are replicas of the
instance's availability groups, and the other instances are fixed lock
types, error classes, wait statistics or the single unnamed instance. A
sample on this host had no duplicates in any of these objects. If pdh
ever logs dropped duplicates for an `MSSQL$` object, declare the field.
- `Wait Statistics` is unusual: every counter is a wait class and every
instance is a statistic, `Average wait time (ms)`,
`Cumulative wait time (ms) per second`, `Waits in progress` and
@@ -380,6 +380,34 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
return g.Wait()
}
// clrInstance is implemented by the rows of the .NET CLR objects.
type clrInstance interface {
instance() (name *string, occurrence int)
}
// suffixDuplicateProcesses keeps processes with the same name apart. The .NET
// CLR objects name instances after the process image and report a name once
// per process, e.g. "powershell" twice for two PowerShell processes. Later
// processes get the suffix "#1", "#2" and so on, which depends on the order
// of the sample and does not identify a process across scrapes.
func suffixDuplicateProcesses[T any, P interface {
*T
clrInstance
}](rows []T) {
pdh.SuffixDuplicates(rows,
func(row *T) *string {
name, _ := P(row).instance()
return name
},
func(row *T) int {
_, occurrence := P(row).instance()
return occurrence
},
)
}
// newPerfCollector adds the counters of object to query. On error,
// pdh.NewQueryCollector has already removed the counters it added. Missing
// objects are optional; missing individual counters remain errors.
@@ -65,18 +65,23 @@ func (c *Collector) buildClrExceptions() error {
type perfDataClrExceptions struct {
Name string
Occurrence int
NumberofExcepsThrown float64 `perfdata:"# of Exceps Thrown"`
NumberofFiltersPersec float64 `perfdata:"# of Filters / sec"`
NumberofFinallysPersec float64 `perfdata:"# of Finallys / sec"`
ThrowToCatchDepthPersec float64 `perfdata:"Throw To Catch Depth / sec"`
}
func (p *perfDataClrExceptions) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrExceptions
if err := c.perfClrExceptions.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Exceptions: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -59,17 +59,22 @@ func (c *Collector) buildClrInterop() error {
type perfDataClrInterop struct {
Name string
Occurrence int
NumberofCCWs float64 `perfdata:"# of CCWs"`
NumberofStubs float64 `perfdata:"# of Stubs"`
Numberofmarshalling float64 `perfdata:"# of marshalling"`
}
func (p *perfDataClrInterop) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrInterop
if err := c.perfClrInterop.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Interop: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -65,18 +65,23 @@ func (c *Collector) buildClrJIT() error {
type perfDataClrJIT struct {
Name string
Occurrence int
NumberofMethodsJitted float64 `perfdata:"# of Methods JITted"`
PercentTimeinJit float64 `perfdata:"% Time in Jit"`
StandardJitFailures float64 `perfdata:"Standard Jit Failures"`
TotalNumberofILBytesJitted float64 `perfdata:"Total # of IL Bytes Jitted"`
}
func (p *perfDataClrJIT) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrJIT
if err := c.perfClrJIT.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Jit: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -95,6 +95,7 @@ func (c *Collector) buildClrLoading() error {
type perfDataClrLoading struct {
Name string
Occurrence int
BytesinLoaderHeap float64 `perfdata:"Bytes in Loader Heap"`
CurrentAssemblies float64 `perfdata:"Current Assemblies"`
CurrentClassesLoaded float64 `perfdata:"Current Classes Loaded"`
@@ -106,12 +107,16 @@ type perfDataClrLoading struct {
Totalappdomainsunloaded float64 `perfdata:"Total appdomains unloaded"`
}
func (p *perfDataClrLoading) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrLoading
if err := c.perfClrLoading.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Loading: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -83,6 +83,7 @@ func (c *Collector) buildClrLocksAndThreads() error {
type perfDataClrLocksAndThreads struct {
Name string
Occurrence int
CurrentQueueLength float64 `perfdata:"Current Queue Length"`
NumberofcurrentlogicalThreads float64 `perfdata:"# of current logical Threads"`
NumberofcurrentphysicalThreads float64 `perfdata:"# of current physical Threads"`
@@ -92,12 +93,16 @@ type perfDataClrLocksAndThreads struct {
TotalNumberofContentions float64 `perfdata:"Total # of Contentions"`
}
func (p *perfDataClrLocksAndThreads) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrLocksAndThreads
if err := c.perfClrLocksAndThreads.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR LocksAndThreads: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -114,6 +114,7 @@ func (c *Collector) buildClrMemory() error {
type perfDataClrMemory struct {
Name string
Occurrence int
AllocatedBytesPersec float64 `perfdata:"Allocated Bytes/sec"`
FinalizationSurvivors float64 `perfdata:"Finalization Survivors"`
Gen0PromotedBytesPerSec float64 `perfdata:"Gen 0 Promoted Bytes/Sec"`
@@ -136,12 +137,16 @@ type perfDataClrMemory struct {
ProcessID float64 `perfdata:"Process ID"`
}
func (p *perfDataClrMemory) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrMemory
if err := c.perfClrMemory.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Memory: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -77,6 +77,7 @@ func (c *Collector) buildClrRemoting() error {
type perfDataClrRemoting struct {
Name string
Occurrence int
Channels float64 `perfdata:"Channels"`
ContextBoundClassesLoaded float64 `perfdata:"Context-Bound Classes Loaded"`
ContextBoundObjectsAllocPersec float64 `perfdata:"Context-Bound Objects Alloc / sec"`
@@ -85,12 +86,16 @@ type perfDataClrRemoting struct {
TotalRemoteCalls float64 `perfdata:"Total Remote Calls"`
}
func (p *perfDataClrRemoting) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrRemoting
if err := c.perfClrRemoting.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Remoting: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -65,18 +65,23 @@ func (c *Collector) buildClrSecurity() error {
type perfDataClrSecurity struct {
Name string
Occurrence int
NumberLinkTimeChecks float64 `perfdata:"# Link Time Checks"`
PercentTimeinRTchecks float64 `perfdata:"% Time in RT checks"`
StackWalkDepth float64 `perfdata:"Stack Walk Depth"`
TotalRuntimeChecks float64 `perfdata:"Total Runtime Checks"`
}
func (p *perfDataClrSecurity) instance() (*string, int) { return &p.Name, p.Occurrence }
func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, _ time.Duration) error {
var dst []perfDataClrSecurity
if err := c.perfClrSecurity.Collect(&dst); err != nil {
return fmt.Errorf("failed to collect .NET CLR Security: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
@@ -40,7 +40,8 @@ func TestClrRemotingCounterPaths(t *testing.T) {
"TotalRemoteCalls": "Total Remote Calls",
}
rowType := reflect.TypeFor[perfDataClrRemoting]()
require.Equal(t, len(expected)+1, rowType.NumField())
// Name and Occurrence hold the instance.
require.Equal(t, len(expected)+2, rowType.NumField())
for field, counter := range expected {
f, found := rowType.FieldByName(field)
@@ -415,6 +415,42 @@ func TestGCZeroBaseDoesNotPublishInvalidRatio(t *testing.T) {
require.True(t, foundAllocated)
}
// TestDuplicateProcessNames checks that processes with the same name, which
// the CLR objects report with the same instance name, are published apart.
func TestDuplicateProcessNames(t *testing.T) {
c := &Collector{}
c.describeClrExceptions()
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: []perfDataClrExceptions{
{Name: "powershell", NumberofExcepsThrown: 1},
{Name: "powershell", Occurrence: 1, NumberofExcepsThrown: 2},
{Name: "powershell#1", NumberofExcepsThrown: 3},
{Name: "powershell", Occurrence: 2, NumberofExcepsThrown: 4},
}}
registry := prometheus.NewRegistry()
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) {
require.NoError(t, c.collectClrExceptions(ch, nil, time.Second))
}})
families, err := registry.Gather()
require.NoError(t, err)
got := map[string]float64{}
for _, family := range families {
if family.GetName() != "windows_netframework_clrexceptions_exceptions_thrown_total" {
continue
}
for _, metric := range family.GetMetric() {
got[metric.GetLabel()[0].GetValue()] = metric.GetCounter().GetValue()
}
}
// "powershell#1" is the name of a process, so the suffixes skip it.
require.Equal(t, map[string]float64{"powershell": 1, "powershell#2": 2, "powershell#1": 3, "powershell#3": 4}, got)
}
func TestMissingObjectErrorClassification(t *testing.T) {
missing := pdh.NewPdhError(pdh.CstatusNoObject)
unrelated := errors.New("access denied")
@@ -284,6 +284,13 @@ func (c *Collector) collectObject(ch chan<- prometheus.Metric, perfDataObject Ob
return fmt.Errorf("failed to collect data: %w", err)
}
// Some objects, e.g. Process, report the same instance name more than
// once. Each occurrence is published with a "#N" suffix, see the docs.
pdh.SuffixDuplicates(rows,
func(row *pdh.Row) *string { return &row.Name },
func(row *pdh.Row) int { return row.Occurrence },
)
metricTypes := counterMetricTypes(perfDataObject)
var errs []error
@@ -344,6 +344,46 @@ func TestBuildInvalidConfig(t *testing.T) {
}
}
// TestCollectorDuplicateInstances checks that the svchost processes, which the
// Process object reports with the same name, are published with suffixes.
func TestCollectorDuplicateInstances(t *testing.T) {
t.Parallel()
c := performancecounter.New(&performancecounter.Config{
Objects: []performancecounter.Object{{
Name: "process", Object: "Process", Instances: []string{"*"},
Counters: []performancecounter.Counter{{Name: "ID Process", Type: "gauge"}},
}},
})
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, "every series is unique")
instances := map[string]bool{}
for _, family := range families {
if family.GetName() != "windows_performancecounter_process_id_process" {
continue
}
for _, metric := range family.GetMetric() {
for _, label := range metric.GetLabel() {
if label.GetName() == "instance" {
instances[label.GetValue()] = true
}
}
}
}
require.True(t, instances["svchost"], "svchost")
require.True(t, instances["svchost#1"], "svchost#1")
}
func TestCollectorClose(t *testing.T) {
t.Parallel()