netframework: collect CLR performance counters through PDH (#2632)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-09 22:47:26 +02:00
committed by GitHub
parent 0b22e0caac
commit e58e4763e2
13 changed files with 924 additions and 191 deletions
+4 -2
View File
@@ -1,11 +1,13 @@
# netframework collector
The netframework collector exposes metrics about dotnet framework.
The netframework collector exposes .NET Framework performance counters using PDH. Raw counter values preserve the existing metric types and units; counters named per second are published as cumulative values where specified below. The `_Global_` instance is excluded. Instance occurrence suffixes distinguish processes with the same name within a scrape and are not stable process identifiers.
For compatibility, JIT and runtime security check time metrics retain their existing raw numerator divided by the performance-clock frequency. The GC time metric uses 100 times its raw numerator divided by the raw base; samples with a zero base omit that metric while retaining the other valid metrics. Missing performance counter objects return no data; initialization errors for missing individual counters are reported.
| | |
|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Metric name prefix** | `netframework_clrexceptions` |
| **Classes** | `Win32_PerfRawData_NETFramework_NETCLRExceptions`, `Win32_PerfRawData_NETFramework_NETCLRInterop`, `Win32_PerfRawData_NETFramework_NETCLRJit`, `Win32_PerfRawData_NETFramework_NETCLRLoading`, `Win32_PerfRawData_NETFramework_NETCLRLocksAndThreads`, `Win32_PerfRawData_NETFramework_NETCLRMemory`, `Win32_PerfRawData_NETFramework_NETCLRRemoting`, `Win32_PerfRawData_NETFramework_NETCLRSecurity` |
| **Performance counter objects** | `.NET CLR Exceptions`, `.NET CLR Interop`, `.NET CLR Jit`, `.NET CLR Loading`, `.NET CLR LocksAndThreads`, `.NET CLR Memory`, `.NET CLR Remoting`, `.NET CLR Security` |
| **Enabled by default?** | No |
## Flags
+138 -10
View File
@@ -25,13 +25,21 @@ import (
"slices"
"strings"
"time"
"unsafe"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
)
type perfCollector[T any] interface {
Collect(dst *[]T) error
Close()
}
const Name = "netframework"
type Config struct {
@@ -63,10 +71,20 @@ const (
collectorClrSecurity = "clrsecurity"
)
// A Collector is a Prometheus Collector for WMI Win32_PerfRawData_NETFramework_NETCLRExceptions metrics.
// A Collector collects .NET Framework performance counters.
type Collector struct {
config Config
miSession *mi.Session
config Config
logger *slog.Logger
perfFrequency float64
closeFns []func()
perfClrExceptions perfCollector[perfDataClrExceptions]
perfClrInterop perfCollector[perfDataClrInterop]
perfClrJIT perfCollector[perfDataClrJIT]
perfClrLoading perfCollector[perfDataClrLoading]
perfClrLocksAndThreads perfCollector[perfDataClrLocksAndThreads]
perfClrMemory perfCollector[perfDataClrMemory]
perfClrRemoting perfCollector[perfDataClrRemoting]
perfClrSecurity perfCollector[perfDataClrSecurity]
collectorFns []func(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error
@@ -179,58 +197,111 @@ func (c *Collector) GetName() string {
}
func (c *Collector) Close() error {
for _, closeFn := range c.closeFns {
closeFn()
}
c.closeFns = nil
c.collectorFns = nil
return nil
}
func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
if len(c.config.CollectorsEnabled) == 0 {
return nil
}
if miSession == nil {
return errors.New("miSession is nil")
}
c.logger = logger
if slices.Contains(c.config.CollectorsEnabled, collectorClrJIT) || slices.Contains(c.config.CollectorsEnabled, collectorClrSecurity) {
var frequency int64
c.miSession = miSession
ret, _, err := windows.NewLazySystemDLL("kernel32.dll").NewProc("QueryPerformanceFrequency").Call(uintptr(unsafe.Pointer(&frequency)))
if ret == 0 || frequency <= 0 {
return fmt.Errorf("QueryPerformanceFrequency: %w", err)
}
c.perfFrequency = float64(frequency)
}
c.collectorFns = make([]func(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error, 0, len(c.config.CollectorsEnabled))
subCollectors := map[string]struct {
build func()
build func() error
collect func(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error
close func()
}{
collectorClrExceptions: {
build: c.buildClrExceptions,
collect: c.collectClrExceptions,
close: func() {
if c.perfClrExceptions != nil {
c.perfClrExceptions.Close()
}
},
},
collectorClrJIT: {
build: c.buildClrJIT,
collect: c.collectClrJIT,
close: func() {
if c.perfClrJIT != nil {
c.perfClrJIT.Close()
}
},
},
collectorClrLoading: {
build: c.buildClrLoading,
collect: c.collectClrLoading,
close: func() {
if c.perfClrLoading != nil {
c.perfClrLoading.Close()
}
},
},
collectorClrInterop: {
build: c.buildClrInterop,
collect: c.collectClrInterop,
close: func() {
if c.perfClrInterop != nil {
c.perfClrInterop.Close()
}
},
},
collectorClrLocksAndThreads: {
build: c.buildClrLocksAndThreads,
collect: c.collectClrLocksAndThreads,
close: func() {
if c.perfClrLocksAndThreads != nil {
c.perfClrLocksAndThreads.Close()
}
},
},
collectorClrMemory: {
build: c.buildClrMemory,
collect: c.collectClrMemory,
close: func() {
if c.perfClrMemory != nil {
c.perfClrMemory.Close()
}
},
},
collectorClrRemoting: {
build: c.buildClrRemoting,
collect: c.collectClrRemoting,
close: func() {
if c.perfClrRemoting != nil {
c.perfClrRemoting.Close()
}
},
},
collectorClrSecurity: {
build: c.buildClrSecurity,
collect: c.collectClrSecurity,
close: func() {
if c.perfClrSecurity != nil {
c.perfClrSecurity.Close()
}
},
},
}
@@ -247,7 +318,12 @@ func (c *Collector) Build(_ *slog.Logger, miSession *mi.Session) error {
}
for _, name := range collectorsEnabled {
subCollectors[name].build()
c.closeFns = append(c.closeFns, subCollectors[name].close)
if err := subCollectors[name].build(); err != nil {
_ = c.Close()
return fmt.Errorf("failed to build %s: %w", name, err)
}
c.collectorFns = append(c.collectorFns, subCollectors[name].collect)
}
@@ -268,3 +344,55 @@ 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)
if err == nil {
return c, nil
}
if c != nil {
c.Close()
}
if onlyMissingObject(err) {
return unavailablePerfCollector[T]{}, nil
}
return nil, err
}
func onlyMissingObject(err error) bool {
if joined, ok := err.(interface{ Unwrap() []error }); ok {
children := joined.Unwrap()
if len(children) == 0 {
return false
}
for _, child := range children {
if !onlyMissingObject(child) {
return false
}
}
return true
}
if wrapped, ok := err.(interface{ Unwrap() error }); ok {
return onlyMissingObject(wrapped.Unwrap())
}
return errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject))
}
type unavailablePerfCollector[T any] struct{}
func (unavailablePerfCollector[T]) Collect(dst *[]T) error {
*dst = nil
return pdh.ErrNoData
}
func (unavailablePerfCollector[T]) Close() {}
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrExceptions() {
func (c *Collector) describeClrExceptions() {
c.numberOfExceptionsThrown = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrExceptions+"_exceptions_thrown_total"),
"Displays the total number of exceptions thrown since the application started. This includes both .NET exceptions and unmanaged exceptions that are converted into .NET exceptions.",
@@ -54,20 +52,28 @@ func (c *Collector) buildClrExceptions() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRExceptions struct {
Name string `mi:"Name"`
func (c *Collector) buildClrExceptions() error {
c.describeClrExceptions()
NumberofExcepsThrown uint32 `mi:"NumberofExcepsThrown"`
NumberofExcepsThrownPersec uint32 `mi:"NumberofExcepsThrownPersec"`
NumberofFiltersPersec uint32 `mi:"NumberofFiltersPersec"`
NumberofFinallysPersec uint32 `mi:"NumberofFinallysPersec"`
ThrowToCatchDepthPersec uint32 `mi:"ThrowToCatchDepthPersec"`
var err error
c.perfClrExceptions, err = newPerfCollector[perfDataClrExceptions](c.logger, ".NET CLR Exceptions")
return err
}
func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRExceptions
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRExceptions")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrExceptions struct {
Name string
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 (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrInterop() {
func (c *Collector) describeClrInterop() {
c.numberOfCCWs = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrInterop+"_com_callable_wrappers_total"),
"Displays the current number of COM callable wrappers (CCWs). A CCW is a proxy for a managed object being referenced from an unmanaged COM client.",
@@ -48,20 +46,27 @@ func (c *Collector) buildClrInterop() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRInterop struct {
Name string `mi:"Name"`
func (c *Collector) buildClrInterop() error {
c.describeClrInterop()
NumberofCCWs uint32 `mi:"NumberofCCWs"`
Numberofmarshalling uint32 `mi:"Numberofmarshalling"`
NumberofStubs uint32 `mi:"NumberofStubs"`
NumberofTLBexportsPersec uint32 `mi:"NumberofTLBexportsPersec"`
NumberofTLBimportsPersec uint32 `mi:"NumberofTLBimportsPersec"`
var err error
c.perfClrInterop, err = newPerfCollector[perfDataClrInterop](c.logger, ".NET CLR Interop")
return err
}
func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRInterop
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRInterop")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrInterop struct {
Name string
NumberofCCWs float64 `perfdata:"# of CCWs"`
NumberofStubs float64 `perfdata:"# of Stubs"`
Numberofmarshalling float64 `perfdata:"# of marshalling"`
}
func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrJIT() {
func (c *Collector) describeClrJIT() {
c.numberOfMethodsJitted = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrJIT+"_jit_methods_total"),
"Displays the total number of methods JIT-compiled since the application started. This counter does not include pre-JIT-compiled methods.",
@@ -54,22 +52,28 @@ func (c *Collector) buildClrJIT() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRJit struct {
Name string `mi:"Name"`
func (c *Collector) buildClrJIT() error {
c.describeClrJIT()
Frequency_PerfTime uint64 `mi:"Frequency_PerfTime"`
ILBytesJittedPersec uint32 `mi:"ILBytesJittedPersec"`
NumberofILBytesJitted uint32 `mi:"NumberofILBytesJitted"`
NumberofMethodsJitted uint32 `mi:"NumberofMethodsJitted"`
PercentTimeinJit uint32 `mi:"PercentTimeinJit"`
StandardJitFailures uint32 `mi:"StandardJitFailures"`
TotalNumberofILBytesJitted uint32 `mi:"TotalNumberofILBytesJitted"`
var err error
c.perfClrJIT, err = newPerfCollector[perfDataClrJIT](c.logger, ".NET CLR Jit")
return err
}
func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRJit
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRJit")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrJIT struct {
Name string
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 (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -87,7 +91,7 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, maxScrapeDuration
ch <- prometheus.MustNewConstMetric(
c.timeInJit,
prometheus.GaugeValue,
float64(process.PercentTimeinJit)/float64(process.Frequency_PerfTime),
float64(process.PercentTimeinJit)/c.perfFrequency,
process.Name,
)
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrLoading() {
func (c *Collector) describeClrLoading() {
c.bytesInLoaderHeap = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_loader_heap_size_bytes"),
"Displays the current size, in bytes, of the memory committed by the class loader across all application domains. Committed memory is the physical space reserved in the disk paging file.",
@@ -84,31 +82,33 @@ func (c *Collector) buildClrLoading() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRLoading struct {
Name string `mi:"Name"`
func (c *Collector) buildClrLoading() error {
c.describeClrLoading()
AssemblySearchLength uint32 `mi:"AssemblySearchLength"`
BytesinLoaderHeap uint64 `mi:"BytesinLoaderHeap"`
Currentappdomains uint32 `mi:"Currentappdomains"`
CurrentAssemblies uint32 `mi:"CurrentAssemblies"`
CurrentClassesLoaded uint32 `mi:"CurrentClassesLoaded"`
PercentTimeLoading uint64 `mi:"PercentTimeLoading"`
Rateofappdomains uint32 `mi:"Rateofappdomains"`
Rateofappdomainsunloaded uint32 `mi:"Rateofappdomainsunloaded"`
RateofAssemblies uint32 `mi:"RateofAssemblies"`
RateofClassesLoaded uint32 `mi:"RateofClassesLoaded"`
RateofLoadFailures uint32 `mi:"RateofLoadFailures"`
TotalAppdomains uint32 `mi:"TotalAppdomains"`
Totalappdomainsunloaded uint32 `mi:"Totalappdomainsunloaded"`
TotalAssemblies uint32 `mi:"TotalAssemblies"`
TotalClassesLoaded uint32 `mi:"TotalClassesLoaded"`
TotalNumberofLoadFailures uint32 `mi:"TotalNumberofLoadFailures"`
var err error
c.perfClrLoading, err = newPerfCollector[perfDataClrLoading](c.logger, ".NET CLR Loading")
return err
}
func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRLoading
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRLoading")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrLoading struct {
Name string
BytesinLoaderHeap float64 `perfdata:"Bytes in Loader Heap"`
CurrentAssemblies float64 `perfdata:"Current Assemblies"`
CurrentClassesLoaded float64 `perfdata:"Current Classes Loaded"`
Currentappdomains float64 `perfdata:"Current appdomains"`
TotalAppdomains float64 `perfdata:"Total Appdomains"`
TotalAssemblies float64 `perfdata:"Total Assemblies"`
TotalClassesLoaded float64 `perfdata:"Total Classes Loaded"`
TotalNumberofLoadFailures float64 `perfdata:"Total # of Load Failures"`
Totalappdomainsunloaded float64 `perfdata:"Total appdomains unloaded"`
}
func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrLocksAndThreads() {
func (c *Collector) describeClrLocksAndThreads() {
c.currentQueueLength = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_current_queue_length"),
"Displays the total number of threads that are currently waiting to acquire a managed lock in the application.",
@@ -72,25 +70,31 @@ func (c *Collector) buildClrLocksAndThreads() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRLocksAndThreads struct {
Name string `mi:"Name"`
func (c *Collector) buildClrLocksAndThreads() error {
c.describeClrLocksAndThreads()
ContentionRatePersec uint32 `mi:"ContentionRatePersec"`
CurrentQueueLength uint32 `mi:"CurrentQueueLength"`
NumberofcurrentlogicalThreads uint32 `mi:"NumberofcurrentlogicalThreads"`
NumberofcurrentphysicalThreads uint32 `mi:"NumberofcurrentphysicalThreads"`
Numberofcurrentrecognizedthreads uint32 `mi:"Numberofcurrentrecognizedthreads"`
Numberoftotalrecognizedthreads uint32 `mi:"Numberoftotalrecognizedthreads"`
QueueLengthPeak uint32 `mi:"QueueLengthPeak"`
QueueLengthPersec uint32 `mi:"QueueLengthPersec"`
RateOfRecognizedThreadsPersec uint32 `mi:"RateOfRecognizedThreadsPersec"`
TotalNumberofContentions uint32 `mi:"TotalNumberofContentions"`
var err error
c.perfClrLocksAndThreads, err = newPerfCollector[perfDataClrLocksAndThreads](c.logger, ".NET CLR LocksAndThreads")
return err
}
func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRLocksAndThreads
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRLocksAndThreads")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrLocksAndThreads struct {
Name string
CurrentQueueLength float64 `perfdata:"Current Queue Length"`
NumberofcurrentlogicalThreads float64 `perfdata:"# of current logical Threads"`
NumberofcurrentphysicalThreads float64 `perfdata:"# of current physical Threads"`
Numberofcurrentrecognizedthreads float64 `perfdata:"# of current recognized threads"`
Numberoftotalrecognizedthreads float64 `perfdata:"# of total recognized threads"`
QueueLengthPeak float64 `perfdata:"Queue Length Peak"`
TotalNumberofContentions float64 `perfdata:"Total # of Contentions"`
}
func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -22,13 +22,11 @@ import (
"strconv"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrMemory() {
func (c *Collector) describeClrMemory() {
c.allocatedBytes = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrMemory+"_allocated_bytes_total"),
"Displays the total number of bytes allocated on the garbage collection heap.",
@@ -103,44 +101,44 @@ func (c *Collector) buildClrMemory() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRMemory struct {
Name string `mi:"Name"`
func (c *Collector) buildClrMemory() error {
c.describeClrMemory()
AllocatedBytesPersec uint64 `mi:"AllocatedBytesPersec"`
FinalizationSurvivors uint64 `mi:"FinalizationSurvivors"`
Frequency_PerfTime uint64 `mi:"Frequency_PerfTime"`
Gen0heapsize uint64 `mi:"Gen0heapsize"`
Gen0PromotedBytesPerSec uint64 `mi:"Gen0PromotedBytesPersec"`
Gen1heapsize uint64 `mi:"Gen1heapsize"`
Gen1PromotedBytesPerSec uint64 `mi:"Gen1PromotedBytesPersec"`
Gen2heapsize uint64 `mi:"Gen2heapsize"`
LargeObjectHeapsize uint64 `mi:"LargeObjectHeapsize"`
NumberBytesinallHeaps uint64 `mi:"NumberBytesinallHeaps"`
NumberGCHandles uint64 `mi:"NumberGCHandles"`
NumberGen0Collections uint64 `mi:"NumberGen0Collections"`
NumberGen1Collections uint64 `mi:"NumberGen1Collections"`
NumberGen2Collections uint64 `mi:"NumberGen2Collections"`
NumberInducedGC uint64 `mi:"NumberInducedGC"`
NumberofPinnedObjects uint64 `mi:"NumberofPinnedObjects"`
NumberofSinkBlocksinuse uint64 `mi:"NumberofSinkBlocksinuse"`
NumberTotalcommittedBytes uint64 `mi:"NumberTotalcommittedBytes"`
NumberTotalreservedBytes uint64 `mi:"NumberTotalreservedBytes"`
// PercentTimeinGC has countertype=PERF_RAW_FRACTION.
// Formula: (100 * CounterValue) / BaseValue
// By docs https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/scripting-articles/ms974615(v=msdn.10)#perf_raw_fraction
PercentTimeinGC uint32 `mi:"PercentTimeinGC"`
// BaseValue is just a "magic" number used to make the calculation come out right.
PercentTimeinGC_base uint32 `mi:"PercentTimeinGC_base"`
ProcessID uint64 `mi:"ProcessID"`
PromotedFinalizationMemoryfromGen0 uint64 `mi:"PromotedFinalizationMemoryfromGen0"`
PromotedMemoryfromGen0 uint64 `mi:"PromotedMemoryfromGen0"`
PromotedMemoryfromGen1 uint64 `mi:"PromotedMemoryfromGen1"`
var err error
c.perfClrMemory, err = newPerfCollector[perfDataClrMemory](c.logger, ".NET CLR Memory")
return err
}
func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRMemory
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRMemory")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrMemory struct {
Name string
AllocatedBytesPersec float64 `perfdata:"Allocated Bytes/sec"`
FinalizationSurvivors float64 `perfdata:"Finalization Survivors"`
Gen0PromotedBytesPerSec float64 `perfdata:"Gen 0 Promoted Bytes/Sec"`
Gen0heapsize float64 `perfdata:"Gen 0 heap size"`
Gen1PromotedBytesPerSec float64 `perfdata:"Gen 1 Promoted Bytes/Sec"`
Gen1heapsize float64 `perfdata:"Gen 1 heap size"`
Gen2heapsize float64 `perfdata:"Gen 2 heap size"`
LargeObjectHeapsize float64 `perfdata:"Large Object Heap size"`
NumberGCHandles float64 `perfdata:"# GC Handles"`
NumberGen0Collections float64 `perfdata:"# Gen 0 Collections"`
NumberGen1Collections float64 `perfdata:"# Gen 1 Collections"`
NumberGen2Collections float64 `perfdata:"# Gen 2 Collections"`
NumberInducedGC float64 `perfdata:"# Induced GC"`
NumberTotalcommittedBytes float64 `perfdata:"# Total committed Bytes"`
NumberTotalreservedBytes float64 `perfdata:"# Total reserved Bytes"`
NumberofPinnedObjects float64 `perfdata:"# of Pinned Objects"`
NumberofSinkBlocksinuse float64 `perfdata:"# of Sink Blocks in use"`
PercentTimeinGC float64 `perfdata:"% Time in GC"`
PercentTimeinGC_base float64 `perfdata:"% Time in GC,secondvalue"`
ProcessID float64 `perfdata:"Process ID"`
}
func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -153,7 +151,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.CounterValue,
float64(process.AllocatedBytesPersec),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -161,7 +159,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.FinalizationSurvivors),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -169,7 +167,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.Gen0heapsize),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen0",
)
@@ -178,7 +176,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.Gen0PromotedBytesPerSec),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen0",
)
@@ -187,7 +185,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.Gen1heapsize),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen1",
)
@@ -196,7 +194,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.Gen1PromotedBytesPerSec),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen1",
)
@@ -205,7 +203,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.Gen2heapsize),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen2",
)
@@ -214,7 +212,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.LargeObjectHeapsize),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"LOH",
)
@@ -223,7 +221,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.NumberGCHandles),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -231,7 +229,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.CounterValue,
float64(process.NumberGen0Collections),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen0",
)
@@ -240,7 +238,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.CounterValue,
float64(process.NumberGen1Collections),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen1",
)
@@ -249,7 +247,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.CounterValue,
float64(process.NumberGen2Collections),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
"Gen2",
)
@@ -258,7 +256,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.CounterValue,
float64(process.NumberInducedGC),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -266,7 +264,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.NumberofPinnedObjects),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -274,7 +272,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.NumberofSinkBlocksinuse),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -282,7 +280,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.NumberTotalcommittedBytes),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
ch <- prometheus.MustNewConstMetric(
@@ -290,15 +288,19 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, maxScrapeDurat
prometheus.GaugeValue,
float64(process.NumberTotalreservedBytes),
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
if process.PercentTimeinGC_base <= 0 {
continue
}
ch <- prometheus.MustNewConstMetric(
c.timeInGC,
prometheus.GaugeValue,
float64(100*process.PercentTimeinGC)/float64(process.PercentTimeinGC_base),
100*process.PercentTimeinGC/process.PercentTimeinGC_base,
process.Name,
strconv.FormatUint(process.ProcessID, 10),
strconv.FormatUint(uint64(process.ProcessID), 10),
)
}
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrRemoting() {
func (c *Collector) describeClrRemoting() {
c.channels = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_channels_total"),
"Displays the total number of remoting channels registered across all application domains since application started.",
@@ -66,22 +64,30 @@ func (c *Collector) buildClrRemoting() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRRemoting struct {
Name string `mi:"Name"`
func (c *Collector) buildClrRemoting() error {
c.describeClrRemoting()
Channels uint32 `mi:"Channels"`
ContextBoundClassesLoaded uint32 `mi:"ContextBoundClassesLoaded"`
ContextBoundObjectsAllocPersec uint32 `mi:"ContextBoundObjectsAllocPersec"`
ContextProxies uint32 `mi:"ContextProxies"`
Contexts uint32 `mi:"Contexts"`
RemoteCallsPersec uint32 `mi:"RemoteCallsPersec"`
TotalRemoteCalls uint32 `mi:"TotalRemoteCalls"`
var err error
c.perfClrRemoting, err = newPerfCollector[perfDataClrRemoting](c.logger, ".NET CLR Remoting")
return err
}
func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRRemoting
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRRemoting")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrRemoting struct {
Name string
Channels float64 `perfdata:"Channels"`
ContextBoundClassesLoaded float64 `perfdata:"Context-Bound Classes Loaded"`
ContextBoundObjectsAllocPersec float64 `perfdata:"Context-Bound Objects Alloc / sec"`
ContextProxies float64 `perfdata:"Context Proxies"`
Contexts float64 `perfdata:"Contexts"`
TotalRemoteCalls float64 `perfdata:"Total Remote Calls"`
}
func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -21,13 +21,11 @@ import (
"fmt"
"time"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
func (c *Collector) buildClrSecurity() {
func (c *Collector) describeClrSecurity() {
c.numberLinkTimeChecks = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrSecurity+"_link_time_checks_total"),
"Displays the total number of link-time code access security checks since the application started.",
@@ -54,21 +52,28 @@ func (c *Collector) buildClrSecurity() {
)
}
type Win32_PerfRawData_NETFramework_NETCLRSecurity struct {
Name string `mi:"Name"`
func (c *Collector) buildClrSecurity() error {
c.describeClrSecurity()
Frequency_PerfTime uint64 `mi:"Frequency_PerfTime"`
NumberLinkTimeChecks uint32 `mi:"NumberLinkTimeChecks"`
PercentTimeinRTchecks uint32 `mi:"PercentTimeinRTchecks"`
PercentTimeSigAuthenticating uint64 `mi:"PercentTimeSigAuthenticating"`
StackWalkDepth uint32 `mi:"StackWalkDepth"`
TotalRuntimeChecks uint32 `mi:"TotalRuntimeChecks"`
var err error
c.perfClrSecurity, err = newPerfCollector[perfDataClrSecurity](c.logger, ".NET CLR Security")
return err
}
func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
var dst []Win32_PerfRawData_NETFramework_NETCLRSecurity
if err := c.miSession.Query(&dst, mi.NamespaceRootCIMv2, utils.Must(mi.NewQuery("SELECT * FROM Win32_PerfRawData_NETFramework_NETCLRSecurity")), maxScrapeDuration); err != nil {
return fmt.Errorf("WMI query failed: %w", err)
type perfDataClrSecurity struct {
Name string
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 (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, _ 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)
}
for _, process := range dst {
@@ -86,7 +91,7 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, maxScrapeDur
ch <- prometheus.MustNewConstMetric(
c.timeInRTChecks,
prometheus.GaugeValue,
float64(process.PercentTimeinRTchecks)/float64(process.Frequency_PerfTime),
float64(process.PercentTimeinRTchecks)/c.perfFrequency,
process.Name,
)
@@ -0,0 +1,86 @@
// 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 netframework
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// These paths are independently listed by Netdata's native Windows perflib
// collector and Mono's CLR counter definitions. Remoting is absent from the
// current Microsoft .NET Framework performance-counter documentation.
// https://github.com/netdata/netdata/blob/master/src/collectors/windows.plugin/perflib-netframework.c
// https://github.com/mono/mono/blob/main/mono/metadata/mono-perfcounters-def.h
func TestClrRemotingCounterPaths(t *testing.T) {
expected := map[string]string{
"Channels": "Channels",
"ContextBoundClassesLoaded": "Context-Bound Classes Loaded",
"ContextBoundObjectsAllocPersec": "Context-Bound Objects Alloc / sec",
"ContextProxies": "Context Proxies",
"Contexts": "Contexts",
"TotalRemoteCalls": "Total Remote Calls",
}
rowType := reflect.TypeFor[perfDataClrRemoting]()
require.Equal(t, len(expected)+1, rowType.NumField())
for field, counter := range expected {
f, found := rowType.FieldByName(field)
require.True(t, found, field)
require.Equal(t, counter, f.Tag.Get("perfdata"), field)
}
}
// Counter names also appear in the Windows counter inventory captured by:
// https://gist.github.com/hdansou/040207527657de0225b68912ff94400e
// Memory names are corroborated by Mono's primary CLR counter definitions.
func TestClrCounterPaths(t *testing.T) {
cases := []struct {
rowType reflect.Type
counters []string
}{
{reflect.TypeFor[perfDataClrExceptions](), []string{"# of Exceps Thrown", "# of Filters / sec", "# of Finallys / sec", "Throw To Catch Depth / sec"}},
{reflect.TypeFor[perfDataClrInterop](), []string{"# of CCWs", "# of Stubs", "# of marshalling"}},
{reflect.TypeFor[perfDataClrJIT](), []string{"# of Methods Jitted", "% Time in Jit", "Standard Jit Failures", "Total # of IL Bytes Jitted"}},
{reflect.TypeFor[perfDataClrLoading](), []string{"Bytes in Loader Heap", "Current appdomains", "Current Assemblies", "Current Classes Loaded", "Total Appdomains", "Total appdomains unloaded", "Total Assemblies", "Total Classes Loaded", "Total # of Load Failures"}},
{reflect.TypeFor[perfDataClrLocksAndThreads](), []string{"Current Queue Length", "# of current logical Threads", "# of current physical Threads", "# of current recognized threads", "# of total recognized threads", "Queue Length Peak", "Total # of Contentions"}},
{reflect.TypeFor[perfDataClrMemory](), []string{"Allocated Bytes/sec", "Finalization Survivors", "Gen 0 heap size", "Gen 0 Promoted Bytes/Sec", "Gen 1 heap size", "Gen 1 Promoted Bytes/Sec", "Gen 2 heap size", "Large Object Heap size", "# GC Handles", "# Gen 0 Collections", "# Gen 1 Collections", "# Gen 2 Collections", "# Induced GC", "# of Pinned Objects", "# of Sink Blocks in use", "# Total committed Bytes", "# Total reserved Bytes", "% Time in GC", "% Time in GC,secondvalue", "Process ID"}},
{reflect.TypeFor[perfDataClrRemoting](), []string{"Channels", "Context-Bound Classes Loaded", "Context-Bound Objects Alloc / sec", "Context Proxies", "Contexts", "Total Remote Calls"}},
{reflect.TypeFor[perfDataClrSecurity](), []string{"# Link Time Checks", "% Time in RT checks", "Stack Walk Depth", "Total Runtime Checks"}},
}
for _, tc := range cases {
t.Run(tc.rowType.Name(), func(t *testing.T) {
actual := make([]string, 0, tc.rowType.NumField()-1)
for field := range tc.rowType.Fields() {
if tag, found := field.Tag.Lookup("perfdata"); found {
actual = append(actual, strings.ToLower(tag))
}
}
expected := make([]string, len(tc.counters))
for i, counter := range tc.counters {
expected[i] = strings.ToLower(counter)
}
require.ElementsMatch(t, expected, actual)
})
}
}
@@ -0,0 +1,486 @@
// 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 netframework
import (
"errors"
"fmt"
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"
)
type fixturePerfCollector[T any] struct {
rows []T
err error
closed int
}
func (c *fixturePerfCollector[T]) Collect(dst *[]T) error {
*dst = append((*dst)[:0], c.rows...)
return c.err
}
func (c *fixturePerfCollector[T]) Close() { c.closed++ }
type fixturePromCollector struct {
collect func(chan<- prometheus.Metric)
}
func (fixturePromCollector) Describe(chan<- *prometheus.Desc) {}
func (c fixturePromCollector) Collect(ch chan<- prometheus.Metric) { c.collect(ch) }
type expectedMetric struct {
name string
metricType dto.MetricType
value float64
area string
}
func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, time.Duration) error, expected []expectedMetric, withPID bool) {
t.Helper()
registry := prometheus.NewRegistry()
var collectErr error
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) { collectErr = collect(ch, time.Second) }})
families, err := registry.Gather()
require.NoError(t, err)
require.NoError(t, collectErr)
wanted := make(map[string]expectedMetric)
for _, process := range []string{"worker", "worker#1", "report_Total"} {
for _, metric := range expected {
wanted[metric.name+"|"+process+"|"+metric.area] = metric
}
}
gotCount := 0
for _, family := range families {
for _, metric := range family.GetMetric() {
labels := make(map[string]string)
for _, label := range metric.GetLabel() {
labels[label.GetName()] = label.GetValue()
}
key := family.GetName() + "|" + labels["process"] + "|" + labels["area"]
want, ok := wanted[key]
require.True(t, ok, "unexpected metric %s", key)
require.Equal(t, want.metricType, family.GetType(), key)
value := metric.GetGauge().GetValue()
if family.GetType() == dto.MetricType_COUNTER {
value = metric.GetCounter().GetValue()
}
require.InDelta(t, want.value, value, 1e-12, key)
labelCount := 1
if withPID {
require.Equal(t, "123", labels["process_id"])
labelCount++
}
if want.area != "" {
labelCount++
}
require.Len(t, labels, labelCount)
delete(wanted, key)
gotCount++
}
}
require.Empty(t, wanted)
require.Equal(t, len(expected)*3, gotCount)
}
func TestClrExceptionsMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrExceptions()
row := perfDataClrExceptions{
NumberofExcepsThrown: 11,
NumberofFiltersPersec: 12,
NumberofFinallysPersec: 13,
ThrowToCatchDepthPersec: 14,
}
rows := make([]perfDataClrExceptions, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: rows}
assertFixtureMetrics(t, c.collectClrExceptions, []expectedMetric{
{"windows_netframework_clrexceptions_exceptions_thrown_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrexceptions_exceptions_filters_total", dto.MetricType_COUNTER, 12, ""},
{"windows_netframework_clrexceptions_exceptions_finallys_total", dto.MetricType_COUNTER, 13, ""},
{"windows_netframework_clrexceptions_throw_to_catch_depth_total", dto.MetricType_COUNTER, 14, ""},
}, false)
}
func TestClrInteropMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrInterop()
row := perfDataClrInterop{
NumberofCCWs: 11,
NumberofStubs: 12,
Numberofmarshalling: 13,
}
rows := make([]perfDataClrInterop, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrInterop = &fixturePerfCollector[perfDataClrInterop]{rows: rows}
assertFixtureMetrics(t, c.collectClrInterop, []expectedMetric{
{"windows_netframework_clrinterop_com_callable_wrappers_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrinterop_interop_marshalling_total", dto.MetricType_COUNTER, 13, ""},
{"windows_netframework_clrinterop_interop_stubs_created_total", dto.MetricType_COUNTER, 12, ""},
}, false)
}
func TestClrJITMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrJIT()
row := perfDataClrJIT{
NumberofMethodsJitted: 11,
PercentTimeinJit: 50,
StandardJitFailures: 13,
TotalNumberofILBytesJitted: 14,
}
rows := make([]perfDataClrJIT, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrJIT = &fixturePerfCollector[perfDataClrJIT]{rows: rows}
assertFixtureMetrics(t, c.collectClrJIT, []expectedMetric{
{"windows_netframework_clrjit_jit_methods_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrjit_jit_time_percent", dto.MetricType_GAUGE, 0.05, ""},
{"windows_netframework_clrjit_jit_standard_failures_total", dto.MetricType_GAUGE, 13, ""},
{"windows_netframework_clrjit_jit_il_bytes_total", dto.MetricType_COUNTER, 14, ""},
}, false)
}
func TestClrLoadingMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrLoading()
row := perfDataClrLoading{
BytesinLoaderHeap: 11,
CurrentAssemblies: 12,
CurrentClassesLoaded: 13,
Currentappdomains: 14,
TotalAppdomains: 15,
TotalAssemblies: 16,
TotalClassesLoaded: 17,
TotalNumberofLoadFailures: 18,
Totalappdomainsunloaded: 19,
}
rows := make([]perfDataClrLoading, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrLoading = &fixturePerfCollector[perfDataClrLoading]{rows: rows}
assertFixtureMetrics(t, c.collectClrLoading, []expectedMetric{
{"windows_netframework_clrloading_loader_heap_size_bytes", dto.MetricType_GAUGE, 11, ""},
{"windows_netframework_clrloading_appdomains_loaded_current", dto.MetricType_GAUGE, 14, ""},
{"windows_netframework_clrloading_assemblies_loaded_current", dto.MetricType_GAUGE, 12, ""},
{"windows_netframework_clrloading_classes_loaded_current", dto.MetricType_GAUGE, 13, ""},
{"windows_netframework_clrloading_appdomains_loaded_total", dto.MetricType_COUNTER, 15, ""},
{"windows_netframework_clrloading_appdomains_unloaded_total", dto.MetricType_COUNTER, 19, ""},
{"windows_netframework_clrloading_assemblies_loaded_total", dto.MetricType_COUNTER, 16, ""},
{"windows_netframework_clrloading_classes_loaded_total", dto.MetricType_COUNTER, 17, ""},
{"windows_netframework_clrloading_class_load_failures_total", dto.MetricType_COUNTER, 18, ""},
}, false)
}
func TestClrLocksAndThreadsMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrLocksAndThreads()
row := perfDataClrLocksAndThreads{
CurrentQueueLength: 11,
NumberofcurrentlogicalThreads: 12,
NumberofcurrentphysicalThreads: 13,
Numberofcurrentrecognizedthreads: 14,
Numberoftotalrecognizedthreads: 15,
QueueLengthPeak: 16,
TotalNumberofContentions: 17,
}
rows := make([]perfDataClrLocksAndThreads, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrLocksAndThreads = &fixturePerfCollector[perfDataClrLocksAndThreads]{rows: rows}
assertFixtureMetrics(t, c.collectClrLocksAndThreads, []expectedMetric{
{"windows_netframework_clrlocksandthreads_current_queue_length", dto.MetricType_GAUGE, 11, ""},
{"windows_netframework_clrlocksandthreads_current_logical_threads", dto.MetricType_GAUGE, 12, ""},
{"windows_netframework_clrlocksandthreads_physical_threads_current", dto.MetricType_GAUGE, 13, ""},
{"windows_netframework_clrlocksandthreads_recognized_threads_current", dto.MetricType_GAUGE, 14, ""},
{"windows_netframework_clrlocksandthreads_recognized_threads_total", dto.MetricType_COUNTER, 15, ""},
{"windows_netframework_clrlocksandthreads_queue_length_total", dto.MetricType_COUNTER, 16, ""},
{"windows_netframework_clrlocksandthreads_contentions_total", dto.MetricType_COUNTER, 17, ""},
}, false)
}
func TestClrMemoryMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrMemory()
row := perfDataClrMemory{
AllocatedBytesPersec: 11,
FinalizationSurvivors: 12,
Gen0PromotedBytesPerSec: 13,
Gen0heapsize: 14,
Gen1PromotedBytesPerSec: 15,
Gen1heapsize: 16,
Gen2heapsize: 17,
LargeObjectHeapsize: 18,
NumberGCHandles: 19,
NumberGen0Collections: 20,
NumberGen1Collections: 21,
NumberGen2Collections: 22,
NumberInducedGC: 23,
NumberTotalcommittedBytes: 24,
NumberTotalreservedBytes: 25,
NumberofPinnedObjects: 26,
NumberofSinkBlocksinuse: 27,
PercentTimeinGC: 50,
PercentTimeinGC_base: 200,
ProcessID: 123,
}
rows := make([]perfDataClrMemory, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrMemory = &fixturePerfCollector[perfDataClrMemory]{rows: rows}
assertFixtureMetrics(t, c.collectClrMemory, []expectedMetric{
{"windows_netframework_clrmemory_allocated_bytes_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrmemory_finalization_survivors", dto.MetricType_GAUGE, 12, ""},
{"windows_netframework_clrmemory_heap_size_bytes", dto.MetricType_GAUGE, 14, "Gen0"},
{"windows_netframework_clrmemory_promoted_bytes", dto.MetricType_GAUGE, 13, "Gen0"},
{"windows_netframework_clrmemory_heap_size_bytes", dto.MetricType_GAUGE, 16, "Gen1"},
{"windows_netframework_clrmemory_promoted_bytes", dto.MetricType_GAUGE, 15, "Gen1"},
{"windows_netframework_clrmemory_heap_size_bytes", dto.MetricType_GAUGE, 17, "Gen2"},
{"windows_netframework_clrmemory_heap_size_bytes", dto.MetricType_GAUGE, 18, "LOH"},
{"windows_netframework_clrmemory_number_gc_handles", dto.MetricType_GAUGE, 19, ""},
{"windows_netframework_clrmemory_collections_total", dto.MetricType_COUNTER, 20, "Gen0"},
{"windows_netframework_clrmemory_collections_total", dto.MetricType_COUNTER, 21, "Gen1"},
{"windows_netframework_clrmemory_collections_total", dto.MetricType_COUNTER, 22, "Gen2"},
{"windows_netframework_clrmemory_induced_gc_total", dto.MetricType_COUNTER, 23, ""},
{"windows_netframework_clrmemory_number_pinned_objects", dto.MetricType_GAUGE, 26, ""},
{"windows_netframework_clrmemory_number_sink_blocksinuse", dto.MetricType_GAUGE, 27, ""},
{"windows_netframework_clrmemory_committed_bytes", dto.MetricType_GAUGE, 24, ""},
{"windows_netframework_clrmemory_reserved_bytes", dto.MetricType_GAUGE, 25, ""},
{"windows_netframework_clrmemory_gc_time_percent", dto.MetricType_GAUGE, 25, ""},
}, true)
}
func TestClrRemotingMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrRemoting()
row := perfDataClrRemoting{
Channels: 11,
ContextBoundClassesLoaded: 12,
ContextBoundObjectsAllocPersec: 13,
ContextProxies: 14,
Contexts: 15,
TotalRemoteCalls: 16,
}
rows := make([]perfDataClrRemoting, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrRemoting = &fixturePerfCollector[perfDataClrRemoting]{rows: rows}
assertFixtureMetrics(t, c.collectClrRemoting, []expectedMetric{
{"windows_netframework_clrremoting_channels_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrremoting_context_bound_classes_loaded", dto.MetricType_GAUGE, 12, ""},
{"windows_netframework_clrremoting_context_bound_objects_total", dto.MetricType_COUNTER, 13, ""},
{"windows_netframework_clrremoting_context_proxies_total", dto.MetricType_COUNTER, 14, ""},
{"windows_netframework_clrremoting_contexts", dto.MetricType_GAUGE, 15, ""},
{"windows_netframework_clrremoting_remote_calls_total", dto.MetricType_COUNTER, 16, ""},
}, false)
}
func TestClrSecurityMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c.describeClrSecurity()
row := perfDataClrSecurity{
NumberLinkTimeChecks: 11,
PercentTimeinRTchecks: 50,
StackWalkDepth: 13,
TotalRuntimeChecks: 14,
}
rows := make([]perfDataClrSecurity, 0, 4)
for _, name := range []string{"_Global_", "worker", "worker#1", "report_Total"} {
row.Name = name
rows = append(rows, row)
}
c.perfClrSecurity = &fixturePerfCollector[perfDataClrSecurity]{rows: rows}
assertFixtureMetrics(t, c.collectClrSecurity, []expectedMetric{
{"windows_netframework_clrsecurity_link_time_checks_total", dto.MetricType_COUNTER, 11, ""},
{"windows_netframework_clrsecurity_rt_checks_time_percent", dto.MetricType_GAUGE, 0.05, ""},
{"windows_netframework_clrsecurity_stack_walk_depth", dto.MetricType_GAUGE, 13, ""},
{"windows_netframework_clrsecurity_runtime_checks_total", dto.MetricType_COUNTER, 14, ""},
}, false)
}
func TestGCZeroBaseDoesNotPublishInvalidRatio(t *testing.T) {
c := &Collector{}
c.describeClrMemory()
c.perfClrMemory = &fixturePerfCollector[perfDataClrMemory]{rows: []perfDataClrMemory{{Name: "worker", ProcessID: 123, AllocatedBytesPersec: 99}}}
registry := prometheus.NewRegistry()
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) { require.NoError(t, c.collectClrMemory(ch, time.Second)) }})
families, err := registry.Gather()
require.NoError(t, err)
foundAllocated := false
for _, family := range families {
require.NotEqual(t, "windows_netframework_clrmemory_gc_time_percent", family.GetName())
if family.GetName() == "windows_netframework_clrmemory_allocated_bytes_total" {
require.InDelta(t, 99.0, family.GetMetric()[0].GetCounter().GetValue(), 1e-12)
foundAllocated = true
}
}
require.True(t, foundAllocated)
}
func TestMissingObjectErrorClassification(t *testing.T) {
missing := pdh.NewPdhError(pdh.CstatusNoObject)
unrelated := errors.New("access denied")
require.True(t, onlyMissingObject(fmt.Errorf("initialization: %w", errors.Join(missing, missing))))
require.False(t, onlyMissingObject(errors.Join(missing, unrelated)))
require.False(t, onlyMissingObject(fmt.Errorf("initialization: %w", errors.Join(missing, unrelated))))
require.False(t, onlyMissingObject(pdh.NewPdhError(pdh.CstatusNoCounter)))
}
func TestNoDataIsReturned(t *testing.T) {
c := &Collector{}
c.describeClrExceptions()
c.perfClrExceptions = unavailablePerfCollector[perfDataClrExceptions]{}
ch := make(chan prometheus.Metric, 16)
require.ErrorIs(t, c.collectClrExceptions(ch, time.Second), pdh.ErrNoData)
require.Empty(t, ch)
}
func TestCloseReleasesInitializedCollectorOnce(t *testing.T) {
c := &Collector{}
fixture := &fixturePerfCollector[perfDataClrExceptions]{}
c.closeFns = []func(){func() {
if c.perfClrExceptions != nil {
c.perfClrExceptions.Close()
}
}}
c.perfClrExceptions = fixture
require.NoError(t, c.Close())
require.NoError(t, c.Close())
require.Equal(t, 1, fixture.closed)
}
func BenchmarkClrMemoryMetricPublication(b *testing.B) {
c := &Collector{}
c.describeClrMemory()
rows := make([]perfDataClrMemory, 100)
for i := range rows {
rows[i] = perfDataClrMemory{Name: fmt.Sprintf("worker#%d", i), ProcessID: float64(i), PercentTimeinGC: 50, PercentTimeinGC_base: 200}
}
c.perfClrMemory = &fixturePerfCollector[perfDataClrMemory]{rows: rows}
ch := make(chan prometheus.Metric, 2100)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
if err := c.collectClrMemory(ch, time.Second); err != nil {
b.Fatal(err)
}
for len(ch) != 0 {
<-ch
}
}
}
func TestCollectRetainsSuccessfulChildMetrics(t *testing.T) {
c := &Collector{}
c.describeClrExceptions()
c.describeClrInterop()
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: []perfDataClrExceptions{{Name: "worker", NumberofExcepsThrown: 37}}}
failure := errors.New("provider failed")
c.perfClrInterop = &fixturePerfCollector[perfDataClrInterop]{err: failure}
c.collectorFns = []func(chan<- prometheus.Metric, time.Duration) error{c.collectClrExceptions, c.collectClrInterop}
registry := prometheus.NewRegistry()
var collectErr error
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) { collectErr = c.Collect(ch, time.Second) }})
families, err := registry.Gather()
require.NoError(t, err)
require.ErrorIs(t, collectErr, failure)
require.Len(t, families, 4)
for _, family := range families {
if family.GetName() == "windows_netframework_clrexceptions_exceptions_thrown_total" {
require.InDelta(t, 37.0, family.GetMetric()[0].GetCounter().GetValue(), 1e-12)
}
}
}
@@ -25,7 +25,6 @@ import (
)
func BenchmarkCollector(b *testing.B) {
// No context name required as Collector source is WMI
testutils.FuncBenchmarkCollector(b, netframework.Name, netframework.NewWithFlags)
}