mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
ci: expand Windows feature fixtures and test Server 2022 (#2503)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
72e10af92d
commit
56137ffdc1
@@ -32,4 +32,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
func TestCollector(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, hyperv.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_dynamic_memory_vm_physical_bytes", prometheus.Labels{"vm": "GitHubActions"})
|
||||
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_vm_processor_count", prometheus.Labels{"vm": "GitHubActions"})
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ const (
|
||||
Name = "logical_disk"
|
||||
subCollectorMetrics = "metrics"
|
||||
subCollectorBitlocker = "bitlocker_status"
|
||||
|
||||
bitlockerStatusLocked = 6
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@@ -514,46 +516,86 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
|
||||
}
|
||||
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
|
||||
c.bitlockerReqCh <- data.Name
|
||||
c.collectBitlocker(ch, data.Name, false)
|
||||
}
|
||||
}
|
||||
|
||||
bitlockerStatus := <-c.bitlockerResCh
|
||||
|
||||
if bitlockerStatus.err != nil {
|
||||
c.logger.Warn("failed to get BitLocker status for "+data.Name,
|
||||
slog.Any("err", bitlockerStatus.err),
|
||||
)
|
||||
if slices.Contains(c.config.CollectorsEnabled, subCollectorBitlocker) {
|
||||
// Locked BitLocker volumes have no LogicalDisk performance instance.
|
||||
seen := make(map[string]struct{}, len(c.perfDataObject))
|
||||
for _, data := range c.perfDataObject {
|
||||
seen[strings.ToUpper(data.Name)] = struct{}{}
|
||||
}
|
||||
|
||||
for mountPoint := range volumes {
|
||||
if len(mountPoint) != 2 || mountPoint[1] != ':' {
|
||||
continue
|
||||
}
|
||||
|
||||
if bitlockerStatus.status == -1 {
|
||||
c.logger.Debug("BitLocker status for "+data.Name+" is unknown",
|
||||
slog.Int("status", bitlockerStatus.status),
|
||||
)
|
||||
|
||||
if _, ok := seen[strings.ToUpper(mountPoint)]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} {
|
||||
val := 0.0
|
||||
if bitlockerStatus.status == i {
|
||||
val = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bitlockerStatus,
|
||||
prometheus.GaugeValue,
|
||||
val,
|
||||
data.Name,
|
||||
status,
|
||||
)
|
||||
if c.config.VolumeExclude.MatchString(mountPoint) || !c.config.VolumeInclude.MatchString(mountPoint) {
|
||||
continue
|
||||
}
|
||||
|
||||
c.collectBitlocker(ch, mountPoint, true)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectBitlocker emits the BitLocker status of a volume. With lockedOnly,
|
||||
// volumes in any other state are skipped.
|
||||
func (c *Collector) collectBitlocker(ch chan<- prometheus.Metric, volume string, lockedOnly bool) {
|
||||
c.bitlockerReqCh <- volume
|
||||
|
||||
bitlockerStatus := <-c.bitlockerResCh
|
||||
|
||||
if bitlockerStatus.err != nil {
|
||||
level := slog.LevelWarn
|
||||
if lockedOnly {
|
||||
// Drive letters without performance data include empty media drives.
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
|
||||
c.logger.Log(context.Background(), level, "failed to get BitLocker status for "+volume,
|
||||
slog.Any("err", bitlockerStatus.err),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if bitlockerStatus.status == -1 {
|
||||
c.logger.Debug("BitLocker status for "+volume+" is unknown",
|
||||
slog.Int("status", bitlockerStatus.status),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if lockedOnly && bitlockerStatus.status != bitlockerStatusLocked {
|
||||
return
|
||||
}
|
||||
|
||||
for i, status := range []string{"disabled", "on", "off", "encrypting", "decrypting", "suspended", "locked", "unknown", "waiting_for_activation"} {
|
||||
val := 0.0
|
||||
if bitlockerStatus.status == i {
|
||||
val = 1.0
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.bitlockerStatus,
|
||||
prometheus.GaugeValue,
|
||||
val,
|
||||
volume,
|
||||
status,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getDriveType(driveType uint32) string {
|
||||
switch driveType {
|
||||
case windows.DRIVE_UNKNOWN:
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/logical_disk"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -45,6 +46,31 @@ func TestCollector(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectorBitlocker(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{
|
||||
CollectorsEnabled: []string{"metrics", "bitlocker_status"},
|
||||
VolumeInclude: types.RegExpAny,
|
||||
VolumeExclude: types.RegExpEmpty,
|
||||
})
|
||||
|
||||
for env, want := range map[string]string{
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_VOLUME": "on",
|
||||
"WINDOWS_EXPORTER_TEST_BITLOCKER_LOCKED_VOLUME": "locked",
|
||||
} {
|
||||
volume := os.Getenv(env)
|
||||
if volume == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
status := testutils.RequireFixtureMetric(t, metrics, logical_disk.Name, "windows_logical_disk_bitlocker_status", prometheus.Labels{
|
||||
"volume": volume,
|
||||
"status": want,
|
||||
})
|
||||
require.NotNil(t, status)
|
||||
require.InDelta(t, 1, status.GetGauge().GetValue(), 0, "BitLocker fixture volume %s is not reported as %s", volume, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectorVolumeFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -32,4 +32,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
func TestCollector(t *testing.T) {
|
||||
metrics := testutils.TestCollector(t, mscluster.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_virtualdisk_size_bytes", prometheus.Labels{"name": "CIVirtualDisk"})
|
||||
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_storagepool_size_bytes", prometheus.Labels{"name": "GitHubActions"})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/mssql"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,7 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, mssql.New, nil)
|
||||
metrics := testutils.TestCollector(t, mssql.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, mssql.Name, "windows_mssql_instance_info", prometheus.Labels{"mssql_instance": "CISQL"})
|
||||
testutils.RequireFixtureMetric(t, metrics, mssql.Name, "windows_mssql_databases_data_files_size_bytes", prometheus.Labels{"mssql_instance": "CISQL", "database": "CIWindowsExporter"})
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@
|
||||
package remote_fx_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/remote_fx"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +31,8 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, remote_fx.New, nil)
|
||||
metrics := testutils.TestCollector(t, remote_fx.New, nil)
|
||||
labels := prometheus.Labels{"session_name": os.Getenv("WINDOWS_EXPORTER_TEST_RDP_SESSION")}
|
||||
testutils.RequireFixtureMetric(t, metrics, remote_fx.Name, "windows_remote_fx_net_sent_bytes_total", labels)
|
||||
testutils.RequireFixtureMetric(t, metrics, remote_fx.Name, "windows_remote_fx_gfx_output_frames_total", labels)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,14 @@
|
||||
package smtp_test
|
||||
|
||||
import (
|
||||
"net"
|
||||
netsmtp "net/smtp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/smtp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +33,16 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, smtp.New, nil)
|
||||
metrics := testutils.TestCollector(t, smtp.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, smtp.Name, "windows_smtp_inbound_connections_total", nil)
|
||||
|
||||
// Exercise the configured virtual server, including its SMTP greeting.
|
||||
dialer := net.Dialer{Timeout: 5 * time.Second}
|
||||
conn, err := dialer.DialContext(t.Context(), "tcp", "127.0.0.1:25")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
require.NoError(t, conn.SetDeadline(time.Now().Add(5*time.Second)))
|
||||
client, err := netsmtp.NewClient(conn, "localhost")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, client.Quit())
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
package terminal_services_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/terminal_services"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +32,23 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, terminal_services.New, nil)
|
||||
metrics := testutils.TestCollector(t, terminal_services.New, nil)
|
||||
|
||||
userName := os.Getenv("WINDOWS_EXPORTER_TEST_RDP_USER")
|
||||
if userName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
sessionName := os.Getenv("WINDOWS_EXPORTER_TEST_RDP_SESSION")
|
||||
require.NotEmpty(t, sessionName, "RDP fixture did not reach an authenticated active session")
|
||||
info := testutils.RequireFixtureMetric(t, metrics, terminal_services.Name, "windows_terminal_services_session_info", prometheus.Labels{
|
||||
"user": os.Getenv("COMPUTERNAME") + `\` + userName,
|
||||
"session_name": sessionName,
|
||||
"state": "active",
|
||||
})
|
||||
require.NotNil(t, info)
|
||||
require.InDelta(t, 1, info.GetGauge().GetValue(), 0, "RDP fixture is no longer active")
|
||||
handles := testutils.RequireFixtureMetric(t, metrics, terminal_services.Name, "windows_terminal_services_handles", prometheus.Labels{"session_name": sessionName})
|
||||
require.NotNil(t, handles)
|
||||
require.Positive(t, handles.GetGauge().GetValue(), "RDP fixture has no session processes")
|
||||
}
|
||||
|
||||
+27
-29
@@ -250,6 +250,14 @@ func Test_MI_Query_Unmarshal(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_MI_FD_Leak(t *testing.T) {
|
||||
// A leak of one handle per query grows far beyond this; MI and the Go
|
||||
// runtime may still open a few handles for worker threads.
|
||||
const (
|
||||
warmupQueries = 20
|
||||
queries = 300
|
||||
maxHandleIncrease = 50
|
||||
)
|
||||
|
||||
application, err := mi.ApplicationInitialize()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, application)
|
||||
@@ -258,46 +266,34 @@ func Test_MI_FD_Leak(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, session)
|
||||
|
||||
currentFileHandle, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
|
||||
queryPrinter, err := mi.NewQuery("SELECT Name FROM Win32_Process")
|
||||
require.NoError(t, err)
|
||||
|
||||
for range 300 {
|
||||
query := func() {
|
||||
var processes []win32Process
|
||||
|
||||
err := session.Query(&processes, mi.NamespaceRootCIMv2, queryPrinter, -1)
|
||||
require.NoError(t, err)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
require.NoError(t, session.Query(&processes, mi.NamespaceRootCIMv2, queryPrinter, -1))
|
||||
}
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
// The first queries initialize MI caches and thread pools.
|
||||
for range warmupQueries {
|
||||
query()
|
||||
}
|
||||
|
||||
baseline, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
for range queries {
|
||||
query()
|
||||
}
|
||||
|
||||
err = session.Close()
|
||||
current, err := testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
require.LessOrEqual(t, int64(current), int64(baseline)+maxHandleIncrease,
|
||||
"handle count grew from %d to %d after %d queries", baseline, current, queries)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
|
||||
err = application.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
currentFileHandle, err = testutils.GetProcessHandleCount(windows.CurrentProcess())
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Log("Current File Handle Count: ", currentFileHandle)
|
||||
require.NoError(t, session.Close())
|
||||
require.NoError(t, application.Close())
|
||||
}
|
||||
|
||||
func Test_MI_QueryTimeout(t *testing.T) {
|
||||
@@ -326,7 +322,9 @@ func Test_MI_QueryTimeout(t *testing.T) {
|
||||
err = operationOptions.SetTimeout(1 * time.Millisecond)
|
||||
require.NoError(t, err)
|
||||
|
||||
operation, err := session.QueryInstances(mi.OperationFlagsStandardRTTI, operationOptions, mi.NamespaceRootCIMv2, mi.QueryDialectWQL, "select Name from win32_process where handle = 0")
|
||||
// A query for a single process can finish within the timeout; reading every
|
||||
// property of every process cannot.
|
||||
operation, err := session.QueryInstances(mi.OperationFlagsStandardRTTI, operationOptions, mi.NamespaceRootCIMv2, mi.QueryDialectWQL, "select * from win32_process")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, operation)
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co
|
||||
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) map[string]*dto.MetricFamily {
|
||||
t.Helper()
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn}))
|
||||
c := fn(conf)
|
||||
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), c.GetName())
|
||||
|
||||
@@ -209,11 +209,11 @@ func (m collectedMetrics) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
// RequireFixtureMetric checks a known CI fixture without requiring it on a
|
||||
// developer's machine. Labels match exactly, ignoring case for Windows names.
|
||||
func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, collectorName, metricName string, labels prometheus.Labels) {
|
||||
func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, collectorName, metricName string, labels prometheus.Labels) *dto.Metric {
|
||||
t.Helper()
|
||||
|
||||
if !slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), collectorName) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
require.Contains(t, families, metricName)
|
||||
@@ -236,11 +236,13 @@ func RequireFixtureMetric(t *testing.T, families map[string]*dto.MetricFamily, c
|
||||
}
|
||||
|
||||
if matched {
|
||||
return
|
||||
return metric
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("metric %s with fixture labels %v was not emitted; got %s", metricName, labels, families[metricName])
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func noDataOnly(err error) bool {
|
||||
|
||||
Reference in New Issue
Block a user