mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
389 lines
11 KiB
Go
389 lines
11 KiB
Go
// 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 collector_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/prometheus-community/windows_exporter/internal/labelcache"
|
|
"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/pkg/collector"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/common/expfmt"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// eventLog records the calls of a collector and its cache in order.
|
|
type eventLog struct {
|
|
mu sync.Mutex
|
|
events []string
|
|
}
|
|
|
|
func (l *eventLog) addf(format string, args ...any) {
|
|
l.mu.Lock()
|
|
l.events = append(l.events, fmt.Sprintf(format, args...))
|
|
l.mu.Unlock()
|
|
}
|
|
|
|
func (l *eventLog) take() []string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
events := l.events
|
|
l.events = nil
|
|
|
|
return events
|
|
}
|
|
|
|
// recordingCache is a scrapecache.Cache that records its calls.
|
|
type recordingCache struct {
|
|
log *eventLog
|
|
active atomic.Bool
|
|
beginPanic atomic.Bool
|
|
endPanic atomic.Bool
|
|
}
|
|
|
|
func (c *recordingCache) BeginScrape() {
|
|
if c.beginPanic.Load() {
|
|
panic("begin panic")
|
|
}
|
|
|
|
c.active.Store(true)
|
|
c.log.addf("begin")
|
|
}
|
|
|
|
func (c *recordingCache) EndScrape(err error) {
|
|
c.active.Store(false)
|
|
|
|
if err != nil {
|
|
// Keep the first line, a panic error contains the stack.
|
|
err = errors.New(strings.SplitN(err.Error(), ".", 2)[0])
|
|
}
|
|
|
|
c.log.addf("end %v", err)
|
|
|
|
if c.endPanic.Load() {
|
|
panic("end panic")
|
|
}
|
|
}
|
|
|
|
func (c *recordingCache) Reset() {
|
|
c.log.addf("reset")
|
|
}
|
|
|
|
// cacheOwner is a collector with a recordingCache.
|
|
type cacheOwner struct {
|
|
log *eventLog
|
|
cache recordingCache
|
|
collectErr error
|
|
collectPanic bool
|
|
delay time.Duration
|
|
}
|
|
|
|
func newCacheOwner() *cacheOwner {
|
|
log := &eventLog{}
|
|
|
|
return &cacheOwner{log: log, cache: recordingCache{log: log}}
|
|
}
|
|
|
|
func (c *cacheOwner) GetName() string { return "test" }
|
|
|
|
func (c *cacheOwner) ScrapeCaches() []scrapecache.Cache {
|
|
return []scrapecache.Cache{&c.cache}
|
|
}
|
|
|
|
func (c *cacheOwner) Build(*slog.Logger, *mi.Session) error {
|
|
c.log.addf("build")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cacheOwner) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
|
c.log.addf("collect in scrape=%t", c.cache.active.Load())
|
|
|
|
time.Sleep(c.delay)
|
|
|
|
if c.collectPanic {
|
|
panic("collect panic")
|
|
}
|
|
|
|
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc("windows_test", "Test metric", nil, nil), prometheus.GaugeValue, 1)
|
|
|
|
if c.delay > 0 {
|
|
c.log.addf("collect returned")
|
|
}
|
|
|
|
return c.collectErr
|
|
}
|
|
|
|
func (c *cacheOwner) Close() error {
|
|
c.log.addf("close")
|
|
|
|
return nil
|
|
}
|
|
|
|
func TestScrapeCacheLifecycle(t *testing.T) {
|
|
owner := newCacheOwner()
|
|
collection := collector.New(collector.Map{"test": owner})
|
|
|
|
require.NoError(t, collection.Build(t.Context(), slog.New(slog.DiscardHandler)))
|
|
require.Equal(t, []string{"reset", "build"}, owner.log.take(), "caches are reset before Build")
|
|
|
|
handler, err := collection.NewHandler(time.Minute, slog.New(slog.DiscardHandler), nil)
|
|
require.NoError(t, err)
|
|
|
|
scrape(t, handler)
|
|
require.Equal(t, []string{"begin", "collect in scrape=true", "end <nil>"}, owner.log.take())
|
|
|
|
owner.collectErr = errors.New("collect failed")
|
|
|
|
scrape(t, handler)
|
|
require.Equal(t, []string{"begin", "collect in scrape=true", "end collect failed"}, owner.log.take(),
|
|
"EndScrape gets the error of Collect")
|
|
|
|
owner.collectErr = nil
|
|
owner.collectPanic = true
|
|
|
|
scrape(t, handler)
|
|
require.Equal(t, []string{"begin", "collect in scrape=true", "end panic in collector test: collect panic"}, owner.log.take(),
|
|
"EndScrape gets the error of a panic")
|
|
|
|
require.NoError(t, collection.Close())
|
|
require.Equal(t, []string{"close", "reset"}, owner.log.take(), "caches are reset after Close")
|
|
}
|
|
|
|
// TestScrapeCachePanics verifies that a panic in a cache fails the collector
|
|
// like a panic in Collect, instead of crashing the exporter.
|
|
func TestScrapeCachePanics(t *testing.T) {
|
|
owner := newCacheOwner()
|
|
collection := collector.New(collector.Map{"test": owner})
|
|
|
|
require.NoError(t, collection.Build(t.Context(), slog.New(slog.DiscardHandler)))
|
|
t.Cleanup(func() { require.NoError(t, collection.Close()) })
|
|
|
|
handler, err := collection.NewHandler(time.Minute, slog.New(slog.DiscardHandler), nil)
|
|
require.NoError(t, err)
|
|
|
|
owner.log.take()
|
|
owner.cache.endPanic.Store(true)
|
|
|
|
out := scrape(t, handler)
|
|
require.Contains(t, out, `windows_exporter_collector_success{collector="test"} 0`)
|
|
require.Contains(t, out, "windows_test 1", "metrics of Collect are kept")
|
|
require.Equal(t, []string{"begin", "collect in scrape=true", "end <nil>"}, owner.log.take())
|
|
|
|
owner.cache.endPanic.Store(false)
|
|
owner.cache.beginPanic.Store(true)
|
|
|
|
out = scrape(t, handler)
|
|
require.Contains(t, out, `windows_exporter_collector_success{collector="test"} 0`)
|
|
require.Empty(t, owner.log.take(), "Collect is not called when the scrape of the caches fails to begin")
|
|
|
|
owner.cache.beginPanic.Store(false)
|
|
|
|
out = scrape(t, handler)
|
|
require.Contains(t, out, `windows_exporter_collector_success{collector="test"} 1`)
|
|
require.Equal(t, []string{"begin", "collect in scrape=true", "end <nil>"}, owner.log.take())
|
|
}
|
|
|
|
func TestScrapeCacheEndsAfterTimedOutCollect(t *testing.T) {
|
|
// The collector's delay and the scrape deadline use synthetic time.
|
|
synctest.Test(t, func(t *testing.T) {
|
|
owner := newCacheOwner()
|
|
owner.delay = 2 * time.Second
|
|
|
|
collection := collector.New(collector.Map{"test": owner})
|
|
require.NoError(t, collection.Build(t.Context(), slog.New(slog.DiscardHandler)))
|
|
owner.log.take()
|
|
|
|
handler, err := collection.NewHandler(300*time.Millisecond, slog.New(slog.DiscardHandler), nil)
|
|
require.NoError(t, err)
|
|
|
|
out := scrape(t, handler)
|
|
require.Contains(t, out, `windows_exporter_collector_timeout{collector="test"} 1`)
|
|
require.Equal(t, []string{"begin", "collect in scrape=true"}, owner.log.take(),
|
|
"the scrape of the caches outlives the scrape budget")
|
|
|
|
time.Sleep(2 * time.Second)
|
|
synctest.Wait()
|
|
|
|
require.Equal(t, []string{"collect returned", "end <nil>"}, owner.log.take(),
|
|
"the scrape ends once Collect returned")
|
|
|
|
require.NoError(t, collection.Close())
|
|
})
|
|
}
|
|
|
|
func TestScrapeCacheDirectCollect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
owner := newCacheOwner()
|
|
|
|
require.NoError(t, owner.Collect(make(chan prometheus.Metric, 1), time.Second))
|
|
require.Equal(t, []string{"collect in scrape=false"}, owner.log.take(),
|
|
"a direct Collect call runs without a scrape")
|
|
}
|
|
|
|
// cachedCollector publishes one series through a labelcache and one through a
|
|
// metriccache, like the built-in collectors.
|
|
type cachedCollector struct {
|
|
labels labelcache.Cache
|
|
metrics metriccache.Cache[string, float64]
|
|
|
|
builds atomic.Int32
|
|
counter *prometheus.Desc
|
|
state *prometheus.Desc
|
|
value float64
|
|
}
|
|
|
|
func (c *cachedCollector) GetName() string { return "cached" }
|
|
|
|
func (c *cachedCollector) ScrapeCaches() []scrapecache.Cache {
|
|
return []scrapecache.Cache{&c.labels, &c.metrics}
|
|
}
|
|
|
|
func (c *cachedCollector) Build(*slog.Logger, *mi.Session) error {
|
|
c.counter = prometheus.NewDesc("windows_test_counter_total", "Test counter", []string{"name", "id"}, nil)
|
|
c.state = prometheus.NewDesc("windows_test_state", "Test state", []string{"name", "state"}, nil)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cachedCollector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
|
ch <- c.labels.Scrape().MustNewConstMetric(c.counter, prometheus.CounterValue, c.value, "a", "1")
|
|
|
|
for _, metric := range c.metrics.Scrape().Get("a", c.value, func() []prometheus.Metric {
|
|
c.builds.Add(1)
|
|
|
|
return []prometheus.Metric{
|
|
prometheus.MustNewConstMetric(c.state, prometheus.GaugeValue, 1, "a", "running"),
|
|
prometheus.MustNewConstMetric(c.state, prometheus.GaugeValue, c.value, "a", "value"),
|
|
}
|
|
}) {
|
|
ch <- metric
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *cachedCollector) Close() error { return nil }
|
|
|
|
// TestScrapeCacheReuse verifies that the collection's scrapes reuse the cached
|
|
// metrics and that they publish the same series and values as a Collect call
|
|
// without the caches.
|
|
func TestScrapeCacheReuse(t *testing.T) {
|
|
cached := &cachedCollector{value: 1}
|
|
collection := collector.New(collector.Map{"cached": cached})
|
|
|
|
require.NoError(t, collection.Build(context.Background(), slog.New(slog.DiscardHandler)))
|
|
t.Cleanup(func() { require.NoError(t, collection.Close()) })
|
|
|
|
handler, err := collection.NewHandler(time.Minute, slog.New(slog.DiscardHandler), nil)
|
|
require.NoError(t, err)
|
|
|
|
uncached := func() string {
|
|
direct := &cachedCollector{value: cached.value}
|
|
require.NoError(t, direct.Build(nil, nil))
|
|
|
|
out, err := gatherCollector(direct)
|
|
require.NoError(t, err)
|
|
|
|
return out
|
|
}
|
|
|
|
for i, value := range []float64{1, 1, 2, 2} {
|
|
cached.value = value
|
|
|
|
out := scrape(t, handler)
|
|
want := uncached()
|
|
|
|
for line := range strings.Lines(want) {
|
|
if strings.HasPrefix(line, "windows_test_") {
|
|
require.Contains(t, out, line, "scrape %d", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
require.Equal(t, int32(2), cached.builds.Load(), "metrics are built once per value")
|
|
|
|
// A new collection that builds the same collector again, as Grafana Alloy
|
|
// does, doesn't publish metrics built from the old descriptors.
|
|
require.NoError(t, collection.Close())
|
|
|
|
collection = collector.New(collector.Map{"cached": cached})
|
|
require.NoError(t, collection.Build(context.Background(), slog.New(slog.DiscardHandler)))
|
|
|
|
handler, err = collection.NewHandler(time.Minute, slog.New(slog.DiscardHandler), nil)
|
|
require.NoError(t, err)
|
|
|
|
scrape(t, handler)
|
|
require.Equal(t, int32(3), cached.builds.Load())
|
|
}
|
|
|
|
// gatherCollector calls Collect of c directly and returns its metrics in the text format.
|
|
func gatherCollector(c collector.Collector) (string, error) {
|
|
metrics := make(chan prometheus.Metric, 16)
|
|
if err := c.Collect(metrics, time.Second); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
close(metrics)
|
|
|
|
registry := prometheus.NewPedanticRegistry()
|
|
if err := registry.Register(collectedMetrics(metrics)); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
families, err := registry.Gather()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var out strings.Builder
|
|
|
|
for _, family := range families {
|
|
if _, err := expfmt.MetricFamilyToText(&out, family); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return out.String(), nil
|
|
}
|
|
|
|
// collectedMetrics is a prometheus.Collector of already collected metrics.
|
|
type collectedMetrics <-chan prometheus.Metric
|
|
|
|
func (m collectedMetrics) Describe(chan<- *prometheus.Desc) {}
|
|
|
|
func (m collectedMetrics) Collect(ch chan<- prometheus.Metric) {
|
|
for metric := range m {
|
|
ch <- metric
|
|
}
|
|
}
|