Add optional TLS support to /metrics endpoint (#7255)

* Use exporter-toolkit to enable optional TLS encryption on /metrics endpoint

Signed-off-by: peppi-lotta <peppi-lotta.saari@est.tech>

* Implement startup listener to signal server readiness

Signed-off-by: peppi-lotta <peppi-lotta.saari@est.tech>

---------

Signed-off-by: peppi-lotta <peppi-lotta.saari@est.tech>
This commit is contained in:
Peppi-Lotta
2026-03-12 22:49:00 +02:00
committed by GitHub
parent a8c802e1b3
commit 7ff001dca7
13 changed files with 553 additions and 8 deletions

View File

@@ -3,18 +3,22 @@ package metrics
import (
"context"
"log/slog"
"net"
"net/http"
"os"
"sync"
"time"
"github.com/coredns/caddy"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/log"
"github.com/coredns/coredns/plugin/pkg/reuseport"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/exporter-toolkit/web"
)
// Metrics holds the prometheus configuration. The metrics' path is fixed to be /metrics .
@@ -34,6 +38,8 @@ type Metrics struct {
zoneMu sync.RWMutex
plugins map[string]struct{} // all available plugins, used to determine which plugin made the client write
tlsConfigPath string
}
// New returns a new instance of Metrics with the given address.
@@ -83,6 +89,32 @@ func (m *Metrics) ZoneNames() []string {
return s
}
// startupListener wraps a net.Listener to detect when Accept() is first called
type startupListener struct {
net.Listener
readyOnce sync.Once
ready chan struct{}
}
func newStartupListener(l net.Listener) *startupListener {
return &startupListener{
Listener: l,
ready: make(chan struct{}),
}
}
func (sl *startupListener) Accept() (net.Conn, error) {
// Signal ready on first Accept() call (server is running)
sl.readyOnce.Do(func() {
close(sl.ready)
})
return sl.Listener.Accept()
}
func (sl *startupListener) Ready() <-chan struct{} {
return sl.ready
}
// OnStartup sets up the metrics on startup.
func (m *Metrics) OnStartup() error {
ln, err := reuseport.Listen("tcp", m.Addr)
@@ -91,7 +123,9 @@ func (m *Metrics) OnStartup() error {
return err
}
m.ln = ln
startupListener := newStartupListener(ln)
m.ln = startupListener
m.lnSetup = true
m.mux = http.NewServeMux()
@@ -99,6 +133,7 @@ func (m *Metrics) OnStartup() error {
// creating some helper variables to avoid data races on m.srv and m.ln
server := &http.Server{
Addr: m.Addr,
Handler: m.mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
@@ -106,10 +141,53 @@ func (m *Metrics) OnStartup() error {
}
m.srv = server
if m.tlsConfigPath == "" {
go func() {
if err := server.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Errorf("Failed to start HTTP metrics server: %s", err)
}
}()
ListenAddr = ln.Addr().String() // For tests.
return nil
}
// Check TLS config file existence
if _, err := os.Stat(m.tlsConfigPath); os.IsNotExist(err) {
log.Errorf("TLS config file does not exist: %s", m.tlsConfigPath)
return err
}
// Create web config for ListenAndServe
webConfig := &web.FlagConfig{
WebListenAddresses: &[]string{m.Addr},
WebSystemdSocket: new(bool), // false by default
WebConfigFile: &m.tlsConfigPath,
}
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Create channels for synchronization
startUpErr := make(chan error, 1)
go func() {
server.Serve(ln)
// Try to start the server and report result if there an error.
// web.Serve() never returns nil, it always returns a non-nil error and
// it doesn't retun anything if server starts successfully.
// startupListener handles capturing succesful startup.
err := web.Serve(m.ln, server, webConfig, logger)
if err != nil && err != http.ErrServerClosed {
log.Errorf("Failed to start HTTPS metrics server: %v", err)
startUpErr <- err
}
}()
// Wait for startup errors
select {
case err := <-startUpErr:
return err
case <-startupListener.Ready():
log.Infof("Server is ready and accepting connections")
}
ListenAddr = ln.Addr().String() // For tests.
return nil
}