mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
430 lines
13 KiB
Go
430 lines
13 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.
|
|
|
|
package labelcache_test
|
|
|
|
import (
|
|
"errors"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/prometheus-community/windows_exporter/internal/labelcache"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
dto "github.com/prometheus/client_model/go"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
//nolint:gochecknoglobals
|
|
var (
|
|
counterDesc = prometheus.NewDesc("test_cpu_seconds_total", "test", []string{"process", "process_id", "mode"}, prometheus.Labels{"const": "x"})
|
|
gaugeDesc = prometheus.NewDesc("test_handles", "test", []string{"process", "process_id"}, nil)
|
|
untypedDesc = prometheus.NewDesc("test_untyped", "test", nil, nil)
|
|
wideDesc = prometheus.NewDesc("test_info", "test", []string{"a", "b", "c", "d", "e", "f", "g"}, nil)
|
|
)
|
|
|
|
func write(t *testing.T, metric prometheus.Metric) *dto.Metric {
|
|
t.Helper()
|
|
|
|
var out dto.Metric
|
|
|
|
require.NoError(t, metric.Write(&out))
|
|
|
|
return &out
|
|
}
|
|
|
|
// Cached metrics must write exactly what prometheus.MustNewConstMetric writes,
|
|
// in the first scrape (new series) and the following ones (shared labels).
|
|
func TestMustNewConstMetric(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
cases := []struct {
|
|
desc *prometheus.Desc
|
|
valueType prometheus.ValueType
|
|
values []string
|
|
}{
|
|
{counterDesc, prometheus.CounterValue, []string{"svchost", "1234", "user"}},
|
|
{counterDesc, prometheus.CounterValue, []string{"svchost", "1234", "privileged"}},
|
|
{gaugeDesc, prometheus.GaugeValue, []string{"svchost", "1234"}},
|
|
{untypedDesc, prometheus.UntypedValue, nil},
|
|
{wideDesc, prometheus.GaugeValue, []string{"1", "2", "3", "4", "5", "6", "7"}},
|
|
}
|
|
|
|
for scrapeNumber := range 3 {
|
|
scrape := cache.Begin()
|
|
|
|
for i, tc := range cases {
|
|
value := float64(scrapeNumber*10 + i)
|
|
got := scrape.MustNewConstMetric(tc.desc, tc.valueType, value, tc.values...)
|
|
want := prometheus.MustNewConstMetric(tc.desc, tc.valueType, value, tc.values...)
|
|
|
|
require.Equal(t, want.Desc(), got.Desc())
|
|
require.True(t, proto.Equal(write(t, want), write(t, got)), "scrape %d, case %d:\nwant %v\ngot %v", scrapeNumber, i, write(t, want), write(t, got))
|
|
}
|
|
|
|
scrape.Commit()
|
|
}
|
|
}
|
|
|
|
// From the second scrape on, a series reuses the label pairs of the previous
|
|
// one; series that a scrape didn't report get new pairs.
|
|
func TestLabelReuse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
labelsOf := func(scrape *labelcache.Scrape, pid string) []*dto.LabelPair {
|
|
return write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", pid)).GetLabel()
|
|
}
|
|
|
|
scrape := cache.Begin()
|
|
first := labelsOf(scrape, "1")
|
|
labelsOf(scrape, "2")
|
|
scrape.Commit()
|
|
|
|
scrape = cache.Begin()
|
|
require.Same(t, first[0], labelsOf(scrape, "1")[0], "label pairs reused")
|
|
scrape.Commit()
|
|
|
|
// Process 2 wasn't reported in the second scrape, so its pairs were dropped.
|
|
scrape = cache.Begin()
|
|
second := labelsOf(scrape, "2")
|
|
require.Equal(t, "2", second[1].GetValue())
|
|
scrape.Commit()
|
|
|
|
scrape = cache.Begin()
|
|
require.Same(t, second[0], labelsOf(scrape, "2")[0])
|
|
scrape.Commit()
|
|
|
|
cache.Reset()
|
|
|
|
scrape = cache.Begin()
|
|
require.NotSame(t, second[0], labelsOf(scrape, "2")[0], "reset drops the cache")
|
|
scrape.Commit()
|
|
}
|
|
|
|
// A scrape begun before Reset must not restore its generation afterwards.
|
|
func TestResetDuringScrape(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
scrape := cache.Begin()
|
|
pairs := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "1")).GetLabel()
|
|
|
|
cache.Reset()
|
|
scrape.Commit()
|
|
|
|
scrape = cache.Begin()
|
|
require.NotSame(t, pairs[0], write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "1")).GetLabel()[0])
|
|
scrape.Commit()
|
|
scrape.Commit()
|
|
}
|
|
|
|
func TestInvalidLabelValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
scrape := cache.Begin()
|
|
|
|
require.Panics(t, func() { scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "only one") })
|
|
require.Panics(t, func() { scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "\xff") })
|
|
}
|
|
|
|
func TestConcurrentScrapes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
cache labelcache.Cache
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
for range 8 {
|
|
wg.Go(func() {
|
|
for range 20 {
|
|
scrape := cache.Begin()
|
|
|
|
var inner sync.WaitGroup
|
|
|
|
for p := range 4 {
|
|
inner.Go(func() {
|
|
for pid := range 50 {
|
|
m := scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "p"+strconv.Itoa(p), strconv.Itoa(pid), "user")
|
|
if err := m.Write(&dto.Metric{}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
inner.Wait()
|
|
scrape.Commit()
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func BenchmarkMustNewConstMetric(b *testing.B) {
|
|
pids := make([]string, 500)
|
|
for i := range pids {
|
|
pids[i] = strconv.Itoa(1000 + i)
|
|
}
|
|
|
|
b.Run("prometheus", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
for _, pid := range pids {
|
|
_ = prometheus.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "svchost", pid, "user")
|
|
}
|
|
}
|
|
})
|
|
|
|
b.Run("labelcache", func(b *testing.B) {
|
|
var cache labelcache.Cache
|
|
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
scrape := cache.Begin()
|
|
for _, pid := range pids {
|
|
_ = scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "svchost", pid, "user")
|
|
}
|
|
|
|
scrape.Commit()
|
|
}
|
|
})
|
|
}
|
|
|
|
//nolint:gochecknoglobals
|
|
var (
|
|
processInfoDesc = prometheus.NewDesc("test_process_info", "test", []string{"process", "process_id", "creating_process_id", "process_group_id", "owner", "cmdline"}, nil)
|
|
processDescs = func() []*prometheus.Desc {
|
|
descs := make([]*prometheus.Desc, 10)
|
|
for i := range descs {
|
|
descs[i] = prometheus.NewDesc("test_process_gauge_"+strconv.Itoa(i), "test", []string{"process", "process_id"}, nil)
|
|
}
|
|
|
|
return descs
|
|
}()
|
|
)
|
|
|
|
// benchmarkProcesses is the number of processes of the benchmarks, with 14
|
|
// series each.
|
|
const benchmarkProcesses = 1000
|
|
|
|
// scrapeProcesses emits the series of processes like the process collector:
|
|
// one info series, three counters by mode and ten gauges per process.
|
|
func scrapeProcesses(cache *labelcache.Cache) {
|
|
const processes = benchmarkProcesses
|
|
|
|
scrape := cache.Begin()
|
|
|
|
for i := range processes {
|
|
name := "process" + strconv.Itoa(i%50)
|
|
pid := strconv.Itoa(1000 + i)
|
|
|
|
_ = scrape.MustNewConstMetric(processInfoDesc, prometheus.GaugeValue, 1, name, pid, "4", pid, `NT AUTHORITY\SYSTEM`, `C:\Windows\system32\svchost.exe -k netsvcs`)
|
|
|
|
for _, mode := range []string{"user", "privileged", "other"} {
|
|
_ = scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, name, pid, mode)
|
|
}
|
|
|
|
for _, desc := range processDescs {
|
|
_ = scrape.MustNewConstMetric(desc, prometheus.GaugeValue, 1, name, pid)
|
|
}
|
|
}
|
|
|
|
scrape.Commit()
|
|
}
|
|
|
|
// BenchmarkRetained reports the heap a cache keeps between scrapes for 1000
|
|
// processes with 14 series each.
|
|
func BenchmarkRetained(b *testing.B) {
|
|
const processes = benchmarkProcesses
|
|
|
|
var stats runtime.MemStats
|
|
|
|
for b.Loop() {
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&stats)
|
|
before := stats.HeapAlloc
|
|
|
|
cache := new(labelcache.Cache)
|
|
scrapeProcesses(cache)
|
|
scrapeProcesses(cache)
|
|
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&stats)
|
|
b.ReportMetric(float64(int64(stats.HeapAlloc)-int64(before))/(processes*14), "retained-B/series")
|
|
runtime.KeepAlive(cache)
|
|
}
|
|
}
|
|
|
|
// BenchmarkScrape measures one steady-state scrape of 1000 processes.
|
|
func BenchmarkScrape(b *testing.B) {
|
|
var cache labelcache.Cache
|
|
|
|
scrapeProcesses(&cache)
|
|
b.ReportAllocs()
|
|
|
|
for b.Loop() {
|
|
scrapeProcesses(&cache)
|
|
}
|
|
}
|
|
|
|
// All series with the same label name and value share one pair. A pair stays
|
|
// while any cached series uses it.
|
|
func TestPairsShared(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
scrape := cache.Begin()
|
|
cpu := write(t, scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "svchost", "1234", "user")).GetLabel()
|
|
handles := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "svchost", "1234")).GetLabel()
|
|
other := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "svchost", "5678")).GetLabel()
|
|
scrape.Commit()
|
|
|
|
// counterDesc has the const label "const", so its pairs are const, mode,
|
|
// process, process_id; gaugeDesc's are process, process_id.
|
|
require.Same(t, cpu[2], handles[0], "process pair shared")
|
|
require.Same(t, cpu[3], handles[1], "process_id pair shared")
|
|
require.Same(t, handles[0], other[0], "process pair shared across processes")
|
|
require.NotSame(t, handles[1], other[1])
|
|
|
|
// Only the gauge of process 1234 is reported: its pairs stay.
|
|
scrape = cache.Begin()
|
|
again := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 2, "svchost", "1234")).GetLabel()
|
|
scrape.Commit()
|
|
|
|
require.Same(t, handles[1], again[1])
|
|
|
|
// Process 5678 disappeared, so its process_id pair was dropped with it.
|
|
scrape = cache.Begin()
|
|
back := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "svchost", "5678")).GetLabel()
|
|
scrape.Commit()
|
|
|
|
require.NotSame(t, other[1], back[1])
|
|
require.Equal(t, other[1].GetValue(), back[1].GetValue())
|
|
}
|
|
|
|
// A call with too few label values must still panic when a series with
|
|
// empty trailing values is cached.
|
|
func TestShortKeyCardinality(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
scrape := cache.Begin()
|
|
_ = scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "a", "b", "")
|
|
|
|
require.Panics(t, func() { scrape.MustNewConstMetric(counterDesc, prometheus.CounterValue, 1, "a", "b") })
|
|
scrape.Commit()
|
|
}
|
|
|
|
// After Commit, a scrape no longer adds series to the cache.
|
|
func TestCommittedScrape(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var cache labelcache.Cache
|
|
|
|
scrape := cache.Begin()
|
|
first := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "1")).GetLabel()
|
|
scrape.Commit()
|
|
|
|
late := write(t, scrape.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "2")).GetLabel()
|
|
|
|
next := cache.Begin()
|
|
require.Same(t, first[0], write(t, next.MustNewConstMetric(gaugeDesc, prometheus.GaugeValue, 1, "p", "1")).GetLabel()[0])
|
|
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)
|
|
}
|