// 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 registry_test import ( "fmt" "log/slog" "net/http" "net/http/httptest" "regexp" "testing" "time" "github.com/prometheus-community/windows_exporter/internal/collector/registry" "github.com/prometheus-community/windows_exporter/internal/types" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/require" winregistry "golang.org/x/sys/windows/registry" ) // collectorAdapter bridges registry.Collector's Collect(ch, time.Duration) error // signature to the prometheus.Collector interface, mirroring the adapter in the // performancecounter collector's tests. type collectorAdapter struct { registry.Collector } // Describe implements the prometheus.Collector interface. func (collectorAdapter) Describe(chan<- *prometheus.Desc) {} // Collect implements the prometheus.Collector interface. // // Unlike the performancecounter adapter, the Collect error is intentionally // discarded rather than panicked on: a missing key is an expected, non-fatal // condition reported via key_success=0, and this test deliberately configures a // bogus key, so Collect returns a non-nil joined error while still emitting // every metric. func (a collectorAdapter) Collect(ch chan<- prometheus.Metric) { _ = a.Collector.Collect(ch, 0) } // TestCollectorMetrics asserts the full value→Desc→metric wiring end to end by // reading known REG_DWORD/REG_QWORD values from a throwaway HKCU key. It exercises // the auto-generated metric name, an explicit metric: override, gauge vs counter // types, constant labels, exact 32- and 64-bit values, and the key_success 1-vs-0 // semantics (a missing key fails just that key, not the whole scrape). Because the // test controls the input values, it asserts exact values rather than the numeric // patterns the performancecounter test must use. Using HKCU keeps it admin-free // and reliable in CI. func TestCollectorMetrics(t *testing.T) { t.Parallel() suffix := time.Now().UnixNano() sub := fmt.Sprintf(`Software\windows_exporter_registry_test_%d`, suffix) keyPath := `HKCU\` + sub k, _, err := winregistry.CreateKey(winregistry.CURRENT_USER, sub, winregistry.SET_VALUE) require.NoError(t, err) // REG_DWORD fits in 32 bits; REG_QWORD is deliberately > math.MaxUint32 to prove // 64-bit handling. Both are well under 2^53, so they convert to float64 exactly. require.NoError(t, k.SetDWordValue("TestDword", 1234)) require.NoError(t, k.SetQWordValue("TestQword", 5_000_000_000)) require.NoError(t, k.Close()) t.Cleanup(func() { _ = winregistry.DeleteKey(winregistry.CURRENT_USER, sub) }) // A second key that does not exist exercises graceful per-key failure: the // collector reports key_success=0 for it instead of aborting the scrape. missingSub := fmt.Sprintf(`Software\windows_exporter_missing_%d`, suffix) missingPath := `HKCU\` + missingSub c := registry.New(®istry.Config{ Keys: []registry.Key{ { Name: "testgroup", Key: keyPath, Values: []registry.Value{ // Auto-named gauge → windows_registry_testgroup_testdword. {Name: "TestDword"}, // Explicit metric: override + counter type + constant label. { Name: "TestQword", Metric: "windows_registry_test_custom_metric", Help: "custom registry help text", Type: "counter", Labels: map[string]string{"foo": "bar"}, }, }, }, { Name: "missing_group", Key: missingPath, Values: []registry.Value{{Name: "AnyValue"}}, }, }, }) require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil)) reg := prometheus.NewRegistry() reg.MustRegister(collectorAdapter{*c}) rw := httptest.NewRecorder() promhttp.HandlerFor(reg, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{}) got := rw.Body.String() require.NotEmpty(t, got) // key_success labels are the configured key name (not the path). // Metric families are emitted sorted by name, and metrics within the // key_success family sorted by label value, so "missing_group" (0) // precedes "testgroup" (1). expected := `# HELP windows_registry_key_success Whether the registry key could be read successfully. # TYPE windows_registry_key_success gauge windows_registry_key_success{name="missing_group"} 0 windows_registry_key_success{name="testgroup"} 1 # HELP windows_registry_test_custom_metric custom registry help text # TYPE windows_registry_test_custom_metric counter windows_registry_test_custom_metric{foo="bar"} 5e+09 # HELP windows_registry_testgroup_testdword windows_exporter: custom registry metric # TYPE windows_registry_testgroup_testdword gauge windows_registry_testgroup_testdword 1234 ` // QuoteMeta escapes the backslashes/braces in the exposition so the exact // values, types, labels, and names are matched literally. require.Regexp(t, "^"+regexp.QuoteMeta(expected), got) } // TestCollectMissingKeyReturnsErrNoData verifies that a key that does not exist // returns an error wrapping types.ErrNoData, so the framework marks the // collector "succeeded with warnings" rather than failed. func TestCollectMissingKeyReturnsErrNoData(t *testing.T) { t.Parallel() c := registry.New(®istry.Config{ Keys: []registry.Key{ { Name: "missing", Key: fmt.Sprintf(`HKCU\Software\windows_exporter_does_not_exist_%d`, time.Now().UnixNano()), Values: []registry.Value{{Name: "AnyValue"}}, }, }, }) require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil)) ch := make(chan prometheus.Metric, 10) err := c.Collect(ch, 0) close(ch) require.Error(t, err) require.ErrorIs(t, err, types.ErrNoData) } // TestCollectMissingValueReturnsErrNoData verifies that a value that does not // exist within an otherwise-openable key also returns types.ErrNoData. func TestCollectMissingValueReturnsErrNoData(t *testing.T) { t.Parallel() sub := fmt.Sprintf(`Software\windows_exporter_registry_missing_val_%d`, time.Now().UnixNano()) k, _, err := winregistry.CreateKey(winregistry.CURRENT_USER, sub, winregistry.SET_VALUE) require.NoError(t, err) require.NoError(t, k.Close()) t.Cleanup(func() { _ = winregistry.DeleteKey(winregistry.CURRENT_USER, sub) }) c := registry.New(®istry.Config{ Keys: []registry.Key{ { Name: "test", Key: `HKCU\` + sub, Values: []registry.Value{{Name: "DoesNotExist"}}, }, }, }) require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil)) ch := make(chan prometheus.Metric, 10) err = c.Collect(ch, 0) close(ch) require.Error(t, err) require.ErrorIs(t, err, types.ErrNoData) } // TestCollectorSharedMetricName proves the supported aggregation pattern from the // docs: two values may share one metric name as long as their labels differ. The // collector deliberately does not reject this; only a true duplicate (same name and // identical labels) is rejected at build time. func TestCollectorSharedMetricName(t *testing.T) { t.Parallel() sub := fmt.Sprintf(`Software\windows_exporter_registry_shared_%d`, time.Now().UnixNano()) k, _, err := winregistry.CreateKey(winregistry.CURRENT_USER, sub, winregistry.SET_VALUE) require.NoError(t, err) require.NoError(t, k.SetDWordValue("Alpha", 1)) require.NoError(t, k.SetDWordValue("Beta", 2)) require.NoError(t, k.Close()) t.Cleanup(func() { _ = winregistry.DeleteKey(winregistry.CURRENT_USER, sub) }) c := registry.New(®istry.Config{ Keys: []registry.Key{ { Name: "shared_test", Key: `HKCU\` + sub, Values: []registry.Value{ {Name: "Alpha", Metric: "windows_registry_shared", Labels: map[string]string{"slot": "a"}}, {Name: "Beta", Metric: "windows_registry_shared", Labels: map[string]string{"slot": "b"}}, }, }, }, }) require.NoError(t, c.Build(slog.New(slog.DiscardHandler), nil)) reg := prometheus.NewRegistry() reg.MustRegister(collectorAdapter{*c}) rw := httptest.NewRecorder() promhttp.HandlerFor(reg, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}).ServeHTTP(rw, &http.Request{}) got := rw.Body.String() // Both series share the metric name and differ only by label, so both are // exported rather than one being dropped as a duplicate. require.Contains(t, got, `windows_registry_shared{slot="a"} 1`) require.Contains(t, got, `windows_registry_shared{slot="b"} 2`) }