From cacfbdd738d4af25b93d8a56be6939739317b44b Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Tue, 19 May 2026 16:03:54 +0100 Subject: [PATCH 1/3] 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 486973851876121261fa3aca70135733dd138c04 Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Wed, 20 May 2026 11:22:00 +0100 Subject: [PATCH 2/3] config: fix license headers for new config package Signed-off-by: Nicolas Takashi --- config/config.go | 2 +- config/config_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index f0f169d33c..ca673d0067 100644 --- a/config/config.go +++ b/config/config.go @@ -1,4 +1,4 @@ -// Copyright 2026 The Prometheus Authors +// 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 diff --git a/config/config_test.go b/config/config_test.go index a39dbf9521..d8871d4faa 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,4 +1,4 @@ -// Copyright 2026 The Prometheus Authors +// 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 From 7e1df8c382e58ad3f37efcbd07c43cf9903153dc Mon Sep 17 00:00:00 2001 From: Nicolas Takashi Date: Thu, 21 May 2026 14:21:30 +0100 Subject: [PATCH 3/3] main: use exporter-toolkit bootstrap runner Signed-off-by: Nicolas Takashi --- config/config.go | 54 ---------------------------- config/config_test.go | 65 ---------------------------------- go.mod | 2 ++ go.sum | 2 -- node_exporter.go | 82 +++++++++++-------------------------------- 5 files changed, 23 insertions(+), 182 deletions(-) delete mode 100644 config/config.go delete mode 100644 config/config_test.go diff --git a/config/config.go b/config/config.go deleted file mode 100644 index ca673d0067..0000000000 --- a/config/config.go +++ /dev/null @@ -1,54 +0,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. - -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 deleted file mode 100644 index d8871d4faa..0000000000 --- a/config/config_test.go +++ /dev/null @@ -1,65 +0,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. - -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") - } -} diff --git a/go.mod b/go.mod index df4d250ee7..e4850e70db 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,8 @@ require ( howett.net/plist v1.0.1 ) +replace github.com/prometheus/exporter-toolkit => /Users/nicolastakashi/workspace/github.com/nicolastakashi/prometheus/exporter-toolkit + require ( cyphar.com/go-pathrs v0.2.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect diff --git a/go.sum b/go.sum index 0e3f769cca..ee019edade 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/exporter-toolkit v0.16.0 h1:xT/j7L2XKF+VJd6B4fpUw6xWabHrSmsUf6mYmFqyu0s= -github.com/prometheus/exporter-toolkit v0.16.0/go.mod h1:d1EL8Z9674xQe/iWhwP2wDyCEoBPbXVeqDbqAUsgJWY= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/safchain/ethtool v0.7.0 h1:rlJzfDetsVvT61uz8x1YIcFn12akMfuPulHtZjtb7Is= diff --git a/node_exporter.go b/node_exporter.go index 2c0e12ccc1..00ad19934b 100644 --- a/node_exporter.go +++ b/node_exporter.go @@ -24,17 +24,12 @@ import ( "slices" "sort" - "github.com/prometheus/common/promslog" - "github.com/prometheus/common/promslog/flag" - "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" promcollectors "github.com/prometheus/client_golang/prometheus/collectors" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/prometheus/common/version" - "github.com/prometheus/exporter-toolkit/web" - "github.com/prometheus/exporter-toolkit/web/kingpinflag" + "github.com/prometheus/exporter-toolkit/bootstrap" "github.com/prometheus/node_exporter/collector" ) @@ -181,18 +176,6 @@ func (h *handler) innerHandler(filters ...string) (http.Handler, error) { func main() { var ( - metricsPath = kingpin.Flag( - "web.telemetry-path", - "Path under which to expose metrics.", - ).Default("/metrics").String() - disableExporterMetrics = kingpin.Flag( - "web.disable-exporter-metrics", - "Exclude metrics about the exporter itself (promhttp_*, process_*, go_*).", - ).Bool() - maxRequests = kingpin.Flag( - "web.max-requests", - "Maximum number of parallel scrape requests. Use 0 to disable.", - ).Default("40").Int() disableDefaultCollectors = kingpin.Flag( "collector.disable-defaults", "Set all collectors to disabled by default.", @@ -200,52 +183,29 @@ func main() { maxProcs = kingpin.Flag( "runtime.gomaxprocs", "The target number of CPUs Go will run on (GOMAXPROCS)", ).Envar("GOMAXPROCS").Default("1").Int() - toolkitFlags = kingpinflag.AddFlags(kingpin.CommandLine, ":9100") ) - promslogConfig := &promslog.Config{} - flag.AddFlags(kingpin.CommandLine, promslogConfig) - kingpin.Version(version.Print("node_exporter")) kingpin.CommandLine.UsageWriter(os.Stdout) - kingpin.HelpFlag.Short('h') - kingpin.Parse() - logger := promslog.New(promslogConfig) - - if *disableDefaultCollectors { - collector.DisableDefaultCollectors() - } - 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) - logger.Debug("Go MAXPROCS", "procs", runtime.GOMAXPROCS(0)) - - http.Handle(*metricsPath, newHandler(!*disableExporterMetrics, *maxRequests, logger)) - if *metricsPath != "/" { - landingConfig := web.LandingConfig{ - Name: "Node Exporter", - Description: "Prometheus Node Exporter", - Version: version.Info(), - Links: []web.LandingLinks{ - { - Address: *metricsPath, - Text: "Metrics", - }, - }, - } - landingPage, err := web.NewLandingPage(landingConfig) - if err != nil { - logger.Error(err.Error()) - os.Exit(1) - } - http.Handle("/", landingPage) - } - - server := &http.Server{} - if err := web.ListenAndServe(server, toolkitFlags, logger); err != nil { - logger.Error(err.Error()) + runner := bootstrap.New(bootstrap.Config{ + App: kingpin.CommandLine, + Name: "node_exporter", + Description: "Prometheus Node Exporter", + DefaultAddress: ":9100", + MetricsHandlerFactory: func(b *bootstrap.Bootstrap) (http.Handler, error) { + if *disableDefaultCollectors { + collector.DisableDefaultCollectors() + } + if user, err := user.Current(); err == nil && user.Uid == "0" { + b.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) + b.Logger.Debug("Go MAXPROCS", "procs", runtime.GOMAXPROCS(0)) + return newHandler(!b.DisableExporterMetrics, b.MaxRequests, b.Logger), nil + }, + }) + + if err := runner.Run(); err != nil { + fmt.Fprintln(os.Stderr, err) os.Exit(1) } }