netframework: sample all .NET CLR counter objects in one PDH query (#2686)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-11 19:50:23 +02:00
committed by GitHub
parent 897855ac16
commit 1f96f71a7c
9 changed files with 40 additions and 17 deletions
@@ -77,11 +77,15 @@ type Collector struct {
// labels keeps the label pairs of the series of the last scrape. The
// counters change on every scrape, but their process labels stay the
// same while the process runs.
labels labelcache.Cache
config Config
logger *slog.Logger
perfFrequency float64
closeFns []func()
labels labelcache.Cache
config Config
logger *slog.Logger
perfFrequency float64
closeFns []func()
// perfQuery samples the counters of all enabled sub-collectors at once.
// The .NET CLR counter provider serves one query at a time and walks
// every .NET process for each, so separate queries wait for each other.
perfQuery *pdh.Query
perfClrExceptions perfCollector[perfDataClrExceptions]
perfClrInterop perfCollector[perfDataClrInterop]
perfClrJIT perfCollector[perfDataClrJIT]
@@ -210,6 +214,9 @@ func (c *Collector) Close() error {
c.collectorFns = nil
c.labels.Reset()
c.perfQuery.Close()
c.perfQuery = nil
return nil
}
@@ -230,6 +237,13 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.perfFrequency = float64(frequency)
}
var err error
c.perfQuery, err = pdh.NewQuery()
if err != nil {
return fmt.Errorf("failed to open .NET CLR performance counter query: %w", err)
}
c.collectorFns = make([]func(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, maxScrapeDuration time.Duration) error, 0, len(c.config.CollectorsEnabled))
subCollectors := map[string]struct {
@@ -340,6 +354,14 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
// Collect sends the metric values for each metric
// to the provided prometheus Metric channel.
func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
// The sub-collectors read this sample. Without any .NET process there is
// no data; the sub-collectors report that themselves.
if c.perfQuery != nil {
if err := c.perfQuery.Collect(); err != nil && !errors.Is(err, pdh.ErrNoData) {
return fmt.Errorf("failed to collect .NET CLR performance counters: %w", err)
}
}
scrape := c.labels.Begin()
defer scrape.Commit()
@@ -354,10 +376,11 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
return g.Wait()
}
// newPerfCollector retains ownership of partially initialized PDH queries.
// Missing objects are optional; missing individual counters remain errors.
func newPerfCollector[T any](logger *slog.Logger, object string) (perfCollector[T], error) {
c, err := pdh.NewCollector[T](logger, pdh.CounterTypeRaw, object, pdh.InstancesAll)
// newPerfCollector adds the counters of object to query and retains
// ownership of partially initialized collectors. Missing objects are optional;
// missing individual counters remain errors.
func newPerfCollector[T any](logger *slog.Logger, query *pdh.Query, object string) (perfCollector[T], error) {
c, err := pdh.NewQueryCollector[T](logger, query, pdh.CounterTypeRaw, object, pdh.InstancesAll)
if err == nil {
return c, nil
}
@@ -58,7 +58,7 @@ func (c *Collector) buildClrExceptions() error {
var err error
c.perfClrExceptions, err = newPerfCollector[perfDataClrExceptions](c.logger, ".NET CLR Exceptions")
c.perfClrExceptions, err = newPerfCollector[perfDataClrExceptions](c.logger, c.perfQuery, ".NET CLR Exceptions")
return err
}
@@ -52,7 +52,7 @@ func (c *Collector) buildClrInterop() error {
var err error
c.perfClrInterop, err = newPerfCollector[perfDataClrInterop](c.logger, ".NET CLR Interop")
c.perfClrInterop, err = newPerfCollector[perfDataClrInterop](c.logger, c.perfQuery, ".NET CLR Interop")
return err
}
@@ -58,7 +58,7 @@ func (c *Collector) buildClrJIT() error {
var err error
c.perfClrJIT, err = newPerfCollector[perfDataClrJIT](c.logger, ".NET CLR Jit")
c.perfClrJIT, err = newPerfCollector[perfDataClrJIT](c.logger, c.perfQuery, ".NET CLR Jit")
return err
}
@@ -88,7 +88,7 @@ func (c *Collector) buildClrLoading() error {
var err error
c.perfClrLoading, err = newPerfCollector[perfDataClrLoading](c.logger, ".NET CLR Loading")
c.perfClrLoading, err = newPerfCollector[perfDataClrLoading](c.logger, c.perfQuery, ".NET CLR Loading")
return err
}
@@ -76,7 +76,7 @@ func (c *Collector) buildClrLocksAndThreads() error {
var err error
c.perfClrLocksAndThreads, err = newPerfCollector[perfDataClrLocksAndThreads](c.logger, ".NET CLR LocksAndThreads")
c.perfClrLocksAndThreads, err = newPerfCollector[perfDataClrLocksAndThreads](c.logger, c.perfQuery, ".NET CLR LocksAndThreads")
return err
}
@@ -107,7 +107,7 @@ func (c *Collector) buildClrMemory() error {
var err error
c.perfClrMemory, err = newPerfCollector[perfDataClrMemory](c.logger, ".NET CLR Memory")
c.perfClrMemory, err = newPerfCollector[perfDataClrMemory](c.logger, c.perfQuery, ".NET CLR Memory")
return err
}
@@ -70,7 +70,7 @@ func (c *Collector) buildClrRemoting() error {
var err error
c.perfClrRemoting, err = newPerfCollector[perfDataClrRemoting](c.logger, ".NET CLR Remoting")
c.perfClrRemoting, err = newPerfCollector[perfDataClrRemoting](c.logger, c.perfQuery, ".NET CLR Remoting")
return err
}
@@ -58,7 +58,7 @@ func (c *Collector) buildClrSecurity() error {
var err error
c.perfClrSecurity, err = newPerfCollector[perfDataClrSecurity](c.logger, ".NET CLR Security")
c.perfClrSecurity, err = newPerfCollector[perfDataClrSecurity](c.logger, c.perfQuery, ".NET CLR Security")
return err
}