From f2787ace72c96a45b5862ee8b80af570a6c8aa93 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Tue, 19 May 2026 16:03:54 +0100 Subject: [PATCH 1/4] config: add reusable exporter config package Signed-off-by: Nicolas Takashi --- config/config.go | 54 +++++++++++++++++++++++++++++++++++ config/config_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 config/config.go create mode 100644 config/config_test.go diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000..f0f169d33c --- /dev/null +++ b/config/config.go @@ -0,0 +1,54 @@ +// Copyright 2026 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. + +package config + +import "fmt" + +const ( + DefaultWebTelemetryPath = "/metrics" + DefaultWebMaxRequests = 40 + DefaultRuntimeGoMaxProcs = 1 +) + +// Config contains the user-facing configuration currently adapted by the +// node_exporter binary into the reusable runtime and HTTP handler layers. +type Config struct { + WebTelemetryPath string + WebDisableExporterMetrics bool + WebMaxRequests int + CollectorDisableDefaults bool + RuntimeGoMaxProcs int + EnabledCollectors []string +} + +func NewConfigWithDefaults() Config { + return Config{ + WebTelemetryPath: DefaultWebTelemetryPath, + WebMaxRequests: DefaultWebMaxRequests, + RuntimeGoMaxProcs: DefaultRuntimeGoMaxProcs, + } +} + +func (c Config) Validate() error { + if c.WebTelemetryPath == "" { + return fmt.Errorf("web telemetry path must not be empty") + } + if c.WebMaxRequests < 0 { + return fmt.Errorf("web max requests must be greater than or equal to zero") + } + if c.RuntimeGoMaxProcs <= 0 { + return fmt.Errorf("runtime gomaxprocs must be greater than zero") + } + return nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000000..a39dbf9521 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,65 @@ +// Copyright 2026 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. + +package config + +import "testing" + +func TestNewConfigWithDefaults(t *testing.T) { + cfg := NewConfigWithDefaults() + + if got, want := cfg.WebTelemetryPath, DefaultWebTelemetryPath; got != want { + t.Errorf("Expected: %q, Got: %q", want, got) + } + if got, want := cfg.WebMaxRequests, DefaultWebMaxRequests; got != want { + t.Errorf("Expected: %d, Got: %d", want, got) + } + if got, want := cfg.RuntimeGoMaxProcs, DefaultRuntimeGoMaxProcs; got != want { + t.Errorf("Expected: %d, Got: %d", want, got) + } +} + +func TestConfigValidate(t *testing.T) { + cfg := NewConfigWithDefaults() + + if err := cfg.Validate(); err != nil { + t.Fatalf("expected nil error, got %v", err) + } +} + +func TestConfigValidateRequiresTelemetryPath(t *testing.T) { + cfg := NewConfigWithDefaults() + cfg.WebTelemetryPath = "" + + if err := cfg.Validate(); err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestConfigValidateRejectsNegativeMaxRequests(t *testing.T) { + cfg := NewConfigWithDefaults() + cfg.WebMaxRequests = -1 + + if err := cfg.Validate(); err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestConfigValidateRejectsNonPositiveGoMaxProcs(t *testing.T) { + cfg := NewConfigWithDefaults() + cfg.RuntimeGoMaxProcs = 0 + + if err := cfg.Validate(); err == nil { + t.Fatal("expected error, got nil") + } +} From bd1a15ee90a8ce7a13e950804c37b871d51f5aa2 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Tue, 19 May 2026 16:16:33 +0100 Subject: [PATCH 2/4] collector: add reusable runtime wrapper Signed-off-by: Nicolas Takashi --- collector/runtime.go | 69 +++++++++++++++++++++++++++++++++++++++ collector/runtime_test.go | 66 +++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 collector/runtime.go create mode 100644 collector/runtime_test.go diff --git a/collector/runtime.go b/collector/runtime.go new file mode 100644 index 0000000000..4dc25bbc7f --- /dev/null +++ b/collector/runtime.go @@ -0,0 +1,69 @@ +// Copyright 2026 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. + +package collector + +import ( + "log/slog" + "sort" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/prometheus/node_exporter/config" +) + +// Runtime represents a single exporter runtime instance built from a reusable +// Config. +type Runtime struct { + collector *NodeCollector +} + +func NewRuntime(cfg config.Config, logger *slog.Logger) (*Runtime, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + if cfg.CollectorDisableDefaults { + DisableDefaultCollectors() + } + + nodeCollector, err := NewNodeCollector(logger, cfg.EnabledCollectors...) + if err != nil { + return nil, err + } + + return &Runtime{collector: nodeCollector}, nil +} + +func (r *Runtime) Collectors() []prometheus.Collector { + return []prometheus.Collector{r.collector} +} + +func (r *Runtime) Registry() (*prometheus.Registry, error) { + registry := prometheus.NewRegistry() + for _, c := range r.Collectors() { + if err := registry.Register(c); err != nil { + return nil, err + } + } + return registry, nil +} + +func (r *Runtime) EnabledCollectors() []string { + enabled := make([]string, 0, len(r.collector.Collectors)) + for name := range r.collector.Collectors { + enabled = append(enabled, name) + } + sort.Strings(enabled) + return enabled +} diff --git a/collector/runtime_test.go b/collector/runtime_test.go new file mode 100644 index 0000000000..e3bc30b04e --- /dev/null +++ b/collector/runtime_test.go @@ -0,0 +1,66 @@ +// Copyright 2026 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. + +package collector + +import ( + "io" + "log/slog" + "testing" + + "github.com/prometheus/node_exporter/config" +) + +func TestNewRuntimeCollectors(t *testing.T) { + runtime, err := NewRuntime(config.NewConfigWithDefaults(), slog.New(slog.NewTextHandler(io.Discard, nil))) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + if got, want := len(runtime.Collectors()), 1; got != want { + t.Errorf("Expected: %d, Got: %d", want, got) + } + + if got := len(runtime.EnabledCollectors()); got == 0 { + t.Fatal("expected at least one enabled collector") + } +} + +func TestNewRuntimeValidateConfig(t *testing.T) { + cfg := config.NewConfigWithDefaults() + cfg.RuntimeGoMaxProcs = 0 + + if _, err := NewRuntime(cfg, slog.New(slog.NewTextHandler(io.Discard, nil))); err == nil { + t.Fatal("expected error, got nil") + } +} + +func TestRuntimeRegistry(t *testing.T) { + runtime, err := NewRuntime(config.NewConfigWithDefaults(), slog.New(slog.NewTextHandler(io.Discard, nil))) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + registry, err := runtime.Registry() + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + metrics, err := registry.Gather() + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + if got := len(metrics); got == 0 { + t.Fatal("expected gathered metrics, got none") + } +} From 0a881ca9dc57d412492d3c02b77ebed284de66f0 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Tue, 19 May 2026 20:13:19 +0100 Subject: [PATCH 3/4] main: wire node_exporter through reusable config and runtime Signed-off-by: Nicolas Takashi --- node_exporter.go | 85 +++++++++++++++++++++++++++++++----------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/node_exporter.go b/node_exporter.go index 2c0e12ccc1..f2b2bd289c 100644 --- a/node_exporter.go +++ b/node_exporter.go @@ -22,7 +22,7 @@ import ( "os/user" "runtime" "slices" - "sort" + "strconv" "github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog/flag" @@ -37,6 +37,7 @@ import ( "github.com/prometheus/exporter-toolkit/web/kingpinflag" "github.com/prometheus/node_exporter/collector" + "github.com/prometheus/node_exporter/config" ) // handler wraps an unfiltered http.Handler but uses a filtered handler, @@ -44,6 +45,8 @@ import ( // newHandler. type handler struct { unfilteredHandler http.Handler + cfg config.Config + runtime *collector.Runtime // enabledCollectors list is used for logging and filtering enabledCollectors []string // exporterMetricsRegistry is a separate registry for the metrics about @@ -54,11 +57,18 @@ type handler struct { logger *slog.Logger } -func newHandler(includeExporterMetrics bool, maxRequests int, logger *slog.Logger) *handler { +func newHandler(cfg config.Config, logger *slog.Logger) (*handler, error) { + runtime, err := collector.NewRuntime(cfg, logger) + if err != nil { + return nil, err + } + h := &handler{ + cfg: cfg, + runtime: runtime, exporterMetricsRegistry: prometheus.NewRegistry(), - includeExporterMetrics: includeExporterMetrics, - maxRequests: maxRequests, + includeExporterMetrics: !cfg.WebDisableExporterMetrics, + maxRequests: cfg.WebMaxRequests, logger: logger, } if h.includeExporterMetrics { @@ -67,12 +77,12 @@ func newHandler(includeExporterMetrics bool, maxRequests int, logger *slog.Logge promcollectors.NewGoCollector(), ) } - if innerHandler, err := h.innerHandler(); err != nil { - panic(fmt.Sprintf("Couldn't create metrics handler: %s", err)) - } else { - h.unfilteredHandler = innerHandler + innerHandler, err := h.innerHandler() + if err != nil { + return nil, fmt.Errorf("couldn't create metrics handler: %w", err) } - return h + h.unfilteredHandler = innerHandler + return h, nil } // ServeHTTP implements http.Handler. @@ -125,29 +135,32 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // (in which case it will log all the collectors enabled via command-line // flags). func (h *handler) innerHandler(filters ...string) (http.Handler, error) { - nc, err := collector.NewNodeCollector(h.logger, filters...) - if err != nil { - return nil, fmt.Errorf("couldn't create collector: %s", err) + runtime := h.runtime + if len(filters) > 0 { + cfg := h.cfg + cfg.EnabledCollectors = filters + r, err := collector.NewRuntime(cfg, h.logger) + if err != nil { + return nil, fmt.Errorf("couldn't create collector runtime: %w", err) + } + runtime = r } // Only log the creation of an unfiltered handler, which should happen // only once upon startup. if len(filters) == 0 { h.logger.Info("Enabled collectors") - for n := range nc.Collectors { - h.enabledCollectors = append(h.enabledCollectors, n) - } - sort.Strings(h.enabledCollectors) + h.enabledCollectors = runtime.EnabledCollectors() for _, c := range h.enabledCollectors { h.logger.Info(c) } } - r := prometheus.NewRegistry() - r.MustRegister(versioncollector.NewCollector("node_exporter")) - if err := r.Register(nc); err != nil { - return nil, fmt.Errorf("couldn't register node collector: %s", err) + r, err := runtime.Registry() + if err != nil { + return nil, fmt.Errorf("couldn't create collector registry: %w", err) } + r.MustRegister(versioncollector.NewCollector("node_exporter")) var handler http.Handler if h.includeExporterMetrics { @@ -184,7 +197,7 @@ func main() { metricsPath = kingpin.Flag( "web.telemetry-path", "Path under which to expose metrics.", - ).Default("/metrics").String() + ).Default(config.DefaultWebTelemetryPath).String() disableExporterMetrics = kingpin.Flag( "web.disable-exporter-metrics", "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).", @@ -192,14 +205,14 @@ func main() { maxRequests = kingpin.Flag( "web.max-requests", "Maximum number of parallel scrape requests. Use 0 to disable.", - ).Default("40").Int() + ).Default(strconv.Itoa(config.DefaultWebMaxRequests)).Int() disableDefaultCollectors = kingpin.Flag( "collector.disable-defaults", "Set all collectors to disabled by default.", ).Default("false").Bool() maxProcs = kingpin.Flag( "runtime.gomaxprocs", "The target number of CPUs Go will run on (GOMAXPROCS)", - ).Envar("GOMAXPROCS").Default("1").Int() + ).Envar("GOMAXPROCS").Default(strconv.Itoa(config.DefaultRuntimeGoMaxProcs)).Int() toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9100") ) @@ -211,26 +224,40 @@ func main() { kingpin.Parse() logger := promslog.New(promslogConfig) - if *disableDefaultCollectors { - collector.DisableDefaultCollectors() + cfg := config.NewConfigWithDefaults() + cfg.WebTelemetryPath = *metricsPath + cfg.WebDisableExporterMetrics = *disableExporterMetrics + cfg.WebMaxRequests = *maxRequests + cfg.CollectorDisableDefaults = *disableDefaultCollectors + cfg.RuntimeGoMaxProcs = *maxProcs + if err := cfg.Validate(); err != nil { + logger.Error(err.Error()) + os.Exit(1) } + logger.Info("Starting node_exporter", "version", version.Info()) logger.Info("Build context", "build_context", version.BuildContext()) if user, err := user.Current(); err == nil && user.Uid == "0" { logger.Warn("Node Exporter is running as root user. This exporter is designed to run as unprivileged user, root is not required.") } - runtime.GOMAXPROCS(*maxProcs) + runtime.GOMAXPROCS(cfg.RuntimeGoMaxProcs) logger.Debug("Go MAXPROCS", "procs", runtime.GOMAXPROCS(0)) - http.Handle(*metricsPath, newHandler(!*disableExporterMetrics, *maxRequests, logger)) - if *metricsPath != "/" { + handler, err := newHandler(cfg, logger) + if err != nil { + logger.Error(err.Error()) + os.Exit(1) + } + + http.Handle(cfg.WebTelemetryPath, handler) + if cfg.WebTelemetryPath != "/" { landingConfig := web.LandingConfig{ Name: "Node Exporter", Description: "Prometheus Node Exporter", Version: version.Info(), Links: []web.LandingLinks{ { - Address: *metricsPath, + Address: cfg.WebTelemetryPath, Text: "Metrics", }, }, From f2d8a9df129ee108837a4093aeed71b946dcf60c Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Tue, 19 May 2026 20:30:05 +0100 Subject: [PATCH 4/4] collector: scope collector runtime state Signed-off-by: Nicolas Takashi --- collector/collector.go | 62 ++++++++++++++++++++++++++------------- collector/cpu_linux.go | 10 ++++--- collector/runtime.go | 21 ++++++++++--- collector/runtime_test.go | 53 +++++++++++++++++++++++++++++++++ node_exporter.go | 4 +-- 5 files changed, 119 insertions(+), 31 deletions(-) diff --git a/collector/collector.go b/collector/collector.go index 18ff7388ca..9e6736ef88 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -49,13 +49,21 @@ const ( ) var ( - factories = make(map[string]func(logger *slog.Logger) (Collector, error)) - initiatedCollectorsMtx = sync.Mutex{} - initiatedCollectors = make(map[string]Collector) - collectorState = make(map[string]*bool) - forcedCollectors = map[string]bool{} // collectors which have been explicitly enabled or disabled + factories = make(map[string]func(logger *slog.Logger) (Collector, error)) + collectorState = make(map[string]*bool) + forcedCollectors = map[string]bool{} // collectors which have been explicitly enabled or disabled ) +type collectorRuntimeState struct { + enabled map[string]bool + collectors map[string]Collector + mtx sync.Mutex +} + +type runtimeConfiguredCollector interface { + configureRuntimeState(*collectorRuntimeState) +} + func registerCollector(collector string, isDefaultEnabled bool, factory func(logger *slog.Logger) (Collector, error)) { var helpDefaultState string if isDefaultEnabled { @@ -80,14 +88,21 @@ type NodeCollector struct { logger *slog.Logger } -// DisableDefaultCollectors sets the collector state to false for all collectors which -// have not been explicitly enabled on the command line. -func DisableDefaultCollectors() { - for c := range collectorState { - if _, ok := forcedCollectors[c]; !ok { - *collectorState[c] = false +func newCollectorRuntimeState(disableDefaultCollectors bool) *collectorRuntimeState { + enabled := make(map[string]bool, len(collectorState)) + for collector, state := range collectorState { + enabled[collector] = *state + if disableDefaultCollectors { + if _, ok := forcedCollectors[collector]; !ok { + enabled[collector] = false + } } } + + return &collectorRuntimeState{ + enabled: enabled, + collectors: make(map[string]Collector), + } } // collectorFlagAction generates a new action function for the given collector @@ -103,39 +118,46 @@ func collectorFlagAction(collector string) func(ctx *kingpin.ParseContext) error } // NewNodeCollector creates a new NodeCollector. -func NewNodeCollector(logger *slog.Logger, filters ...string) (*NodeCollector, error) { +func (s *collectorRuntimeState) NewNodeCollector(logger *slog.Logger, filters ...string) (*NodeCollector, error) { f := make(map[string]bool) for _, filter := range filters { - enabled, exist := collectorState[filter] + enabled, exist := s.enabled[filter] if !exist { return nil, fmt.Errorf("missing collector: %s", filter) } - if !*enabled { + if !enabled { return nil, fmt.Errorf("disabled collector: %s", filter) } f[filter] = true } collectors := make(map[string]Collector) - initiatedCollectorsMtx.Lock() - defer initiatedCollectorsMtx.Unlock() - for key, enabled := range collectorState { - if !*enabled || (len(f) > 0 && !f[key]) { + s.mtx.Lock() + defer s.mtx.Unlock() + for key, enabled := range s.enabled { + if !enabled || (len(f) > 0 && !f[key]) { continue } - if collector, ok := initiatedCollectors[key]; ok { + if collector, ok := s.collectors[key]; ok { collectors[key] = collector } else { collector, err := factories[key](logger.With("collector", key)) if err != nil { return nil, err } + s.configureCollector(collector) collectors[key] = collector - initiatedCollectors[key] = collector + s.collectors[key] = collector } } return &NodeCollector{Collectors: collectors, logger: logger}, nil } +func (s *collectorRuntimeState) configureCollector(collector Collector) { + if configuredCollector, ok := collector.(runtimeConfiguredCollector); ok { + configuredCollector.configureRuntimeState(s) + } +} + // Describe implements the prometheus.Collector interface. func (n NodeCollector) Describe(ch chan<- *prometheus.Desc) { ch <- scrapeDurationDesc diff --git a/collector/cpu_linux.go b/collector/cpu_linux.go index 30fe305ac0..3a4647aaa1 100644 --- a/collector/cpu_linux.go +++ b/collector/cpu_linux.go @@ -50,6 +50,7 @@ type cpuCollector struct { cpuStats map[int64]procfs.CPUStat cpuStatsMutex sync.Mutex isolatedCpus []uint16 + cpuFreqEnabled bool cpuFlagsIncludeRegexp *regexp.Regexp cpuBugsIncludeRegexp *regexp.Regexp @@ -150,6 +151,10 @@ func NewCPUCollector(logger *slog.Logger) (Collector, error) { return c, nil } +func (c *cpuCollector) configureRuntimeState(state *collectorRuntimeState) { + c.cpuFreqEnabled = state.enabled["cpufreq"] +} + func (c *cpuCollector) compileIncludeFlags(flagsIncludeFlag, bugsIncludeFlag *string) error { if (*flagsIncludeFlag != "" || *bugsIncludeFlag != "") && !*enableCPUInfo { *enableCPUInfo = true @@ -219,10 +224,7 @@ func (c *cpuCollector) updateInfo(ch chan<- prometheus.Metric) error { cpu.CacheSize) } - cpuFreqEnabled, ok := collectorState["cpufreq"] - if !ok || cpuFreqEnabled == nil { - c.logger.Debug("cpufreq key missing or nil value in collectorState map") - } else if *cpuFreqEnabled { + if c.cpuFreqEnabled { for _, cpu := range info { ch <- prometheus.MustNewConstMetric(c.cpuFrequencyHz, prometheus.GaugeValue, diff --git a/collector/runtime.go b/collector/runtime.go index 4dc25bbc7f..2b5bbd6d9e 100644 --- a/collector/runtime.go +++ b/collector/runtime.go @@ -25,7 +25,9 @@ import ( // Runtime represents a single exporter runtime instance built from a reusable // Config. type Runtime struct { + state *collectorRuntimeState collector *NodeCollector + logger *slog.Logger } func NewRuntime(cfg config.Config, logger *slog.Logger) (*Runtime, error) { @@ -33,16 +35,27 @@ func NewRuntime(cfg config.Config, logger *slog.Logger) (*Runtime, error) { return nil, err } - if cfg.CollectorDisableDefaults { - DisableDefaultCollectors() + state := newCollectorRuntimeState(cfg.CollectorDisableDefaults) + + return newRuntime(state, logger, cfg.EnabledCollectors) +} + +func newRuntime(state *collectorRuntimeState, logger *slog.Logger, enabledCollectors []string) (*Runtime, error) { + nodeCollector, err := state.NewNodeCollector(logger, enabledCollectors...) + if err != nil { + return nil, err } - nodeCollector, err := NewNodeCollector(logger, cfg.EnabledCollectors...) + return &Runtime{state: state, collector: nodeCollector, logger: logger}, nil +} + +func (r *Runtime) Filtered(enabledCollectors ...string) (*Runtime, error) { + filtered, err := newRuntime(r.state, r.logger, enabledCollectors) if err != nil { return nil, err } - return &Runtime{collector: nodeCollector}, nil + return filtered, nil } func (r *Runtime) Collectors() []prometheus.Collector { diff --git a/collector/runtime_test.go b/collector/runtime_test.go index e3bc30b04e..8616ed5ba1 100644 --- a/collector/runtime_test.go +++ b/collector/runtime_test.go @@ -64,3 +64,56 @@ func TestRuntimeRegistry(t *testing.T) { t.Fatal("expected gathered metrics, got none") } } + +func TestNewRuntimeDisableDefaultsDoesNotMutateGlobalCollectorState(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + disabledDefaultsCfg := config.NewConfigWithDefaults() + disabledDefaultsCfg.CollectorDisableDefaults = true + + if _, err := NewRuntime(disabledDefaultsCfg, logger); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + runtime, err := NewRuntime(config.NewConfigWithDefaults(), logger) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + if got := len(runtime.EnabledCollectors()); got == 0 { + t.Fatal("expected enabled collectors after constructing runtime with defaults") + } +} + +func TestRuntimeFilteredUsesBaseCollectorState(t *testing.T) { + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + originalCPUEnabled := *collectorState["cpu"] + _, originalCPUForced := forcedCollectors["cpu"] + *collectorState["cpu"] = true + forcedCollectors["cpu"] = true + t.Cleanup(func() { + *collectorState["cpu"] = originalCPUEnabled + if originalCPUForced { + forcedCollectors["cpu"] = true + } else { + delete(forcedCollectors, "cpu") + } + }) + + cfg := config.NewConfigWithDefaults() + cfg.CollectorDisableDefaults = true + cfg.EnabledCollectors = []string{"cpu"} + + runtime, err := NewRuntime(cfg, logger) + if err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + if _, err := runtime.Filtered("cpu"); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + if _, err := runtime.Filtered("meminfo"); err == nil { + t.Fatal("expected error for collector disabled in base runtime") + } +} diff --git a/node_exporter.go b/node_exporter.go index f2b2bd289c..4a8e07c5a5 100644 --- a/node_exporter.go +++ b/node_exporter.go @@ -137,9 +137,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *handler) innerHandler(filters ...string) (http.Handler, error) { runtime := h.runtime if len(filters) > 0 { - cfg := h.cfg - cfg.EnabledCollectors = filters - r, err := collector.NewRuntime(cfg, h.logger) + r, err := h.runtime.Filtered(filters...) if err != nil { return nil, fmt.Errorf("couldn't create collector runtime: %w", err) }