// 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" "log/slog" "maps" "slices" "strings" "time" "unsafe" "github.com/alecthomas/kingpin/v2" "github.com/prometheus-community/windows_exporter/internal/labelcache" "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 { CollectorsEnabled []string `yaml:"enabled"` } //nolint:gochecknoglobals var ConfigDefaults = Config{ CollectorsEnabled: []string{ collectorClrExceptions, collectorClrInterop, collectorClrJIT, collectorClrLoading, collectorClrLocksAndThreads, collectorClrMemory, collectorClrRemoting, collectorClrSecurity, }, } const ( collectorClrExceptions = "clrexceptions" collectorClrInterop = "clrinterop" collectorClrJIT = "clrjit" collectorClrLoading = "clrloading" collectorClrLocksAndThreads = "clrlocksandthreads" collectorClrMemory = "clrmemory" collectorClrRemoting = "clrremoting" collectorClrSecurity = "clrsecurity" ) // A Collector collects .NET Framework performance counters. type Collector struct { // labels keeps the label pairs of the series of the last scrape. The // counters change on every scrape, but their process labels stay the // same while the process runs. labels labelcache.Cache config Config logger *slog.Logger perfFrequency float64 closeFns []func() // perfQuery samples the counters of all enabled sub-collectors at once. // The .NET CLR counter provider serves one query at a time and walks // every .NET process for each, so separate queries wait for each other. perfQuery *pdh.Query perfClrExceptions perfCollector[perfDataClrExceptions] perfClrInterop perfCollector[perfDataClrInterop] perfClrJIT perfCollector[perfDataClrJIT] perfClrLoading perfCollector[perfDataClrLoading] perfClrLocksAndThreads perfCollector[perfDataClrLocksAndThreads] perfClrMemory perfCollector[perfDataClrMemory] perfClrRemoting perfCollector[perfDataClrRemoting] perfClrSecurity perfCollector[perfDataClrSecurity] collectorFns []func(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, maxScrapeDuration time.Duration) error // clrexceptions numberOfExceptionsThrown *prometheus.Desc numberOfFilters *prometheus.Desc numberOfFinally *prometheus.Desc throwToCatchDepth *prometheus.Desc // clrinterop numberOfCCWs *prometheus.Desc numberOfMarshalling *prometheus.Desc numberOfStubs *prometheus.Desc // clrjit numberOfMethodsJitted *prometheus.Desc timeInJit *prometheus.Desc standardJitFailures *prometheus.Desc totalNumberOfILBytesJitted *prometheus.Desc // clrloading bytesInLoaderHeap *prometheus.Desc currentAppDomains *prometheus.Desc currentAssemblies *prometheus.Desc currentClassesLoaded *prometheus.Desc totalAppDomains *prometheus.Desc totalAppDomainsUnloaded *prometheus.Desc totalAssemblies *prometheus.Desc totalClassesLoaded *prometheus.Desc totalNumberOfLoadFailures *prometheus.Desc // clrlocksandthreads currentQueueLength *prometheus.Desc numberOfCurrentLogicalThreads *prometheus.Desc numberOfCurrentPhysicalThreads *prometheus.Desc numberOfCurrentRecognizedThreads *prometheus.Desc numberOfTotalRecognizedThreads *prometheus.Desc queueLengthPeak *prometheus.Desc totalNumberOfContentions *prometheus.Desc // clrmemory allocatedBytes *prometheus.Desc finalizationSurvivors *prometheus.Desc heapSize *prometheus.Desc promotedBytes *prometheus.Desc numberGCHandles *prometheus.Desc numberCollections *prometheus.Desc numberInducedGC *prometheus.Desc numberOfPinnedObjects *prometheus.Desc numberOfSinkBlocksInUse *prometheus.Desc numberTotalCommittedBytes *prometheus.Desc numberTotalReservedBytes *prometheus.Desc timeInGC *prometheus.Desc // clrremoting channels *prometheus.Desc contextBoundClassesLoaded *prometheus.Desc contextBoundObjects *prometheus.Desc contextProxies *prometheus.Desc contexts *prometheus.Desc totalRemoteCalls *prometheus.Desc // clrsecurity numberLinkTimeChecks *prometheus.Desc timeInRTChecks *prometheus.Desc stackWalkDepth *prometheus.Desc totalRuntimeChecks *prometheus.Desc } func New(config *Config) *Collector { if config == nil { config = &ConfigDefaults } if config.CollectorsEnabled == nil { config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled } c := &Collector{ config: *config, } return c } func NewWithFlags(app *kingpin.Application) *Collector { c := &Collector{ config: ConfigDefaults, } c.config.CollectorsEnabled = make([]string, 0) var collectorsEnabled string app.Flag( "collector.netframework.enabled", "Comma-separated list of collectors to use. Defaults to all, if not specified.", ).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled) app.Action(func(*kingpin.ParseContext) error { c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",") return nil }) return c } func (c *Collector) GetName() string { return Name } func (c *Collector) Close() error { for _, closeFn := range c.closeFns { closeFn() } c.closeFns = nil c.collectorFns = nil c.labels.Reset() c.perfQuery.Close() c.perfQuery = nil return nil } func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error { if len(c.config.CollectorsEnabled) == 0 { return nil } c.logger = logger if slices.Contains(c.config.CollectorsEnabled, collectorClrJIT) || slices.Contains(c.config.CollectorsEnabled, collectorClrSecurity) { var frequency int64 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) } var err error c.perfQuery, err = pdh.NewQuery() if err != nil { return fmt.Errorf("failed to open .NET CLR performance counter query: %w", err) } c.collectorFns = make([]func(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, maxScrapeDuration time.Duration) error, 0, len(c.config.CollectorsEnabled)) subCollectors := map[string]struct { build func() error collect func(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, 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() } }, }, } // Sort a copy, to not modify the slice of the caller or ConfigDefaults. // Result must order, to prevent test failures. collectorsEnabled := slices.Compact(slices.Sorted(slices.Values(c.config.CollectorsEnabled))) for _, name := range collectorsEnabled { if _, ok := subCollectors[name]; !ok { return fmt.Errorf("unknown sub collector: %s. Possible values: %s", name, strings.Join(slices.Sorted(maps.Keys(subCollectors)), ", "), ) } } for _, name := range collectorsEnabled { 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) } return nil } // Collect sends the metric values for each metric // to the provided prometheus Metric channel. func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error { // The sub-collectors read this sample. Without any .NET process there is // no data; the sub-collectors report that themselves. if c.perfQuery != nil { if err := c.perfQuery.Collect(); err != nil && !errors.Is(err, pdh.ErrNoData) { return fmt.Errorf("failed to collect .NET CLR performance counters: %w", err) } } scrape := c.labels.Begin() defer scrape.Commit() var g recovery.Group for _, fn := range c.collectorFns { g.Go(func() error { return fn(ch, scrape, maxScrapeDuration) }) } return g.Wait() } // newPerfCollector adds the counters of object to query and retains // ownership of partially initialized collectors. Missing objects are optional; // missing individual counters remain errors. func newPerfCollector[T any](logger *slog.Logger, query *pdh.Query, object string) (perfCollector[T], error) { c, err := pdh.NewQueryCollector[T](logger, query, pdh.CounterTypeRaw, object, pdh.InstancesAll) if err == nil { return c, nil } 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() {}