Skip to content

Commit 3592cfb

Browse files
committed
Expose metrics http server for extra endpoints
This allows users to register extra http endpoints on the http server that serves metrics.
1 parent f284dc3 commit 3592cfb

File tree

3 files changed

+84
-4
lines changed

3 files changed

+84
-4
lines changed

pkg/manager/internal.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const (
5050

5151
defaultReadinessEndpoint = "/readyz"
5252
defaultLivenessEndpoint = "/healthz"
53+
defaultMetricsEndpoint = "/metrics"
5354
)
5455

5556
var log = logf.RuntimeLog.WithName("manager")
@@ -95,6 +96,9 @@ type controllerManager struct {
9596
// metricsListener is used to serve prometheus metrics
9697
metricsListener net.Listener
9798

99+
// metricsExtraHandlers contains extra handlers to register on http server that serves metrics.
100+
metricsExtraHandlers map[string]http.Handler
101+
98102
// healthProbeListener is used to serve liveness probe
99103
healthProbeListener net.Listener
100104

@@ -260,6 +264,25 @@ func (cm *controllerManager) SetFields(i interface{}) error {
260264
return nil
261265
}
262266

267+
// AddMetricsExtraHandler adds extra handler served on path to the http server that serves metrics.
268+
func (cm *controllerManager) AddMetricsExtraHandler(path string, handler http.Handler) error {
269+
if path == defaultMetricsEndpoint {
270+
return fmt.Errorf("overriding builtin %s endpoint is not allowed", defaultMetricsEndpoint)
271+
}
272+
273+
cm.mu.Lock()
274+
defer cm.mu.Unlock()
275+
276+
_, found := cm.metricsExtraHandlers[path]
277+
if found {
278+
return fmt.Errorf("can't register extra handler by duplicate path %q on metrics http server", path)
279+
}
280+
281+
cm.metricsExtraHandlers[path] = handler
282+
log.V(2).Info("Registering metrics http server extra handler", "path", path)
283+
return nil
284+
}
285+
263286
// AddHealthzCheck allows you to add Healthz checker
264287
func (cm *controllerManager) AddHealthzCheck(name string, check healthz.Checker) error {
265288
cm.mu.Lock()
@@ -341,19 +364,28 @@ func (cm *controllerManager) GetWebhookServer() *webhook.Server {
341364
}
342365

343366
func (cm *controllerManager) serveMetrics(stop <-chan struct{}) {
344-
var metricsPath = "/metrics"
345367
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{
346368
ErrorHandling: promhttp.HTTPErrorOnError,
347369
})
348370
// TODO(JoelSpeed): Use existing Kubernetes machinery for serving metrics
349371
mux := http.NewServeMux()
350-
mux.Handle(metricsPath, handler)
372+
mux.Handle(defaultMetricsEndpoint, handler)
373+
374+
func() {
375+
cm.mu.Lock()
376+
defer cm.mu.Unlock()
377+
378+
for path, extraHandler := range cm.metricsExtraHandlers {
379+
mux.Handle(path, extraHandler)
380+
}
381+
}()
382+
351383
server := http.Server{
352384
Handler: mux,
353385
}
354386
// Run the server
355387
go func() {
356-
log.Info("starting metrics server", "path", metricsPath)
388+
log.Info("starting metrics server", "path", defaultMetricsEndpoint)
357389
if err := server.Serve(cm.metricsListener); err != nil && err != http.ErrServerClosed {
358390
cm.errSignal.SignalError(err)
359391
}
@@ -367,6 +399,8 @@ func (cm *controllerManager) serveMetrics(stop <-chan struct{}) {
367399
}
368400

369401
func (cm *controllerManager) serveHealthProbes(stop <-chan struct{}) {
402+
// TODO(hypnoglow): refactor locking to use anonymous func in the similar way
403+
// it's done in serveMetrics.
370404
cm.mu.Lock()
371405
mux := http.NewServeMux()
372406

pkg/manager/manager.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package manager
1919
import (
2020
"fmt"
2121
"net"
22+
"net/http"
2223
"time"
2324

2425
"github.com/go-logr/logr"
@@ -54,6 +55,13 @@ type Manager interface {
5455
// interface - e.g. inject.Client.
5556
SetFields(interface{}) error
5657

58+
// AddMetricsExtraHandler adds an extra handler served on path to the http server that serves metrics.
59+
// Might be useful to register some diagnostic endpoints e.g. pprof. Note that these endpoints meant to be
60+
// sensitive and shouldn't be exposed publicly.
61+
// If the simple path -> handler mapping offered here is not enough, a new http server/listener should be added as
62+
// Runnable to the manager via Add method.
63+
AddMetricsExtraHandler(path string, handler http.Handler) error
64+
5765
// AddHealthzCheck allows you to add Healthz checker
5866
AddHealthzCheck(name string, check healthz.Checker) error
5967

@@ -282,6 +290,9 @@ func New(config *rest.Config, options Options) (Manager, error) {
282290
return nil, err
283291
}
284292

293+
// By default we have no extra endpoints to expose on metrics http server.
294+
metricsExtraHandlers := make(map[string]http.Handler)
295+
285296
// Create health probes listener. This will throw an error if the bind
286297
// address is invalid or already in use.
287298
healthProbeListener, err := options.newHealthProbeListener(options.HealthProbeBindAddress)
@@ -302,6 +313,7 @@ func New(config *rest.Config, options Options) (Manager, error) {
302313
resourceLock: resourceLock,
303314
mapper: mapper,
304315
metricsListener: metricsListener,
316+
metricsExtraHandlers: metricsExtraHandlers,
305317
internalStop: stop,
306318
internalStopper: stop,
307319
port: options.Port,

pkg/manager/manager_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ var _ = Describe("manger.Manager", func() {
412412
Expect(resp.StatusCode).To(Equal(200))
413413
})
414414

415-
It("should not serve anything other than metrics endpoint", func(done Done) {
415+
It("should not serve anything other than metrics endpoint by default", func(done Done) {
416416
opts.MetricsBindAddress = ":0"
417417
m, err := New(cfg, opts)
418418
Expect(err).NotTo(HaveOccurred())
@@ -469,6 +469,40 @@ var _ = Describe("manger.Manager", func() {
469469
ok := metrics.Registry.Unregister(one)
470470
Expect(ok).To(BeTrue())
471471
})
472+
473+
It("should serve extra endpoints", func(done Done) {
474+
opts.MetricsBindAddress = ":0"
475+
m, err := New(cfg, opts)
476+
Expect(err).NotTo(HaveOccurred())
477+
478+
err = m.AddMetricsExtraHandler("/debug", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
479+
_, _ = w.Write([]byte("Some debug info"))
480+
}))
481+
Expect(err).NotTo(HaveOccurred())
482+
483+
// Should error when we add another extra endpoint on the already registered path.
484+
err = m.AddMetricsExtraHandler("/debug", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
485+
_, _ = w.Write([]byte("Another debug info"))
486+
}))
487+
Expect(err).To(HaveOccurred())
488+
489+
s := make(chan struct{})
490+
defer close(s)
491+
go func() {
492+
defer GinkgoRecover()
493+
Expect(m.Start(s)).NotTo(HaveOccurred())
494+
close(done)
495+
}()
496+
497+
endpoint := fmt.Sprintf("http://%s/debug", listener.Addr().String())
498+
resp, err := http.Get(endpoint)
499+
Expect(err).NotTo(HaveOccurred())
500+
Expect(resp.StatusCode).To(Equal(http.StatusOK))
501+
502+
body, err := ioutil.ReadAll(resp.Body)
503+
Expect(err).NotTo(HaveOccurred())
504+
Expect(string(body)).To(Equal("Some debug info"))
505+
})
472506
})
473507
})
474508

0 commit comments

Comments
 (0)