netframework: add process_id to all metrics and process filters (#2767)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 21:20:11 +02:00
committed by GitHub
parent 3555e9c4bc
commit ea6126fb67
15 changed files with 610 additions and 188 deletions
+16
View File
@@ -33,6 +33,22 @@ below comes from a CI probe.
- The `.NET CLR Memory` sizes and counts are `PERF_COUNTER_LARGE_RAWCOUNT`
gauges in bytes or counts; `# Gen N Collections` and `# Induced GC` are
cumulative. The object also has a `Process ID` counter.
- Only `.NET CLR Memory` has `Process ID`. For the `process_id` label of the
other objects, `Build` adds a second collector for that counter to the
shared query whenever a sub-collector other than `clrmemory` is enabled,
and `Collect` maps the Windows instance name and `Occurrence` (before
`pdh.SuffixDuplicates`) to the process ID before the sub-collectors start.
This relies on an undocumented assumption: the provider lists the
processes in the same order in every object of one sample. Evidence:
`PdhCollectQueryData` does one `RegQueryValueEx(HKEY_PERFORMANCE_DATA)`
per collect for the whole query, and the CLR provider writes every
object of that call from the same instance list. Separate queries per
object, or a provider that orders objects differently, would break the
join for duplicate names. With `ProcessNameFormat=1` (instance names like
`w3wp_1234`) names are unique and the order does not matter. An instance without a valid `Process ID` gets
`process_id=""`. Process filters match the name before the occurrence
suffix and are applied before the suffixes are added, so all objects of a
sample get the same suffixes.
## Counter reference
+166 -2
View File
@@ -22,7 +22,9 @@ import (
"fmt"
"log/slog"
"maps"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -31,6 +33,8 @@ import (
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/pdh"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
"github.com/prometheus/client_golang/prometheus"
)
@@ -43,7 +47,9 @@ type perfCollector[T any] interface {
const Name = "netframework"
type Config struct {
CollectorsEnabled []string `yaml:"enabled"`
CollectorsEnabled []string `yaml:"enabled"`
ProcessInclude *regexp.Regexp `yaml:"process-include"`
ProcessExclude *regexp.Regexp `yaml:"process-exclude"`
}
//nolint:gochecknoglobals
@@ -58,6 +64,8 @@ var ConfigDefaults = Config{
collectorClrRemoting,
collectorClrSecurity,
},
ProcessInclude: types.RegExpAny,
ProcessExclude: types.RegExpEmpty,
}
const (
@@ -92,6 +100,14 @@ type Collector struct {
perfClrMemory perfCollector[perfDataClrMemory]
perfClrRemoting perfCollector[perfDataClrRemoting]
perfClrSecurity perfCollector[perfDataClrSecurity]
// perfClrProcessID reads the Process ID counter of the .NET CLR Memory
// object for the sub-collectors of the other objects, see processIDs.
perfClrProcessID perfCollector[perfDataClrProcessID]
processIDRows []perfDataClrProcessID
// processIDs maps the instances of the current sample to their process
// ID. Collect fills it before it starts the sub-collectors, which only
// read it.
processIDs map[processKey]string
collectorFns []func(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, maxScrapeDuration time.Duration) error
@@ -170,6 +186,14 @@ func New(config *Config) *Collector {
config.CollectorsEnabled = ConfigDefaults.CollectorsEnabled
}
if config.ProcessInclude == nil {
config.ProcessInclude = ConfigDefaults.ProcessInclude
}
if config.ProcessExclude == nil {
config.ProcessExclude = ConfigDefaults.ProcessExclude
}
c := &Collector{
config: *config,
}
@@ -190,6 +214,18 @@ func NewWithFlags(app *kingpin.Application) *Collector {
"Comma-separated list of collectors to use. Defaults to all, if not specified.",
).Default(strings.Join(ConfigDefaults.CollectorsEnabled, ",")).StringVar(&collectorsEnabled)
utils.RegexpFlag(app,
"collector.netframework.process-exclude",
"Regexp of processes to exclude. Process name must both match include and not match exclude to be included.",
"", &c.config.ProcessExclude,
)
utils.RegexpFlag(app,
"collector.netframework.process-include",
"Regexp of processes to include. Process name must both match include and not match exclude to be included.",
".+", &c.config.ProcessInclude,
)
app.Action(func(*kingpin.ParseContext) error {
c.config.CollectorsEnabled = strings.Split(collectorsEnabled, ",")
@@ -216,6 +252,14 @@ func (c *Collector) Close() error {
c.closeFns = nil
c.collectorFns = nil
if c.perfClrProcessID != nil {
c.perfClrProcessID.Close()
c.perfClrProcessID = nil
}
c.processIDRows = nil
c.processIDs = nil
c.perfQuery.Close()
c.perfQuery = nil
@@ -340,6 +384,19 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.collectorFns = append(c.collectorFns, subCollectors[name].collect)
}
// Only the .NET CLR Memory object has a Process ID counter. The
// clrmemory rows carry their own, the other sub-collectors look it up.
// With clrmemory enabled, Process ID is read twice on purpose: the
// sub-collectors run concurrently, so they cannot share the clrmemory rows.
if slices.ContainsFunc(collectorsEnabled, func(name string) bool { return name != collectorClrMemory }) {
c.perfClrProcessID, err = newPerfCollector[perfDataClrProcessID](c.logger, c.perfQuery, ".NET CLR Memory")
if err != nil {
_ = c.Close()
return fmt.Errorf("failed to build the process ID lookup: %w", err)
}
}
return nil
}
@@ -354,6 +411,10 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
}
}
// The sub-collectors publish their metrics even if the process IDs are
// unknown, with an empty process_id label.
processIDErr := c.collectProcessIDs()
scrape := c.labels.Scrape()
var g recovery.Group
@@ -364,7 +425,70 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
})
}
return g.Wait()
return errors.Join(processIDErr, g.Wait())
}
// processKey identifies an instance of a sample by the name Windows reported
// and its occurrence, before suffixDuplicateProcesses changes the name.
type processKey struct {
name string
occurrence int
}
type perfDataClrProcessID struct {
Name string
Occurrence int
ProcessID float64 `perfdata:"Process ID"`
}
// collectProcessIDs maps the instances of the .NET CLR Memory object of the
// current sample to their process IDs. The join assumes, without
// documentation to back it, that the n-th instance with a name is the same
// process in every .NET CLR object of a sample: PdhCollectQueryData does one
// RegQueryValueEx(HKEY_PERFORMANCE_DATA) per collect for the whole query,
// and the CLR provider writes every object from the same instance list.
// Sampling the objects in separate queries would break the join for
// duplicate names. An instance without a valid Process ID has no
// entry. Without any .NET process, the map is empty and the sub-collectors
// report that there is no data.
func (c *Collector) collectProcessIDs() error {
if c.processIDs == nil {
c.processIDs = make(map[processKey]string)
} else {
clear(c.processIDs)
}
if c.perfClrProcessID == nil {
return nil
}
if err := c.perfClrProcessID.Collect(&c.processIDRows); err != nil {
if errors.Is(err, pdh.ErrNoData) {
return nil
}
return fmt.Errorf("failed to collect the process IDs of .NET CLR Memory: %w", err)
}
for _, row := range c.processIDRows {
if row.Name == "_Global_" {
continue
}
c.processIDs[processKey{name: row.Name, occurrence: row.Occurrence}] = strconv.FormatUint(uint64(row.ProcessID), 10)
}
return nil
}
// isProcessIncluded reports whether name, the process name as Windows reports
// it without an occurrence suffix, passes the process filters.
func (c *Collector) isProcessIncluded(name string) bool {
if c.config.ProcessInclude != nil && !c.config.ProcessInclude.MatchString(name) {
return false
}
return c.config.ProcessExclude == nil || !c.config.ProcessExclude.MatchString(name)
}
// clrInstance is implemented by the rows of the .NET CLR objects.
@@ -372,6 +496,46 @@ type clrInstance interface {
instance() (name *string, occurrence int)
}
// filterProcesses removes the _Global_ instance and the processes that do not
// pass the process filters from rows. The filters match the name that Windows
// reports, so they apply to every process with that name, like the filters
// of the process collector. Filtering before suffixDuplicateProcesses keeps
// the suffixes the same in every object of a sample.
func filterProcesses[T any, P interface {
*T
clrInstance
}](c *Collector, rows []T) []T {
filter := !utils.IsMatchAll(c.config.ProcessInclude) || !utils.IsMatchNone(c.config.ProcessExclude)
return slices.DeleteFunc(rows, func(row T) bool {
name, _ := P(&row).instance()
return *name == "_Global_" || (filter && !c.isProcessIncluded(*name))
})
}
// prepareProcesses filters rows, looks up the process ID of every row and
// then adds occurrence suffixes to duplicate process names. The process ID
// of rows[i] is processIDs[i]; it is empty if the .NET CLR Memory object has
// no valid Process ID for the instance, e.g. if clrmemory is disabled and
// the object is unavailable. The process label still keeps the series apart.
func prepareProcesses[T any, P interface {
*T
clrInstance
}](c *Collector, rows []T) ([]T, []string) {
rows = filterProcesses[T, P](c, rows)
processIDs := make([]string, len(rows))
for i := range rows {
name, occurrence := P(&rows[i]).instance()
processIDs[i] = c.processIDs[processKey{name: *name, occurrence: occurrence}]
}
suffixDuplicateProcesses[T, P](rows)
return rows, processIDs
}
// suffixDuplicateProcesses keeps processes with the same name apart. The .NET
// CLR objects name instances after the process image and report a name once
// per process, e.g. "powershell" twice for two PowerShell processes. Later
@@ -30,25 +30,25 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfFilters = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrExceptions+"_exceptions_filters_total"),
"Displays the total number of .NET exception filters executed. An exception filter evaluates regardless of whether an exception is handled.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfFinally = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrExceptions+"_exceptions_finallys_total"),
"Displays the total number of finally blocks executed. Only the finally blocks executed for an exception are counted; finally blocks on normal code paths are not counted by this counter.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.throwToCatchDepth = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrExceptions+"_throw_to_catch_depth_total"),
"Displays the total number of stack frames traversed, from the frame that threw the exception to the frame that handled the exception.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -80,18 +80,15 @@ func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, scrape *la
return fmt.Errorf("failed to collect .NET CLR Exceptions: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.numberOfExceptionsThrown,
prometheus.CounterValue,
float64(process.NumberofExcepsThrown),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -99,6 +96,7 @@ func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, scrape *la
prometheus.CounterValue,
float64(process.NumberofFiltersPersec),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -106,6 +104,7 @@ func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, scrape *la
prometheus.CounterValue,
float64(process.NumberofFinallysPersec),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -113,6 +112,7 @@ func (c *Collector) collectClrExceptions(ch chan<- prometheus.Metric, scrape *la
prometheus.CounterValue,
float64(process.ThrowToCatchDepthPersec),
process.Name,
processIDs[i],
)
}
@@ -30,19 +30,19 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfMarshalling = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrInterop+"_interop_marshalling_total"),
"Displays the total number of times arguments and return values have been marshaled from managed to unmanaged code, and vice versa, since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfStubs = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrInterop+"_interop_stubs_created_total"),
"Displays the current number of stubs created by the common language runtime. Stubs are responsible for marshaling arguments and return values from managed to unmanaged code, and vice versa, during a COM interop call or a platform invoke call.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -73,18 +73,15 @@ func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, scrape *label
return fmt.Errorf("failed to collect .NET CLR Interop: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.numberOfCCWs,
prometheus.CounterValue,
float64(process.NumberofCCWs),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -92,6 +89,7 @@ func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.Numberofmarshalling),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -99,6 +97,7 @@ func (c *Collector) collectClrInterop(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.NumberofStubs),
process.Name,
processIDs[i],
)
}
@@ -30,25 +30,25 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.timeInJit = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrJIT+"_jit_time_ratio"),
"Displays the ratio (0-1) of elapsed time spent in JIT compilation since the last JIT compilation phase. This counter is updated at the end of every JIT compilation phase. A JIT compilation phase occurs when a method and its dependencies are compiled.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.standardJitFailures = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrJIT+"_jit_standard_failures_total"),
"Displays the peak number of methods the JIT compiler has failed to compile since the application started. This failure can occur if the MSIL cannot be verified or if there is an internal error in the JIT compiler.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalNumberOfILBytesJitted = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrJIT+"_jit_il_bytes_total"),
"Displays the total number of Microsoft intermediate language (MSIL) bytes compiled by the just-in-time (JIT) compiler since the application started",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -81,18 +81,15 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcach
return fmt.Errorf("failed to collect .NET CLR Jit: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.numberOfMethodsJitted,
prometheus.CounterValue,
float64(process.NumberofMethodsJitted),
process.Name,
processIDs[i],
)
if process.PercentTimeinJitBase > 0 {
@@ -101,6 +98,7 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcach
prometheus.GaugeValue,
process.PercentTimeinJit/process.PercentTimeinJitBase,
process.Name,
processIDs[i],
)
}
@@ -109,6 +107,7 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcach
prometheus.CounterValue,
float64(process.StandardJitFailures),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -116,6 +115,7 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcach
prometheus.CounterValue,
float64(process.TotalNumberofILBytesJitted),
process.Name,
processIDs[i],
)
}
@@ -30,55 +30,55 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.currentAppDomains = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_appdomains_loaded_current"),
"Displays the current number of application domains loaded in this application.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.currentAssemblies = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_assemblies_loaded_current"),
"Displays the current number of assemblies loaded across all application domains in the currently running application. If the assembly is loaded as domain-neutral from multiple application domains, this counter is incremented only once.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.currentClassesLoaded = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_classes_loaded_current"),
"Displays the current number of classes loaded in all assemblies.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalAppDomains = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_appdomains_loaded_total"),
"Displays the peak number of application domains loaded since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalAppDomainsUnloaded = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_appdomains_unloaded_total"),
"Displays the total number of application domains unloaded since the application started. If an application domain is loaded and unloaded multiple times, this counter increments each time the application domain is unloaded.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalAssemblies = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_assemblies_loaded_total"),
"Displays the total number of assemblies loaded since the application started. If the assembly is loaded as domain-neutral from multiple application domains, this counter is incremented only once.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalClassesLoaded = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_classes_loaded_total"),
"Displays the cumulative number of classes loaded in all assemblies since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalNumberOfLoadFailures = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLoading+"_class_load_failures_total"),
"Displays the peak number of classes that have failed to load since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -115,18 +115,15 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
return fmt.Errorf("failed to collect .NET CLR Loading: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.bytesInLoaderHeap,
prometheus.GaugeValue,
float64(process.BytesinLoaderHeap),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -134,6 +131,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.GaugeValue,
float64(process.Currentappdomains),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -141,6 +139,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.GaugeValue,
float64(process.CurrentAssemblies),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -148,6 +147,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.GaugeValue,
float64(process.CurrentClassesLoaded),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -155,6 +155,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.TotalAppdomains),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -162,6 +163,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.Totalappdomainsunloaded),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -169,6 +171,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.TotalAssemblies),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -176,6 +179,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.TotalClassesLoaded),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -183,6 +187,7 @@ func (c *Collector) collectClrLoading(ch chan<- prometheus.Metric, scrape *label
prometheus.CounterValue,
float64(process.TotalNumberofLoadFailures),
process.Name,
processIDs[i],
)
}
@@ -30,43 +30,43 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfCurrentLogicalThreads = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_current_logical_threads"),
"Displays the number of current managed thread objects in the application. This counter maintains the count of both running and stopped threads. ",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfCurrentPhysicalThreads = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_physical_threads_current"),
"Displays the number of native operating system threads created and owned by the common language runtime to act as underlying threads for managed thread objects. This counter's value does not include the threads used by the runtime in its internal operations; it is a subset of the threads in the operating system process.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfCurrentRecognizedThreads = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_recognized_threads_current"),
"Displays the number of threads that are currently recognized by the runtime. These threads are associated with a corresponding managed thread object. The runtime does not create these threads, but they have run inside the runtime at least once.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.numberOfTotalRecognizedThreads = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_recognized_threads_total"),
"Displays the total number of threads that have been recognized by the runtime since the application started. These threads are associated with a corresponding managed thread object. The runtime does not create these threads, but they have run inside the runtime at least once.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.queueLengthPeak = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_queue_length_total"),
"Displays the total number of threads that waited to acquire a managed lock since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalNumberOfContentions = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrLocksAndThreads+"_contentions_total"),
"Displays the total number of times that threads in the runtime have attempted to acquire a managed lock unsuccessfully.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -101,18 +101,15 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
return fmt.Errorf("failed to collect .NET CLR LocksAndThreads: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.currentQueueLength,
prometheus.GaugeValue,
float64(process.CurrentQueueLength),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -120,6 +117,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.GaugeValue,
float64(process.NumberofcurrentlogicalThreads),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -127,6 +125,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.GaugeValue,
float64(process.NumberofcurrentphysicalThreads),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -134,6 +133,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.GaugeValue,
float64(process.Numberofcurrentrecognizedthreads),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -141,6 +141,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.CounterValue,
float64(process.Numberoftotalrecognizedthreads),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -148,6 +149,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.CounterValue,
float64(process.QueueLengthPeak),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -155,6 +157,7 @@ func (c *Collector) collectClrLocksAndThreads(ch chan<- prometheus.Metric, scrap
prometheus.CounterValue,
float64(process.TotalNumberofContentions),
process.Name,
processIDs[i],
)
}
@@ -145,19 +145,18 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
return fmt.Errorf("failed to collect .NET CLR Memory: %w", err)
}
dst = filterProcesses(c, dst)
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
processID := strconv.FormatUint(uint64(process.ProcessID), 10)
ch <- scrape.MustNewConstMetric(
c.allocatedBytes,
prometheus.CounterValue,
float64(process.AllocatedBytesPersec),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -165,7 +164,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.FinalizationSurvivors),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -173,7 +172,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.Gen0heapsize),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen0",
)
@@ -182,7 +181,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.Gen0PromotedBytesPerSec),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen0",
)
@@ -191,7 +190,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.Gen1heapsize),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen1",
)
@@ -200,7 +199,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.Gen1PromotedBytesPerSec),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen1",
)
@@ -209,7 +208,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.Gen2heapsize),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen2",
)
@@ -218,7 +217,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.LargeObjectHeapsize),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"LOH",
)
@@ -227,7 +226,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.NumberGCHandles),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -235,7 +234,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.CounterValue,
float64(process.NumberGen0Collections),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen0",
)
@@ -244,7 +243,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.CounterValue,
float64(process.NumberGen1Collections),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen1",
)
@@ -253,7 +252,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.CounterValue,
float64(process.NumberGen2Collections),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
"Gen2",
)
@@ -262,7 +261,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.CounterValue,
float64(process.NumberInducedGC),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -270,7 +269,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.NumberofPinnedObjects),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -278,7 +277,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.NumberofSinkBlocksinuse),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -286,7 +285,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.NumberTotalcommittedBytes),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
ch <- scrape.MustNewConstMetric(
@@ -294,7 +293,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
float64(process.NumberTotalreservedBytes),
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
if process.PercentTimeinGC_base <= 0 {
@@ -306,7 +305,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
prometheus.GaugeValue,
process.PercentTimeinGC/process.PercentTimeinGC_base,
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
processID,
)
}
@@ -30,37 +30,37 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.contextBoundClassesLoaded = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_context_bound_classes_loaded"),
"Displays the current number of context-bound classes that are loaded.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.contextBoundObjects = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_context_bound_objects_total"),
"Displays the total number of context-bound objects allocated.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.contextProxies = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_context_proxies_total"),
"Displays the total number of remoting proxy objects in this process since it started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.contexts = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_contexts"),
"Displays the current number of remoting contexts in the application.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalRemoteCalls = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrRemoting+"_remote_calls_total"),
"Displays the total number of remote procedure calls invoked since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -94,18 +94,15 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
return fmt.Errorf("failed to collect .NET CLR Remoting: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.channels,
prometheus.CounterValue,
float64(process.Channels),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -113,6 +110,7 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
prometheus.GaugeValue,
float64(process.ContextBoundClassesLoaded),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -120,6 +118,7 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
prometheus.CounterValue,
float64(process.ContextBoundObjectsAllocPersec),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -127,6 +126,7 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
prometheus.CounterValue,
float64(process.ContextProxies),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -134,6 +134,7 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
prometheus.GaugeValue,
float64(process.Contexts),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -141,6 +142,7 @@ func (c *Collector) collectClrRemoting(ch chan<- prometheus.Metric, scrape *labe
prometheus.CounterValue,
float64(process.TotalRemoteCalls),
process.Name,
processIDs[i],
)
}
@@ -30,25 +30,25 @@ 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.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.timeInRTChecks = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrSecurity+"_rt_checks_time_ratio"),
"Displays the ratio (0-1) of time spent performing runtime code access security checks in the last sample.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.stackWalkDepth = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrSecurity+"_stack_walk_depth"),
"Displays the depth of the stack during that last runtime code access security check.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
c.totalRuntimeChecks = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrSecurity+"_runtime_checks_total"),
"Displays the total number of runtime code access security checks performed since the application started.",
[]string{"process"},
[]string{"process", "process_id"},
nil,
)
}
@@ -81,18 +81,15 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labe
return fmt.Errorf("failed to collect .NET CLR Security: %w", err)
}
suffixDuplicateProcesses(dst)
for _, process := range dst {
if process.Name == "_Global_" {
continue
}
dst, processIDs := prepareProcesses(c, dst)
for i, process := range dst {
ch <- scrape.MustNewConstMetric(
c.numberLinkTimeChecks,
prometheus.CounterValue,
float64(process.NumberLinkTimeChecks),
process.Name,
processIDs[i],
)
if process.PercentTimeinRTchecksBase > 0 {
@@ -101,6 +98,7 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labe
prometheus.GaugeValue,
process.PercentTimeinRTchecks/process.PercentTimeinRTchecksBase,
process.Name,
processIDs[i],
)
}
@@ -109,6 +107,7 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labe
prometheus.GaugeValue,
float64(process.StackWalkDepth),
process.Name,
processIDs[i],
)
ch <- scrape.MustNewConstMetric(
@@ -116,6 +115,7 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labe
prometheus.CounterValue,
float64(process.TotalRuntimeChecks),
process.Name,
processIDs[i],
)
}
@@ -58,7 +58,19 @@ type expectedMetric struct {
area string
}
func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, *labelcache.Scrape, time.Duration) error, expected []expectedMetric, withPID bool) {
// fixtureProcessIDs are the process IDs of the fixture processes. The
// .NET CLR Memory object has no valid Process ID for report_Total.
func fixtureProcessIDs() map[processKey]string {
return map[processKey]string{
{name: "worker"}: "101",
{name: "worker#1"}: "102",
}
}
// assertFixtureMetrics checks the metrics of the fixture processes worker,
// worker#1 and report_Total. processIDs has the expected process_id label of
// each process.
func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, *labelcache.Scrape, time.Duration) error, expected []expectedMetric, processIDs map[string]string) {
t.Helper()
// A first scrape fills the label cache, so the checked scrape publishes
@@ -112,13 +124,11 @@ func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, *
require.InDelta(t, want.value, value, 1e-12, key)
labelCount := 1
processID, ok := labels["process_id"]
require.True(t, ok, "missing process_id label of %s", key)
require.Equal(t, processIDs[labels["process"]], processID, key)
if withPID {
require.Equal(t, "123", labels["process_id"])
labelCount++
}
labelCount := 2
if want.area != "" {
labelCount++
@@ -136,7 +146,7 @@ func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, *
}
func TestClrExceptionsMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrExceptions()
row := perfDataClrExceptions{
@@ -158,11 +168,11 @@ func TestClrExceptionsMetricCompatibility(t *testing.T) {
{"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)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrInteropMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrInterop()
row := perfDataClrInterop{
@@ -182,11 +192,11 @@ func TestClrInteropMetricCompatibility(t *testing.T) {
{"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)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrJITMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrJIT()
row := perfDataClrJIT{
@@ -209,11 +219,11 @@ func TestClrJITMetricCompatibility(t *testing.T) {
{"windows_netframework_clrjit_jit_time_ratio", dto.MetricType_GAUGE, 0.25, ""},
{"windows_netframework_clrjit_jit_standard_failures_total", dto.MetricType_COUNTER, 13, ""},
{"windows_netframework_clrjit_jit_il_bytes_total", dto.MetricType_COUNTER, 14, ""},
}, false)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrLoadingMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrLoading()
row := perfDataClrLoading{
@@ -245,11 +255,11 @@ func TestClrLoadingMetricCompatibility(t *testing.T) {
{"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)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrLocksAndThreadsMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrLocksAndThreads()
row := perfDataClrLocksAndThreads{
@@ -277,7 +287,7 @@ func TestClrLocksAndThreadsMetricCompatibility(t *testing.T) {
{"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)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrMemoryMetricCompatibility(t *testing.T) {
@@ -333,11 +343,11 @@ func TestClrMemoryMetricCompatibility(t *testing.T) {
{"windows_netframework_clrmemory_committed_bytes", dto.MetricType_GAUGE, 24, ""},
{"windows_netframework_clrmemory_reserved_bytes", dto.MetricType_GAUGE, 25, ""},
{"windows_netframework_clrmemory_gc_time_ratio", dto.MetricType_GAUGE, 0.25, ""},
}, true)
}, map[string]string{"worker": "123", "worker#1": "123", "report_Total": "123"})
}
func TestClrRemotingMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrRemoting()
row := perfDataClrRemoting{
@@ -363,11 +373,11 @@ func TestClrRemotingMetricCompatibility(t *testing.T) {
{"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)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestClrSecurityMetricCompatibility(t *testing.T) {
c := &Collector{}
c := &Collector{processIDs: fixtureProcessIDs()}
c.describeClrSecurity()
row := perfDataClrSecurity{
@@ -390,7 +400,7 @@ func TestClrSecurityMetricCompatibility(t *testing.T) {
{"windows_netframework_clrsecurity_rt_checks_time_ratio", dto.MetricType_GAUGE, 0.25, ""},
{"windows_netframework_clrsecurity_stack_walk_depth", dto.MetricType_GAUGE, 13, ""},
{"windows_netframework_clrsecurity_runtime_checks_total", dto.MetricType_COUNTER, 14, ""},
}, false)
}, map[string]string{"worker": "101", "worker#1": "102", "report_Total": ""})
}
func TestGCZeroBaseDoesNotPublishInvalidRatio(t *testing.T) {
@@ -0,0 +1,200 @@
// 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"
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/labelcache"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
)
// gatherProcessIDs gathers the metrics of collect and returns the process_id
// label of each process label of the metric family name.
func gatherProcessIDs(t *testing.T, collect func(chan<- prometheus.Metric) error, name string) (map[string]string, error) {
t.Helper()
var collectErr error
registry := prometheus.NewRegistry()
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) { collectErr = collect(ch) }})
families, err := registry.Gather()
require.NoError(t, err)
got := map[string]string{}
for _, family := range families {
if family.GetName() != name {
continue
}
for _, metric := range family.GetMetric() {
labels := make(map[string]string)
for _, label := range metric.GetLabel() {
labels[label.GetName()] = label.GetValue()
}
require.Len(t, labels, 2)
got[labels["process"]] = labels["process_id"]
}
}
return got, collectErr
}
// gatherExceptionsThrown runs Collect with the clrexceptions sub-collector of
// c and returns the process_id label of each process of
// windows_netframework_clrexceptions_exceptions_thrown_total.
func gatherExceptionsThrown(t *testing.T, c *Collector) (map[string]string, error) {
t.Helper()
c.describeClrExceptions()
c.collectorFns = []func(chan<- prometheus.Metric, *labelcache.Scrape, time.Duration) error{c.collectClrExceptions}
return gatherProcessIDs(t, func(ch chan<- prometheus.Metric) error {
return c.Collect(ch, time.Second)
}, "windows_netframework_clrexceptions_exceptions_thrown_total")
}
// TestProcessIDLookup checks that the metrics of the other .NET CLR objects
// get the process ID of the .NET CLR Memory instance with the same name and
// occurrence, and an empty process_id if .NET CLR Memory has none.
func TestProcessIDLookup(t *testing.T) {
c := New(nil)
c.perfClrProcessID = &fixturePerfCollector[perfDataClrProcessID]{rows: []perfDataClrProcessID{
{Name: "_Global_", ProcessID: 0},
{Name: "w3wp", ProcessID: 10},
{Name: "w3wp#1", ProcessID: 30},
{Name: "w3wp", Occurrence: 1, ProcessID: 20},
}}
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: []perfDataClrExceptions{
{Name: "_Global_"},
{Name: "w3wp"},
{Name: "w3wp#1"},
{Name: "w3wp", Occurrence: 1},
{Name: "powershell"},
}}
got, err := gatherExceptionsThrown(t, c)
require.NoError(t, err)
// "w3wp#1" is the name of a process, so the second w3wp is "w3wp#2".
require.Equal(t, map[string]string{"w3wp": "10", "w3wp#1": "30", "w3wp#2": "20", "powershell": ""}, got)
// A scrape does not keep the process IDs of an earlier one.
c.perfClrProcessID = &fixturePerfCollector[perfDataClrProcessID]{rows: []perfDataClrProcessID{{Name: "w3wp", ProcessID: 40}}}
got, err = gatherExceptionsThrown(t, c)
require.NoError(t, err)
require.Equal(t, map[string]string{"w3wp": "40", "w3wp#1": "", "w3wp#2": "", "powershell": ""}, got)
}
// TestProcessIDLookupFailure checks that the metrics are published with an
// empty process_id if the process IDs are unavailable, and that only a real
// failure is reported.
func TestProcessIDLookupFailure(t *testing.T) {
failure := errors.New("provider failed")
for _, tc := range []struct {
name string
lookup perfCollector[perfDataClrProcessID]
wantErr error
}{
{"missing object", unavailablePerfCollector[perfDataClrProcessID]{}, nil},
{"failure", &fixturePerfCollector[perfDataClrProcessID]{err: failure}, failure},
} {
t.Run(tc.name, func(t *testing.T) {
c := New(nil)
c.perfClrProcessID = tc.lookup
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: []perfDataClrExceptions{{Name: "w3wp"}}}
got, err := gatherExceptionsThrown(t, c)
if tc.wantErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tc.wantErr)
}
require.Equal(t, map[string]string{"w3wp": ""}, got)
})
}
}
// TestProcessFilter checks that the process filters match the name that
// Windows reports, without the occurrence suffix, and that the process IDs
// still match after filtering.
func TestProcessFilter(t *testing.T) {
for _, tc := range []struct {
name string
include string
exclude string
expected map[string]string
}{
{"default", ".+", "", map[string]string{"w3wp": "10", "w3wp#1": "30", "w3wp#2": "20", "powershell": "40"}},
// Without the process named "w3wp#1", the second w3wp is "w3wp#1".
{"include", "w3wp", "", map[string]string{"w3wp": "10", "w3wp#1": "20"}},
{"include name with #", "w3wp#1", "", map[string]string{"w3wp#1": "30"}},
{"exclude", ".+", "w3wp.*", map[string]string{"powershell": "40"}},
{"include and exclude", "w3wp.*", "w3wp", map[string]string{"w3wp#1": "30"}},
} {
t.Run(tc.name, func(t *testing.T) {
include, err := utils.CompileAnchored(tc.include)
require.NoError(t, err)
exclude, err := utils.CompileAnchored(tc.exclude)
require.NoError(t, err)
c := New(&Config{ProcessInclude: include, ProcessExclude: exclude})
c.perfClrProcessID = &fixturePerfCollector[perfDataClrProcessID]{rows: []perfDataClrProcessID{
{Name: "w3wp", ProcessID: 10},
{Name: "w3wp#1", ProcessID: 30},
{Name: "powershell", ProcessID: 40},
{Name: "w3wp", Occurrence: 1, ProcessID: 20},
}}
c.perfClrExceptions = &fixturePerfCollector[perfDataClrExceptions]{rows: []perfDataClrExceptions{
{Name: "w3wp"},
{Name: "w3wp#1"},
{Name: "powershell"},
{Name: "w3wp", Occurrence: 1},
}}
got, err := gatherExceptionsThrown(t, c)
require.NoError(t, err)
require.Equal(t, tc.expected, got)
// The clrmemory metrics have the same process labels.
c.describeClrMemory()
c.perfClrMemory = &fixturePerfCollector[perfDataClrMemory]{rows: []perfDataClrMemory{
{Name: "w3wp", ProcessID: 10},
{Name: "w3wp#1", ProcessID: 30},
{Name: "powershell", ProcessID: 40},
{Name: "w3wp", Occurrence: 1, ProcessID: 20},
}}
got, err = gatherProcessIDs(t, func(ch chan<- prometheus.Metric) error {
return c.collectClrMemory(ch, nil, time.Second)
}, "windows_netframework_clrmemory_allocated_bytes_total")
require.NoError(t, err)
require.Equal(t, tc.expected, got)
})
}
}
+2
View File
@@ -570,6 +570,8 @@ func TestRegexpFlags(t *testing.T) {
"collector.logical_disk.volume-include",
"collector.net.nic-exclude",
"collector.net.nic-include",
"collector.netframework.process-exclude",
"collector.netframework.process-include",
"collector.physical_disk.disk-exclude",
"collector.physical_disk.disk-include",
"collector.printer.exclude",