mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-06 20:59:04 +02:00
chore: consolidate CI and expand Windows test coverage (#2498)
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
@@ -85,6 +85,19 @@ func (c *Collector) Close() error {
|
||||
}
|
||||
|
||||
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
c.buildDescriptors()
|
||||
|
||||
var err error
|
||||
|
||||
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Certification Authority", pdh.InstancesAll)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Certification Authority collector: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) buildDescriptors() {
|
||||
c.requestsPerSecond = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "requests_total"),
|
||||
"Total certificate requests processed",
|
||||
@@ -163,15 +176,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
[]string{"cert_template"},
|
||||
nil,
|
||||
)
|
||||
|
||||
var err error
|
||||
|
||||
c.perfDataCollector, err = pdh.NewCollector[perfDataCounterValues](logger.With(slog.String("collector", Name)), pdh.CounterTypeRaw, "Certification Authority", pdh.InstancesAll)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Certification Authority collector: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
||||
@@ -180,7 +184,13 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
|
||||
return fmt.Errorf("failed to collect Certification Authority (ADCS) metrics: %w", err)
|
||||
}
|
||||
|
||||
for _, data := range c.perfDataObject {
|
||||
c.collectMetrics(ch, c.perfDataObject)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Collector) collectMetrics(ch chan<- prometheus.Metric, rows []perfDataCounterValues) {
|
||||
for _, data := range rows {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.requestsPerSecond,
|
||||
prometheus.CounterValue,
|
||||
@@ -272,6 +282,4 @@ func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error
|
||||
data.Name,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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 adcs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCollectMetrics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
values := perfDataCounterValues{
|
||||
Name: "WebServer",
|
||||
RequestsPerSecond: 17,
|
||||
RequestProcessingTime: 1500,
|
||||
RetrievalsPerSecond: 19,
|
||||
RetrievalProcessingTime: 2250,
|
||||
FailedRequestsPerSecond: 3,
|
||||
IssuedRequestsPerSecond: 11,
|
||||
PendingRequestsPerSecond: 2,
|
||||
RequestCryptographicSigningTime: 500,
|
||||
RequestPolicyModuleProcessingTime: 750,
|
||||
ChallengeResponsesPerSecond: 13,
|
||||
ChallengeResponseProcessingTime: 1250,
|
||||
SignedCertificateTimestampListsPerSecond: 23,
|
||||
SignedCertificateTimestampListProcessingTime: 1750,
|
||||
}
|
||||
expected := map[string]struct {
|
||||
metricType dto.MetricType
|
||||
value float64
|
||||
}{
|
||||
"windows_adcs_requests_total": {metricType: dto.MetricType_COUNTER, value: 17},
|
||||
"windows_adcs_request_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.5},
|
||||
"windows_adcs_retrievals_total": {metricType: dto.MetricType_COUNTER, value: 19},
|
||||
"windows_adcs_retrievals_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 2.25},
|
||||
"windows_adcs_failed_requests_total": {metricType: dto.MetricType_COUNTER, value: 3},
|
||||
"windows_adcs_issued_requests_total": {metricType: dto.MetricType_COUNTER, value: 11},
|
||||
"windows_adcs_pending_requests_total": {metricType: dto.MetricType_COUNTER, value: 2},
|
||||
"windows_adcs_request_cryptographic_signing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 0.5},
|
||||
"windows_adcs_request_policy_module_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 0.75},
|
||||
"windows_adcs_challenge_responses_total": {metricType: dto.MetricType_COUNTER, value: 13},
|
||||
"windows_adcs_challenge_response_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.25},
|
||||
"windows_adcs_signed_certificate_timestamp_lists_total": {metricType: dto.MetricType_COUNTER, value: 23},
|
||||
"windows_adcs_signed_certificate_timestamp_list_processing_time_seconds": {metricType: dto.MetricType_GAUGE, value: 1.75},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
rows []perfDataCounterValues
|
||||
}{
|
||||
{name: "no templates"},
|
||||
{name: "zero counters", rows: []perfDataCounterValues{{Name: "ZeroTemplate"}}},
|
||||
{name: "independent templates", rows: []perfDataCounterValues{values, {Name: "ZeroTemplate"}}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := New(nil)
|
||||
c.buildDescriptors()
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
require.NoError(t, registry.Register(metricCollector{collector: c, rows: tc.rows}))
|
||||
families, err := registry.Gather()
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(tc.rows) == 0 {
|
||||
require.Empty(t, families)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.Len(t, families, len(expected))
|
||||
|
||||
for _, family := range families {
|
||||
want, ok := expected[family.GetName()]
|
||||
require.True(t, ok, "unexpected metric %s", family.GetName())
|
||||
require.Equal(t, want.metricType, family.GetType())
|
||||
require.Len(t, family.GetMetric(), len(tc.rows))
|
||||
templates := make(map[string]bool, len(tc.rows))
|
||||
|
||||
for _, metric := range family.GetMetric() {
|
||||
require.Len(t, metric.GetLabel(), 1)
|
||||
label := metric.GetLabel()[0]
|
||||
require.Equal(t, "cert_template", label.GetName())
|
||||
require.False(t, templates[label.GetValue()], "duplicate template sample")
|
||||
templates[label.GetValue()] = true
|
||||
|
||||
expectedValue := 0.0
|
||||
if label.GetValue() == values.Name {
|
||||
expectedValue = want.value
|
||||
} else {
|
||||
require.Equal(t, "ZeroTemplate", label.GetValue())
|
||||
}
|
||||
|
||||
actual := metric.GetGauge().GetValue()
|
||||
if want.metricType == dto.MetricType_COUNTER {
|
||||
actual = metric.GetCounter().GetValue()
|
||||
}
|
||||
|
||||
require.InDelta(t, expectedValue, actual, 0, "%s for template %s", family.GetName(), label.GetValue())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type metricCollector struct {
|
||||
collector *Collector
|
||||
rows []perfDataCounterValues
|
||||
}
|
||||
|
||||
func (c metricCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
prometheus.DescribeByCollect(c, ch)
|
||||
}
|
||||
|
||||
func (c metricCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
c.collector.collectMetrics(ch, c.rows)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/container"
|
||||
"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, container.New, nil)
|
||||
metrics := testutils.TestCollector(t, container.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "hostprocess", "hostprocess": "true"})
|
||||
testutils.RequireFixtureMetric(t, metrics, container.Name, "windows_container_available", prometheus.Labels{"container": "nanoserver", "hostprocess": "false"})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/dhcp"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, dhcp.New, nil)
|
||||
metrics := testutils.TestCollector(t, dhcp.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, dhcp.Name, "windows_dhcp_scope_info", prometheus.Labels{"name": "GitHubActions", "scope": "192.0.2.0/24"})
|
||||
}
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
package file_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/file"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,7 +32,51 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, file.New, &file.Config{
|
||||
FilePatterns: []string{"*.*"},
|
||||
path := filepath.Join(t.TempDir(), "test.txt")
|
||||
content := []byte("windows_exporter test")
|
||||
require.NoError(t, os.WriteFile(path, content, 0o600))
|
||||
|
||||
metrics := testutils.TestCollector(t, file.New, &file.Config{
|
||||
FilePatterns: []string{path},
|
||||
})
|
||||
require.Contains(t, metrics, "windows_file_size_bytes")
|
||||
require.Len(t, metrics["windows_file_size_bytes"].GetMetric(), 1)
|
||||
require.InDelta(t, len(content), metrics["windows_file_size_bytes"].GetMetric()[0].GetGauge().GetValue(), 0)
|
||||
require.Contains(t, metrics, "windows_file_mtime_timestamp_seconds")
|
||||
require.Positive(t, metrics["windows_file_mtime_timestamp_seconds"].GetMetric()[0].GetGauge().GetValue())
|
||||
}
|
||||
|
||||
func TestCollectorPatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
directory := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(directory, "first.txt"), []byte("first"), 0o600))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(directory, "second.log"), []byte("second"), 0o600))
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
pattern string
|
||||
count int
|
||||
}{
|
||||
{name: "all files", pattern: "*", count: 2},
|
||||
{name: "extension", pattern: "*.txt", count: 1},
|
||||
{name: "case insensitive", pattern: "FIRST.TXT", count: 1},
|
||||
{name: "missing file", pattern: "missing.txt", count: 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
metrics := testutils.TestCollector(t, file.New, &file.Config{
|
||||
FilePatterns: []string{filepath.Join(directory, tc.pattern)},
|
||||
})
|
||||
if tc.count == 0 {
|
||||
require.Empty(t, metrics)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.Contains(t, metrics, "windows_file_size_bytes")
|
||||
require.Len(t, metrics["windows_file_size_bytes"].GetMetric(), tc.count)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/fsrmquota"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, fsrmquota.New, nil)
|
||||
metrics := testutils.TestCollector(t, fsrmquota.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, fsrmquota.Name, "windows_fsrmquota_size_bytes", prometheus.Labels{"path": `C:\fsrm-ci`})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/hyperv"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, hyperv.New, nil)
|
||||
metrics := testutils.TestCollector(t, hyperv.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, hyperv.Name, "windows_hyperv_dynamic_memory_vm_physical_bytes", prometheus.Labels{"vm": "GitHubActions"})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/iis"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, iis.New, nil)
|
||||
metrics := testutils.TestCollector(t, iis.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, iis.Name, "windows_iis_requests_total", prometheus.Labels{"site": "Default Web Site", "method": "get"})
|
||||
}
|
||||
|
||||
@@ -18,12 +18,15 @@
|
||||
package logical_disk_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"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/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -37,6 +40,46 @@ func BenchmarkCollector(b *testing.B) {
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, logical_disk.New, &logical_disk.Config{
|
||||
VolumeInclude: types.RegExpAny,
|
||||
CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled,
|
||||
VolumeInclude: types.RegExpAny,
|
||||
})
|
||||
}
|
||||
|
||||
func TestCollectorVolumeFilters(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
systemDrive := os.Getenv("SystemDrive")
|
||||
require.NotEmpty(t, systemDrive)
|
||||
matchDrive := regexp.MustCompile("^" + regexp.QuoteMeta(systemDrive) + "$")
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
config logical_disk.Config
|
||||
included bool
|
||||
}{
|
||||
{name: "include system drive", config: logical_disk.Config{CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled, VolumeInclude: matchDrive}, included: true},
|
||||
{name: "exclude system drive", config: logical_disk.Config{CollectorsEnabled: logical_disk.ConfigDefaults.CollectorsEnabled, VolumeInclude: types.RegExpAny, VolumeExclude: matchDrive}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
metrics := testutils.TestCollector(t, logical_disk.New, &tc.config)
|
||||
if tc.included {
|
||||
require.Contains(t, metrics, "windows_logical_disk_size_bytes")
|
||||
require.Len(t, metrics["windows_logical_disk_size_bytes"].GetMetric(), 1)
|
||||
}
|
||||
|
||||
for _, metric := range metrics["windows_logical_disk_size_bytes"].GetMetric() {
|
||||
for _, label := range metric.GetLabel() {
|
||||
if label.GetName() == "volume" {
|
||||
if tc.included {
|
||||
require.Equal(t, systemDrive, label.GetValue())
|
||||
} else {
|
||||
require.NotEqual(t, systemDrive, label.GetValue())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/mscluster"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, mscluster.New, nil)
|
||||
metrics := testutils.TestCollector(t, mscluster.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, mscluster.Name, "windows_mscluster_virtualdisk_size_bytes", prometheus.Labels{"name": "CIVirtualDisk"})
|
||||
}
|
||||
|
||||
@@ -30,7 +30,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
t.Skip("Skipping test as it requires WMI data")
|
||||
|
||||
testutils.TestCollector(t, netframework.New, nil)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,5 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
testutils.TestCollector(t, os.New, nil)
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (c *Collector) GetName() string {
|
||||
}
|
||||
|
||||
func (c *Collector) Close() error {
|
||||
for _, object := range c.config.Objects {
|
||||
for _, object := range c.objects {
|
||||
object.collector.Close()
|
||||
}
|
||||
|
||||
|
||||
@@ -188,6 +188,8 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
|
||||
},
|
||||
})
|
||||
|
||||
t.Cleanup(func() { require.NoError(t, perfDataCollector.Close()) })
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
err := perfDataCollector.Build(logger, nil)
|
||||
|
||||
@@ -203,7 +205,8 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
|
||||
registry.MustRegister(collectorAdapter{*perfDataCollector})
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
promhttp.HandlerFor(registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{})
|
||||
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}).ServeHTTP(rw, &http.Request{})
|
||||
require.Equal(t, http.StatusOK, rw.Code)
|
||||
got := rw.Body.String()
|
||||
|
||||
require.NotEmpty(t, got)
|
||||
@@ -212,3 +215,23 @@ windows_performancecounter_processor_information_processor_time\{core="0,0",stat
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectorClose(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
c := performancecounter.New(&performancecounter.Config{
|
||||
Objects: []performancecounter.Object{{
|
||||
Name: "memory", Object: "Memory",
|
||||
Counters: []performancecounter.Counter{{Name: "Available Bytes", Type: "gauge"}},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil))
|
||||
t.Cleanup(func() { require.NoError(t, c.Close()) })
|
||||
|
||||
metrics := make(chan prometheus.Metric, 10)
|
||||
require.NoError(t, c.Collect(metrics, 0))
|
||||
require.NoError(t, c.Close())
|
||||
|
||||
// A closed collector must no longer hold usable native counter queries.
|
||||
require.ErrorIs(t, c.Collect(metrics, 0), pdh.ErrPerformanceCounterNotInitialized)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/printer"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -35,5 +36,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, printer.New, nil)
|
||||
metrics := testutils.TestCollector(t, printer.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, printer.Name, "windows_printer_job_count", prometheus.Labels{"printer": "CIPrinter"})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/smb"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, smb.New, nil)
|
||||
metrics := testutils.TestCollector(t, smb.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, smb.Name, "windows_smb_server_shares_received_bytes_total", prometheus.Labels{"share": `\\*\CIShare`})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/smbclient"
|
||||
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func BenchmarkCollector(b *testing.B) {
|
||||
@@ -29,5 +30,6 @@ func BenchmarkCollector(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestCollector(t *testing.T) {
|
||||
testutils.TestCollector(t, smbclient.New, nil)
|
||||
metrics := testutils.TestCollector(t, smbclient.New, nil)
|
||||
testutils.RequireFixtureMetric(t, metrics, smbclient.Name, "windows_smbclient_data_queue_seconds_total", prometheus.Labels{"share": "CIShare"})
|
||||
}
|
||||
|
||||
@@ -111,17 +111,6 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
|
||||
c.logger.Info("update collector is in an experimental state! The configuration and metrics may change in future. Please report any issues.")
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
initErrCh := make(chan error, 1)
|
||||
go c.scheduleUpdateStatus(ctx, logger, initErrCh, c.config.Online)
|
||||
|
||||
c.ctxCancelFn = cancel
|
||||
|
||||
if err := <-initErrCh; err != nil {
|
||||
return fmt.Errorf("failed to initialize Windows Update collector: %w", err)
|
||||
}
|
||||
|
||||
c.pendingUpdate = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(types.Namespace, Name, "pending_info"),
|
||||
"Expose information for a single pending update item",
|
||||
@@ -150,6 +139,17 @@ func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
|
||||
nil,
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
initErrCh := make(chan error, 1)
|
||||
go c.scheduleUpdateStatus(ctx, logger, initErrCh, c.config.Online)
|
||||
|
||||
c.ctxCancelFn = cancel
|
||||
|
||||
if err := <-initErrCh; err != nil {
|
||||
return fmt.Errorf("failed to initialize Windows Update collector: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -279,14 +279,13 @@ func (c *Collector) scheduleUpdateStatus(ctx context.Context, logger *slog.Logge
|
||||
c.mu.Lock()
|
||||
c.metricsBuf = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
continue
|
||||
} else {
|
||||
c.mu.Lock()
|
||||
c.metricsBuf = metricsBuf
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.metricsBuf = metricsBuf
|
||||
c.mu.Unlock()
|
||||
|
||||
// Failed searches also observe the interval and cancellation.
|
||||
select {
|
||||
case <-time.After(c.config.ScrapeInterval):
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -104,12 +104,12 @@ func ParseConfigFile(args []string) string {
|
||||
return configFile
|
||||
}
|
||||
|
||||
if strings.HasSuffix(cliFlag, "-config.file") {
|
||||
if len(os.Args) <= i+1 {
|
||||
if cliFlag == "--config.file" || cliFlag == "-config.file" {
|
||||
if len(args) <= i+1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return os.Args[i+1]
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// 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 config_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/prometheus-community/windows_exporter/internal/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseConfigFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{name: "absent"},
|
||||
{name: "long joined", args: []string{"--config.file=test.yaml"}, want: "test.yaml"},
|
||||
{name: "short joined", args: []string{"-config.file=test.yaml"}, want: "test.yaml"},
|
||||
{name: "long separate", args: []string{"--config.file", "test.yaml"}, want: "test.yaml"},
|
||||
{name: "short separate", args: []string{"-config.file", "test.yaml"}, want: "test.yaml"},
|
||||
{name: "after other flags", args: []string{"--log.level=debug", "--config.file", "test.yaml"}, want: "test.yaml"},
|
||||
{name: "missing value", args: []string{"--config.file"}},
|
||||
{name: "empty value", args: []string{"--config.file="}},
|
||||
{name: "unrelated suffix", args: []string{"--other-config.file", "test.yaml"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tc.want, config.ParseConfigFile(tc.args))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConfigFileResolver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
contents string
|
||||
errorText string
|
||||
}{
|
||||
{name: "empty"},
|
||||
{name: "valid", contents: "log:\n level: debug\nweb:\n listen-address: ['127.0.0.1:9182', '[::1]:9182']\n"},
|
||||
{name: "unknown key", contents: "unknown: value", errorText: "configuration file validation error"},
|
||||
{name: "unknown collector option", contents: "collector:\n cpu:\n unknown: true\n", errorText: "configuration file validation error"},
|
||||
{name: "malformed YAML", contents: "log: [", errorText: "configuration file validation error"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte(tc.contents), 0o600))
|
||||
|
||||
resolver, err := config.NewConfigFileResolver(path)
|
||||
if tc.errorText != "" {
|
||||
require.ErrorContains(t, err, tc.errorText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resolver)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("missing file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := config.NewConfigFileResolver(filepath.Join(t.TempDir(), "missing.yaml"))
|
||||
require.ErrorContains(t, err, "failed to open configuration file")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
extraArgs []string
|
||||
want string
|
||||
errorText string
|
||||
}{
|
||||
{name: "config defaults", want: "debug"},
|
||||
{name: "CLI overrides config", extraArgs: []string{"--log.level=error"}, want: "error"},
|
||||
{name: "invalid CLI flag", extraArgs: []string{"--unknown"}, errorText: "failed to bind configuration"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "config.yaml")
|
||||
require.NoError(t, os.WriteFile(path, []byte("log:\n level: debug\n"), 0o600))
|
||||
|
||||
app := kingpin.New("test", "test")
|
||||
app.Flag("config.file", "config").String()
|
||||
level := app.Flag("log.level", "level").Default("info").String()
|
||||
|
||||
args := append([]string{"--config.file=" + path}, tc.extraArgs...)
|
||||
|
||||
err := config.Parse(app, args)
|
||||
if tc.errorText != "" {
|
||||
require.ErrorContains(t, err, tc.errorText)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.want, *level)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 httphandler_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/httphandler"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHealthHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
httphandler.NewHealthHandler().ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/health", nil))
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
require.Equal(t, "application/json", response.Header().Get("Content-Type"))
|
||||
require.JSONEq(t, `{"status":"ok"}`, response.Body.String())
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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 httphandler_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/httphandler"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus-community/windows_exporter/pkg/collector"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMetricsHTTPHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
query string
|
||||
options *httphandler.Options
|
||||
status int
|
||||
included []string
|
||||
excluded []string
|
||||
}{
|
||||
{name: "default", status: http.StatusOK, included: []string{"windows_test_first 42", "windows_test_second 42", "go_goroutines", "windows_exporter_build_info"}},
|
||||
{name: "disable exporter metrics", options: &httphandler.Options{DisableExporterMetrics: true, TimeoutMargin: 0.5}, status: http.StatusOK, included: []string{"windows_test_first 42", "windows_exporter_build_info"}, excluded: []string{"go_goroutines", "process_cpu_seconds_total"}},
|
||||
{name: "filter", query: "?collect[]=first", status: http.StatusOK, included: []string{"windows_test_first 42"}, excluded: []string{"windows_test_second"}},
|
||||
{name: "multiple collectors", query: "?collect[]=first&collect[]=second", status: http.StatusOK, included: []string{"windows_test_first 42", "windows_test_second 42"}},
|
||||
{name: "unknown collector", query: "?collect[]=missing", status: http.StatusBadRequest, included: []string{"unknown collector missing"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
collection := collector.New(collector.Map{
|
||||
"first": &testCollector{name: "first"},
|
||||
"second": &testCollector{name: "second"},
|
||||
})
|
||||
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, tc.options)
|
||||
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics"+tc.query, nil)
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
|
||||
require.Equal(t, tc.status, response.Code)
|
||||
|
||||
for _, value := range tc.included {
|
||||
require.Contains(t, response.Body.String(), value)
|
||||
}
|
||||
|
||||
for _, value := range tc.excluded {
|
||||
require.NotContains(t, response.Body.String(), value)
|
||||
}
|
||||
|
||||
// Filtering one request must not remove collectors from later requests.
|
||||
if tc.query == "?collect[]=first" {
|
||||
response = httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil))
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
require.Contains(t, response.Body.String(), "windows_test_second 42")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHTTPHandlerCollectorStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
panicValue bool
|
||||
success string
|
||||
}{
|
||||
{name: "success", success: "1"},
|
||||
{name: "failure", err: errors.New("collector failed"), success: "0"},
|
||||
{name: "no data", err: types.ErrNoData, success: "1"},
|
||||
{name: "panic", panicValue: true, success: "0"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
collection := collector.New(collector.Map{"test": &testCollector{name: "test", err: tc.err, panicValue: tc.panicValue}})
|
||||
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, &httphandler.Options{DisableExporterMetrics: true})
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil))
|
||||
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
require.Contains(t, response.Body.String(), `windows_exporter_collector_success{collector="test"} `+tc.success)
|
||||
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 0`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHTTPHandlerScrapeTimeout(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
header string
|
||||
timeout bool
|
||||
}{
|
||||
{name: "missing header"},
|
||||
{name: "invalid header", header: "invalid"},
|
||||
{name: "zero header", header: "0"},
|
||||
{name: "sufficient timeout", header: "2"},
|
||||
{name: "timeout", header: "0.1", timeout: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// The collector's delay and deadline use synthetic time.
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
collection := collector.New(collector.Map{"test": &testCollector{name: "test", delay: time.Second}})
|
||||
handler := httphandler.New(slog.New(slog.DiscardHandler), collection, &httphandler.Options{DisableExporterMetrics: true})
|
||||
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/metrics", nil)
|
||||
request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", tc.header)
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
handler.ServeHTTP(response, request)
|
||||
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
|
||||
if tc.timeout {
|
||||
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 1`)
|
||||
require.NotContains(t, response.Body.String(), "windows_test_test 42")
|
||||
|
||||
// Finish the delayed collector and its drain goroutine before leaving the bubble.
|
||||
time.Sleep(time.Second)
|
||||
synctest.Wait()
|
||||
} else {
|
||||
require.Contains(t, response.Body.String(), `windows_exporter_collector_timeout{collector="test"} 0`)
|
||||
require.Contains(t, response.Body.String(), "windows_test_test 42")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testCollector struct {
|
||||
name string
|
||||
err error
|
||||
panicValue bool
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (c *testCollector) GetName() string { return c.name }
|
||||
func (c *testCollector) Build(_ *slog.Logger, _ *mi.Session) error { return nil }
|
||||
func (c *testCollector) Close() error { return nil }
|
||||
func (c *testCollector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
|
||||
if c.panicValue {
|
||||
panic("test collector panic")
|
||||
}
|
||||
|
||||
time.Sleep(c.delay)
|
||||
|
||||
if c.err != nil {
|
||||
return c.err
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc("windows_test_"+c.name, "Test metric", nil, nil), prometheus.GaugeValue, 42)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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 httphandler_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/httphandler"
|
||||
"github.com/prometheus/common/version"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVersionHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
response := httptest.NewRecorder()
|
||||
httphandler.NewVersionHandler().ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/version", nil))
|
||||
require.Equal(t, http.StatusOK, response.Code)
|
||||
|
||||
var got map[string]string
|
||||
require.NoError(t, json.Unmarshal(response.Body.Bytes(), &got))
|
||||
require.Equal(t, map[string]string{
|
||||
"version": version.Version, "revision": version.Revision,
|
||||
"branch": version.Branch, "buildUser": version.BuildUser,
|
||||
"buildDate": version.BuildDate, "goVersion": version.GoVersion,
|
||||
}, got)
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -29,8 +31,11 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/update"
|
||||
"github.com/prometheus-community/windows_exporter/internal/mi"
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus-community/windows_exporter/pkg/collector"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
@@ -52,81 +57,206 @@ func FuncBenchmarkCollector[C collector.Collector](b *testing.B, name string, co
|
||||
|
||||
metrics := make(chan prometheus.Metric)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
<-metrics
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
for range metrics {
|
||||
}
|
||||
}()
|
||||
})
|
||||
b.Cleanup(func() {
|
||||
close(metrics)
|
||||
wg.Wait()
|
||||
assert.NoError(b, collectors.Close())
|
||||
})
|
||||
|
||||
for b.Loop() {
|
||||
require.NoError(b, c.Collect(metrics, 0))
|
||||
}
|
||||
|
||||
require.NoError(b, collectors.Close())
|
||||
}
|
||||
|
||||
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) {
|
||||
// TestCollector validates real Windows collector output. CI lists provisioned
|
||||
// collectors in WINDOWS_EXPORTER_TEST_COLLECTORS so setup failures cannot skip.
|
||||
func TestCollector[C collector.Collector, V any](t *testing.T, fn func(*V) C, conf *V) map[string]*dto.MetricFamily {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
metrics []prometheus.Metric
|
||||
err error
|
||||
)
|
||||
|
||||
logger := slog.New(slog.DiscardHandler)
|
||||
c := fn(conf)
|
||||
ch := make(chan prometheus.Metric, 10000)
|
||||
required := slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), c.GetName())
|
||||
|
||||
miApp, err := mi.ApplicationInitialize()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, miApp.Close()) })
|
||||
|
||||
miSession, err := miApp.NewSession(nil)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { assert.NoError(t, miSession.Close()) })
|
||||
t.Cleanup(func() { assert.NoError(t, c.Close()) })
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, c.Close())
|
||||
require.NoError(t, miSession.Close())
|
||||
require.NoError(t, miApp.Close())
|
||||
})
|
||||
if err := c.Build(logger, miSession); err != nil {
|
||||
if !required && unsupportedCollector(err) {
|
||||
t.Skipf("collector %s is not supported: %v", c.GetName(), err)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
require.NoError(t, err, "build %s", c.GetName())
|
||||
}
|
||||
|
||||
// PDH rate counters need a second sample after initialization.
|
||||
time.Sleep(time.Second)
|
||||
|
||||
var families map[string]*dto.MetricFamily
|
||||
for scrape := range 2 {
|
||||
families = collectMetrics(t, c, required)
|
||||
t.Logf("%s scrape %d: %d metric families", c.GetName(), scrape+1, len(families))
|
||||
}
|
||||
|
||||
return families
|
||||
}
|
||||
|
||||
func unsupportedCollector(err error) bool {
|
||||
return errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE) ||
|
||||
errors.Is(err, mi.MI_RESULT_INVALID_QUERY) ||
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)) ||
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) ||
|
||||
errors.Is(err, pdh.ErrPerformanceCounterNotInitialized) ||
|
||||
errors.Is(err, pdh.ErrNoData) ||
|
||||
errors.Is(err, update.ErrUpdateServiceDisabled) ||
|
||||
errors.Is(err, os.ErrNotExist) ||
|
||||
errors.Is(err, windows.Errno(2151088411))
|
||||
}
|
||||
|
||||
func collectMetrics(t *testing.T, c collector.Collector, required bool) map[string]*dto.MetricFamily {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
metrics collectedMetrics
|
||||
collectErr error
|
||||
)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
for metric := range ch {
|
||||
metrics = append(metrics, metric)
|
||||
}
|
||||
})
|
||||
|
||||
err = c.Build(logger, miSession)
|
||||
// Stop the receiver even if Collect panics or an assertion ends the test.
|
||||
func() {
|
||||
defer func() {
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
}()
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE),
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)),
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)),
|
||||
errors.Is(err, update.ErrUpdateServiceDisabled),
|
||||
errors.Is(err, os.ErrNotExist):
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
err := c.Collect(ch, 30*time.Second)
|
||||
if errors.Is(err, update.ErrNoUpdates) && required {
|
||||
deadline := time.Now().Add(time.Minute)
|
||||
for errors.Is(err, update.ErrNoUpdates) && time.Now().Before(deadline) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
err = c.Collect(ch, 30*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
collectErr = err
|
||||
}()
|
||||
|
||||
// Empty optional instance groups may report no data alongside valid metrics.
|
||||
// Inspect every joined error so an unrelated failure cannot be hidden.
|
||||
if noDataOnly(collectErr) && len(metrics) > 0 {
|
||||
t.Logf("collector %s has empty optional instance groups: %v", c.GetName(), collectErr)
|
||||
collectErr = nil
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
err = c.Collect(ch, 0)
|
||||
|
||||
switch {
|
||||
// container collector
|
||||
case errors.Is(err, windows.Errno(2151088411)),
|
||||
errors.Is(err, pdh.ErrPerformanceCounterNotInitialized),
|
||||
errors.Is(err, pdh.ErrNoData),
|
||||
errors.Is(err, mi.MI_RESULT_INVALID_NAMESPACE),
|
||||
errors.Is(err, mi.MI_RESULT_INVALID_QUERY),
|
||||
errors.Is(err, update.ErrNoUpdates):
|
||||
t.Skip("collector not supported on this system")
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
if !required && (unsupportedCollector(collectErr) || errors.Is(collectErr, update.ErrNoUpdates)) {
|
||||
t.Skipf("collector %s is not supported: %v", c.GetName(), collectErr)
|
||||
}
|
||||
|
||||
close(ch)
|
||||
require.NoError(t, collectErr, "collect %s", c.GetName())
|
||||
|
||||
wg.Wait()
|
||||
if required {
|
||||
require.NotEmpty(t, metrics, "provisioned collector %s emitted no metrics", c.GetName())
|
||||
}
|
||||
|
||||
registry := prometheus.NewPedanticRegistry()
|
||||
require.NoError(t, registry.Register(metrics))
|
||||
gathered, err := registry.Gather()
|
||||
require.NoError(t, err, "invalid metrics from %s", c.GetName())
|
||||
|
||||
families := make(map[string]*dto.MetricFamily, len(gathered))
|
||||
for _, family := range gathered {
|
||||
families[family.GetName()] = family
|
||||
if strings.HasSuffix(family.GetName(), "_collector_success") {
|
||||
for _, metric := range family.GetMetric() {
|
||||
require.InDelta(t, 1, metric.GetGauge().GetValue(), 0, "failed child collector: %s", metric)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return families
|
||||
}
|
||||
|
||||
type collectedMetrics []prometheus.Metric
|
||||
|
||||
func (m collectedMetrics) Describe(ch chan<- *prometheus.Desc) {
|
||||
prometheus.DescribeByCollect(m, ch)
|
||||
}
|
||||
|
||||
func (m collectedMetrics) Collect(ch chan<- prometheus.Metric) {
|
||||
for _, metric := range m {
|
||||
ch <- 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) {
|
||||
t.Helper()
|
||||
|
||||
if !slices.Contains(strings.Split(os.Getenv("WINDOWS_EXPORTER_TEST_COLLECTORS"), ","), collectorName) {
|
||||
return
|
||||
}
|
||||
|
||||
require.Contains(t, families, metricName)
|
||||
|
||||
for _, metric := range families[metricName].GetMetric() {
|
||||
matched := true
|
||||
|
||||
for name, value := range labels {
|
||||
found := false
|
||||
|
||||
for _, label := range metric.GetLabel() {
|
||||
if label.GetName() == name && strings.EqualFold(label.GetValue(), value) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
matched = matched && found
|
||||
}
|
||||
|
||||
if matched {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Fatalf("metric %s with fixture labels %v was not emitted; got %s", metricName, labels, families[metricName])
|
||||
}
|
||||
|
||||
func noDataOnly(err error) bool {
|
||||
if joined, ok := err.(interface{ Unwrap() []error }); ok {
|
||||
for _, cause := range joined.Unwrap() {
|
||||
if !noDataOnly(cause) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if wrapped, ok := err.(interface{ Unwrap() error }); ok {
|
||||
return noDataOnly(wrapped.Unwrap())
|
||||
}
|
||||
|
||||
return errors.Is(err, pdh.ErrNoData) || errors.Is(err, types.ErrNoData)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 testutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNoDataOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
failure := errors.New("collector failed")
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{name: "nil"},
|
||||
{name: "PDH no data", err: pdh.ErrNoData, want: true},
|
||||
{name: "collector no data", err: types.ErrNoData, want: true},
|
||||
{name: "wrapped", err: fmt.Errorf("empty instances: %w", pdh.ErrNoData), want: true},
|
||||
{name: "joined empty groups", err: errors.Join(pdh.ErrNoData, types.ErrNoData), want: true},
|
||||
{name: "failure", err: failure},
|
||||
{name: "mixed failures", err: errors.Join(pdh.ErrNoData, failure)},
|
||||
{name: "nested mixed failures", err: fmt.Errorf("child: %w", errors.Join(pdh.ErrNoData, failure))},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tc.want, noDataOnly(tc.err))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user