You've already forked postgres_exporter
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:
committed by
GitHub
parent
f10a723513
commit
5baef8f1b8
@@ -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 {
|
||||
|
||||
47
cmd/postgres_exporter/reload.go
Normal file
47
cmd/postgres_exporter/reload.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
168
cmd/postgres_exporter/reload_test.go
Normal file
168
cmd/postgres_exporter/reload_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user