Skip to content

Commit dc0018e

Browse files
committed
feat: run HTTP server standalone without Kubernetes
Extract /healthz, /conditions and /debug/pprof into a new httpexporter package so those endpoints are available when --enable-k8s-exporter=false. Previously the HTTP server was started only inside the k8s exporter, making it unavailable in non-Kubernetes environments. The new httpexporter tracks conditions in-memory via ExportProblems and starts on --port regardless of whether the Kubernetes API server is reachable. Signed-off-by: Tobias Giese <tgiese@nvidia.com>
1 parent 7bff5a4 commit dc0018e

5 files changed

Lines changed: 374 additions & 43 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ certain backends. Some of them can be disabled at compile-time using a build tag
7575
|----------|:-----------|:--------------------|
7676
| Kubernetes exporter | Kubernetes exporter reports node problems to Kubernetes API server: temporary problems get reported as Events, and permanent problems get reported as Node Conditions. |
7777
| Prometheus exporter | Prometheus exporter reports node problems and metrics locally as Prometheus metrics |
78+
| HTTP exporter | HTTP exporter serves the local `/healthz`, `/conditions` and `/debug/pprof` endpoints. It keeps node conditions in memory and does not require a Kubernetes API server, so it also works when `--enable-k8s-exporter` is `false`. |
7879
| [Stackdriver exporter](https://github.com/kubernetes/node-problem-detector/blob/master/config/exporter/stackdriver-exporter.json) | Stackdriver exporter reports node problems and metrics to Stackdriver Monitoring API. | disable_stackdriver_exporter
7980

8081
# Usage
@@ -122,8 +123,14 @@ For example, to run without auth, use the following config:
122123
http://APISERVER_IP:APISERVER_PORT?inClusterConfig=false
123124
```
124125
Refer to [heapster docs](https://github.com/kubernetes/heapster/blob/master/docs/source-configuration.md#kubernetes) for a complete list of available options.
125-
* `--address`: The address to bind the node problem detector server.
126-
* `--port`: The port to bind the node problem detector server. Use 0 to disable.
126+
127+
#### For HTTP exporter
128+
129+
The HTTP exporter serves `/healthz`, `/conditions` and `/debug/pprof`. It does not talk to the
130+
Kubernetes API server, so these endpoints are available even when `--enable-k8s-exporter` is `false`.
131+
132+
* `--address`: The address to bind the node problem detector server, default to `127.0.0.1`.
133+
* `--port`: The port to bind the node problem detector server, default to 20256. Use 0 to disable.
127134

128135
#### For Prometheus exporter
129136

cmd/nodeproblemdetector/node_problem_detector.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
_ "k8s.io/node-problem-detector/cmd/nodeproblemdetector/problemdaemonplugins"
2626
"k8s.io/node-problem-detector/cmd/options"
2727
"k8s.io/node-problem-detector/pkg/exporters"
28+
"k8s.io/node-problem-detector/pkg/exporters/httpexporter"
2829
"k8s.io/node-problem-detector/pkg/exporters/k8sexporter"
2930
"k8s.io/node-problem-detector/pkg/exporters/prometheusexporter"
3031
"k8s.io/node-problem-detector/pkg/problemdaemon"
@@ -51,6 +52,10 @@ func npdMain(ctx context.Context, npdo *options.NodeProblemDetectorOptions) erro
5152

5253
// Initialize exporters.
5354
defaultExporters := []types.Exporter{}
55+
if he := httpexporter.NewExporterOrDie(npdo); he != nil {
56+
defaultExporters = append(defaultExporters, he)
57+
klog.Info("HTTP exporter started.")
58+
}
5459
if ke := k8sexporter.NewExporterOrDie(ctx, npdo); ke != nil {
5560
defaultExporters = append(defaultExporters, ke)
5661
klog.Info("K8s exporter started.")
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package httpexporter
18+
19+
import (
20+
"net"
21+
"net/http"
22+
"net/http/pprof"
23+
"strconv"
24+
"sync"
25+
26+
"k8s.io/klog/v2"
27+
28+
"k8s.io/node-problem-detector/cmd/options"
29+
"k8s.io/node-problem-detector/pkg/types"
30+
"k8s.io/node-problem-detector/pkg/util"
31+
)
32+
33+
type httpExporter struct {
34+
mu sync.RWMutex
35+
conditions map[string]types.Condition
36+
}
37+
38+
// NewExporterOrDie creates the standalone HTTP exporter and starts the server.
39+
// Returns nil if --port is 0 (disabled). Panics on bind errors.
40+
func NewExporterOrDie(npdo *options.NodeProblemDetectorOptions) types.Exporter {
41+
if npdo.ServerPort <= 0 {
42+
return nil
43+
}
44+
45+
he := &httpExporter{
46+
conditions: make(map[string]types.Condition),
47+
}
48+
49+
addr := net.JoinHostPort(npdo.ServerAddress, strconv.Itoa(npdo.ServerPort))
50+
mux := he.buildMux()
51+
go func() {
52+
if err := http.ListenAndServe(addr, mux); err != nil {
53+
klog.Fatalf("Failed to start HTTP server: %v", err)
54+
}
55+
}()
56+
57+
klog.Infof("HTTP exporter started on %s", addr)
58+
return he
59+
}
60+
61+
func (he *httpExporter) buildMux() *http.ServeMux {
62+
mux := http.NewServeMux()
63+
64+
// Add healthz http request handler. Always return ok now, add more health check
65+
// logic in the future.
66+
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
67+
w.WriteHeader(http.StatusOK)
68+
if _, err := w.Write([]byte("ok")); err != nil {
69+
klog.Errorf("Failed to write response: %v", err)
70+
}
71+
})
72+
73+
// Add the handler to serve condition http request.
74+
mux.HandleFunc("/conditions", func(w http.ResponseWriter, r *http.Request) {
75+
util.ReturnHTTPJson(w, he.getConditions())
76+
})
77+
78+
// register pprof
79+
mux.HandleFunc("/debug/pprof/", pprof.Index)
80+
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
81+
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
82+
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
83+
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
84+
85+
return mux
86+
}
87+
88+
func (he *httpExporter) ExportProblems(status *types.Status) {
89+
he.mu.Lock()
90+
defer he.mu.Unlock()
91+
for _, cdt := range status.Conditions {
92+
he.conditions[cdt.Type] = cdt
93+
}
94+
}
95+
96+
func (he *httpExporter) getConditions() []types.Condition {
97+
he.mu.RLock()
98+
defer he.mu.RUnlock()
99+
conditions := make([]types.Condition, 0, len(he.conditions))
100+
for _, c := range he.conditions {
101+
conditions = append(conditions, c)
102+
}
103+
return conditions
104+
}

0 commit comments

Comments
 (0)