diff --git a/docs/collector.netframework.md b/docs/collector.netframework.md index 4be86c63..e8f985f9 100644 --- a/docs/collector.netframework.md +++ b/docs/collector.netframework.md @@ -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 diff --git a/internal/collector/netframework/netframework.go b/internal/collector/netframework/netframework.go index 3940c94b..fdc547cb 100644 --- a/internal/collector/netframework/netframework.go +++ b/internal/collector/netframework/netframework.go @@ -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() {} diff --git a/internal/collector/netframework/netframework_clrexceptions.go b/internal/collector/netframework/netframework_clrexceptions.go index 129702a5..24483aa1 100644 --- a/internal/collector/netframework/netframework_clrexceptions.go +++ b/internal/collector/netframework/netframework_clrexceptions.go @@ -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 { diff --git a/internal/collector/netframework/netframework_clrinterop.go b/internal/collector/netframework/netframework_clrinterop.go index 8e1184f6..364522e2 100644 --- a/internal/collector/netframework/netframework_clrinterop.go +++ b/internal/collector/netframework/netframework_clrinterop.go @@ -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 { diff --git a/internal/collector/netframework/netframework_clrjit.go b/internal/collector/netframework/netframework_clrjit.go index 6377c129..25ae4d01 100644 --- a/internal/collector/netframework/netframework_clrjit.go +++ b/internal/collector/netframework/netframework_clrjit.go @@ -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, ) diff --git a/internal/collector/netframework/netframework_clrloading.go b/internal/collector/netframework/netframework_clrloading.go index 40891194..cdd8a0f7 100644 --- a/internal/collector/netframework/netframework_clrloading.go +++ b/internal/collector/netframework/netframework_clrloading.go @@ -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 { diff --git a/internal/collector/netframework/netframework_clrlocksandthreads.go b/internal/collector/netframework/netframework_clrlocksandthreads.go index 197e4b08..a0b241b6 100644 --- a/internal/collector/netframework/netframework_clrlocksandthreads.go +++ b/internal/collector/netframework/netframework_clrlocksandthreads.go @@ -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 { diff --git a/internal/collector/netframework/netframework_clrmemory.go b/internal/collector/netframework/netframework_clrmemory.go index 4a718886..c5b43164 100644 --- a/internal/collector/netframework/netframework_clrmemory.go +++ b/internal/collector/netframework/netframework_clrmemory.go @@ -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), ) } diff --git a/internal/collector/netframework/netframework_clrremoting.go b/internal/collector/netframework/netframework_clrremoting.go index 27b8f983..0143d78c 100644 --- a/internal/collector/netframework/netframework_clrremoting.go +++ b/internal/collector/netframework/netframework_clrremoting.go @@ -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 { diff --git a/internal/collector/netframework/netframework_clrsecurity.go b/internal/collector/netframework/netframework_clrsecurity.go index 87e33036..24e385de 100644 --- a/internal/collector/netframework/netframework_clrsecurity.go +++ b/internal/collector/netframework/netframework_clrsecurity.go @@ -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, ) diff --git a/internal/collector/netframework/netframework_counter_paths_test.go b/internal/collector/netframework/netframework_counter_paths_test.go new file mode 100644 index 00000000..98be0543 --- /dev/null +++ b/internal/collector/netframework/netframework_counter_paths_test.go @@ -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) + }) + } +} diff --git a/internal/collector/netframework/netframework_pdh_test.go b/internal/collector/netframework/netframework_pdh_test.go new file mode 100644 index 00000000..9eb55f50 --- /dev/null +++ b/internal/collector/netframework/netframework_pdh_test.go @@ -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) + } + } +} diff --git a/internal/collector/netframework/netframework_test.go b/internal/collector/netframework/netframework_test.go index 28fc5582..af23e5d0 100644 --- a/internal/collector/netframework/netframework_test.go +++ b/internal/collector/netframework/netframework_test.go @@ -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) }