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
@@ -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")