mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 18:19:04 +02:00
Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Claude <noreply@anthropic.com>
452 lines
12 KiB
Go
452 lines
12 KiB
Go
// 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"
|
|
"io"
|
|
"log/slog"
|
|
"maps"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/alecthomas/kingpin/v2"
|
|
"github.com/prometheus-community/windows_exporter/internal/mi"
|
|
"github.com/prometheus-community/windows_exporter/internal/multiplier"
|
|
"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
|
|
}
|
|
|
|
// Reject unknown keys like the configuration file does, so that a typo
|
|
// fails instead of silently dropping the setting.
|
|
decoder := yaml.NewDecoder(strings.NewReader(keys))
|
|
decoder.KnownFields(true)
|
|
|
|
if err := decoder.Decode(&c.config.Keys); err != nil && !errors.Is(err, io.EOF) {
|
|
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{"key"},
|
|
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)
|
|
|
|
// seenSeries maps each metric name plus constant label set to the value
|
|
// that first produced it. Two values resolving to the same series would make
|
|
// every scrape fail to gather, so duplicates are rejected here as well.
|
|
seenSeries := make(map[string]string)
|
|
|
|
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
|
|
}
|
|
|
|
m, err := multiplier.New(value.Multiplier)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("value %q of key %s: %w", value.Name, label, err))
|
|
|
|
continue
|
|
}
|
|
|
|
value.multiplier = m
|
|
|
|
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
|
|
}
|
|
|
|
series := seriesKey(value.Metric, value.Labels)
|
|
if prevValue, seen := seenSeries[series]; seen {
|
|
errs = append(errs, fmt.Errorf(
|
|
"value %q of key %s: metric %q with labels %v duplicates the series of %s; values sharing a metric name must have distinct labels",
|
|
value.Name, label, value.Metric, value.Labels, prevValue,
|
|
))
|
|
|
|
continue
|
|
}
|
|
|
|
seenSeries[series] = fmt.Sprintf("value %q of key %s", value.Name, label)
|
|
|
|
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 {
|
|
// Child failures are reported by key_success and classified as no-data warnings here.
|
|
return fmt.Errorf(
|
|
"failed to open registry key %s: %s: %w",
|
|
key.label,
|
|
err.Error(),
|
|
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: %s: %w",
|
|
value.Name,
|
|
key.label,
|
|
err.Error(),
|
|
types.ErrNoData,
|
|
))
|
|
|
|
continue
|
|
}
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
|
value.desc,
|
|
value.metricType,
|
|
value.multiplier.Apply(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
|
|
}
|
|
|
|
// seriesKey returns a string identifying the series a value is exported as: its
|
|
// metric name plus its constant labels sorted by name. Names and values are
|
|
// quoted, so distinct label sets can never produce the same key.
|
|
func seriesKey(metric string, labels map[string]string) string {
|
|
var sb strings.Builder
|
|
|
|
sb.WriteString(strconv.Quote(metric))
|
|
|
|
for _, name := range slices.Sorted(maps.Keys(labels)) {
|
|
sb.WriteString(",")
|
|
sb.WriteString(strconv.Quote(name))
|
|
sb.WriteString("=")
|
|
sb.WriteString(strconv.Quote(labels[name]))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// 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), "_"), "_")
|
|
}
|