mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-06 12:49:05 +02:00
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:
co-authored by
Jan-Otto Kröpke
parent
6e4a2c7138
commit
1d2ccf9910
@@ -45,6 +45,7 @@ A Prometheus exporter for Windows machines.
|
||||
| [physical_disk](docs/collector.physical_disk.md) | physical disk metrics | ✓ |
|
||||
| [printer](docs/collector.printer.md) | Printer metrics | |
|
||||
| [process](docs/collector.process.md) | Per-process metrics | |
|
||||
| [registry](docs/collector.registry.md) | Windows registry values (REG_DWORD and REG_QWORD) | |
|
||||
| [remote_fx](docs/collector.remote_fx.md) | RemoteFX protocol (RDP) metrics | |
|
||||
| [scheduled_task](docs/collector.scheduled_task.md) | Scheduled Tasks metrics | |
|
||||
| [service](docs/collector.service.md) | Service state metrics | ✓ |
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
# registry collector
|
||||
|
||||
The registry collector exposes configured Windows registry values as named metrics.
|
||||
Only values of type `REG_DWORD` and `REG_QWORD` are supported.
|
||||
|
||||
Like the [performancecounter](collector.performancecounter.md) collector, each
|
||||
registry value is mapped to its own metric: the key is a grouping container, and
|
||||
every value under it declares the metric name, type, and labels it is exported as.
|
||||
|
||||
|
||||
|
||||
| | |
|
||||
|---------------------|------------------|
|
||||
| Metric name prefix | `registry` |
|
||||
| Data source | Windows Registry |
|
||||
| Enabled by default? | No |
|
||||
|
||||
## Flags
|
||||
|
||||
### `--collector.registry.keys`
|
||||
|
||||
Keys is a list of registry keys to collect values from. The value takes the form
|
||||
of a JSON array of objects. YAML is supported.
|
||||
|
||||
> [!CAUTION]
|
||||
> If you are using a configuration file, the value must be kept as a string.
|
||||
>
|
||||
> Use a `|-` to keep the value as a string.
|
||||
|
||||
#### Example
|
||||
|
||||
```yaml
|
||||
collector:
|
||||
registry:
|
||||
keys: |-
|
||||
- name: windows_nt
|
||||
key: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
|
||||
values:
|
||||
- name: CurrentMajorVersionNumber
|
||||
metric: windows_registry_windows_major_version
|
||||
- name: memory_management
|
||||
key: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
|
||||
values:
|
||||
- name: ClearPageFileAtShutdown
|
||||
```
|
||||
|
||||
#### Schema
|
||||
|
||||
YAML:
|
||||
|
||||
```yaml
|
||||
- name: windows_nt # required, unique id for the key group
|
||||
key: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
|
||||
values:
|
||||
- name: CurrentMajorVersionNumber # registry value name
|
||||
metric: windows_registry_windows_major_version # optional
|
||||
help: Windows major version number # optional
|
||||
type: gauge # optional
|
||||
labels: # optional
|
||||
product: windows
|
||||
- name: memory_management # required
|
||||
key: HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
|
||||
values:
|
||||
- name: ClearPageFileAtShutdown
|
||||
```
|
||||
|
||||
JSON:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "windows_nt",
|
||||
"key": "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
|
||||
"values": [
|
||||
{
|
||||
"name": "CurrentMajorVersionNumber",
|
||||
"metric": "windows_registry_windows_major_version",
|
||||
"help": "Windows major version number",
|
||||
"type": "gauge",
|
||||
"labels": { "product": "windows" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "memory_management",
|
||||
"key": "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management",
|
||||
"values": [
|
||||
{ "name": "ClearPageFileAtShutdown" }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### name
|
||||
|
||||
Required, unique id for the key group. It is used as the `name` label on
|
||||
`windows_registry_key_success`, to identify the key in logs, and to seed
|
||||
auto-generated metric names (see [Metric naming](#metric-naming)). Each key must
|
||||
have a distinct `name`; duplicates are rejected at build time.
|
||||
|
||||
#### key
|
||||
|
||||
The full path of the registry key, including the hive. Both short and long hive
|
||||
names are accepted, case-insensitive, and forward slashes may be used instead of
|
||||
backslashes:
|
||||
|
||||
| Short | Long |
|
||||
| --- | --- |
|
||||
| `HKLM` | `HKEY_LOCAL_MACHINE` |
|
||||
| `HKCU` | `HKEY_CURRENT_USER` |
|
||||
| `HKU` | `HKEY_USERS` |
|
||||
| `HKCR` | `HKEY_CLASSES_ROOT` |
|
||||
| `HKCC` | `HKEY_CURRENT_CONFIG` |
|
||||
|
||||
Each key must be unique after normalization (compared case-insensitively).
|
||||
Subkeys are never recursed into. To collect values from a subkey, add it as a
|
||||
separate entry.
|
||||
|
||||
#### values
|
||||
|
||||
The list of registry values to read from the key. At least one value must be
|
||||
listed; the value names are matched case-insensitively.
|
||||
|
||||
A value name listed more than once under one key (compared case-insensitively) is
|
||||
rejected as a duplicate. If a value is missing at scrape time or is not a
|
||||
`REG_DWORD`/`REG_QWORD`, the scrape reports a failure for that key via
|
||||
`windows_registry_key_success`.
|
||||
|
||||
#### values Sub-Schema
|
||||
|
||||
##### name
|
||||
|
||||
The name of the registry value to collect. Required.
|
||||
|
||||
##### metric
|
||||
|
||||
The name of the metric to expose. Optional — if omitted, a name is generated
|
||||
automatically. See [Metric naming](#metric-naming) for the exact rules and
|
||||
examples.
|
||||
|
||||
The combination of metric name and labels must be unique across all configured
|
||||
keys and values. Two values may deliberately share a `metric` name when their
|
||||
labels differ (a common way to aggregate the same measurement from several keys).
|
||||
A true duplicate — the same name *and* identical labels — is dropped and logged at
|
||||
scrape time.
|
||||
|
||||
##### help
|
||||
|
||||
The metric `# HELP` text. Optional — if omitted, it defaults to
|
||||
`windows_exporter: custom registry metric`.
|
||||
|
||||
##### type
|
||||
|
||||
The metric type. The value can be `gauge` or `counter`. If not specified, it
|
||||
defaults to `gauge`, which suits the configuration-style values typically stored
|
||||
in the registry.
|
||||
|
||||
This key is optional.
|
||||
|
||||
##### labels
|
||||
|
||||
Labels is a map of key-value pairs that will be added as constant labels to the
|
||||
metric. Two values may share the same `metric` name as long as their labels
|
||||
distinguish the resulting series.
|
||||
|
||||
This key is optional.
|
||||
|
||||
## Metrics
|
||||
|
||||
The registry collector returns one metric per configured value, named and typed
|
||||
according to the configuration, plus a per-key success metric.
|
||||
|
||||
| Name | Description | Type | Labels |
|
||||
| --- | --- | --- | --- |
|
||||
| *user defined* | Numeric value of a configured REG_DWORD or REG_QWORD value | gauge / counter | *user defined* |
|
||||
| `windows_registry_key_success` | Whether the key could be opened and all of its configured values read (0, 1) | gauge | `name` |
|
||||
|
||||
A `windows_registry_key_success` value of `0` means the key could not be opened
|
||||
*or* at least one configured value was missing or not a `REG_DWORD`/`REG_QWORD`;
|
||||
values that did read successfully are still exported.
|
||||
|
||||
The `name` label on `windows_registry_key_success` is the configured key
|
||||
[`name`](#name), used verbatim. Because each key must have a unique `name`, the
|
||||
success metric can never emit duplicate series.
|
||||
|
||||
### Metric naming
|
||||
|
||||
Each value is exported under its own metric name. You can set it explicitly with
|
||||
the `metric` field, or let the collector generate one.
|
||||
|
||||
**Explicit name.** When `metric` is set, it is used verbatim. You are responsible
|
||||
for following the Prometheus
|
||||
[naming conventions](https://prometheus.io/docs/practices/naming/) (a `windows_`
|
||||
prefix, and a unit suffix such as `_bytes` or `_seconds` where applicable):
|
||||
|
||||
```yaml
|
||||
values:
|
||||
- name: CurrentMajorVersionNumber
|
||||
metric: windows_registry_windows_major_version
|
||||
```
|
||||
|
||||
→ `windows_registry_windows_major_version`
|
||||
|
||||
**Auto-generated name.** When `metric` is omitted, the name is assembled as:
|
||||
|
||||
```
|
||||
windows_registry_<group>_<value>
|
||||
```
|
||||
|
||||
where `<group>` is the key's [`name`](#name) and `<value>` is the registry value
|
||||
name. The whole string is then lowercased, every character that is not a letter or
|
||||
digit is replaced with `_`, and leading/trailing `_` are trimmed.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
- name: windows_nt
|
||||
key: HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion
|
||||
values:
|
||||
- name: CurrentMajorVersionNumber
|
||||
```
|
||||
|
||||
→ `windows_registry_windows_nt_currentmajorversionnumber`
|
||||
|
||||
Each special character is replaced individually rather than collapsed, so a value
|
||||
named `Foo (Bar)` becomes `foo__bar` (note the double underscore). Prefer an
|
||||
explicit `metric` for readable names over awkward value names.
|
||||
|
||||
> [!NOTE]
|
||||
> The group `name` is not a namespace override: `name: cpu` produces
|
||||
> `windows_registry_cpu_...`, not `windows_cpu_...`. Use an explicit `metric` to
|
||||
> control the entire name.
|
||||
|
||||
### Example metric
|
||||
|
||||
For the example configuration above:
|
||||
|
||||
```
|
||||
# HELP windows_registry_windows_major_version windows_exporter: custom registry metric
|
||||
# TYPE windows_registry_windows_major_version gauge
|
||||
windows_registry_windows_major_version{product="windows"} 10
|
||||
# HELP windows_registry_memory_management_clearpagefileatshutdown windows_exporter: custom registry metric
|
||||
# TYPE windows_registry_memory_management_clearpagefileatshutdown gauge
|
||||
windows_registry_memory_management_clearpagefileatshutdown 0
|
||||
# HELP windows_registry_key_success Whether the registry key could be read successfully.
|
||||
# TYPE windows_registry_key_success gauge
|
||||
windows_registry_key_success{name="windows_nt"} 1
|
||||
windows_registry_key_success{name="memory_management"} 1
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `REG_DWORD` values are interpreted as unsigned 32-bit integers. Applications
|
||||
that store negative numbers in a DWORD will surface as large positive values.
|
||||
- `REG_QWORD` values larger than 2^53 lose precision when converted to the
|
||||
64-bit float used by Prometheus.
|
||||
|
||||
## Useful queries
|
||||
|
||||
Alert if a registry value deviates from the expected value:
|
||||
|
||||
```
|
||||
windows_registry_memory_management_clearpagefileatshutdown != 0
|
||||
```
|
||||
|
||||
## Alerting examples
|
||||
|
||||
**prometheus.rules**
|
||||
|
||||
```yaml
|
||||
- alert: RegistryKeyReadFailure
|
||||
expr: windows_registry_key_success == 0
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Registry key could not be read (instance {{ $labels.instance }})"
|
||||
description: "The registry key {{ $labels.name }} could not be read for 15 minutes."
|
||||
```
|
||||
@@ -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(®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) 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(®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`)
|
||||
}
|
||||
@@ -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, ®istry.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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/physical_disk"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/printer"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/process"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/registry"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/remote_fx"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/scheduled_task"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/service"
|
||||
@@ -79,7 +80,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/pdh"
|
||||
"github.com/prometheus-community/windows_exporter/internal/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
winregistry "golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
// NewWithFlags To be called by the exporter for collector initialization before running kingpin.Parse.
|
||||
@@ -130,6 +131,7 @@ func NewWithConfig(config Config) *Collection {
|
||||
collectors[physical_disk.Name] = physical_disk.New(&config.PhysicalDisk)
|
||||
collectors[printer.Name] = printer.New(&config.Printer)
|
||||
collectors[process.Name] = process.New(&config.Process)
|
||||
collectors[registry.Name] = registry.New(&config.Registry)
|
||||
collectors[remote_fx.Name] = remote_fx.New(&config.RemoteFx)
|
||||
collectors[scheduled_task.Name] = scheduled_task.New(&config.ScheduledTask)
|
||||
collectors[service.Name] = service.New(&config.Service)
|
||||
@@ -241,7 +243,7 @@ func (c *Collection) Build(ctx context.Context, logger *slog.Logger) error {
|
||||
|
||||
for err := range errCh {
|
||||
if errors.Is(err, pdh.ErrNoData) ||
|
||||
errors.Is(err, registry.ErrNotExist) ||
|
||||
errors.Is(err, winregistry.ErrNotExist) ||
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoObject)) ||
|
||||
errors.Is(err, pdh.NewPdhError(pdh.CstatusNoCounter)) ||
|
||||
errors.Is(err, mi.MI_RESULT_INVALID_OPERATION_TIMEOUT) ||
|
||||
|
||||
@@ -50,6 +50,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/physical_disk"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/printer"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/process"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/registry"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/remote_fx"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/scheduled_task"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/service"
|
||||
@@ -100,6 +101,7 @@ type Config struct {
|
||||
PhysicalDisk physical_disk.Config `yaml:"physical_disk"`
|
||||
Printer printer.Config `yaml:"printer"`
|
||||
Process process.Config `yaml:"process"`
|
||||
Registry registry.Config `yaml:"registry"`
|
||||
RemoteFx remote_fx.Config `yaml:"remote_fx"`
|
||||
ScheduledTask scheduled_task.Config `yaml:"scheduled_task"`
|
||||
Service service.Config `yaml:"service"`
|
||||
@@ -154,6 +156,7 @@ var ConfigDefaults = Config{
|
||||
PhysicalDisk: physical_disk.ConfigDefaults,
|
||||
Printer: printer.ConfigDefaults,
|
||||
Process: process.ConfigDefaults,
|
||||
Registry: registry.ConfigDefaults,
|
||||
RemoteFx: remote_fx.ConfigDefaults,
|
||||
ScheduledTask: scheduled_task.ConfigDefaults,
|
||||
Service: service.ConfigDefaults,
|
||||
|
||||
@@ -54,6 +54,7 @@ import (
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/physical_disk"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/printer"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/process"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/registry"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/remote_fx"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/scheduled_task"
|
||||
"github.com/prometheus-community/windows_exporter/internal/collector/service"
|
||||
@@ -111,6 +112,7 @@ var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{
|
||||
physical_disk.Name: NewBuilderWithFlags(physical_disk.NewWithFlags),
|
||||
printer.Name: NewBuilderWithFlags(printer.NewWithFlags),
|
||||
process.Name: NewBuilderWithFlags(process.NewWithFlags),
|
||||
registry.Name: NewBuilderWithFlags(registry.NewWithFlags),
|
||||
remote_fx.Name: NewBuilderWithFlags(remote_fx.NewWithFlags),
|
||||
scheduled_task.Name: NewBuilderWithFlags(scheduled_task.NewWithFlags),
|
||||
service.Name: NewBuilderWithFlags(service.NewWithFlags),
|
||||
|
||||
Reference in New Issue
Block a user