Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 42 additions & 20 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions collector/cpu_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
82 changes: 82 additions & 0 deletions collector/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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 {
state *collectorRuntimeState
collector *NodeCollector
logger *slog.Logger
}

func NewRuntime(cfg config.Config, logger *slog.Logger) (*Runtime, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}

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
}

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 filtered, 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
}
119 changes: 119 additions & 0 deletions collector/runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// 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")
}
}

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")
}
}
54 changes: 54 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading