collector: let the collection own the label and metric caches (#2740)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 00:32:08 +02:00
committed by GitHub
parent f6aad2c284
commit a1ac462daf
38 changed files with 1141 additions and 133 deletions
+7 -3
View File
@@ -29,6 +29,7 @@ import (
"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/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
)
@@ -141,13 +142,17 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels}
}
func (c *Collector) Close() error {
c.gpuEnginePerfDataCollector.Close()
c.gpuAdapterMemoryPerfDataCollector.Close()
c.gpuLocalAdapterMemoryPerfDataCollector.Close()
c.gpuNonLocalAdapterMemoryPerfDataCollector.Close()
c.gpuProcessMemoryPerfDataCollector.Close()
c.labels.Reset()
c.instances.reset()
return nil
@@ -409,8 +414,7 @@ func (c *Collector) getGPUDevice(luid string) (gpuDevice, bool) {
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
errs := make([]error, 0)
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
instances := c.instances.begin()
defer instances.commit()
+7 -3
View File
@@ -30,6 +30,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/labelcache"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/osversion"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
"github.com/prometheus/client_golang/prometheus"
)
@@ -184,13 +185,17 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels}
}
func (c *Collector) Close() error {
for _, fn := range c.closeFns {
fn()
}
c.closeFns = nil
c.labels.Reset()
return nil
}
@@ -359,8 +364,7 @@ func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
// Collect sends the metric values for each metric
// to the provided prometheus Metric channel.
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
var g recovery.Group
+13 -7
View File
@@ -30,6 +30,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
"github.com/prometheus/client_golang/prometheus"
)
@@ -122,6 +123,17 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{
&c.clusterMetricCache,
&c.networkMetricCache,
&c.nodeMetricCache,
&c.resourceMetricCache,
&c.resourceGroupMetricCache,
}
}
func (c *Collector) Close() error {
c.lifecycleMu.Lock()
defer c.lifecycleMu.Unlock()
@@ -129,7 +141,7 @@ func (c *Collector) Close() error {
return c.closeSources()
}
// closeSources releases the ClusAPI sources and drops the cached metrics. A
// closeSources releases the ClusAPI sources. A
// source whose Close fails is dropped as well: the clusapi.Cluster has already
// released its handle, and a later Build must be able to create new sources
// (Grafana Alloy rebuilds collectors in place).
@@ -155,12 +167,6 @@ func (c *Collector) closeSources() error {
closeSource(c.resourceGroupSource, func() { c.resourceGroupSource = nil })
closeSource(c.sharedVolumesSource, func() { c.sharedVolumesSource = nil })
c.clusterMetricCache.Reset()
c.networkMetricCache.Reset()
c.nodeMetricCache.Reset()
c.resourceMetricCache.Reset()
c.resourceGroupMetricCache.Reset()
return errors.Join(errs...)
}
@@ -679,9 +679,6 @@ func (c *Collector) buildClusterDescriptors() {
[]string{"name"},
nil,
)
// Cached metrics refer to the descriptors of an earlier build.
c.clusterMetricCache.Reset()
}
func (c *Collector) collectCluster(ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) error {
@@ -695,11 +692,7 @@ func (c *Collector) collectCluster(ch chan<- prometheus.Metric, maxScrapeDuratio
}
func (c *Collector) publishCluster(ch chan<- prometheus.Metric, cluster clusapi.Object, build uint16, resultErr error) error {
scrape := c.clusterMetricCache.Begin()
if cluster.Name == "" {
commitScrape(scrape, 0, resultErr)
return resultErr
}
@@ -797,8 +790,7 @@ func (c *Collector) publishCluster(ch chan<- prometheus.Metric, cluster clusapi.
{name: "DetectedCloudPlatform", desc: c.clusterDetectedCloudPlatform, minBuild: ws2022, older: neverPublish},
}
_, resultErr = publishCachedObjectFields(ch, scrape, nil, "cluster", cluster, fields, build, resultErr)
commitScrape(scrape, 1, resultErr)
_, resultErr = publishCachedObjectFields(ch, c.clusterMetricCache.Scrape(), nil, "cluster", cluster, fields, build, resultErr)
return resultErr
}
@@ -28,6 +28,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
"github.com/prometheus-community/windows_exporter/internal/osversion"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -290,7 +291,11 @@ func BenchmarkClusterPublication(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if err := c.publishCluster(ch, cluster, osversion.LTSC2022, nil); err != nil {
cacheScrape := scrapecache.Begin(c)
err := c.publishCluster(ch, cluster, osversion.LTSC2022, nil)
cacheScrape.End(err)
if err != nil {
b.Fatal(err)
}
@@ -102,9 +102,6 @@ func (c *Collector) buildNetworkDescriptors() {
[]string{"name"},
nil,
)
// Cached metrics refer to the descriptors of an earlier build.
c.networkMetricCache.Reset()
}
// Collect sends the metric values for each metric
@@ -128,7 +125,7 @@ func (c *Collector) publishNetworks(ch chan<- prometheus.Metric, networks []clus
{name: "State", desc: c.networkState},
}
scrape := c.networkMetricCache.Begin()
scrape := c.networkMetricCache.Scrape()
var key []byte
@@ -142,7 +139,5 @@ func (c *Collector) publishNetworks(ch chan<- prometheus.Metric, networks []clus
key, resultErr = publishCachedObjectFields(ch, scrape, key, "network", network, fields, 0, resultErr)
}
commitScrape(scrape, len(networks), resultErr)
return resultErr
}
@@ -26,6 +26,7 @@ import (
"time"
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -175,7 +176,11 @@ func BenchmarkNetworkPublication(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if err := c.publishNetworks(ch, networks, nil); err != nil {
cacheScrape := scrapecache.Begin(c)
err := c.publishNetworks(ch, networks, nil)
cacheScrape.End(err)
if err != nil {
b.Fatal(err)
}
@@ -175,9 +175,6 @@ func (c *Collector) buildNodeDescriptors() {
[]string{"name"},
nil,
)
// Cached metrics refer to the descriptors of an earlier build.
c.nodeMetricCache.Reset()
}
// Collect sends the metric values for each metric
@@ -214,7 +211,7 @@ func (c *Collector) publishNodes(ch chan<- prometheus.Metric, nodes []clusapi.Ob
}
nodeNames := make([]string, 0, len(nodes))
scrape := c.nodeMetricCache.Begin()
scrape := c.nodeMetricCache.Scrape()
var key []byte
@@ -227,7 +224,5 @@ func (c *Collector) publishNodes(ch chan<- prometheus.Metric, nodes []clusapi.Ob
nodeNames = append(nodeNames, node.Name)
}
commitScrape(scrape, len(nodes), resultErr)
return nodeNames, resultErr
}
@@ -28,6 +28,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
"github.com/prometheus-community/windows_exporter/internal/osversion"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -268,7 +269,11 @@ func BenchmarkNodePublication(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if _, err := c.publishNodes(ch, nodes, osversion.LTSC2022, nil); err != nil {
cacheScrape := scrapecache.Begin(c)
_, err := c.publishNodes(ch, nodes, osversion.LTSC2022, nil)
cacheScrape.End(err)
if err != nil {
b.Fatal(err)
}
@@ -183,13 +183,3 @@ func publishCachedObjectFields(ch chan<- prometheus.Metric, scrape *metriccache.
return key, resultErr
}
// commitScrape commits a scrape unless it collected nothing because reading
// the objects failed, so a failed read keeps the cached metrics.
func commitScrape[K, V comparable](scrape *metriccache.Scrape[K, V], collected int, err error) {
if collected == 0 && err != nil {
return
}
scrape.Commit()
}
@@ -210,9 +210,6 @@ func (c *Collector) buildResourceDescriptors() {
[]string{"type", "owner_group", "name"},
nil,
)
// Cached metrics refer to the descriptors of an earlier build.
c.resourceMetricCache.Reset()
}
// Collect sends the metric values for each metric
@@ -283,7 +280,7 @@ func (c *Collector) publishResources(ch chan<- prometheus.Metric, resources []cl
}
nodeNamesValue := string(nodeNamesKey)
scrape := c.resourceMetricCache.Begin()
scrape := c.resourceMetricCache.Scrape()
for _, resource := range resources {
if !resource.IdentityValid {
@@ -345,8 +342,6 @@ func (c *Collector) publishResources(ch chan<- prometheus.Metric, resources []cl
}
}
commitScrape(scrape, len(resources), resultErr)
return resultErr
}
@@ -26,6 +26,7 @@ import (
"time"
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -289,7 +290,11 @@ func BenchmarkResourcePublication(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if err := c.publishResources(ch, resources, []string{"nodeA", "nodeB"}, nil); err != nil {
cacheScrape := scrapecache.Begin(c)
err := c.publishResources(ch, resources, []string{"nodeA", "nodeB"}, nil)
cacheScrape.End(err)
if err != nil {
b.Fatal(err)
}
@@ -305,7 +310,9 @@ func TestResourceMetricCache(t *testing.T) {
publish := func(resource clusapi.Resource, nodeNames ...string) ([]prometheus.Metric, error) {
ch := make(chan prometheus.Metric, 100)
cacheScrape := scrapecache.Begin(c)
err := c.publishResources(ch, []clusapi.Resource{resource}, nodeNames, nil)
cacheScrape.End(err)
close(ch)
@@ -175,9 +175,6 @@ func (c *Collector) buildResourceGroupDescriptors() {
[]string{"name"},
nil,
)
// Cached metrics refer to the descriptors of an earlier build.
c.resourceGroupMetricCache.Reset()
}
// Collect sends the metric values for each metric
@@ -209,7 +206,7 @@ func (c *Collector) publishResourceGroups(ch chan<- prometheus.Metric, groups []
{name: "State", desc: c.resourceGroupState},
}
scrape := c.resourceGroupMetricCache.Begin()
scrape := c.resourceGroupMetricCache.Scrape()
var nodeNamesKey []byte
for _, nodeName := range nodeNames {
@@ -257,8 +254,6 @@ func (c *Collector) publishResourceGroups(ch chan<- prometheus.Metric, groups []
}
}
commitScrape(scrape, len(groups), resultErr)
return resultErr
}
@@ -27,6 +27,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/headers/clusapi"
"github.com/prometheus-community/windows_exporter/internal/osversion"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
@@ -370,7 +371,11 @@ func BenchmarkResourceGroupPublication(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if err := c.publishResourceGroups(ch, groups, []string{"nodeA", "nodeB"}, osversion.LTSC2022, nil); err != nil {
cacheScrape := scrapecache.Begin(c)
err := c.publishResourceGroups(ch, groups, []string{"nodeA", "nodeB"}, osversion.LTSC2022, nil)
cacheScrape.End(err)
if err != nil {
b.Fatal(err)
}
+8 -11
View File
@@ -33,6 +33,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/metriccache"
"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/client_golang/prometheus"
@@ -184,10 +185,13 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels, &c.nicMetricCache}
}
func (c *Collector) Close() error {
c.perfDataCollector.Close()
c.nicMetricCache.Reset()
c.labels.Reset()
c.adapterAddressesBuf = nil
return nil
@@ -299,10 +303,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
nil,
)
// Cached metrics refer to the descriptors of an earlier Build.
c.nicMetricCache.Reset()
c.labels.Reset()
var err error
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Network Interface", pdh.InstancesAll)
@@ -345,8 +345,7 @@ func (c *Collector) collect(ch chan<- prometheus.Metric) error {
return fmt.Errorf("failed to collect Network Information metrics: %w", err)
}
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
for _, data := range c.perfDataObject {
if c.config.NicExclude.MatchString(data.Name) || !c.config.NicInclude.MatchString(data.Name) {
@@ -460,7 +459,7 @@ func (c *Collector) collectNICInfo(ch chan<- prometheus.Metric) error {
return err
}
scrape := c.nicMetricCache.Begin()
scrape := c.nicMetricCache.Scrape()
var addresses []byte
@@ -497,8 +496,6 @@ func (c *Collector) collectNICInfo(ch chan<- prometheus.Metric) error {
}
}
scrape.Commit()
return nil
}
+2 -1
View File
@@ -23,6 +23,7 @@ import (
"testing"
"github.com/prometheus-community/windows_exporter/internal/collector/net"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -49,6 +50,6 @@ func BenchmarkCollector(b *testing.B) {
})
for b.Loop() {
require.NoError(b, c.Collect(metrics, 0))
require.NoError(b, testutils.Collect(c, metrics, 0))
}
}
@@ -31,6 +31,7 @@ import (
"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/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/utils/recovery"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/sys/windows"
@@ -205,6 +206,11 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels}
}
func (c *Collector) Close() error {
for _, closeFn := range c.closeFns {
closeFn()
@@ -212,7 +218,6 @@ func (c *Collector) Close() error {
c.closeFns = nil
c.collectorFns = nil
c.labels.Reset()
c.perfQuery.Close()
c.perfQuery = nil
@@ -362,8 +367,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
}
}
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
var g recovery.Group
+7 -9
View File
@@ -28,6 +28,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/headers/winspool"
"github.com/prometheus-community/windows_exporter/internal/metriccache"
"github.com/prometheus-community/windows_exporter/internal/mi"
"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/client_golang/prometheus"
@@ -129,8 +130,6 @@ func NewWithFlags(app *kingpin.Application) *Collector {
}
func (c *Collector) Close() error {
c.metricCache.Reset()
return nil
}
@@ -154,14 +153,16 @@ func (c *Collector) Build(_ *slog.Logger, _ *mi.Session) error {
nil,
)
// Cached metrics refer to the descriptors of an earlier Build.
c.metricCache.Reset()
return nil
}
func (c *Collector) GetName() string { return Name }
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.metricCache}
}
// Collect reads printers and jobs from the print spooler.
// Win32_Printer is not used because the CIMWin32 provider calls DeviceCapabilities
// for every printer, which blocks for about a minute on an unreachable network printer.
@@ -172,7 +173,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
return fmt.Errorf("failed to enumerate printers: %w", err)
}
scrape := c.metricCache.Begin()
scrape := c.metricCache.Scrape()
var errs []error
@@ -218,9 +219,6 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
}
}
// Printers that were not collected in this scrape are dropped from the cache.
scrape.Commit()
return errors.Join(errs...)
}
+1 -1
View File
@@ -51,5 +51,5 @@ func TestCollectorWithoutMISession(t *testing.T) {
ch := make(chan prometheus.Metric, 1000)
require.NoError(t, c.Collect(ch, time.Second))
require.NoError(t, testutils.Collect(c, ch, time.Second))
}
+7 -3
View File
@@ -30,6 +30,7 @@ import (
"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/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus-community/windows_exporter/internal/utils"
"github.com/prometheus/client_golang/prometheus"
@@ -158,6 +159,11 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels}
}
func (c *Collector) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -165,7 +171,6 @@ func (c *Collector) Close() error {
c.snapshot = snapshotBuffer{}
c.processes = nil
c.infoCache = nil
c.labels.Reset()
// The session belongs to the caller of Build.
c.miSession = nil
@@ -362,8 +367,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
infos := c.resolveProcessInfo(processes)
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
for i, process := range processes {
name := process.name
@@ -32,6 +32,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/ole"
"github.com/prometheus-community/windows_exporter/internal/ole/taskschd"
"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/client_golang/prometheus"
@@ -172,9 +173,12 @@ func (c *Collector) GetName() string {
return Name
}
func (c *Collector) Close() error {
c.metricCache.Reset()
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.metricCache}
}
func (c *Collector) Close() error {
return nil
}
@@ -212,9 +216,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.folderFilter = newFolderFilter(c.config.TaskInclude, c.config.TaskExclude)
// Cached metrics refer to the descriptors of an earlier Build.
c.metricCache.Reset()
return nil
}
@@ -261,7 +262,7 @@ func (c *Collector) taskReader() taskReader {
}
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, scheduledTasks ScheduledTasks) {
scrape := c.metricCache.Begin()
scrape := c.metricCache.Scrape()
for _, task := range scheduledTasks {
values := taskValues{
@@ -278,8 +279,6 @@ func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, scheduledTasks S
ch <- metric
}
}
scrape.Commit()
}
// taskMetrics builds the metrics of a task.
@@ -24,6 +24,7 @@ import (
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/expfmt"
@@ -252,7 +253,9 @@ func TestCollectMetricsCache(t *testing.T) {
for i, scrape := range []ScheduledTasks{tasks, tasks, changed, changed} {
// Collect once outside the registry, which calls Collect for Describe too.
collected := make(chan prometheus.Metric, len(scrape)*19)
cacheScrape := scrapecache.Begin(c)
c.collectMetrics(collected, scrape)
cacheScrape.End(nil)
close(collected)
var sent []prometheus.Metric
@@ -305,7 +308,9 @@ func TestCollectMetricsCache(t *testing.T) {
_, ok = scrape.Load("/Added", taskValues{state: TASK_STATE_QUEUED, lastTaskResult: SCHED_S_TASK_QUEUED})
require.True(t, ok)
// Build creates new descriptors, so the cache must not return metrics built from the old ones.
// Build creates new descriptors, so the collection resets the caches of
// the collector before it, which must include the metric cache.
scrapecache.Reset(c)
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
scrape = c.metricCache.Begin()
@@ -363,7 +368,9 @@ func BenchmarkCollectMetrics(b *testing.B) {
c.metricCache.Reset()
}
cacheScrape := scrapecache.Begin(c)
c.collectMetrics(ch, tasks)
cacheScrape.End(nil)
for len(ch) > 0 {
<-ch
+7 -9
View File
@@ -34,6 +34,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/metriccache"
"github.com/prometheus-community/windows_exporter/internal/mi"
"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"
@@ -176,6 +177,11 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.metricCache}
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
@@ -212,9 +218,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
nil,
)
// Cached metrics refer to the descriptors of an earlier Build.
c.metricCache.Reset()
c.apiStateValues = map[uint32]string{
windows.SERVICE_CONTINUE_PENDING: "continue pending",
windows.SERVICE_PAUSE_PENDING: "pause pending",
@@ -269,8 +272,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
}
func (c *Collector) Close() error {
c.metricCache.Reset()
if err := c.configCache.close(); err != nil {
c.logger.Warn("Failed to close the service configuration cache",
slog.Any("err", err),
@@ -315,7 +316,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
)
}
scrape := c.metricCache.Begin()
scrape := c.metricCache.Scrape()
servicesCh := make(chan serviceEntry, len(services))
@@ -340,9 +341,6 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
err = g.Wait()
// Services that were not collected in this scrape are dropped from the cache.
scrape.Commit()
return err
}
@@ -27,6 +27,7 @@ import (
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -44,7 +45,12 @@ func (c serviceMetricCollector) Describe(ch chan<- *prometheus.Desc) {
}
func (c serviceMetricCollector) Collect(ch chan<- prometheus.Metric) {
assert.NoError(c.t, c.collector.Collect(ch, 0))
// Collect within a scrape of the caches, as the collection does.
cacheScrape := scrapecache.Begin(c.collector)
err := c.collector.Collect(ch, 0)
cacheScrape.End(err)
assert.NoError(c.t, err)
}
// serviceMetrics scrapes the collector and returns the value of each series of
@@ -31,6 +31,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/metriccache"
"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/client_golang/prometheus"
"golang.org/x/sys/windows"
@@ -141,14 +142,16 @@ func (c *Collector) GetName() string {
return Name
}
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels, &c.sessionCache}
}
func (c *Collector) Close() error {
c.perfDataCollectorTerminalServicesSession.Close()
c.perfDataCollectorBroker.Close()
c.perfDataQuery.Close()
c.sessionCache.Reset()
c.labels.Reset()
// hServer is 0 if Build failed before opening the server.
if c.hServer == 0 || c.hServer == windows.InvalidHandle {
return nil
@@ -163,9 +166,6 @@ func (c *Collector) Close() error {
func (c *Collector) Build(logger *slog.Logger, miSession *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
c.sessionCache.Reset()
c.labels.Reset()
c.sessionInfo = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "session_info"),
"Terminal Services sessions info",
@@ -356,8 +356,7 @@ func (c *Collector) collectTSSessionCounters(ch chan<- prometheus.Metric) error
return fmt.Errorf("failed to collect Terminal Services Session metrics: %w", err)
}
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
names := make(map[string]bool)
@@ -521,9 +520,7 @@ func (c *Collector) collectWTSSessions(ch chan<- prometheus.Metric) error {
return fmt.Errorf("failed to enumerate WTS sessions: %w", err)
}
scrape := c.sessionCache.Begin()
// Sessions that were not collected in this scrape are dropped from the cache.
defer scrape.Commit()
scrape := c.sessionCache.Scrape()
for _, session := range sessions {
// only connect metrics for remote named sessions
@@ -23,6 +23,7 @@ import (
"testing"
"time"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -48,7 +49,10 @@ func TestCollectorWithoutSessionObject(t *testing.T) {
for range 2 {
ch := make(chan prometheus.Metric, 1024)
require.NoError(t, c.Collect(ch, time.Second))
cacheScrape := scrapecache.Begin(c)
err := c.Collect(ch, time.Second)
cacheScrape.End(err)
require.NoError(t, err)
close(ch)
for metric := range ch {
+7 -4
View File
@@ -31,6 +31,7 @@ import (
"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/scrapecache"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
"go.yaml.in/yaml/v3"
@@ -144,9 +145,12 @@ func (c *Collector) GetName() string {
return Name
}
func (c *Collector) Close() error {
c.labels.Reset()
// ScrapeCaches returns the caches that the collection resets and begins a scrape of around Collect.
func (c *Collector) ScrapeCaches() []scrapecache.Cache {
return []scrapecache.Cache{&c.labels}
}
func (c *Collector) Close() error {
return nil
}
@@ -459,8 +463,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
deadline = time.Now().Add(maxScrapeDuration)
}
scrape := c.labels.Begin()
defer scrape.Commit()
scrape := c.labels.Scrape()
var errs []error
+92
View File
@@ -25,10 +25,15 @@
// Label pairs are interned: all series with the same label name and value,
// such as the process and process_id labels of the series of one process,
// share one pair, so the cache keeps little more than one slice per series.
//
// A collector lists its caches in ScrapeCaches (see internal/scrapecache).
// The collection resets them around Build and Close and begins and ends a
// scrape around each Collect call, which gets it from [Cache.Scrape].
package labelcache
import (
"sync"
"sync/atomic"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
@@ -117,6 +122,14 @@ type Cache struct {
// resets counts Reset calls, so that a scrape begun before a Reset doesn't
// add series afterwards.
resets uint64
// scrapeMu guards active and the creation of current, so that the
// workers of a scrape create one scrape and none outlives EndScrape.
scrapeMu sync.Mutex
// active is set between BeginScrape and EndScrape.
active bool
// current is the scrape that Scrape created after BeginScrape, or nil.
current atomic.Pointer[Scrape]
}
// Scrape creates the metrics of one scrape.
@@ -128,9 +141,14 @@ type Scrape struct {
// metrics without the cache, so they can't add series to the set the
// commit decided.
committed bool
// used is set by the first MustNewConstMetric call.
used atomic.Bool
}
// Begin starts a scrape. Call Commit when it is done.
//
// Collectors don't call Begin: the collection begins and ends their scrapes
// through BeginScrape and EndScrape, and Collect gets its scrape from [Cache.Scrape].
func (c *Cache) Begin() *Scrape {
c.mu.Lock()
defer c.mu.Unlock()
@@ -140,6 +158,76 @@ func (c *Cache) Begin() *Scrape {
return &Scrape{cache: c, generation: c.generation, resets: c.resets}
}
// BeginScrape begins a scrape. The collection calls it before Collect.
func (c *Cache) BeginScrape() {
c.scrapeMu.Lock()
c.current.Store(nil)
c.active = true
c.scrapeMu.Unlock()
}
// EndScrape ends the scrape begun by BeginScrape. The collection calls it
// once Collect returned err.
//
// A scrape that Collect never asked for leaves the cache unchanged. Otherwise
// the scrape is committed, unless Collect failed before it created a metric:
// then the series of the previous scrapes stay.
func (c *Cache) EndScrape(err error) {
c.scrapeMu.Lock()
c.active = false
s := c.current.Swap(nil)
c.scrapeMu.Unlock()
if s == nil {
return
}
if err != nil && !s.used.Load() {
s.discard()
return
}
s.Commit()
}
// Scrape returns the scrape of the running Collect call, which it creates on
// the first call. It is nil outside of a scrape begun by the collection, for
// example when Collect is called directly; a nil Scrape creates metrics
// without the cache.
//
// Only Collect and the workers it waits for may call Scrape: after Collect
// returned, it returns nil or the scrape of a later Collect call.
func (c *Cache) Scrape() *Scrape {
if s := c.current.Load(); s != nil {
return s
}
c.scrapeMu.Lock()
defer c.scrapeMu.Unlock()
// Another worker of the scrape may have created it meanwhile.
if s := c.current.Load(); s != nil {
return s
}
if !c.active {
return nil
}
s := c.Begin()
c.current.Store(s)
return s
}
// discard ends the scrape without dropping any series.
func (s *Scrape) discard() {
s.cache.mu.Lock()
s.committed = true
s.cache.mu.Unlock()
}
// Reset drops all cached label pairs, for example when the collector closes.
func (c *Cache) Reset() {
c.mu.Lock()
@@ -225,6 +313,10 @@ func (c *Cache) intern(labels []*dto.LabelPair) []*dto.LabelPair {
//
//nolint:ireturn // Const metrics have no exported type.
func (s *Scrape) MustNewConstMetric(desc *prometheus.Desc, valueType prometheus.ValueType, value float64, labelValues ...string) prometheus.Metric {
if s != nil && !s.used.Load() {
s.used.Store(true)
}
if s == nil || len(labelValues) > maxLabels || !knownValueType(valueType) {
return prometheus.MustNewConstMetric(desc, valueType, value, append([]string(nil), labelValues...)...)
}
+69
View File
@@ -16,6 +16,7 @@
package labelcache_test
import (
"errors"
"runtime"
"strconv"
"sync"
@@ -358,3 +359,71 @@ func TestCommittedScrape(t *testing.T) {
require.NotSame(t, late[1], write(t, next.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "2")).GetLabel()[1], "late series was not cached")
next.Commit()
}
// scrapeWith runs one scrape of the pids the way the collection does around
// a Collect call that returns err, and returns the label pairs of each series.
func scrapeWith(t *testing.T, cache *labelcache.Cache, err error, pids ...string) [][]*dto.LabelPair {
t.Helper()
cache.BeginScrape()
defer cache.EndScrape(err)
scrape := cache.Scrape()
labels := make([][]*dto.LabelPair, 0, len(pids))
for _, pid := range pids {
labels = append(labels, write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", pid)).GetLabel())
}
return labels
}
func TestBeginEndScrape(t *testing.T) {
t.Parallel()
var cache labelcache.Cache
require.Nil(t, cache.Scrape(), "no scrape outside of BeginScrape")
first := scrapeWith(t, &cache, nil, "1", "2")
require.Nil(t, cache.Scrape(), "EndScrape ends the scrape")
second := scrapeWith(t, &cache, nil, "1", "2")
require.Same(t, first[0][1], second[0][1], "label pairs reused")
require.Same(t, first[1][1], second[1][1])
// A Collect call that never asked for the scrape leaves the cache unchanged.
cache.BeginScrape()
cache.EndScrape(nil)
// A failed Collect that created no metric keeps the series.
scrapeWith(t, &cache, errors.New("read failed"))
third := scrapeWith(t, &cache, nil, "1", "2")
require.Same(t, first[1][1], third[1][1], "failed empty scrape kept the series")
// A failed Collect that created metrics commits its series.
scrapeWith(t, &cache, errors.New("partial"), "1")
fourth := scrapeWith(t, &cache, nil, "1", "2")
require.Same(t, first[0][1], fourth[0][1])
require.NotSame(t, first[1][1], fourth[1][1], "partial scrape dropped series 2")
// A successful empty scrape drops every series.
scrapeWith(t, &cache, nil)
fifth := scrapeWith(t, &cache, nil, "1")
require.NotSame(t, fourth[0][1], fifth[0][1])
// Reset drops the series, also those of a running scrape.
cache.BeginScrape()
write(t, cache.Scrape().MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "1"))
cache.Reset()
cache.EndScrape(nil)
sixth := scrapeWith(t, &cache, nil, "1")
require.NotSame(t, fifth[0][1], sixth[0][1])
// EndScrape without a scrape does nothing.
cache.EndScrape(nil)
}
+109 -5
View File
@@ -20,10 +20,15 @@
// the allocations of collectors that publish many state-set series per object.
// Const metrics are immutable and a registry only reads them during Gather, so
// sending the same metric again in a later scrape is safe.
//
// A collector lists its caches in ScrapeCaches (see internal/scrapecache).
// The collection resets them around Build and Close and begins and ends a
// scrape around each Collect call, which gets it from [Cache.Scrape].
package metriccache
import (
"sync"
"sync/atomic"
"github.com/prometheus/client_golang/prometheus"
)
@@ -40,6 +45,14 @@ type Cache[K, V comparable] struct {
// resets counts Reset calls, so that a scrape begun before a Reset
// doesn't commit afterwards.
resets uint64
// scrapeMu guards active and the creation of current, so that the
// workers of a scrape create one scrape and none outlives EndScrape.
scrapeMu sync.Mutex
// active is set between BeginScrape and EndScrape.
active bool
// current is the scrape that Scrape created after BeginScrape, or nil.
current atomic.Pointer[Scrape[K, V]]
}
type entry[V comparable] struct {
@@ -48,7 +61,8 @@ type entry[V comparable] struct {
}
// Scrape collects the entries of one scrape. Its methods may be called by the
// concurrent workers of one scrape.
// concurrent workers of one scrape. A nil Scrape builds every metric and
// caches nothing.
type Scrape[K, V comparable] struct {
cache *Cache[K, V]
resets uint64
@@ -63,6 +77,10 @@ type Scrape[K, V comparable] struct {
}
// Begin starts a scrape based on the last committed generation.
//
// Collectors don't call Begin: the collection begins and ends their scrapes
// through BeginScrape and EndScrape, and Collect gets its scrape from
// [Cache.Scrape].
func (c *Cache[K, V]) Begin() *Scrape[K, V] {
c.mu.Lock()
prev, resets := c.entries, c.resets
@@ -76,6 +94,73 @@ func (c *Cache[K, V]) Begin() *Scrape[K, V] {
}
}
// BeginScrape begins a scrape. The collection calls it before Collect.
func (c *Cache[K, V]) BeginScrape() {
c.scrapeMu.Lock()
c.current.Store(nil)
c.active = true
c.scrapeMu.Unlock()
}
// EndScrape ends the scrape begun by BeginScrape. The collection calls it
// once Collect returned err.
//
// A scrape that Collect never asked for leaves the cache unchanged. Otherwise
// the scrape is committed, unless Collect failed before it loaded or stored an
// entry: then the entries of the previous scrape stay, so a failed read of the
// objects doesn't drop them.
func (c *Cache[K, V]) EndScrape(err error) {
c.scrapeMu.Lock()
c.active = false
s := c.current.Swap(nil)
c.scrapeMu.Unlock()
if s == nil {
return
}
s.mu.Lock()
empty := len(s.next) == 0
s.mu.Unlock()
if err != nil && empty {
s.discard()
return
}
s.Commit()
}
// Scrape returns the scrape of the running Collect call, which it creates on
// the first call. It is nil outside of a scrape begun by the collection, for
// example when Collect is called directly.
//
// Only Collect and the workers it waits for may call Scrape: after Collect
// returned, it returns nil or the scrape of a later Collect call.
func (c *Cache[K, V]) Scrape() *Scrape[K, V] {
if s := c.current.Load(); s != nil {
return s
}
c.scrapeMu.Lock()
defer c.scrapeMu.Unlock()
// Another worker of the scrape may have created it meanwhile.
if s := c.current.Load(); s != nil {
return s
}
if !c.active {
return nil
}
s := c.Begin()
c.current.Store(s)
return s
}
// Reset drops all cached metrics, for example after their descriptors changed.
// Scrapes begun before Reset don't commit their entries.
func (c *Cache[K, V]) Reset() {
@@ -89,6 +174,10 @@ func (c *Cache[K, V]) Reset() {
// an equal value, and keeps them for the next scrape. The caller must not
// modify the returned slice.
func (s *Scrape[K, V]) Load(key K, value V) ([]prometheus.Metric, bool) {
if s == nil {
return nil, false
}
e, ok := s.prev[key]
if !ok || e.value != value {
return nil, false
@@ -106,6 +195,10 @@ func (s *Scrape[K, V]) Load(key K, value V) ([]prometheus.Metric, bool) {
// Store keeps metrics built from value for key. The caller must not modify
// metrics afterwards.
func (s *Scrape[K, V]) Store(key K, value V, metrics []prometheus.Metric) {
if s == nil {
return
}
s.mu.Lock()
if s.next != nil {
s.next[key] = entry[V]{value: value, metrics: metrics}
@@ -133,11 +226,11 @@ func (s *Scrape[K, V]) Get(key K, value V, build func() []prometheus.Metric) []p
// while a timed-out scrape was still collecting. Calls on the scrape after
// Commit don't change the cache.
func (s *Scrape[K, V]) Commit() {
s.mu.Lock()
next := s.next
s.next = nil
s.mu.Unlock()
if s == nil {
return
}
next := s.discard()
if next == nil {
// Committed before.
return
@@ -149,3 +242,14 @@ func (s *Scrape[K, V]) Commit() {
}
s.cache.mu.Unlock()
}
// discard ends the scrape without changing the cache and returns its entries.
func (s *Scrape[K, V]) discard() map[K]entry[V] {
s.mu.Lock()
defer s.mu.Unlock()
next := s.next
s.next = nil
return next
}
+120
View File
@@ -16,6 +16,7 @@
package metriccache_test
import (
"errors"
"strconv"
"sync"
"testing"
@@ -221,6 +222,125 @@ func TestScrapeConcurrentWorkers(t *testing.T) {
}
}
func TestNilScrape(t *testing.T) {
t.Parallel()
var cache metriccache.Cache[string, string]
scrape := cache.Scrape()
require.Nil(t, scrape, "no scrape outside of BeginScrape")
built := 0
buildCounted := func() []prometheus.Metric {
built++
return build("a", "up")
}
metrics := scrape.Get("a", "up", buildCounted)
require.Len(t, metrics, 1)
_, ok := scrape.Load("a", "up")
require.False(t, ok)
scrape.Store("a", "up", metrics)
scrape.Commit()
scrape.Get("a", "up", buildCounted)
require.Equal(t, 2, built, "a nil scrape caches nothing")
}
// scrapeWith runs one scrape of keys the way the collection does around a
// Collect call that returns err.
func scrapeWith(cache *metriccache.Cache[string, string], err error, keys ...string) []prometheus.Metric {
cache.BeginScrape()
defer cache.EndScrape(err)
scrape := cache.Scrape()
metrics := make([]prometheus.Metric, 0, len(keys))
for _, key := range keys {
metrics = append(metrics, scrape.Get(key, "up", func() []prometheus.Metric {
return build(key, "up")
})...)
}
return metrics
}
func TestBeginEndScrape(t *testing.T) {
t.Parallel()
var cache metriccache.Cache[string, string]
first := scrapeWith(&cache, nil, "a", "b")
require.Nil(t, cache.Scrape(), "EndScrape ends the scrape")
second := scrapeWith(&cache, nil, "a", "b")
require.Same(t, first[0], second[0], "metrics reused")
require.Same(t, first[1], second[1])
// A Collect call that never asked for the scrape leaves the cache unchanged.
cache.BeginScrape()
cache.EndScrape(nil)
// A failed Collect that used no entry keeps the previous ones.
scrapeWith(&cache, errors.New("read failed"))
third := scrapeWith(&cache, nil, "a", "b")
require.Same(t, first[0], third[0], "failed empty scrape kept the entries")
// A failed Collect that used entries commits them.
scrapeWith(&cache, errors.New("partial"), "a")
fourth := scrapeWith(&cache, nil, "a", "b")
require.Same(t, first[0], fourth[0])
require.NotSame(t, first[1], fourth[1], "partial scrape dropped b")
// A successful empty scrape drops every entry.
scrapeWith(&cache, nil)
fifth := scrapeWith(&cache, nil, "a")
require.NotSame(t, fourth[0], fifth[0])
// Reset drops the entries, also those of a running scrape.
cache.BeginScrape()
cache.Scrape().Get("a", "up", func() []prometheus.Metric { return build("a", "up") })
cache.Reset()
cache.EndScrape(nil)
sixth := scrapeWith(&cache, nil, "a")
require.NotSame(t, fifth[0], sixth[0])
// EndScrape without a scrape does nothing.
cache.EndScrape(nil)
}
func TestScrapeCreatedOnce(t *testing.T) {
t.Parallel()
var (
cache metriccache.Cache[string, string]
wg sync.WaitGroup
scrapes [8]*metriccache.Scrape[string, string]
)
cache.BeginScrape()
for i := range scrapes {
wg.Go(func() {
scrapes[i] = cache.Scrape()
})
}
wg.Wait()
cache.EndScrape(nil)
for _, scrape := range scrapes {
require.NotNil(t, scrape)
require.Same(t, scrapes[0], scrape, "the workers of a scrape share one scrape")
}
}
func BenchmarkScrape(b *testing.B) {
var cache metriccache.Cache[string, int]
+86
View File
@@ -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.
// Package scrapecache lets the collection own the caches that collectors keep
// between scrapes, such as internal/labelcache and internal/metriccache.
//
// A collector lists its caches with ScrapeCaches. The collection resets them
// before Build and after Close, and begins a scrape of each cache before
// Collect and ends it after Collect returned. Collect calls of one collector
// never overlap, so a scrape always belongs to exactly one Collect call, even
// one that outlives its scrape budget.
//
// Outside of the collection, for example when a test or a library consumer
// calls Collect directly, the caches have no scrape and every metric is built
// without them.
package scrapecache
// Cache is a cache that a collector keeps between scrapes.
type Cache interface {
// BeginScrape begins the scrape of a Collect call.
BeginScrape()
// EndScrape ends the scrape after Collect returned err.
EndScrape(err error)
// Reset drops the cached entries. Their descriptors may be stale after
// a new Build.
Reset()
}
// Owner is implemented by collectors that keep caches between scrapes.
type Owner interface {
// ScrapeCaches returns the caches of the collector. The collection may
// call it before Build.
ScrapeCaches() []Cache
}
// Scrape is a scrape of the caches of a collector.
type Scrape struct {
caches []Cache
}
// Begin begins a scrape of the caches of collector, if it is an [Owner].
// Call End with the error of Collect once Collect returned.
func Begin(collector any) Scrape {
owner, ok := collector.(Owner)
if !ok {
return Scrape{}
}
caches := owner.ScrapeCaches()
for _, cache := range caches {
cache.BeginScrape()
}
return Scrape{caches: caches}
}
// End ends the scrape after Collect returned err.
func (s Scrape) End(err error) {
for _, cache := range s.caches {
cache.EndScrape(err)
}
}
// Reset resets the caches of collector, if it is an [Owner].
func Reset(collector any) {
owner, ok := collector.(Owner)
if !ok {
return
}
for _, cache := range owner.ScrapeCaches() {
cache.Reset()
}
}
+75
View File
@@ -0,0 +1,75 @@
// 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.
package scrapecache_test
import (
"errors"
"fmt"
"testing"
"github.com/prometheus-community/windows_exporter/internal/scrapecache"
"github.com/stretchr/testify/require"
)
type cache struct {
name string
events *[]string
}
func (c cache) BeginScrape() {
*c.events = append(*c.events, c.name+" begin")
}
func (c cache) EndScrape(err error) {
*c.events = append(*c.events, fmt.Sprintf("%s end %v", c.name, err))
}
func (c cache) Reset() {
*c.events = append(*c.events, c.name+" reset")
}
type owner struct {
events []string
calls int
}
func (o *owner) ScrapeCaches() []scrapecache.Cache {
o.calls++
return []scrapecache.Cache{cache{name: "a", events: &o.events}, cache{name: "b", events: &o.events}}
}
func TestOwner(t *testing.T) {
t.Parallel()
o := &owner{}
scrapecache.Reset(o)
cacheScrape := scrapecache.Begin(o)
require.Equal(t, []string{"a reset", "b reset", "a begin", "b begin"}, o.events)
cacheScrape.End(errors.New("failed"))
require.Equal(t, []string{"a end failed", "b end failed"}, o.events[4:])
require.Equal(t, 2, o.calls, "Begin asks for the caches once per scrape")
}
func TestNotAnOwner(t *testing.T) {
t.Parallel()
scrapecache.Reset(struct{}{})
scrapecache.Begin(struct{}{}).End(errors.New("ignored"))
}
+24 -4
View File
@@ -31,6 +31,7 @@ import (
"github.com/prometheus-community/windows_exporter/internal/collector/update"
"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/pkg/collector"
"github.com/prometheus/client_golang/prometheus"
@@ -69,10 +70,22 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co
})
for b.Loop() {
require.NoError(b, c.Collect(metrics, 0))
require.NoError(b, Collect(c, metrics, 0))
}
}
// Collect calls c.Collect within a scrape of the caches of c, as the
// collection does. Calling c.Collect directly collects without the caches.
func Collect(c collector.Collector, ch chan<- prometheus.Metric, maxScrapeDuration time.Duration) (err error) {
cacheScrape := scrapecache.Begin(c)
defer func() {
cacheScrape.End(err)
}()
return c.Collect(ch, maxScrapeDuration)
}
// TestCollector validates real Windows collector output. CI lists provisioned
// collectors in WINDOWS_EXPORTER_TEST_COLLECTORS so setup failures cannot skip.
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) map[string]*dto.MetricFamily {
@@ -89,7 +102,14 @@ func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, co
miSession, err := miApp.NewSession(nil)
require.NoError(t, err)
t.Cleanup(func() { assert.NoError(t, miSession.Close()) })
t.Cleanup(func() { assert.NoError(t, c.Close()) })
t.Cleanup(func() {
assert.NoError(t, c.Close())
scrapecache.Reset(c)
})
// Like the collection, start without cached metrics and collect through
// the caches, so the second scrape publishes cached metrics.
scrapecache.Reset(c)
if err := c.Build(logger, miSession); err != nil {
if !required && unsupportedBuild(err) {
@@ -159,13 +179,13 @@ func collectMetrics(t *testing.T, c collector.Collector, required bool) map[stri
wg.Wait()
}()
err := c.Collect(ch, 30*time.Second)
err := Collect(c, ch, 30*time.Second)
if errors.Is(err, update.ErrNoUpdates) && required {
deadline := time.Now().Add(time.Minute)
for errors.Is(err, update.ErrNoUpdates) && time.Now().Before(deadline) {
time.Sleep(100 * time.Millisecond)
err = c.Collect(ch, 30*time.Second)
err = Collect(c, ch, 30*time.Second)
}
}