diff --git a/AGENTS.md b/AGENTS.md index 7a1cc820..7ffc3f1d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,7 +143,11 @@ exercised a live Windows role. - Keep CLI and YAML validation consistent. Reject unknown fields, unsupported sub-collector names and invalid values; preserve CLI precedence and repeated flag/list behavior. Changes to config shapes also need schema, flag parsing - and documentation updates. + and documentation updates. A collector is registered once, in the + `registrations` table of `pkg/collector/map.go`, with a `Config` field whose + YAML key is the collector name and a `docs/collector..md`; + `TestRegistrations` checks that they agree. The configuration file schema in + `internal/config` only checks keys: the flags parse and validate the values. - For textfiles, keep HELP/type metadata consistent across files and preserve unrelated valid series when reporting duplicates or parse failures. Publish producer output by replacing a temporary file in the same directory. File diff --git a/UPGRADE.md b/UPGRADE.md index 7e6eabcf..75f8fda4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -115,6 +115,10 @@ windows_exporter 0.32 rejects configuration it used to ignore. The exporter stop ``` The key `scrape_interval` was accepted by 0.31 but had no effect. It now fails validation. +- **pagefile and smbclient blocks.** In a configuration file, the `collector` blocks of the `pagefile` and `smbclient` collectors are named after the collectors. The keys `paging` and `smb_client` were accepted by 0.31 but had no effect, as both collectors have no options. They now fail validation. +- **Invalid values.** An invalid value in the configuration file, such as `compress: maybe`, stops the exporter with the line and key of the value, also when the same flag is set on the command line. + +The configuration file now accepts every value the matching command-line flag accepts: quoted booleans and numbers (`compress: "true"`, `max-size: "10"`), comma-separated lists for list options (`enabled: cluster,node`), YAML lists for comma-separated options (`collectors.enabled: [cpu, memory]`) and YAML merge keys (`<<`). 0.31 rejected them. Removed settings stop the exporter at startup if they are still set: `--collector.process.counter-version` (see [process collector](#process-collector)), `--collector.container.containerd-state-dir` (see [container collector](#container-collector)) and the collector names `filetime` and `thermalzone` in `--collectors.enabled`. The same applies to their configuration file keys. @@ -423,6 +427,7 @@ These changes affect Go programs that embed `github.com/prometheus-community/win - `container.Config.ContainerDStateDir` was replaced by `container.Config.CRIEndpoint`. See [container collector](#container-collector). - `process.Config.CounterVersion` was removed. See [process collector](#process-collector). - New field in `collector.Config`: `StorageSpaces`. +- The YAML keys of `collector.Config.Paging` and `collector.Config.SMBClient` are now `pagefile` and `smbclient`, the collector names, instead of `paging` and `smb_client`. The Go field names are unchanged. Both configurations are empty, so this only matters if you decode YAML with unknown fields rejected. - `scheduled_task.ConfigDefaults.TaskExclude`, also used when `scheduled_task.Config.TaskExclude` is nil, now excludes `/Microsoft/.+`, like the flag. New field `scheduled_task.Config.MissedRuns`, false by default. - `collector.Config.ThermalZone` was removed together with the `thermalzone` collector. - New method `Collection.NewHandlerWithContext`. `Collection.NewHandler` is unchanged. diff --git a/internal/config/config.go b/internal/config/config.go index 1cc965ea..988607bc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -21,7 +21,10 @@ import ( "errors" "fmt" "io" + "maps" "os" + "reflect" + "slices" "strings" "github.com/alecthomas/kingpin/v2" @@ -30,7 +33,9 @@ import ( ) // configFile represents the structure of the windows_exporter configuration file, -// including configuration from the collector and web packages. +// including configuration from the collector and web packages. Each leaf is a +// flag; the field types document the value of the flag, which parses and +// validates it. [validateKeys] checks the keys of the file against this schema. type configFile struct { Debug struct { Enabled bool `yaml:"enabled"` @@ -45,17 +50,17 @@ type configFile struct { Format string `yaml:"format"` File string `yaml:"file"` Rotation struct { - MaxSize string `yaml:"max-size"` - MaxBackups string `yaml:"max-backups"` - Compress bool `yaml:"compress"` + MaxSize int `yaml:"max-size"` + MaxBackups int `yaml:"max-backups"` + Compress bool `yaml:"compress"` } `yaml:"rotation"` } `yaml:"log"` Process struct { Priority string `yaml:"priority"` - MemoryLimit string `yaml:"memory-limit"` + MemoryLimit int64 `yaml:"memory-limit"` } `yaml:"process"` Scrape struct { - TimeoutMargin string `yaml:"timeout-margin"` + TimeoutMargin float64 `yaml:"timeout-margin"` } `yaml:"scrape"` Telemetry struct { Path string `yaml:"path"` @@ -76,6 +81,8 @@ type getFlagger interface { // Resolver represents a configuration file resolver for kingpin. type Resolver struct { flags map[string][]string + // lines maps the keys of the configuration file to their lines. + lines map[string]int } // Parse parses the command line arguments and configuration files. @@ -135,12 +142,9 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) { _ = file.Close() }() - var configFileStructure configFile + var document yaml.Node - decoder := yaml.NewDecoder(file) - decoder.KnownFields(true) - - if err = decoder.Decode(&configFileStructure); err != nil { + if err = yaml.NewDecoder(file).Decode(&document); err != nil { // Handle EOF error gracefully, indicating no configuration was found. if errors.Is(err, io.EOF) { return &Resolver{flags: flags}, nil @@ -149,18 +153,18 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) { return nil, fmt.Errorf("configuration file validation error: %w", err) } - _, err = file.Seek(0, io.SeekStart) - if err != nil { - return nil, fmt.Errorf("failed to rewind file: %w", err) - } - + // Decoding first rejects duplicate keys and expands aliases within the + // limits of the decoder before the schema walks the document. var rawValues map[string]any - decoder = yaml.NewDecoder(file) - if err = decoder.Decode(&rawValues); err != nil { + if err = document.Decode(&rawValues); err != nil { return nil, fmt.Errorf("failed to parse configuration file: %w", err) } + if err = validateKeys(&document, "", reflect.TypeFor[configFile]()); err != nil { + return nil, fmt.Errorf("configuration file validation error: %w", err) + } + // Flatten nested YAML values flattenedValues := flatten(rawValues) for k, v := range flattenedValues { @@ -169,16 +173,25 @@ func NewConfigFileResolver(filePath string) (*Resolver, error) { } } - return &Resolver{flags: flags}, nil + lines := map[string]int{} + keyLines(&document, "", lines) + + return &Resolver{flags: flags, lines: lines}, nil } -func (c *Resolver) setDefault(v getFlagger) { - for name, values := range c.flags { +func (c *Resolver) setDefault(v getFlagger) error { + for _, name := range slices.Sorted(maps.Keys(c.flags)) { + values := c.flags[name] + f := v.GetFlag(name) if f == nil { continue } + if err := checkFlagValue(f, values); err != nil { + return fmt.Errorf("line %d: %s: %w", c.lines[name], name, err) + } + // A list sets a repeatable flag, like web.listen-address, once per element. // Other flags take the list as a comma-separated value. if repeatable, ok := f.Model().Value.(interface{ IsCumulative() bool }); ok && repeatable.IsCumulative() { @@ -187,6 +200,8 @@ func (c *Resolver) setDefault(v getFlagger) { f.Default(strings.Join(values, ",")) } } + + return nil } // Bind sets active flags with their default values from the configuration file(s). @@ -197,10 +212,12 @@ func (c *Resolver) Bind(app *kingpin.Application, args []string) error { return err } - c.setDefault(app) + if err = c.setDefault(app); err != nil { + return err + } if pc.SelectedCommand != nil { - c.setDefault(pc.SelectedCommand) + return c.setDefault(pc.SelectedCommand) } return nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 999c1fbf..86aa5612 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -109,6 +109,21 @@ func TestNewConfigFileResolver(t *testing.T) { contents: "collector:\n registry:\n keys: |-\n - name: test\n key: HKLM\\SOFTWARE\n values:\n - name: test\n metrc: test\n", errorText: "field metrc not found in type registry.Value", }, + {name: "quoted bool", contents: "log:\n rotation:\n compress: \"true\"\n"}, + {name: "quoted number", contents: "log:\n rotation:\n max-size: \"10\"\n"}, + {name: "list as string", contents: "collector:\n mscluster:\n enabled: cluster,node\n"}, + {name: "string as list", contents: "collectors:\n enabled: [cpu, memory]\n"}, + {name: "duration as string", contents: "collector:\n hyperv:\n virtual-hard-disk:\n refresh-interval: 30s\n"}, + {name: "renamed collector block", contents: "collector:\n pagefile:\n smbclient:\n"}, + {name: "old collector block", contents: "collector:\n paging:\n", errorText: "line 2: field paging not found in collector"}, + {name: "unknown nested key", contents: "collector:\n cpu:\n unknown: true\n", errorText: "line 3: field unknown not found in collector.cpu"}, + {name: "unknown top-level key", contents: "unknown: value\n", errorText: "line 1: field unknown not found in the configuration"}, + {name: "mapping as value", contents: "log:\n level:\n debug: true\n", errorText: "line 3: log.level must be a value or a list of values"}, + {name: "mapping in list", contents: "web:\n listen-address:\n - port: 9182\n", errorText: "line 3: web.listen-address must be a value or a list of values"}, + {name: "value as section", contents: "log: debug\n", errorText: "line 1: log must be a mapping"}, + {name: "merge key", contents: "collector:\n service: &filter\n include: a\n process:\n <<: *filter\n exclude: b\n"}, + {name: "merged section", contents: "log: &log\n level: debug\nweb:\n <<: *log\n", errorText: "line 2: field level not found in web"}, + {name: "duplicate key", contents: "log:\n level: debug\n level: info\n", errorText: "already defined"}, { name: "unknown wmi option", contents: "collector:\n wmi:\n query: |-\n - name: test\n", @@ -174,6 +189,11 @@ func TestNewConfigFileResolverFlags(t *testing.T) { value, values := sampleConfigValue(t, field, block) + // These flags only accept some strings. + if sample, ok := map[string]string{"log.level": "debug", "log.format": "json"}[flag.Name]; ok { + value, values = sample, []string{sample} + } + contents := value for _, key := range slices.Backward(keys) { contents = map[string]any{key: contents} @@ -431,6 +451,8 @@ func sampleConfigValue(t *testing.T, typ reflect.Type, block bool) (any, []strin case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return 1, []string{"1"} + case reflect.Float32, reflect.Float64: + return 1.5, []string{"1.5"} case reflect.Slice, reflect.Interface: return []string{"test1", "test2"}, []string{"test1", "test2"} case reflect.String: @@ -525,3 +547,75 @@ func TestParseEmptyValue(t *testing.T) { }) } } + +// TestParseFlagValues verifies that the configuration file takes the values the +// flags accept, whatever the Go type of the configuration, and that an invalid +// value fails with its key and line, even if the command line sets the flag. +func TestParseFlagValues(t *testing.T) { + t.Parallel() + + mainFlags := mainFlagNames(t) + + for _, tc := range []struct { + name string + config string + args []string + flag string + want string + errorText string + }{ + {name: "quoted bool", config: "log:\n rotation:\n compress: \"true\"\n", flag: "log.rotation.compress", want: "true"}, + {name: "quoted int", config: "log:\n rotation:\n max-size: \"10\"\n", flag: "log.rotation.max-size", want: "10"}, + { + name: "comma-separated list", config: "collector:\n mscluster:\n enabled: cluster,node\n", + flag: "collector.mscluster.enabled", want: "cluster,node", + }, + { + name: "list of strings", config: "collector:\n mscluster:\n enabled: [cluster, node]\n", + flag: "collector.mscluster.enabled", want: "cluster,node", + }, + { + name: "repeatable flag", config: "web:\n listen-address: ['127.0.0.1:9182', '[::1]:9182']\n", + flag: "web.listen-address", want: "127.0.0.1:9182,[::1]:9182", + }, + { + name: "merge key", config: "collector:\n service: &filter\n include: a\n process:\n <<: *filter\n exclude: b\n", + flag: "collector.process.include", want: "a", + }, + { + name: "CLI overrides config", config: "log:\n rotation:\n compress: true\n", + args: []string{"--no-log.rotation.compress"}, flag: "log.rotation.compress", want: "false", + }, + { + name: "invalid bool", config: "log:\n rotation:\n compress: maybe\n", + errorText: "line 3: log.rotation.compress: strconv.ParseBool", + }, + { + name: "invalid int", config: "debug:\n enabled: true\nlog:\n rotation:\n max-size: ten\n", + errorText: "line 5: log.rotation.max-size: strconv.Parse", + }, + { + name: "invalid value set on the CLI", config: "log:\n rotation:\n compress: maybe\n", + args: []string{"--log.rotation.compress"}, errorText: "line 3: log.rotation.compress: strconv.ParseBool", + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte(tc.config), 0o600)) + + app := newApp(mainFlags) + + err := config.Parse(app, append([]string{"--config.file=" + path}, tc.args...)) + if tc.errorText != "" { + require.ErrorContains(t, err, tc.errorText) + + return + } + + require.NoError(t, err) + require.Equal(t, tc.want, app.GetFlag(tc.flag).Model().Value.String()) + }) + } +} diff --git a/internal/config/schema.go b/internal/config/schema.go new file mode 100644 index 00000000..4b1c2fa0 --- /dev/null +++ b/internal/config/schema.go @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package config + +import ( + "fmt" + "reflect" + "slices" + "strings" + + "github.com/alecthomas/kingpin/v2" + "go.yaml.in/yaml/v3" +) + +// validateKeys checks that node, the value of the dotted key path, only uses +// the keys of the schema typ. +// +// The configuration file sets flags, and the flags parse and validate the +// values (see [checkFlagValue]). So the schema only defines the keys: a leaf +// takes any scalar or a list of scalars, whatever the type of its field. +// Decoding the values into the field types would reject values that the flags +// accept, like compress: "true" or a comma-separated list for a list field. +// +// A struct that decodes itself, like the performancecounter block, is decoded +// to validate its contents. +func validateKeys(node *yaml.Node, path string, typ reflect.Type) error { + switch node.Kind { + case yaml.DocumentNode: + for _, content := range node.Content { + if err := validateKeys(content, path, typ); err != nil { + return err + } + } + + return nil + case yaml.AliasNode: + return validateKeys(node.Alias, path, typ) + case yaml.ScalarNode: + if node.Tag == "!!null" { + // A key without a value leaves the flags unchanged. + return nil + } + case yaml.MappingNode, yaml.SequenceNode: + } + + if reflect.PointerTo(typ).Implements(reflect.TypeFor[yaml.Unmarshaler]()) { + //nolint:wrapcheck // the decoder errors name the line and field. + return node.Decode(reflect.New(typ).Interface()) + } + + if typ.Kind() != reflect.Struct { + if !isValue(node) { + return fmt.Errorf("line %d: %s must be a value or a list of values", node.Line, displayPath(path)) + } + + return nil + } + + if node.Kind != yaml.MappingNode { + return fmt.Errorf("line %d: %s must be a mapping", node.Line, displayPath(path)) + } + + for i := 0; i+1 < len(node.Content); i += 2 { + key, value := node.Content[i], node.Content[i+1] + + if isMerge(key) { + // A merge key (<<) adds the keys of other mappings. + for _, source := range mergeSources(value) { + if err := validateKeys(source, path, typ); err != nil { + return err + } + } + + continue + } + + field, ok := schemaField(typ, key.Value) + if !ok { + return fmt.Errorf("line %d: field %s not found in %s", key.Line, key.Value, displayPath(path)) + } + + if err := validateKeys(value, joinPath(path, key.Value), field.Type); err != nil { + return err + } + } + + return nil +} + +// isValue reports whether node is the value of a flag: a scalar, or a list of +// scalars for a repeatable or list flag. +func isValue(node *yaml.Node) bool { + switch node.Kind { + case yaml.ScalarNode: + return true + case yaml.AliasNode: + return isValue(node.Alias) + case yaml.SequenceNode: + for _, item := range node.Content { + if item.Kind == yaml.AliasNode { + item = item.Alias + } + + if item.Kind != yaml.ScalarNode { + return false + } + } + + return true + case yaml.DocumentNode, yaml.MappingNode: + } + + return false +} + +// keyLines records the line of every dotted key path below node in lines. +// Explicit keys take precedence over the keys of merged mappings, like in the +// decoded values. +func keyLines(node *yaml.Node, path string, lines map[string]int) { + switch node.Kind { + case yaml.DocumentNode: + for _, content := range node.Content { + keyLines(content, path, lines) + } + case yaml.AliasNode: + keyLines(node.Alias, path, lines) + case yaml.MappingNode: + for i := 0; i+1 < len(node.Content); i += 2 { + if isMerge(node.Content[i]) { + for _, source := range mergeSources(node.Content[i+1]) { + keyLines(source, path, lines) + } + } + } + + for i := 0; i+1 < len(node.Content); i += 2 { + key, value := node.Content[i], node.Content[i+1] + if isMerge(key) { + continue + } + + keyPath := joinPath(path, key.Value) + lines[keyPath] = key.Line + keyLines(value, keyPath, lines) + } + case yaml.ScalarNode, yaml.SequenceNode: + } +} + +func isMerge(key *yaml.Node) bool { + return key.Kind == yaml.ScalarNode && key.Tag == "!!merge" +} + +// mergeSources returns the mappings that a merge key merges: an alias, a +// mapping or a list of them. +func mergeSources(value *yaml.Node) []*yaml.Node { + if value.Kind == yaml.SequenceNode { + return value.Content + } + + return []*yaml.Node{value} +} + +func joinPath(path, key string) string { + if path == "" { + return key + } + + return path + "." + key +} + +func displayPath(path string) string { + if path == "" { + return "the configuration" + } + + return path +} + +// schemaField returns the field of the struct typ with the YAML key name, +// looking into the fields inlined with the yaml inline option. +func schemaField(typ reflect.Type, name string) (reflect.StructField, bool) { + for field := range typ.Fields() { + if !field.IsExported() { + continue + } + + key, options, _ := strings.Cut(field.Tag.Get("yaml"), ",") + + if slices.Contains(strings.Split(options, ","), "inline") && field.Type.Kind() == reflect.Struct { + if inlined, ok := schemaField(field.Type, name); ok { + return inlined, true + } + + continue + } + + if key == "" { + key = strings.ToLower(field.Name) + } + + if key != "-" && key == name { + return field, true + } + } + + return reflect.StructField{}, false +} + +// checkFlagValue checks that the flag accepts the values from the +// configuration file. +// +// The flags only parse their default values in kingpin.Application.Parse, +// which does not tell the configuration key, and does not parse them at all +// if the flag is also set on the command line. kingpin cannot copy a flag +// value, so the check sets the flag value itself. That is safe: Parse sets it +// again, to the same default or to the command-line value. A repeatable flag +// would append the values twice, so its list is restored after the check. +func checkFlagValue(flag *kingpin.FlagClause, values []string) error { + value := flag.Model().Value + + if repeatable, ok := value.(interface{ IsCumulative() bool }); !ok || !repeatable.IsCumulative() { + //nolint:wrapcheck // the caller adds the key and line. + return value.Set(strings.Join(values, ",")) + } + + getter, ok := value.(kingpin.Getter) + if !ok { + return nil + } + + list := reflect.ValueOf(getter.Get()) + if list.Kind() != reflect.Pointer || list.Elem().Kind() != reflect.Slice { + // The list cannot be restored; Parse checks the values. + return nil + } + + saved := reflect.ValueOf(list.Elem().Interface()) + defer list.Elem().Set(saved) + + for _, v := range values { + if err := value.Set(v); err != nil { + //nolint:wrapcheck // the caller adds the key and line. + return err + } + } + + return nil +} diff --git a/internal/config/schema_internal_test.go b/internal/config/schema_internal_test.go new file mode 100644 index 00000000..cdc35903 --- /dev/null +++ b/internal/config/schema_internal_test.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build windows + +package config + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v3" +) + +// TestValidateKeysInline verifies that the keys of a field inlined with the +// yaml inline option belong to the enclosing mapping. +func TestValidateKeysInline(t *testing.T) { + t.Parallel() + + type Inner struct { + Level string `yaml:"level"` + } + + type schema struct { + Log struct { + Inner `yaml:",inline"` + + Format string `yaml:"format"` + } `yaml:"log"` + } + + for _, tc := range []struct { + name string + contents string + errorText string + }{ + {name: "inlined key", contents: "log:\n level: debug\n format: json\n"}, + {name: "unknown key", contents: "log:\n inner: debug\n", errorText: "line 2: field inner not found in log"}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var document yaml.Node + require.NoError(t, yaml.Unmarshal([]byte(tc.contents), &document)) + + err := validateKeys(&document, "", reflect.TypeFor[schema]()) + if tc.errorText != "" { + require.EqualError(t, err, tc.errorText) + + return + } + + require.NoError(t, err) + }) + } +} diff --git a/pkg/collector/collection.go b/pkg/collector/collection.go index 7df29381..5066d777 100644 --- a/pkg/collector/collection.go +++ b/pkg/collector/collection.go @@ -28,56 +28,6 @@ import ( gotime "time" "github.com/alecthomas/kingpin/v2" - "github.com/prometheus-community/windows_exporter/internal/collector/ad" - "github.com/prometheus-community/windows_exporter/internal/collector/adcs" - "github.com/prometheus-community/windows_exporter/internal/collector/adfs" - "github.com/prometheus-community/windows_exporter/internal/collector/cache" - "github.com/prometheus-community/windows_exporter/internal/collector/container" - "github.com/prometheus-community/windows_exporter/internal/collector/cpu" - "github.com/prometheus-community/windows_exporter/internal/collector/cpu_info" - "github.com/prometheus-community/windows_exporter/internal/collector/dfsr" - "github.com/prometheus-community/windows_exporter/internal/collector/dhcp" - "github.com/prometheus-community/windows_exporter/internal/collector/diskdrive" - "github.com/prometheus-community/windows_exporter/internal/collector/dmi" - "github.com/prometheus-community/windows_exporter/internal/collector/dns" - "github.com/prometheus-community/windows_exporter/internal/collector/exchange" - "github.com/prometheus-community/windows_exporter/internal/collector/file" - "github.com/prometheus-community/windows_exporter/internal/collector/fsrmquota" - "github.com/prometheus-community/windows_exporter/internal/collector/gpu" - "github.com/prometheus-community/windows_exporter/internal/collector/hyperv" - "github.com/prometheus-community/windows_exporter/internal/collector/iis" - "github.com/prometheus-community/windows_exporter/internal/collector/license" - "github.com/prometheus-community/windows_exporter/internal/collector/logical_disk" - "github.com/prometheus-community/windows_exporter/internal/collector/memory" - "github.com/prometheus-community/windows_exporter/internal/collector/mscluster" - "github.com/prometheus-community/windows_exporter/internal/collector/msmq" - "github.com/prometheus-community/windows_exporter/internal/collector/mssql" - "github.com/prometheus-community/windows_exporter/internal/collector/net" - "github.com/prometheus-community/windows_exporter/internal/collector/netframework" - "github.com/prometheus-community/windows_exporter/internal/collector/nps" - "github.com/prometheus-community/windows_exporter/internal/collector/os" - "github.com/prometheus-community/windows_exporter/internal/collector/pagefile" - "github.com/prometheus-community/windows_exporter/internal/collector/performancecounter" - "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" - "github.com/prometheus-community/windows_exporter/internal/collector/smb" - "github.com/prometheus-community/windows_exporter/internal/collector/smbclient" - "github.com/prometheus-community/windows_exporter/internal/collector/smtp" - "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" - "github.com/prometheus-community/windows_exporter/internal/collector/system" - "github.com/prometheus-community/windows_exporter/internal/collector/tcp" - "github.com/prometheus-community/windows_exporter/internal/collector/terminal_services" - "github.com/prometheus-community/windows_exporter/internal/collector/textfile" - "github.com/prometheus-community/windows_exporter/internal/collector/time" - "github.com/prometheus-community/windows_exporter/internal/collector/udp" - "github.com/prometheus-community/windows_exporter/internal/collector/update" - "github.com/prometheus-community/windows_exporter/internal/collector/vmware" - "github.com/prometheus-community/windows_exporter/internal/collector/wmi" "github.com/prometheus-community/windows_exporter/internal/mi" "github.com/prometheus-community/windows_exporter/internal/pdh" "github.com/prometheus-community/windows_exporter/internal/types" @@ -100,57 +50,10 @@ func NewWithFlags(app *kingpin.Application) *Collection { // //goland:noinspection GoUnusedExportedFunction func NewWithConfig(config Config) *Collection { - collectors := Map{} - collectors[ad.Name] = ad.New(&config.AD) - collectors[adcs.Name] = adcs.New(&config.ADCS) - collectors[adfs.Name] = adfs.New(&config.ADFS) - collectors[cache.Name] = cache.New(&config.Cache) - collectors[container.Name] = container.New(&config.Container) - collectors[cpu.Name] = cpu.New(&config.CPU) - collectors[cpu_info.Name] = cpu_info.New(&config.CPUInfo) - collectors[dfsr.Name] = dfsr.New(&config.DFSR) - collectors[dhcp.Name] = dhcp.New(&config.Dhcp) - collectors[diskdrive.Name] = diskdrive.New(&config.DiskDrive) - collectors[dmi.Name] = dmi.New(&config.DMI) - collectors[dns.Name] = dns.New(&config.DNS) - collectors[exchange.Name] = exchange.New(&config.Exchange) - collectors[file.Name] = file.New(&config.File) - collectors[fsrmquota.Name] = fsrmquota.New(&config.Fsrmquota) - collectors[gpu.Name] = gpu.New(&config.GPU) - collectors[hyperv.Name] = hyperv.New(&config.HyperV) - collectors[iis.Name] = iis.New(&config.IIS) - collectors[license.Name] = license.New(&config.License) - collectors[logical_disk.Name] = logical_disk.New(&config.LogicalDisk) - collectors[memory.Name] = memory.New(&config.Memory) - collectors[mscluster.Name] = mscluster.New(&config.MSCluster) - collectors[msmq.Name] = msmq.New(&config.Msmq) - collectors[mssql.Name] = mssql.New(&config.Mssql) - collectors[net.Name] = net.New(&config.Net) - collectors[netframework.Name] = netframework.New(&config.NetFramework) - collectors[nps.Name] = nps.New(&config.Nps) - collectors[os.Name] = os.New(&config.OS) - collectors[pagefile.Name] = pagefile.New(&config.Paging) - collectors[performancecounter.Name] = performancecounter.New(&config.PerformanceCounter) - 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) - collectors[smb.Name] = smb.New(&config.SMB) - collectors[smbclient.Name] = smbclient.New(&config.SMBClient) - collectors[smtp.Name] = smtp.New(&config.SMTP) - collectors[storage_spaces.Name] = storage_spaces.New(&config.StorageSpaces) - collectors[system.Name] = system.New(&config.System) - collectors[tcp.Name] = tcp.New(&config.TCP) - collectors[terminal_services.Name] = terminal_services.New(&config.TerminalServices) - collectors[textfile.Name] = textfile.New(&config.Textfile) - collectors[time.Name] = time.New(&config.Time) - collectors[udp.Name] = udp.New(&config.UDP) - collectors[update.Name] = update.New(&config.Update) - collectors[vmware.Name] = vmware.New(&config.Vmware) - collectors[wmi.Name] = wmi.New(&config.WMI) + collectors := make(Map, len(registrations)) + for _, r := range registrations { + collectors[r.name] = r.newWithConfig(&config) + } return New(collectors) } diff --git a/pkg/collector/config.go b/pkg/collector/config.go index a70927fe..168778c0 100644 --- a/pkg/collector/config.go +++ b/pkg/collector/config.go @@ -99,7 +99,7 @@ type Config struct { NetFramework netframework.Config `yaml:"netframework"` Nps nps.Config `yaml:"nps"` OS os.Config `yaml:"os"` - Paging pagefile.Config `yaml:"paging"` + Paging pagefile.Config `yaml:"pagefile"` PerformanceCounter performancecounter.Config `yaml:"performancecounter"` PhysicalDisk physical_disk.Config `yaml:"physical_disk"` Printer printer.Config `yaml:"printer"` @@ -109,7 +109,7 @@ type Config struct { ScheduledTask scheduled_task.Config `yaml:"scheduled_task"` Service service.Config `yaml:"service"` SMB smb.Config `yaml:"smb"` - SMBClient smbclient.Config `yaml:"smb_client"` + SMBClient smbclient.Config `yaml:"smbclient"` SMTP smtp.Config `yaml:"smtp"` StorageSpaces storage_spaces.Config `yaml:"storage_spaces"` System system.Config `yaml:"system"` @@ -127,55 +127,11 @@ type Config struct { // //nolint:gochecknoglobals //goland:noinspection GoUnusedGlobalVariable -var ConfigDefaults = Config{ - AD: ad.ConfigDefaults, - ADCS: adcs.ConfigDefaults, - ADFS: adfs.ConfigDefaults, - Cache: cache.ConfigDefaults, - Container: container.ConfigDefaults, - CPU: cpu.ConfigDefaults, - CPUInfo: cpu_info.ConfigDefaults, - DFSR: dfsr.ConfigDefaults, - Dhcp: dhcp.ConfigDefaults, - DiskDrive: diskdrive.ConfigDefaults, - DMI: dmi.ConfigDefaults, - DNS: dns.ConfigDefaults, - Exchange: exchange.ConfigDefaults, - File: file.ConfigDefaults, - Fsrmquota: fsrmquota.ConfigDefaults, - GPU: gpu.ConfigDefaults, - HyperV: hyperv.ConfigDefaults, - IIS: iis.ConfigDefaults, - License: license.ConfigDefaults, - LogicalDisk: logical_disk.ConfigDefaults, - Memory: memory.ConfigDefaults, - MSCluster: mscluster.ConfigDefaults, - Msmq: msmq.ConfigDefaults, - Mssql: mssql.ConfigDefaults, - Net: net.ConfigDefaults, - NetFramework: netframework.ConfigDefaults, - Nps: nps.ConfigDefaults, - OS: os.ConfigDefaults, - Paging: pagefile.ConfigDefaults, - PerformanceCounter: performancecounter.ConfigDefaults, - PhysicalDisk: physical_disk.ConfigDefaults, - Printer: printer.ConfigDefaults, - Process: process.ConfigDefaults, - Registry: registry.ConfigDefaults, - RemoteFx: remote_fx.ConfigDefaults, - ScheduledTask: scheduled_task.ConfigDefaults, - Service: service.ConfigDefaults, - SMB: smb.ConfigDefaults, - SMBClient: smbclient.ConfigDefaults, - SMTP: smtp.ConfigDefaults, - StorageSpaces: storage_spaces.ConfigDefaults, - System: system.ConfigDefaults, - TCP: tcp.ConfigDefaults, - TerminalServices: terminal_services.ConfigDefaults, - Textfile: textfile.ConfigDefaults, - Time: time.ConfigDefaults, - UDP: udp.ConfigDefaults, - Update: update.ConfigDefaults, - Vmware: vmware.ConfigDefaults, - WMI: wmi.ConfigDefaults, -} +var ConfigDefaults = func() Config { + var config Config + for _, r := range registrations { + r.setDefaults(&config) + } + + return config +}() diff --git a/pkg/collector/map.go b/pkg/collector/map.go index d08781da..5b5b215e 100644 --- a/pkg/collector/map.go +++ b/pkg/collector/map.go @@ -74,65 +74,114 @@ import ( "github.com/prometheus-community/windows_exporter/internal/collector/wmi" ) +// registration ties a collector name to its constructors, its field in +// [Config] and its default configuration. It is the single source of +// [BuildersWithFlags], [NewWithConfig] and [ConfigDefaults]. +type registration struct { + name string + newWithFlags BuilderWithFlags[Collector] + newWithConfig func(config *Config) Collector + setDefaults func(config *Config) + // field returns the address of the collector's field in config. Tests + // use it to match the registrations to the fields of [Config]. + field func(config *Config) any +} + +// register returns the registration of a collector. Its type parameters +// ensure that the constructors, the defaults and the [Config] field use the +// same configuration type. +func register[C Collector, T any]( + name string, + newWithConfig func(*T) C, + newWithFlags BuilderWithFlags[C], + defaults T, + field func(*Config) *T, +) registration { + return registration{ + name: name, + newWithFlags: NewBuilderWithFlags(newWithFlags), + newWithConfig: func(config *Config) Collector { return newWithConfig(field(config)) }, + setDefaults: func(config *Config) { *field(config) = defaults }, + field: func(config *Config) any { return field(config) }, + } +} + +// registrations lists every built-in collector. A new collector needs its +// field in [Config], its documentation in docs/collector..md and one +// entry here. +// +//nolint:gochecknoglobals +var registrations = []registration{ + register(ad.Name, ad.New, ad.NewWithFlags, ad.ConfigDefaults, func(c *Config) *ad.Config { return &c.AD }), + register(adcs.Name, adcs.New, adcs.NewWithFlags, adcs.ConfigDefaults, func(c *Config) *adcs.Config { return &c.ADCS }), + register(adfs.Name, adfs.New, adfs.NewWithFlags, adfs.ConfigDefaults, func(c *Config) *adfs.Config { return &c.ADFS }), + register(cache.Name, cache.New, cache.NewWithFlags, cache.ConfigDefaults, func(c *Config) *cache.Config { return &c.Cache }), + register(container.Name, container.New, container.NewWithFlags, container.ConfigDefaults, func(c *Config) *container.Config { return &c.Container }), + register(cpu.Name, cpu.New, cpu.NewWithFlags, cpu.ConfigDefaults, func(c *Config) *cpu.Config { return &c.CPU }), + register(cpu_info.Name, cpu_info.New, cpu_info.NewWithFlags, cpu_info.ConfigDefaults, func(c *Config) *cpu_info.Config { return &c.CPUInfo }), + register(dfsr.Name, dfsr.New, dfsr.NewWithFlags, dfsr.ConfigDefaults, func(c *Config) *dfsr.Config { return &c.DFSR }), + register(dhcp.Name, dhcp.New, dhcp.NewWithFlags, dhcp.ConfigDefaults, func(c *Config) *dhcp.Config { return &c.Dhcp }), + register(diskdrive.Name, diskdrive.New, diskdrive.NewWithFlags, diskdrive.ConfigDefaults, func(c *Config) *diskdrive.Config { return &c.DiskDrive }), + register(dmi.Name, dmi.New, dmi.NewWithFlags, dmi.ConfigDefaults, func(c *Config) *dmi.Config { return &c.DMI }), + register(dns.Name, dns.New, dns.NewWithFlags, dns.ConfigDefaults, func(c *Config) *dns.Config { return &c.DNS }), + register(exchange.Name, exchange.New, exchange.NewWithFlags, exchange.ConfigDefaults, func(c *Config) *exchange.Config { return &c.Exchange }), + register(file.Name, file.New, file.NewWithFlags, file.ConfigDefaults, func(c *Config) *file.Config { return &c.File }), + register(fsrmquota.Name, fsrmquota.New, fsrmquota.NewWithFlags, fsrmquota.ConfigDefaults, func(c *Config) *fsrmquota.Config { return &c.Fsrmquota }), + register(gpu.Name, gpu.New, gpu.NewWithFlags, gpu.ConfigDefaults, func(c *Config) *gpu.Config { return &c.GPU }), + register(hyperv.Name, hyperv.New, hyperv.NewWithFlags, hyperv.ConfigDefaults, func(c *Config) *hyperv.Config { return &c.HyperV }), + register(iis.Name, iis.New, iis.NewWithFlags, iis.ConfigDefaults, func(c *Config) *iis.Config { return &c.IIS }), + register(license.Name, license.New, license.NewWithFlags, license.ConfigDefaults, func(c *Config) *license.Config { return &c.License }), + register(logical_disk.Name, logical_disk.New, logical_disk.NewWithFlags, logical_disk.ConfigDefaults, func(c *Config) *logical_disk.Config { return &c.LogicalDisk }), + register(memory.Name, memory.New, memory.NewWithFlags, memory.ConfigDefaults, func(c *Config) *memory.Config { return &c.Memory }), + register(mscluster.Name, mscluster.New, mscluster.NewWithFlags, mscluster.ConfigDefaults, func(c *Config) *mscluster.Config { return &c.MSCluster }), + register(msmq.Name, msmq.New, msmq.NewWithFlags, msmq.ConfigDefaults, func(c *Config) *msmq.Config { return &c.Msmq }), + register(mssql.Name, mssql.New, mssql.NewWithFlags, mssql.ConfigDefaults, func(c *Config) *mssql.Config { return &c.Mssql }), + register(net.Name, net.New, net.NewWithFlags, net.ConfigDefaults, func(c *Config) *net.Config { return &c.Net }), + register(netframework.Name, netframework.New, netframework.NewWithFlags, netframework.ConfigDefaults, func(c *Config) *netframework.Config { return &c.NetFramework }), + register(nps.Name, nps.New, nps.NewWithFlags, nps.ConfigDefaults, func(c *Config) *nps.Config { return &c.Nps }), + register(os.Name, os.New, os.NewWithFlags, os.ConfigDefaults, func(c *Config) *os.Config { return &c.OS }), + register(pagefile.Name, pagefile.New, pagefile.NewWithFlags, pagefile.ConfigDefaults, func(c *Config) *pagefile.Config { return &c.Paging }), + register(performancecounter.Name, performancecounter.New, performancecounter.NewWithFlags, performancecounter.ConfigDefaults, func(c *Config) *performancecounter.Config { return &c.PerformanceCounter }), + register(physical_disk.Name, physical_disk.New, physical_disk.NewWithFlags, physical_disk.ConfigDefaults, func(c *Config) *physical_disk.Config { return &c.PhysicalDisk }), + register(printer.Name, printer.New, printer.NewWithFlags, printer.ConfigDefaults, func(c *Config) *printer.Config { return &c.Printer }), + register(process.Name, process.New, process.NewWithFlags, process.ConfigDefaults, func(c *Config) *process.Config { return &c.Process }), + register(registry.Name, registry.New, registry.NewWithFlags, registry.ConfigDefaults, func(c *Config) *registry.Config { return &c.Registry }), + register(remote_fx.Name, remote_fx.New, remote_fx.NewWithFlags, remote_fx.ConfigDefaults, func(c *Config) *remote_fx.Config { return &c.RemoteFx }), + register(scheduled_task.Name, scheduled_task.New, scheduled_task.NewWithFlags, scheduled_task.ConfigDefaults, func(c *Config) *scheduled_task.Config { return &c.ScheduledTask }), + register(service.Name, service.New, service.NewWithFlags, service.ConfigDefaults, func(c *Config) *service.Config { return &c.Service }), + register(smb.Name, smb.New, smb.NewWithFlags, smb.ConfigDefaults, func(c *Config) *smb.Config { return &c.SMB }), + register(smbclient.Name, smbclient.New, smbclient.NewWithFlags, smbclient.ConfigDefaults, func(c *Config) *smbclient.Config { return &c.SMBClient }), + register(smtp.Name, smtp.New, smtp.NewWithFlags, smtp.ConfigDefaults, func(c *Config) *smtp.Config { return &c.SMTP }), + register(storage_spaces.Name, storage_spaces.New, storage_spaces.NewWithFlags, storage_spaces.ConfigDefaults, func(c *Config) *storage_spaces.Config { return &c.StorageSpaces }), + register(system.Name, system.New, system.NewWithFlags, system.ConfigDefaults, func(c *Config) *system.Config { return &c.System }), + register(tcp.Name, tcp.New, tcp.NewWithFlags, tcp.ConfigDefaults, func(c *Config) *tcp.Config { return &c.TCP }), + register(terminal_services.Name, terminal_services.New, terminal_services.NewWithFlags, terminal_services.ConfigDefaults, func(c *Config) *terminal_services.Config { return &c.TerminalServices }), + register(textfile.Name, textfile.New, textfile.NewWithFlags, textfile.ConfigDefaults, func(c *Config) *textfile.Config { return &c.Textfile }), + register(time.Name, time.New, time.NewWithFlags, time.ConfigDefaults, func(c *Config) *time.Config { return &c.Time }), + register(udp.Name, udp.New, udp.NewWithFlags, udp.ConfigDefaults, func(c *Config) *udp.Config { return &c.UDP }), + register(update.Name, update.New, update.NewWithFlags, update.ConfigDefaults, func(c *Config) *update.Config { return &c.Update }), + register(vmware.Name, vmware.New, vmware.NewWithFlags, vmware.ConfigDefaults, func(c *Config) *vmware.Config { return &c.Vmware }), + register(wmi.Name, wmi.New, wmi.NewWithFlags, wmi.ConfigDefaults, func(c *Config) *wmi.Config { return &c.WMI }), +} + func NewBuilderWithFlags[C Collector](fn BuilderWithFlags[C]) BuilderWithFlags[Collector] { return func(app *kingpin.Application) Collector { return fn(app) } } +// BuildersWithFlags holds the flag-based constructor of every built-in +// collector, keyed by the collector name. +// //nolint:gochecknoglobals -var BuildersWithFlags = map[string]BuilderWithFlags[Collector]{ - ad.Name: NewBuilderWithFlags(ad.NewWithFlags), - adcs.Name: NewBuilderWithFlags(adcs.NewWithFlags), - adfs.Name: NewBuilderWithFlags(adfs.NewWithFlags), - cache.Name: NewBuilderWithFlags(cache.NewWithFlags), - container.Name: NewBuilderWithFlags(container.NewWithFlags), - cpu.Name: NewBuilderWithFlags(cpu.NewWithFlags), - cpu_info.Name: NewBuilderWithFlags(cpu_info.NewWithFlags), - dfsr.Name: NewBuilderWithFlags(dfsr.NewWithFlags), - dhcp.Name: NewBuilderWithFlags(dhcp.NewWithFlags), - diskdrive.Name: NewBuilderWithFlags(diskdrive.NewWithFlags), - dmi.Name: NewBuilderWithFlags(dmi.NewWithFlags), - dns.Name: NewBuilderWithFlags(dns.NewWithFlags), - exchange.Name: NewBuilderWithFlags(exchange.NewWithFlags), - file.Name: NewBuilderWithFlags(file.NewWithFlags), - fsrmquota.Name: NewBuilderWithFlags(fsrmquota.NewWithFlags), - gpu.Name: NewBuilderWithFlags(gpu.NewWithFlags), - hyperv.Name: NewBuilderWithFlags(hyperv.NewWithFlags), - iis.Name: NewBuilderWithFlags(iis.NewWithFlags), - license.Name: NewBuilderWithFlags(license.NewWithFlags), - logical_disk.Name: NewBuilderWithFlags(logical_disk.NewWithFlags), - memory.Name: NewBuilderWithFlags(memory.NewWithFlags), - mscluster.Name: NewBuilderWithFlags(mscluster.NewWithFlags), - msmq.Name: NewBuilderWithFlags(msmq.NewWithFlags), - mssql.Name: NewBuilderWithFlags(mssql.NewWithFlags), - net.Name: NewBuilderWithFlags(net.NewWithFlags), - netframework.Name: NewBuilderWithFlags(netframework.NewWithFlags), - nps.Name: NewBuilderWithFlags(nps.NewWithFlags), - os.Name: NewBuilderWithFlags(os.NewWithFlags), - pagefile.Name: NewBuilderWithFlags(pagefile.NewWithFlags), - performancecounter.Name: NewBuilderWithFlags(performancecounter.NewWithFlags), - 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), - smb.Name: NewBuilderWithFlags(smb.NewWithFlags), - smbclient.Name: NewBuilderWithFlags(smbclient.NewWithFlags), - smtp.Name: NewBuilderWithFlags(smtp.NewWithFlags), - storage_spaces.Name: NewBuilderWithFlags(storage_spaces.NewWithFlags), - system.Name: NewBuilderWithFlags(system.NewWithFlags), - tcp.Name: NewBuilderWithFlags(tcp.NewWithFlags), - terminal_services.Name: NewBuilderWithFlags(terminal_services.NewWithFlags), - textfile.Name: NewBuilderWithFlags(textfile.NewWithFlags), - time.Name: NewBuilderWithFlags(time.NewWithFlags), - udp.Name: NewBuilderWithFlags(udp.NewWithFlags), - update.Name: NewBuilderWithFlags(update.NewWithFlags), - vmware.Name: NewBuilderWithFlags(vmware.NewWithFlags), - wmi.Name: NewBuilderWithFlags(wmi.NewWithFlags), -} +var BuildersWithFlags = func() map[string]BuilderWithFlags[Collector] { + builders := make(map[string]BuilderWithFlags[Collector], len(registrations)) + for _, r := range registrations { + builders[r.name] = r.newWithFlags + } + + return builders +}() // Available returns a sorted list of available collectors. // diff --git a/pkg/collector/registration_internal_test.go b/pkg/collector/registration_internal_test.go new file mode 100644 index 00000000..2db76b49 --- /dev/null +++ b/pkg/collector/registration_internal_test.go @@ -0,0 +1,213 @@ +// 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 collector + +import ( + "maps" + "path/filepath" + "reflect" + "slices" + "strings" + "testing" + + "github.com/alecthomas/kingpin/v2" + "github.com/prometheus-community/windows_exporter/internal/collector/ad" + "github.com/prometheus-community/windows_exporter/internal/collector/adcs" + "github.com/prometheus-community/windows_exporter/internal/collector/adfs" + "github.com/prometheus-community/windows_exporter/internal/collector/cache" + "github.com/prometheus-community/windows_exporter/internal/collector/container" + "github.com/prometheus-community/windows_exporter/internal/collector/cpu" + "github.com/prometheus-community/windows_exporter/internal/collector/cpu_info" + "github.com/prometheus-community/windows_exporter/internal/collector/dfsr" + "github.com/prometheus-community/windows_exporter/internal/collector/dhcp" + "github.com/prometheus-community/windows_exporter/internal/collector/diskdrive" + "github.com/prometheus-community/windows_exporter/internal/collector/dmi" + "github.com/prometheus-community/windows_exporter/internal/collector/dns" + "github.com/prometheus-community/windows_exporter/internal/collector/exchange" + "github.com/prometheus-community/windows_exporter/internal/collector/file" + "github.com/prometheus-community/windows_exporter/internal/collector/fsrmquota" + "github.com/prometheus-community/windows_exporter/internal/collector/gpu" + "github.com/prometheus-community/windows_exporter/internal/collector/hyperv" + "github.com/prometheus-community/windows_exporter/internal/collector/iis" + "github.com/prometheus-community/windows_exporter/internal/collector/license" + "github.com/prometheus-community/windows_exporter/internal/collector/logical_disk" + "github.com/prometheus-community/windows_exporter/internal/collector/memory" + "github.com/prometheus-community/windows_exporter/internal/collector/mscluster" + "github.com/prometheus-community/windows_exporter/internal/collector/msmq" + "github.com/prometheus-community/windows_exporter/internal/collector/mssql" + "github.com/prometheus-community/windows_exporter/internal/collector/net" + "github.com/prometheus-community/windows_exporter/internal/collector/netframework" + "github.com/prometheus-community/windows_exporter/internal/collector/nps" + "github.com/prometheus-community/windows_exporter/internal/collector/os" + "github.com/prometheus-community/windows_exporter/internal/collector/pagefile" + "github.com/prometheus-community/windows_exporter/internal/collector/performancecounter" + "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" + "github.com/prometheus-community/windows_exporter/internal/collector/smb" + "github.com/prometheus-community/windows_exporter/internal/collector/smbclient" + "github.com/prometheus-community/windows_exporter/internal/collector/smtp" + "github.com/prometheus-community/windows_exporter/internal/collector/storage_spaces" + "github.com/prometheus-community/windows_exporter/internal/collector/system" + "github.com/prometheus-community/windows_exporter/internal/collector/tcp" + "github.com/prometheus-community/windows_exporter/internal/collector/terminal_services" + "github.com/prometheus-community/windows_exporter/internal/collector/textfile" + "github.com/prometheus-community/windows_exporter/internal/collector/time" + "github.com/prometheus-community/windows_exporter/internal/collector/udp" + "github.com/prometheus-community/windows_exporter/internal/collector/update" + "github.com/prometheus-community/windows_exporter/internal/collector/vmware" + "github.com/prometheus-community/windows_exporter/internal/collector/wmi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestRegistrations verifies that every place that lists the collectors agrees +// with the registrations: the flag builders, NewWithConfig, the fields and +// YAML keys of Config, ConfigDefaults and the collector documentation. +func TestRegistrations(t *testing.T) { + t.Parallel() + + names := make([]string, 0, len(registrations)) + for _, r := range registrations { + require.NotContains(t, names, r.name, "collector %s is registered twice", r.name) + names = append(names, r.name) + } + + slices.Sort(names) + + t.Run("flag builders", func(t *testing.T) { + t.Parallel() + + require.Equal(t, names, Available()) + + app := kingpin.New("test", "test") + for name, builder := range BuildersWithFlags { + assert.Equal(t, name, builder(app).GetName(), "flag builder of collector %s", name) + } + }) + + t.Run("NewWithConfig", func(t *testing.T) { + t.Parallel() + + collection := NewWithConfig(ConfigDefaults) + require.Equal(t, names, slices.Sorted(maps.Keys(collection.collectors))) + + for name, c := range collection.collectors { + assert.Equal(t, name, c.GetName(), "NewWithConfig collector %s", name) + } + }) + + t.Run("Config fields", func(t *testing.T) { + t.Parallel() + + var config Config + + value := reflect.ValueOf(&config).Elem() + + // Fields of size zero can share an address, so the type is part of + // the key. Every collector has its own configuration type. + type fieldKey struct { + addr uintptr + typ reflect.Type + } + + keyOf := func(ptr reflect.Value) fieldKey { + return fieldKey{addr: ptr.Pointer(), typ: ptr.Type().Elem()} + } + + fields := map[fieldKey]reflect.StructField{} + for i := range value.NumField() { + fields[keyOf(value.Field(i).Addr())] = value.Type().Field(i) + } + + for _, r := range registrations { + key := keyOf(reflect.ValueOf(r.field(&config))) + + field, ok := fields[key] + if !assert.True(t, ok, "collector %s has no field in Config", r.name) { + continue + } + + configKey, _, _ := strings.Cut(field.Tag.Get("yaml"), ",") + assert.Equal(t, r.name, configKey, "YAML key of Config.%s", field.Name) + + delete(fields, key) + } + + for _, field := range fields { + assert.Fail(t, "Config field without collector registration", "Config.%s", field.Name) + } + }) + + t.Run("ConfigDefaults", func(t *testing.T) { + t.Parallel() + + // The defaults of every collector, listed independently of the + // registrations. + defaults := map[string]any{ + ad.Name: ad.ConfigDefaults, adcs.Name: adcs.ConfigDefaults, adfs.Name: adfs.ConfigDefaults, + cache.Name: cache.ConfigDefaults, container.Name: container.ConfigDefaults, cpu.Name: cpu.ConfigDefaults, + cpu_info.Name: cpu_info.ConfigDefaults, dfsr.Name: dfsr.ConfigDefaults, dhcp.Name: dhcp.ConfigDefaults, + diskdrive.Name: diskdrive.ConfigDefaults, dmi.Name: dmi.ConfigDefaults, dns.Name: dns.ConfigDefaults, + exchange.Name: exchange.ConfigDefaults, file.Name: file.ConfigDefaults, fsrmquota.Name: fsrmquota.ConfigDefaults, + gpu.Name: gpu.ConfigDefaults, hyperv.Name: hyperv.ConfigDefaults, iis.Name: iis.ConfigDefaults, + license.Name: license.ConfigDefaults, logical_disk.Name: logical_disk.ConfigDefaults, memory.Name: memory.ConfigDefaults, + mscluster.Name: mscluster.ConfigDefaults, msmq.Name: msmq.ConfigDefaults, mssql.Name: mssql.ConfigDefaults, + net.Name: net.ConfigDefaults, netframework.Name: netframework.ConfigDefaults, nps.Name: nps.ConfigDefaults, + os.Name: os.ConfigDefaults, pagefile.Name: pagefile.ConfigDefaults, performancecounter.Name: performancecounter.ConfigDefaults, + physical_disk.Name: physical_disk.ConfigDefaults, printer.Name: printer.ConfigDefaults, process.Name: process.ConfigDefaults, + registry.Name: registry.ConfigDefaults, remote_fx.Name: remote_fx.ConfigDefaults, scheduled_task.Name: scheduled_task.ConfigDefaults, + service.Name: service.ConfigDefaults, smb.Name: smb.ConfigDefaults, smbclient.Name: smbclient.ConfigDefaults, + smtp.Name: smtp.ConfigDefaults, storage_spaces.Name: storage_spaces.ConfigDefaults, system.Name: system.ConfigDefaults, + tcp.Name: tcp.ConfigDefaults, terminal_services.Name: terminal_services.ConfigDefaults, textfile.Name: textfile.ConfigDefaults, + time.Name: time.ConfigDefaults, udp.Name: udp.ConfigDefaults, update.Name: update.ConfigDefaults, + vmware.Name: vmware.ConfigDefaults, wmi.Name: wmi.ConfigDefaults, + } + + require.Len(t, defaults, len(registrations)) + + for _, r := range registrations { + want, ok := defaults[r.name] + if !assert.True(t, ok, "collector %s has no defaults in the test", r.name) { + continue + } + + got := reflect.ValueOf(r.field(&ConfigDefaults)).Elem().Interface() + assert.Equal(t, want, got, "ConfigDefaults of collector %s", r.name) + } + }) + + t.Run("documentation", func(t *testing.T) { + t.Parallel() + + files, err := filepath.Glob(filepath.Join("..", "..", "docs", "collector.*.md")) + require.NoError(t, err) + + documented := make([]string, 0, len(files)) + + for _, file := range files { + documented = append(documented, strings.TrimSuffix(strings.TrimPrefix(filepath.Base(file), "collector."), ".md")) + } + + require.ElementsMatch(t, names, documented, "docs/collector..md files") + }) +}