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:
Jan-Otto Kröpke
2026-10-07 09:15:58 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 72e10af92d
commit 56137ffdc1
16 changed files with 862 additions and 356 deletions
+1
View File
@@ -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"})
}
+67 -25
View File
@@ -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"})
}
+4 -1
View File
@@ -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)
}
+16 -1
View File
@@ -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
View File
@@ -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)
+6 -4
View File
@@ -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 {