mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
wmi: reuse label pairs between scrapes (#2699)
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
a343bada16
commit
f12d4624c9
@@ -29,6 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -73,6 +74,10 @@ type Collector struct {
|
||||
|
||||
queries []Query
|
||||
|
||||
// labels keeps the label pairs of the returned instances between scrapes,
|
||||
// so that a query returning many instances doesn't rebuild them each time.
|
||||
labels labelcache.Cache
|
||||
|
||||
querySuccessDesc *prometheus.Desc
|
||||
queryDurationDesc *prometheus.Desc
|
||||
}
|
||||
@@ -140,6 +145,8 @@ func (c *Collector) GetName() string {
|
||||
}
|
||||
|
||||
func (c *Collector) Close() error {
|
||||
c.labels.Reset()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -452,11 +459,14 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
|
||||
deadline = time.Now().Add(maxScrapeDuration)
|
||||
}
|
||||
|
||||
scrape := c.labels.Begin()
|
||||
defer scrape.Commit()
|
||||
|
||||
var errs []error
|
||||
|
||||
for _, query := range c.queries {
|
||||
startTime := time.Now()
|
||||
err := c.collectQuery(ch, query, deadline)
|
||||
err := c.collectQuery(ch, scrape, query, deadline)
|
||||
duration := time.Since(startTime)
|
||||
success := 1.0
|
||||
|
||||
@@ -478,7 +488,10 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
|
||||
slog.Any("err", err),
|
||||
)
|
||||
} else {
|
||||
c.logger.Debug(fmt.Sprintf("wmi query %s succeeded after %s", query.Name, duration))
|
||||
c.logger.Debug("wmi query succeeded",
|
||||
slog.String("query", query.Name),
|
||||
slog.Duration("duration", duration),
|
||||
)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
@@ -499,7 +512,7 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, maxScrapeDuration time.
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (c *Collector) collectQuery(ch chan<- prometheus.Metric, query Query, deadline time.Time) error {
|
||||
func (c *Collector) collectQuery(ch chan<- prometheus.Metric, scrape *labelcache.Scrape, query Query, deadline time.Time) error {
|
||||
// Zero means no timeout, which leaves the MI default in place.
|
||||
var timeout time.Duration
|
||||
|
||||
@@ -569,7 +582,7 @@ func (c *Collector) collectQuery(ch chan<- prometheus.Metric, query Query, deadl
|
||||
continue
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
ch <- scrape.MustNewConstMetric(
|
||||
property.desc,
|
||||
property.metricType,
|
||||
value,
|
||||
|
||||
@@ -65,15 +65,27 @@ func scrape(t *testing.T, c *wmi.Collector) string {
|
||||
func scrapeWithTimeout(t *testing.T, c *wmi.Collector, maxScrapeDuration time.Duration) string {
|
||||
t.Helper()
|
||||
|
||||
return newScraper(t, c, maxScrapeDuration)()
|
||||
}
|
||||
|
||||
// newScraper builds c against a live MI session and returns a function that
|
||||
// scrapes it, so that a test can scrape the same collector more than once.
|
||||
func newScraper(t *testing.T, c *wmi.Collector, maxScrapeDuration time.Duration) func() string {
|
||||
t.Helper()
|
||||
|
||||
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), newSession(t)))
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
reg.MustRegister(collectorAdapter{c, maxScrapeDuration})
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
promhttp.HandlerFor(reg, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{})
|
||||
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError})
|
||||
|
||||
return rw.Body.String()
|
||||
return func() string {
|
||||
rw := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rw, &http.Request{})
|
||||
|
||||
return rw.Body.String()
|
||||
}
|
||||
}
|
||||
|
||||
// collect builds c against a live MI session and returns the number of emitted
|
||||
@@ -102,10 +114,13 @@ func collect(t *testing.T, c *wmi.Collector) (int, error) {
|
||||
// constant labels, boolean conversion, a metric name shared by two queries,
|
||||
// and the query_success semantics for an unknown class, a non-numeric
|
||||
// property next to a numeric one, and a label property of unsupported type.
|
||||
//
|
||||
// The collector is scraped twice: the second scrape reuses the label pairs
|
||||
// cached by the first and must produce the same output.
|
||||
func TestCollectorMetrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := scrape(t, wmi.New(&wmi.Config{
|
||||
next := newScraper(t, wmi.New(&wmi.Config{
|
||||
Queries: []wmi.Query{
|
||||
{
|
||||
Name: "kernel",
|
||||
@@ -172,16 +187,7 @@ func TestCollectorMetrics(t *testing.T) {
|
||||
Properties: []wmi.Property{{Name: "Primary"}},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Durations vary between runs; their presence is asserted separately.
|
||||
var lines []string
|
||||
|
||||
for line := range strings.SplitSeq(got, "\n") {
|
||||
if !strings.Contains(line, "windows_wmi_query_duration_seconds") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}), 5*time.Second)
|
||||
|
||||
expected := `# HELP windows_wmi_kernel_processid windows_exporter: custom WMI metric
|
||||
# TYPE windows_wmi_kernel_processid gauge
|
||||
@@ -212,10 +218,23 @@ windows_wmi_test_shared{query="a"} 1
|
||||
windows_wmi_test_shared{query="b"} 1
|
||||
`
|
||||
|
||||
require.Equal(t, expected, strings.Join(lines, "\n"))
|
||||
for range 2 {
|
||||
got := next()
|
||||
|
||||
for _, name := range []string{"kernel", "missing_class", "os", "partial", "shared_a", "shared_b", "unsupported_label"} {
|
||||
require.Regexp(t, `(?m)^windows_wmi_query_duration_seconds\{name="`+name+`"\} \d`, got)
|
||||
// Durations vary between runs; their presence is asserted separately.
|
||||
var lines []string
|
||||
|
||||
for line := range strings.SplitSeq(got, "\n") {
|
||||
if !strings.Contains(line, "windows_wmi_query_duration_seconds") {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, expected, strings.Join(lines, "\n"))
|
||||
|
||||
for _, name := range []string{"kernel", "missing_class", "os", "partial", "shared_a", "shared_b", "unsupported_label"} {
|
||||
require.Regexp(t, `(?m)^windows_wmi_query_duration_seconds\{name="`+name+`"\} \d`, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,18 @@ func BenchmarkCollector(b *testing.B) {
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkCollectorProcess runs a query returning one instance per process,
|
||||
// so that the cost of many series per scrape shows.
|
||||
func BenchmarkCollectorProcess(b *testing.B) {
|
||||
queries := `[{"name":"process","class":"Win32_Process","label_properties":[{"name":"ProcessId"},{"name":"Name"}],"properties":[{"name":"WorkingSetSize"},{"name":"ThreadCount"},{"name":"HandleCount"}]}]`
|
||||
|
||||
testutils.FuncBenchmarkCollector(b, wmi.Name, wmi.NewWithFlags, func(app *kingpin.Application) {
|
||||
// The queries are only read by the application action, which runs on parse.
|
||||
_, err := app.Parse([]string{"--collector.wmi.queries", queries})
|
||||
require.NoError(b, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user