1
0
mirror of https://github.com/prometheus-community/postgres_exporter.git synced 2026-10-23 12:08:57 +03:00

Add configuration reload support (#1406)

Signed-off-by: Chaithanya Nallamothu <chaithanyan5177@gmail.com>
This commit is contained in:
Chaithanya Nallamothu
2026-10-16 16:56:40 +02:00
committed by GitHub
parent f10a723513
commit 5baef8f1b8
5 changed files with 285 additions and 0 deletions

View File

@@ -18,9 +18,11 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"sort"
"strconv"
"strings"
"syscall"
"time"
"github.com/alecthomas/kingpin/v2"
@@ -234,6 +236,12 @@ func main() {
}
http.HandleFunc("/probe", handleProbe(logger, authHandler, cfg))
http.HandleFunc("/-/reload", handleReload(authHandler, *configFile))
reloadSignals := make(chan os.Signal, 1)
signal.Notify(reloadSignals, syscall.SIGHUP)
defer signal.Stop(reloadSignals)
go handleReloadSignal(reloadSignals, authHandler, *configFile, logger)
srv := &http.Server{}
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {

View File

@@ -0,0 +1,47 @@
// 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 main
import (
"fmt"
"log/slog"
"net/http"
"os"
"github.com/prometheus-community/postgres_exporter/config"
)
func handleReload(authHandler *config.Handler, configFile string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if err := authHandler.ReloadAuthConfig(configFile, logger); err != nil {
http.Error(w, fmt.Sprintf("failed to reload configuration: %v", err), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}
func handleReloadSignal(signals <-chan os.Signal, authHandler *config.Handler, configFile string, logger *slog.Logger) {
for range signals {
if err := authHandler.ReloadAuthConfig(configFile, logger); err != nil {
logger.Error("Error reloading config", "err", err)
}
}
}

View File

@@ -0,0 +1,168 @@
// 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 main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"testing"
"github.com/prometheus-community/postgres_exporter/config"
"github.com/prometheus/client_golang/prometheus"
)
func TestHandleReload(t *testing.T) {
tests := []struct {
name string
method string
configFile string
wantStatus int
wantBody string
}{
{
name: "successful reload",
method: http.MethodPost,
configFile: "../../config/testdata/config-good.yaml",
wantStatus: http.StatusOK,
},
{
name: "invalid config",
method: http.MethodPost,
configFile: "../../config/testdata/config-bad-auth-module.yaml",
wantStatus: http.StatusInternalServerError,
wantBody: "failed to reload configuration:",
},
{
name: "method not allowed",
method: http.MethodGet,
configFile: "../../config/testdata/config-good.yaml",
wantStatus: http.StatusMethodNotAllowed,
wantBody: "method not allowed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
authHandler, err := config.NewHandler(prometheus.NewRegistry())
if err != nil {
t.Fatalf("NewHandler() error = %v", err)
}
handler := handleReload(authHandler, tt.configFile)
request := httptest.NewRequest(tt.method, "/-/reload", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if got := response.Code; got != tt.wantStatus {
t.Fatalf("status code = %d, want %d", got, tt.wantStatus)
}
if tt.wantBody != "" && !strings.Contains(response.Body.String(), tt.wantBody) {
t.Fatalf("response body = %q, want it to contain %q", response.Body.String(), tt.wantBody)
}
})
}
}
func TestHandleReloadConcurrentRequests(t *testing.T) {
authHandler, err := config.NewHandler(prometheus.NewRegistry())
if err != nil {
t.Fatalf("NewHandler() error = %v", err)
}
handler := handleReload(authHandler, "../../config/testdata/config-good.yaml")
const requests = 10
errs := make(chan error, requests)
var wg sync.WaitGroup
for range requests {
wg.Add(1)
go func() {
defer wg.Done()
request := httptest.NewRequest(http.MethodPost, "/-/reload", nil)
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
if response.Code != http.StatusOK {
errs <- fmt.Errorf("status code = %d, want %d", response.Code, http.StatusOK)
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
t.Error(err)
}
}
func TestHandleReloadSignal(t *testing.T) {
dir := t.TempDir()
configFile := filepath.Join(dir, "postgres_exporter.yml")
initial := []byte(`auth_modules:
first:
type: userpass
userpass:
username: before
password: firstpass
`)
if err := os.WriteFile(configFile, initial, 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
authHandler, err := config.NewHandler(prometheus.NewRegistry())
if err != nil {
t.Fatalf("NewHandler() error = %v", err)
}
if err := authHandler.ReloadAuthConfig(configFile, logger); err != nil {
t.Fatalf("ReloadAuthConfig() initial load error = %v", err)
}
updated := []byte(`auth_modules:
first:
type: userpass
userpass:
username: after
password: firstpass
`)
if err := os.WriteFile(configFile, updated, 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
signals := make(chan os.Signal, 1)
done := make(chan struct{})
go func() {
handleReloadSignal(signals, authHandler, configFile, logger)
close(done)
}()
signals <- syscall.SIGHUP
close(signals)
<-done
if got, want := authHandler.GetAuthConfig().AuthModules["first"].UserPass.Username, "after"; got != want {
t.Fatalf("username after SIGHUP = %q, want %q", got, want)
}
}

View File

@@ -19,6 +19,7 @@ import (
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
)
func TestNewConfigWithDefaults(t *testing.T) {
@@ -327,3 +328,60 @@ func TestLoadBadConfigs(t *testing.T) {
})
}
}
func TestReloadAuthConfigPreservesLastKnownGoodConfig(t *testing.T) {
ch, err := NewHandler(prometheus.NewRegistry())
if err != nil {
t.Fatalf("NewHandler() error = %v", err)
}
if err := ch.ReloadAuthConfig("testdata/config-good.yaml", nil); err != nil {
t.Fatalf("ReloadAuthConfig() initial load error = %v", err)
}
before := ch.GetAuthConfig()
if got, want := before.AuthModules["first"].UserPass.Username, "first"; got != want {
t.Fatalf("username before failed reload = %q, want %q", got, want)
}
if err := ch.ReloadAuthConfig("testdata/config-bad-auth-module.yaml", nil); err == nil {
t.Fatal("ReloadAuthConfig() error = nil, want error")
}
after := ch.GetAuthConfig()
if got, want := after.AuthModules["first"].UserPass.Username, "first"; got != want {
t.Fatalf("username after failed reload = %q, want %q", got, want)
}
}
func TestReloadAuthConfigMetrics(t *testing.T) {
ch, err := NewHandler(prometheus.NewRegistry())
if err != nil {
t.Fatalf("NewHandler() error = %v", err)
}
if err := ch.ReloadAuthConfig("testdata/config-good.yaml", nil); err != nil {
t.Fatalf("ReloadAuthConfig() error = %v", err)
}
if got, want := testutil.ToFloat64(ch.configReloadSuccess), float64(1); got != want {
t.Fatalf("config reload success = %v, want %v", got, want)
}
successTimestamp := testutil.ToFloat64(ch.configReloadSeconds)
if successTimestamp == 0 {
t.Fatal("config reload success timestamp = 0, want non-zero")
}
if err := ch.ReloadAuthConfig("testdata/config-bad-auth-module.yaml", nil); err == nil {
t.Fatal("ReloadAuthConfig() error = nil, want error")
}
if got, want := testutil.ToFloat64(ch.configReloadSuccess), float64(0); got != want {
t.Fatalf("config reload success after failed reload = %v, want %v", got, want)
}
if got := testutil.ToFloat64(ch.configReloadSeconds); got != successTimestamp {
t.Fatalf("config reload success timestamp after failed reload = %v, want %v", got, successTimestamp)
}
}

View File

@@ -135,6 +135,10 @@ auth_modules:
sslmode: require
```
The configuration file can be reloaded without restarting the exporter. Send a `SIGHUP` signal to the process or make a `POST` request to `/-/reload`.
A successful reload replaces the current configuration. If the new configuration cannot be read or parsed, the existing configuration remains active. In that case, `POST /-/reload` returns a non-2xx response and a `SIGHUP` reload logs the error.
## Deprecated options
These remain for backward compatibility but shouldn't be used in new deployments: