registry: new collector for generating metrics from Windows registry entries (#2428)

Co-authored-by: Jan-Otto Kröpke <mail@jkroepke.de>
Signed-off-by: Doug Luxem <doug@luxem.org>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Doug Luxem
2026-10-05 11:08:23 +02:00
committed by GitHub
co-authored by Jan-Otto Kröpke
parent 6e4a2c7138
commit 1d2ccf9910
10 changed files with 1221 additions and 2 deletions
+386
View File
@@ -0,0 +1,386 @@
// 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
import (
"errors"
"fmt"
"log/slog"
"regexp"
"slices"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/mi"
"github.com/prometheus-community/windows_exporter/internal/types"
"github.com/prometheus/client_golang/prometheus"
"go.yaml.in/yaml/v3"
winregistry "golang.org/x/sys/windows/registry"
)
const Name = "registry"
var reNonAlphaNum = regexp.MustCompile(`[^a-zA-Z0-9]`)
type Config struct {
Keys []Key `yaml:"keys"`
}
//nolint:gochecknoglobals
var ConfigDefaults = Config{
Keys: make([]Key, 0),
}
// A Collector is a Prometheus collector for Windows registry values.
type Collector struct {
config Config
logger *slog.Logger
keys []Key
keySuccessDesc *prometheus.Desc
}
func New(config *Config) *Collector {
if config == nil {
config = &ConfigDefaults
}
if config.Keys == nil {
config.Keys = ConfigDefaults.Keys
}
c := &Collector{
config: *config,
}
return c
}
func NewWithFlags(app *kingpin.Application) *Collector {
c := &Collector{
config: ConfigDefaults,
}
var keys string
app.Flag(
"collector.registry.keys",
"Registry keys to collect REG_DWORD and REG_QWORD values from. See docs for more information on how to use this flag. By default, no keys are collected.",
).Default("").StringVar(&keys)
app.Action(func(*kingpin.ParseContext) error {
if keys == "" {
return nil
}
if err := yaml.Unmarshal([]byte(keys), &c.config.Keys); err != nil {
return fmt.Errorf("failed to parse keys %s: %w", keys, err)
}
return nil
})
return c
}
func (c *Collector) GetName() string {
return Name
}
func (c *Collector) Close() error {
return nil
}
func (c *Collector) Build(logger *slog.Logger, _ *mi.Session) error {
c.logger = logger.With(slog.String("collector", Name))
c.logger.Info("registry collector is in an experimental state! It may subject to change.")
c.keySuccessDesc = prometheus.NewDesc(
prometheus.BuildFQName(types.Namespace, Name, "key_success"),
"Whether the registry key could be read successfully.",
[]string{"name"},
nil,
)
c.keys = make([]Key, 0, len(c.config.Keys))
labels := make([]string, 0, len(c.config.Keys))
groups := make([]string, 0, len(c.config.Keys))
var errs []error
// seenMetricHelps and seenMetricTypes track the first-seen help text and
// value type for each metric name. Prometheus requires all metrics sharing
// a name to carry identical help text and type; mismatches cause silent
// metric drops at scrape time. Catching them here turns that into a loud
// build-time error.
seenMetricHelps := make(map[string]string)
seenMetricTypes := make(map[string]prometheus.ValueType)
for _, key := range c.config.Keys {
if key.Key == "" {
errs = append(errs, errors.New("key is required"))
continue
}
hive, subPath, label, err := parseKeyPath(key.Key)
if err != nil {
errs = append(errs, err)
continue
}
if slices.Contains(labels, label) {
errs = append(errs, fmt.Errorf("key %s is duplicated", label))
continue
}
labels = append(labels, label)
if len(key.Values) == 0 {
errs = append(errs, fmt.Errorf("key %s has no values configured", label))
continue
}
if key.Name == "" {
errs = append(errs, fmt.Errorf("name is required for key %s", label))
continue
}
if slices.Contains(groups, key.Name) {
errs = append(errs, fmt.Errorf("key name %q is duplicated", key.Name))
continue
}
groups = append(groups, key.Name)
group := key.Name
values := make([]Value, 0, len(key.Values))
valueNames := make([]string, 0, len(key.Values))
for _, value := range key.Values {
if value.Name == "" {
errs = append(errs, fmt.Errorf("value name is required for key %s", label))
continue
}
// Registry value names are matched case-insensitively, so lowercase
// them to keep reads consistent across systems.
value.Name = strings.ToLower(value.Name)
if slices.Contains(valueNames, value.Name) {
errs = append(errs, fmt.Errorf("value name %q of key %s is duplicated", value.Name, label))
continue
}
valueNames = append(valueNames, value.Name)
// If no metric name is given, derive one from the key group and value
// name, mirroring the performancecounter collector.
if value.Metric == "" {
value.Metric = sanitizeMetricName(
fmt.Sprintf("%s_%s_%s_%s", types.Namespace, Name, group, value.Name),
)
}
switch value.Type {
case "", "gauge":
value.metricType = prometheus.GaugeValue
case "counter":
value.metricType = prometheus.CounterValue
default:
errs = append(errs, fmt.Errorf("value %q of key %s has invalid type %q, must be \"gauge\" or \"counter\"", value.Name, label, value.Type))
continue
}
help := value.Help
if help == "" {
help = "windows_exporter: custom registry metric"
}
if prevHelp, seen := seenMetricHelps[value.Metric]; seen {
if prevHelp != help {
errs = append(errs, fmt.Errorf(
"value %q of key %s: metric %q must have the same help text as other values sharing this metric name (got %q, want %q)",
value.Name, label, value.Metric, help, prevHelp,
))
continue
}
if seenMetricTypes[value.Metric] != value.metricType {
errs = append(errs, fmt.Errorf(
"value %q of key %s: metric %q must have the same type as other values sharing this metric name",
value.Name, label, value.Metric,
))
continue
}
} else {
seenMetricHelps[value.Metric] = help
seenMetricTypes[value.Metric] = value.metricType
}
value.desc = prometheus.NewDesc(
value.Metric,
help,
nil,
value.Labels,
)
values = append(values, value)
}
// Build only resolves and validates the static configuration; it does not
// open the key. Whether a key can actually be opened and read depends on
// runtime conditions (existence, ACLs, transient locks) that are reported
// per key via the key_success metric at scrape time. Opening keys here
// would turn a recoverable, observable per-key condition into a fatal
// collector build error that aborts the whole exporter.
key.hive = hive
key.subPath = subPath
key.label = label
key.Values = values
c.keys = append(c.keys, key)
}
return errors.Join(errs...)
}
// Collect sends the metric values for each configured registry key
// to the provided prometheus Metric channel.
func (c *Collector) Collect(ch chan<- prometheus.Metric, _ time.Duration) error {
var errs []error
for _, key := range c.keys {
err := c.collectKey(ch, key)
success := 1.0
if err != nil {
errs = append(errs, err)
success = 0.0
c.logger.Warn("registry key collection failed",
slog.String("key", key.label),
slog.Any("err", err),
)
} else {
c.logger.Debug("registry key collection succeeded",
slog.String("key", key.label),
)
}
ch <- prometheus.MustNewConstMetric(
c.keySuccessDesc,
prometheus.GaugeValue,
success,
key.Name,
)
}
return errors.Join(errs...)
}
func (c *Collector) collectKey(ch chan<- prometheus.Metric, key Key) error {
rk, err := winregistry.OpenKey(key.hive, key.subPath, winregistry.QUERY_VALUE)
if err != nil {
return fmt.Errorf("failed to open registry key %s: %w", key.label, errors.Join(err, types.ErrNoData))
}
defer func() {
_ = rk.Close()
}()
var errs []error
for _, value := range key.Values {
val, _, err := rk.GetIntegerValue(value.Name)
if err != nil {
errs = append(errs, fmt.Errorf("failed to read value %q of registry key %s: %w", value.Name, key.label, errors.Join(err, types.ErrNoData)))
continue
}
ch <- prometheus.MustNewConstMetric(
value.desc,
value.metricType,
float64(val),
)
}
return errors.Join(errs...)
}
// parseKeyPath splits a full registry key path into its hive and sub path,
// accepting short (HKLM) and long (HKEY_LOCAL_MACHINE) hive names as well as
// forward slashes as separators. The returned label is the normalized path
// with the short hive name and backslashes, lowercased so that the same key
// produces an identical label regardless of how it is cased in the registry or
// the configuration. The registry matches keys case-insensitively, so
// lowercasing the label can never collide with a distinct key.
func parseKeyPath(path string) (winregistry.Key, string, string, error) {
normalized := strings.Trim(strings.ReplaceAll(path, "/", `\`), `\`)
hiveName, subPath, _ := strings.Cut(normalized, `\`)
var (
hive winregistry.Key
label string
)
switch strings.ToUpper(hiveName) {
case "HKLM", "HKEY_LOCAL_MACHINE":
hive, label = winregistry.LOCAL_MACHINE, "hklm"
case "HKCU", "HKEY_CURRENT_USER":
hive, label = winregistry.CURRENT_USER, "hkcu"
case "HKU", "HKEY_USERS":
hive, label = winregistry.USERS, "hku"
case "HKCR", "HKEY_CLASSES_ROOT":
hive, label = winregistry.CLASSES_ROOT, "hkcr"
case "HKCC", "HKEY_CURRENT_CONFIG":
hive, label = winregistry.CURRENT_CONFIG, "hkcc"
default:
return 0, "", "", fmt.Errorf("unknown registry hive %q in key %q", hiveName, path)
}
if subPath != "" {
label += `\` + strings.ToLower(subPath)
}
return hive, subPath, label, nil
}
// sanitizeMetricName turns an arbitrary string into a valid Prometheus metric
// name by lowercasing it, replacing every non-alphanumeric character with an
// underscore, and trimming leading and trailing underscores.
func sanitizeMetricName(name string) string {
return strings.Trim(reNonAlphaNum.ReplaceAllString(strings.ToLower(name), "_"), "_")
}
@@ -0,0 +1,73 @@
// 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
import "testing"
func TestParseKeyPathNormalization(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
input string
wantLabel string
}{
{"short hive lowercased", `HKLM\SOFTWARE\Foo`, `hklm\software\foo`},
{"long hive normalized to short", `HKEY_LOCAL_MACHINE\SOFTWARE\Foo`, `hklm\software\foo`},
{"forward slashes converted", `HKLM/SOFTWARE/Foo`, `hklm\software\foo`},
{"mixed case sub path lowercased", `hklm\SoFtWaRe\BAR`, `hklm\software\bar`},
{"surrounding slashes trimmed", `\HKLM\SOFTWARE\Foo\`, `hklm\software\foo`},
{"hive only", `HKCU`, `hkcu`},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
_, _, label, err := parseKeyPath(tc.input)
if err != nil {
t.Fatalf("parseKeyPath(%q) returned error: %v", tc.input, err)
}
if label != tc.wantLabel {
t.Errorf("parseKeyPath(%q) label = %q, want %q", tc.input, label, tc.wantLabel)
}
})
}
}
func TestSanitizeMetricName(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
input string
want string
}{
{"already valid", "windows_registry_foo", "windows_registry_foo"},
{"spaces and case", "Windows NT Version", "windows_nt_version"},
{"backslashes from key path", `windows_registry_hklm\software\foo`, "windows_registry_hklm_software_foo"},
{"surrounding non-alphanumerics trimmed", "%value%", "value"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if got := sanitizeMetricName(tc.input); got != tc.want {
t.Errorf("sanitizeMetricName(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}
@@ -0,0 +1,255 @@
// 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(&registry.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(&registry.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(&registry.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) would be dropped and logged by the registry at scrape 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(&registry.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`)
}
@@ -0,0 +1,165 @@
// 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 (
"log/slog"
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/prometheus-community/windows_exporter/internal/collector/registry"
"github.com/prometheus-community/windows_exporter/internal/utils/testutils"
"github.com/stretchr/testify/require"
)
func BenchmarkCollector(b *testing.B) {
keys := `[{"name":"windows_nt","key":"HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","values":[{"name":"CurrentMajorVersionNumber"}]}]`
testutils.FuncBenchmarkCollector(b, registry.Name, registry.NewWithFlags, func(app *kingpin.Application) {
app.GetFlag("collector.registry.keys").StringVar(&keys)
})
}
func TestCollector(t *testing.T) {
t.Parallel()
testutils.TestCollector(t, registry.New, &registry.Config{
Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{{Name: "CurrentMajorVersionNumber"}},
},
{
Name: "memory_management",
Key: `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management`,
Values: []registry.Value{{Name: "ClearPageFileAtShutdown", Type: "gauge"}},
},
},
})
}
func TestCollectorBuildErrors(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
config registry.Config
}{
{
name: "unknown hive",
config: registry.Config{Keys: []registry.Key{{Key: `BOGUS\Foo`}}},
},
{
name: "empty key",
config: registry.Config{Keys: []registry.Key{{Key: ""}}},
},
{
name: "key with no values",
config: registry.Config{Keys: []registry.Key{
{Name: "windows_nt", Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`},
}},
},
{
name: "duplicate key",
config: registry.Config{Keys: []registry.Key{
{Name: "nt_a", Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`},
{Name: "nt_b", Key: `HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion`},
}},
},
{
name: "duplicate key differing only by case",
config: registry.Config{Keys: []registry.Key{
{Name: "nt_a", Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`},
{Name: "nt_b", Key: `hklm\software\microsoft\windows nt\currentversion`},
}},
},
{
name: "duplicate value differing only by case",
config: registry.Config{Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{{Name: "CurrentMajorVersionNumber"}, {Name: "currentmajorversionnumber"}},
},
}},
},
{
name: "missing value name",
config: registry.Config{Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{{Name: ""}},
},
}},
},
{
name: "invalid value type",
config: registry.Config{Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{{Name: "CurrentMajorVersionNumber", Type: "histogram"}},
},
}},
},
{
name: "shared metric name with inconsistent help text",
config: registry.Config{Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{
{Name: "CurrentMajorVersionNumber", Metric: "windows_version", Help: "version help a"},
{Name: "CurrentMinorVersionNumber", Metric: "windows_version", Help: "version help b"},
},
},
}},
},
{
name: "shared metric name with inconsistent type",
config: registry.Config{Keys: []registry.Key{
{
Name: "windows_nt",
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{
{Name: "CurrentMajorVersionNumber", Metric: "windows_version", Type: "gauge"},
{Name: "CurrentMinorVersionNumber", Metric: "windows_version", Type: "counter"},
},
},
}},
},
{
name: "missing name",
config: registry.Config{Keys: []registry.Key{
{
Key: `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion`,
Values: []registry.Value{{Name: "CurrentMajorVersionNumber"}},
},
}},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
c := registry.New(&tc.config)
require.Error(t, c.Build(slog.New(slog.DiscardHandler), nil))
})
}
}
+53
View File
@@ -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 registry
import (
"github.com/prometheus/client_golang/prometheus"
"go.yaml.in/yaml/v3"
winregistry "golang.org/x/sys/windows/registry"
)
type Key struct {
Name string `json:"name" yaml:"name"`
Key string `json:"key" yaml:"key"`
Values []Value `json:"values" yaml:"values"`
// resolved at Build time
hive winregistry.Key
subPath string
label string
}
type Value struct {
Name string `json:"name" yaml:"name"`
Metric string `json:"metric" yaml:"metric"`
Help string `json:"help" yaml:"help"`
Type string `json:"type" yaml:"type"`
Labels map[string]string `json:"labels" yaml:"labels"`
// resolved at Build time
desc *prometheus.Desc
metricType prometheus.ValueType
}
// UnmarshalYAML is a no-op, so the strict config file validation accepts the
// keys being provided as a string. See the performancecounter collector.
func (*Config) UnmarshalYAML(*yaml.Node) error {
return nil
}