mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
259 lines
7.7 KiB
Go
259 lines
7.7 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 contains tests that apply to every collector.
|
|
package collector_test
|
|
|
|
import (
|
|
"log/slog"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/dfsr"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/dns"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/exchange"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/hyperv"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/logical_disk"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/mscluster"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/mssql"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/net"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/netframework"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/tcp"
|
|
"github.com/prometheus-community/windows_exporter/internal/collector/update"
|
|
"github.com/prometheus-community/windows_exporter/internal/mi"
|
|
"github.com/prometheus-community/windows_exporter/pkg/collector"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newMISession(t *testing.T) *mi.Session {
|
|
t.Helper()
|
|
|
|
app, err := mi.ApplicationInitialize()
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { require.NoError(t, app.Close()) })
|
|
|
|
session, err := app.NewSession(nil)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() { require.NoError(t, session.Close()) })
|
|
|
|
return session
|
|
}
|
|
|
|
// TestBuildCloseCycle builds, collects and closes every collector repeatedly and
|
|
// fails if goroutines grow with every cycle. Grafana Alloy embeds the collectors
|
|
// and rebuilds them on every configuration reload, so a leak per cycle grows
|
|
// without bound there. Collectors whose Build fails on this host, e.g. because a
|
|
// role is not installed, are closed after the failed Build, which must release
|
|
// everything a partial Build allocated. The test does not run in parallel,
|
|
// because goroutine counts are process-wide.
|
|
func TestBuildCloseCycle(t *testing.T) {
|
|
const cycles = 5
|
|
|
|
logger := slog.New(slog.DiscardHandler)
|
|
miSession := newMISession(t)
|
|
|
|
for _, name := range collector.Available() {
|
|
t.Run(name, func(t *testing.T) {
|
|
if name == update.Name {
|
|
t.Skip("the update worker finishes a running Windows Update search before it exits")
|
|
}
|
|
|
|
cycle := func() {
|
|
app := kingpin.New("windows_exporter", "")
|
|
c := collector.BuildersWithFlags[name](app)
|
|
|
|
_, err := app.Parse(nil)
|
|
require.NoError(t, err)
|
|
|
|
if err := c.Build(logger, miSession); err == nil {
|
|
collect(t, c)
|
|
}
|
|
|
|
require.NoError(t, c.Close())
|
|
}
|
|
|
|
// The first cycle loads DLLs and may start goroutines that live as long as the process.
|
|
cycle()
|
|
|
|
before := runtime.NumGoroutine()
|
|
|
|
for range cycles {
|
|
cycle()
|
|
}
|
|
|
|
// Closed PDH collectors stop their worker goroutines asynchronously.
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
|
|
for runtime.NumGoroutine()-before >= cycles && time.Now().Before(deadline) {
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
require.Less(t, runtime.NumGoroutine()-before, cycles,
|
|
"goroutines grew with every Build/Close cycle of collector %s", name,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func collect(t *testing.T, c collector.Collector) {
|
|
t.Helper()
|
|
|
|
ch := make(chan prometheus.Metric)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Go(func() {
|
|
for range ch {
|
|
}
|
|
})
|
|
|
|
// Errors are expected for collectors without data on this host.
|
|
_ = c.Collect(ch, 10*time.Second)
|
|
|
|
close(ch)
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestBuildRejectsUnknownSubCollector(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := slog.New(slog.DiscardHandler)
|
|
miSession := newMISession(t)
|
|
unknown := []string{"does_not_exist"}
|
|
|
|
for _, c := range []collector.Collector{
|
|
dfsr.New(&dfsr.Config{CollectorsEnabled: unknown}),
|
|
dhcp.New(&dhcp.Config{CollectorsEnabled: unknown}),
|
|
dns.New(&dns.Config{CollectorsEnabled: unknown}),
|
|
exchange.New(&exchange.Config{CollectorsEnabled: unknown}),
|
|
hyperv.New(&hyperv.Config{CollectorsEnabled: unknown}),
|
|
logical_disk.New(&logical_disk.Config{CollectorsEnabled: unknown}),
|
|
mscluster.New(&mscluster.Config{CollectorsEnabled: unknown}),
|
|
mssql.New(&mssql.Config{CollectorsEnabled: unknown}),
|
|
net.New(&net.Config{CollectorsEnabled: unknown}),
|
|
netframework.New(&netframework.Config{CollectorsEnabled: unknown}),
|
|
storage_spaces.New(&storage_spaces.Config{CollectorsEnabled: unknown}),
|
|
tcp.New(&tcp.Config{CollectorsEnabled: unknown}),
|
|
} {
|
|
t.Run(c.GetName(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
err := c.Build(logger, miSession)
|
|
require.ErrorContains(t, err, "unknown sub collector: does_not_exist. Possible values: ")
|
|
require.NoError(t, c.Close())
|
|
})
|
|
}
|
|
}
|
|
|
|
// Build must not sort the CollectorsEnabled slice of the caller in place.
|
|
// It may be the slice of the exported ConfigDefaults.
|
|
func TestBuildDoesNotModifyCollectorsEnabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := slog.New(slog.DiscardHandler)
|
|
miSession := newMISession(t)
|
|
|
|
for _, tc := range []struct {
|
|
enabled []string
|
|
new func(enabled []string) collector.Collector
|
|
}{
|
|
{
|
|
enabled: []string{"Autodiscover", "ActiveSync"},
|
|
new: func(enabled []string) collector.Collector {
|
|
return exchange.New(&exchange.Config{CollectorsEnabled: enabled})
|
|
},
|
|
},
|
|
{
|
|
enabled: []string{"host", "datastore"},
|
|
new: func(enabled []string) collector.Collector {
|
|
return hyperv.New(&hyperv.Config{CollectorsEnabled: enabled})
|
|
},
|
|
},
|
|
{
|
|
enabled: []string{"locks", "info"},
|
|
new: func(enabled []string) collector.Collector {
|
|
return mssql.New(&mssql.Config{CollectorsEnabled: enabled})
|
|
},
|
|
},
|
|
{
|
|
enabled: []string{"clrmemory", "clrexceptions"},
|
|
new: func(enabled []string) collector.Collector {
|
|
return netframework.New(&netframework.Config{CollectorsEnabled: enabled})
|
|
},
|
|
},
|
|
} {
|
|
c := tc.new(tc.enabled)
|
|
|
|
t.Run(c.GetName(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
want := slices.Clone(tc.enabled)
|
|
|
|
// Build errors are expected if the role is not installed.
|
|
_ = c.Build(logger, miSession)
|
|
|
|
require.NoError(t, c.Close())
|
|
require.Equal(t, want, tc.enabled)
|
|
})
|
|
}
|
|
}
|
|
|
|
// New must apply the default sub collectors if CollectorsEnabled is nil,
|
|
// otherwise library users that don't set it get no metrics.
|
|
func TestNewDefaultsCollectorsEnabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := logical_disk.New(&logical_disk.Config{})
|
|
|
|
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
|
|
|
|
t.Cleanup(func() { require.NoError(t, c.Close()) })
|
|
|
|
ch := make(chan prometheus.Metric)
|
|
|
|
var (
|
|
wg sync.WaitGroup
|
|
descs []string
|
|
)
|
|
|
|
wg.Go(func() {
|
|
for metric := range ch {
|
|
descs = append(descs, metric.Desc().String())
|
|
}
|
|
})
|
|
|
|
err := c.Collect(ch, 10*time.Second)
|
|
|
|
close(ch)
|
|
wg.Wait()
|
|
|
|
require.NoError(t, err)
|
|
require.True(t, slices.ContainsFunc(descs, func(desc string) bool {
|
|
return strings.Contains(desc, `fqName: "windows_logical_disk_read_bytes_total"`)
|
|
}), "metrics sub collector is not enabled by default")
|
|
}
|