Files
windows_exporter/internal/collector/lifecycle_test.go
T

257 lines
7.6 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/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}),
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")
}