collector: publish base units and _total counter names (#2743)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 10:48:26 +02:00
committed by GitHub
parent dc8092f46d
commit 3fb18521f6
131 changed files with 1674 additions and 1459 deletions
@@ -25,7 +25,6 @@ import (
"slices"
"strings"
"time"
"unsafe"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/labelcache"
@@ -34,7 +33,6 @@ import (
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"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 {
@@ -78,11 +76,10 @@ type Collector struct {
// labels keeps the label pairs of the series of the last scrape. The
// counters change on every scrape, but their process labels stay the
// same while the process runs.
labels labelcache.Cache
config Config
logger *slog.Logger
perfFrequency float64
closeFns []func()
labels labelcache.Cache
config Config
logger *slog.Logger
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.
@@ -231,16 +228,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}
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
@@ -34,8 +34,8 @@ func (c *Collector) describeClrJIT() {
nil,
)
c.timeInJit = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrJIT+"_jit_time_percent"),
"Displays the percentage of time spent in JIT compilation. 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.",
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"},
nil,
)
@@ -68,6 +68,7 @@ type perfDataClrJIT struct {
Occurrence int
NumberofMethodsJitted float64 `perfdata:"# of Methods JITted"`
PercentTimeinJit float64 `perfdata:"% Time in Jit"`
PercentTimeinJitBase float64 `perfdata:"% Time in Jit,secondvalue"`
StandardJitFailures float64 `perfdata:"Standard Jit Failures"`
TotalNumberofILBytesJitted float64 `perfdata:"Total # of IL Bytes Jitted"`
}
@@ -94,16 +95,18 @@ func (c *Collector) collectClrJIT(ch chan<- prometheus.Metric, scrape *labelcach
process.Name,
)
ch <- scrape.MustNewConstMetric(
c.timeInJit,
prometheus.GaugeValue,
float64(process.PercentTimeinJit)/c.perfFrequency,
process.Name,
)
if process.PercentTimeinJitBase > 0 {
ch <- scrape.MustNewConstMetric(
c.timeInJit,
prometheus.GaugeValue,
process.PercentTimeinJit/process.PercentTimeinJitBase,
process.Name,
)
}
ch <- scrape.MustNewConstMetric(
c.standardJitFailures,
prometheus.GaugeValue,
prometheus.CounterValue,
float64(process.StandardJitFailures),
process.Name,
)
@@ -95,8 +95,8 @@ func (c *Collector) describeClrMemory() {
nil,
)
c.timeInGC = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrMemory+"_gc_time_percent"),
"Displays the percentage of time that was spent performing a garbage collection in the last sample.",
prometheus.BuildFQName(types.Namespace, Name, collectorClrMemory+"_gc_time_ratio"),
"Displays the ratio (0-1) of elapsed time spent performing a garbage collection since the end of the last garbage collection cycle.",
[]string{"process", "process_id"},
nil,
)
@@ -304,7 +304,7 @@ func (c *Collector) collectClrMemory(ch chan<- prometheus.Metric, scrape *labelc
ch <- scrape.MustNewConstMetric(
c.timeInGC,
prometheus.GaugeValue,
100*process.PercentTimeinGC/process.PercentTimeinGC_base,
process.PercentTimeinGC/process.PercentTimeinGC_base,
process.Name,
strconv.FormatUint(uint64(process.ProcessID), 10),
)
@@ -34,8 +34,8 @@ func (c *Collector) describeClrSecurity() {
nil,
)
c.timeInRTChecks = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, collectorClrSecurity+"_rt_checks_time_percent"),
"Displays the percentage of time spent performing runtime code access security checks in the last sample.",
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"},
nil,
)
@@ -64,12 +64,13 @@ func (c *Collector) buildClrSecurity() error {
}
type perfDataClrSecurity struct {
Name string
Occurrence int
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"`
Name string
Occurrence int
NumberLinkTimeChecks float64 `perfdata:"# Link Time Checks"`
PercentTimeinRTchecks float64 `perfdata:"% Time in RT checks"`
PercentTimeinRTchecksBase float64 `perfdata:"% Time in RT checks,secondvalue"`
StackWalkDepth float64 `perfdata:"Stack Walk Depth"`
TotalRuntimeChecks float64 `perfdata:"Total Runtime Checks"`
}
func (p *perfDataClrSecurity) instance() (*string, int) { return &p.Name, p.Occurrence }
@@ -94,12 +95,14 @@ func (c *Collector) collectClrSecurity(ch chan<- prometheus.Metric, scrape *labe
process.Name,
)
ch <- scrape.MustNewConstMetric(
c.timeInRTChecks,
prometheus.GaugeValue,
float64(process.PercentTimeinRTchecks)/c.perfFrequency,
process.Name,
)
if process.PercentTimeinRTchecksBase > 0 {
ch <- scrape.MustNewConstMetric(
c.timeInRTChecks,
prometheus.GaugeValue,
process.PercentTimeinRTchecks/process.PercentTimeinRTchecksBase,
process.Name,
)
}
ch <- scrape.MustNewConstMetric(
c.stackWalkDepth,
@@ -60,12 +60,12 @@ func TestClrCounterPaths(t *testing.T) {
}{
{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[perfDataClrJIT](), []string{"# of Methods Jitted", "% Time in Jit", "% Time in Jit,secondvalue", "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"}},
{reflect.TypeFor[perfDataClrSecurity](), []string{"# Link Time Checks", "% Time in RT checks", "% Time in RT checks,secondvalue", "Stack Walk Depth", "Total Runtime Checks"}},
}
for _, tc := range cases {
t.Run(tc.rowType.Name(), func(t *testing.T) {
@@ -136,7 +136,7 @@ func assertFixtureMetrics(t *testing.T, collect func(chan<- prometheus.Metric, *
}
func TestClrExceptionsMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrExceptions()
row := perfDataClrExceptions{
@@ -162,7 +162,7 @@ func TestClrExceptionsMetricCompatibility(t *testing.T) {
}
func TestClrInteropMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrInterop()
row := perfDataClrInterop{
@@ -186,12 +186,13 @@ func TestClrInteropMetricCompatibility(t *testing.T) {
}
func TestClrJITMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrJIT()
row := perfDataClrJIT{
NumberofMethodsJitted: 11,
PercentTimeinJit: 50,
PercentTimeinJitBase: 200,
StandardJitFailures: 13,
TotalNumberofILBytesJitted: 14,
}
@@ -205,14 +206,14 @@ func TestClrJITMetricCompatibility(t *testing.T) {
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_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)
}
func TestClrLoadingMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrLoading()
row := perfDataClrLoading{
@@ -248,7 +249,7 @@ func TestClrLoadingMetricCompatibility(t *testing.T) {
}
func TestClrLocksAndThreadsMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrLocksAndThreads()
row := perfDataClrLocksAndThreads{
@@ -280,7 +281,7 @@ func TestClrLocksAndThreadsMetricCompatibility(t *testing.T) {
}
func TestClrMemoryMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrMemory()
row := perfDataClrMemory{
@@ -331,12 +332,12 @@ func TestClrMemoryMetricCompatibility(t *testing.T) {
{"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, ""},
{"windows_netframework_clrmemory_gc_time_ratio", dto.MetricType_GAUGE, 0.25, ""},
}, true)
}
func TestClrRemotingMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrRemoting()
row := perfDataClrRemoting{
@@ -366,14 +367,15 @@ func TestClrRemotingMetricCompatibility(t *testing.T) {
}
func TestClrSecurityMetricCompatibility(t *testing.T) {
c := &Collector{perfFrequency: 1000}
c := &Collector{}
c.describeClrSecurity()
row := perfDataClrSecurity{
NumberLinkTimeChecks: 11,
PercentTimeinRTchecks: 50,
StackWalkDepth: 13,
TotalRuntimeChecks: 14,
NumberLinkTimeChecks: 11,
PercentTimeinRTchecks: 50,
PercentTimeinRTchecksBase: 200,
StackWalkDepth: 13,
TotalRuntimeChecks: 14,
}
rows := make([]perfDataClrSecurity, 0, 4)
@@ -385,7 +387,7 @@ func TestClrSecurityMetricCompatibility(t *testing.T) {
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_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)
@@ -403,7 +405,7 @@ func TestGCZeroBaseDoesNotPublishInvalidRatio(t *testing.T) {
foundAllocated := false
for _, family := range families {
require.NotEqual(t, "windows_netframework_clrmemory_gc_time_percent", family.GetName())
require.NotEqual(t, "windows_netframework_clrmemory_gc_time_ratio", family.GetName())
if family.GetName() == "windows_netframework_clrmemory_allocated_bytes_total" {
require.InDelta(t, 99.0, family.GetMetric()[0].GetCounter().GetValue(), 1e-12)
@@ -415,6 +417,31 @@ func TestGCZeroBaseDoesNotPublishInvalidRatio(t *testing.T) {
require.True(t, foundAllocated)
}
func TestJITAndSecurityZeroBaseDoNotPublishInvalidRatio(t *testing.T) {
c := &Collector{}
c.describeClrJIT()
c.describeClrSecurity()
c.perfClrJIT = &fixturePerfCollector[perfDataClrJIT]{rows: []perfDataClrJIT{{Name: "worker", PercentTimeinJit: 50, NumberofMethodsJitted: 7}}}
c.perfClrSecurity = &fixturePerfCollector[perfDataClrSecurity]{rows: []perfDataClrSecurity{{Name: "worker", PercentTimeinRTchecks: 50, TotalRuntimeChecks: 7}}}
registry := prometheus.NewRegistry()
registry.MustRegister(fixturePromCollector{collect: func(ch chan<- prometheus.Metric) {
require.NoError(t, c.collectClrJIT(ch, nil, time.Second))
require.NoError(t, c.collectClrSecurity(ch, nil, time.Second))
}})
families, err := registry.Gather()
require.NoError(t, err)
names := make([]string, 0, len(families))
for _, family := range families {
names = append(names, family.GetName())
}
require.Contains(t, names, "windows_netframework_clrjit_jit_methods_total")
require.Contains(t, names, "windows_netframework_clrsecurity_runtime_checks_total")
require.NotContains(t, names, "windows_netframework_clrjit_jit_time_ratio")
require.NotContains(t, names, "windows_netframework_clrsecurity_rt_checks_time_ratio")
}
// TestDuplicateProcessNames checks that processes with the same name, which
// the CLR objects report with the same instance name, are published apart.
func TestDuplicateProcessNames(t *testing.T) {