Skip to content

Commit cc751b7

Browse files
committed
Add config module
The config module supports adding configuration to the exporter via a config file. This supports adding authentication details in a config file so that /probe requests can specify authentication for endpoints Signed-off-by: Joe Adams <[email protected]>
1 parent 713461d commit cc751b7

File tree

9 files changed

+249
-6
lines changed

9 files changed

+249
-6
lines changed

cmd/postgres_exporter/main.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/go-kit/log"
2121
"github.com/go-kit/log/level"
2222
"github.com/prometheus-community/postgres_exporter/collector"
23+
"github.com/prometheus-community/postgres_exporter/config"
2324
"github.com/prometheus/client_golang/prometheus"
2425
"github.com/prometheus/client_golang/prometheus/promhttp"
2526
"github.com/prometheus/common/promlog"
@@ -31,6 +32,11 @@ import (
3132
)
3233

3334
var (
35+
c = config.ConfigHandler{
36+
Config: &config.Config{},
37+
}
38+
39+
configFile = kingpin.Flag("config.file", "Promehteus exporter configuration file.").Default("postres_exporter.yml").String()
3440
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
3541
webConfig = webflag.AddFlags(kingpin.CommandLine)
3642
metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("PG_EXPORTER_WEB_TELEMETRY_PATH").String()
@@ -85,6 +91,11 @@ func main() {
8591
return
8692
}
8793

94+
if err := c.ReloadConfig(*configFile, logger); err != nil {
95+
// This is not fatal, but it means that auth must be provided for every dsn.
96+
level.Error(logger).Log("msg", "Error loading config", "err", err)
97+
}
98+
8899
dsns, err := getDataSources()
89100
if err != nil {
90101
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())

cmd/postgres_exporter/probe.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,53 @@
1414
package main
1515

1616
import (
17+
"fmt"
1718
"net/http"
1819
"time"
1920

2021
"github.com/go-kit/log"
22+
"github.com/go-kit/log/level"
2123
"github.com/prometheus-community/postgres_exporter/collector"
24+
"github.com/prometheus-community/postgres_exporter/config"
2225
"github.com/prometheus/client_golang/prometheus"
2326
"github.com/prometheus/client_golang/prometheus/promhttp"
2427
)
2528

2629
func handleProbe(logger log.Logger) http.HandlerFunc {
2730
return func(w http.ResponseWriter, r *http.Request) {
2831
ctx := r.Context()
32+
conf := c.GetConfig()
2933
params := r.URL.Query()
3034
target := params.Get("target")
3135
if target == "" {
3236
http.Error(w, "target is required", http.StatusBadRequest)
3337
return
3438
}
39+
var authModule config.AuthModule
40+
authModuleName := params.Get("auth_module")
41+
if authModuleName == "" {
42+
level.Info(logger).Log("msg", "no auth_module specified, using default")
43+
} else {
44+
var ok bool
45+
authModule, ok = conf.AuthModules[authModuleName]
46+
if !ok {
47+
http.Error(w, fmt.Sprintf("auth_module %s not found", authModuleName), http.StatusBadRequest)
48+
return
49+
}
50+
if authModule.UserPass.Username == "" || authModule.UserPass.Password == "" {
51+
http.Error(w, fmt.Sprintf("auth_module %s has no username or password", authModuleName), http.StatusBadRequest)
52+
return
53+
}
54+
}
55+
56+
dsn, err := authModule.ConfigureTarget(target)
57+
if err != nil {
58+
level.Error(logger).Log("msg", "failed to configure target", "err", err)
59+
http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest)
60+
return
61+
}
3562

3663
// TODO: Timeout
37-
// TODO: Auth Module
3864

3965
probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
4066
Name: "probe_success",
@@ -46,18 +72,14 @@ func handleProbe(logger log.Logger) http.HandlerFunc {
4672
})
4773

4874
tl := log.With(logger, "target", target)
49-
_ = tl
5075

5176
start := time.Now()
5277
registry := prometheus.NewRegistry()
5378
registry.MustRegister(probeSuccessGauge)
5479
registry.MustRegister(probeDurationGauge)
5580

56-
// TODO(@sysadmind): this is a temp hack until we have a proper auth module
57-
target = "postgres://postgres:test@localhost:5432/circle_test?sslmode=disable"
58-
5981
// Run the probe
60-
pc, err := collector.NewProbeCollector(tl, registry, target)
82+
pc, err := collector.NewProbeCollector(tl, registry, dsn)
6183
if err != nil {
6284
probeSuccessGauge.Set(0)
6385
probeDurationGauge.Set(time.Since(start).Seconds())

config/config.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright 2022 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package config
15+
16+
import (
17+
"fmt"
18+
"net/url"
19+
"os"
20+
"strings"
21+
"sync"
22+
23+
"github.com/go-kit/log"
24+
"github.com/prometheus/client_golang/prometheus"
25+
"gopkg.in/yaml.v3"
26+
)
27+
28+
var (
29+
configReloadSuccess = prometheus.NewGauge(prometheus.GaugeOpts{
30+
Namespace: "postgres_exporter",
31+
Name: "config_last_reload_successful",
32+
Help: "Postgres exporter config loaded successfully.",
33+
})
34+
35+
configReloadSeconds = prometheus.NewGauge(prometheus.GaugeOpts{
36+
Namespace: "postgres_exporter",
37+
Name: "config_last_reload_success_timestamp_seconds",
38+
Help: "Timestamp of the last successful configuration reload.",
39+
})
40+
)
41+
42+
func init() {
43+
prometheus.MustRegister(configReloadSuccess)
44+
prometheus.MustRegister(configReloadSeconds)
45+
}
46+
47+
type Config struct {
48+
AuthModules map[string]AuthModule `yaml:"auth_modules"`
49+
}
50+
51+
type AuthModule struct {
52+
Type string `yaml:"type"`
53+
UserPass UserPass `yaml:"userpass,omitempty"`
54+
// Add alternative auth modules here
55+
Options map[string]string `yaml:"options"`
56+
}
57+
58+
type UserPass struct {
59+
Username string `yaml:"username"`
60+
Password string `yaml:"password"`
61+
}
62+
63+
type ConfigHandler struct {
64+
sync.RWMutex
65+
Config *Config
66+
}
67+
68+
func (ch *ConfigHandler) GetConfig() *Config {
69+
ch.RLock()
70+
defer ch.RUnlock()
71+
return ch.Config
72+
}
73+
74+
func (ch *ConfigHandler) ReloadConfig(f string, logger log.Logger) error {
75+
config := &Config{}
76+
var err error
77+
defer func() {
78+
if err != nil {
79+
configReloadSuccess.Set(0)
80+
} else {
81+
configReloadSuccess.Set(1)
82+
configReloadSeconds.SetToCurrentTime()
83+
}
84+
}()
85+
86+
yamlReader, err := os.Open(f)
87+
if err != nil {
88+
return fmt.Errorf("Error opening config file %q: %s", f, err)
89+
}
90+
defer yamlReader.Close()
91+
decoder := yaml.NewDecoder(yamlReader)
92+
decoder.KnownFields(true)
93+
94+
if err = decoder.Decode(config); err != nil {
95+
return fmt.Errorf("Error parsing config file %q: %s", f, err)
96+
}
97+
98+
ch.Lock()
99+
ch.Config = config
100+
ch.Unlock()
101+
return nil
102+
}
103+
104+
func (m AuthModule) ConfigureTarget(target string) (string, error) {
105+
// ip:port urls do not parse properly and that is the typical way users interact with postgres
106+
t := fmt.Sprintf("exporter://%s", target)
107+
u, err := url.Parse(t)
108+
if err != nil {
109+
return "", err
110+
}
111+
112+
if m.Type == "userpass" {
113+
u.User = url.UserPassword(m.UserPass.Username, m.UserPass.Password)
114+
}
115+
116+
query := u.Query()
117+
for k, v := range m.Options {
118+
query.Set(k, v)
119+
}
120+
u.RawQuery = query.Encode()
121+
122+
parsed := u.String()
123+
trim := strings.TrimPrefix(parsed, "exporter://")
124+
125+
return trim, nil
126+
}

config/config_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright 2022 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package config
15+
16+
import (
17+
"testing"
18+
)
19+
20+
func TestLoadConfig(t *testing.T) {
21+
ch := &ConfigHandler{
22+
Config: &Config{},
23+
}
24+
25+
err := ch.ReloadConfig("testdata/config-good.yaml", nil)
26+
if err != nil {
27+
t.Errorf("Error loading config: %s", err)
28+
}
29+
}
30+
31+
func TestLoadBadConfigs(t *testing.T) {
32+
ch := &ConfigHandler{
33+
Config: &Config{},
34+
}
35+
36+
tests := []struct {
37+
input string
38+
want string
39+
}{
40+
{
41+
input: "testdata/config-bad-auth-module.yaml",
42+
want: "Error parsing config file \"testdata/config-bad-auth-module.yaml\": yaml: unmarshal errors:\n line 3: field pretendauth not found in type config.AuthModule",
43+
},
44+
{
45+
input: "testdata/config-bad-extra-field.yaml",
46+
want: "Error parsing config file \"testdata/config-bad-extra-field.yaml\": yaml: unmarshal errors:\n line 8: field doesNotExist not found in type config.AuthModule",
47+
},
48+
}
49+
50+
for _, test := range tests {
51+
t.Run(test.input, func(t *testing.T) {
52+
got := ch.ReloadConfig(test.input, nil)
53+
if got == nil || got.Error() != test.want {
54+
t.Fatalf("ReloadConfig(%q) = %v, want %s", test.input, got, test.want)
55+
}
56+
})
57+
}
58+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
auth_modules:
2+
foo:
3+
pretendauth:
4+
username: test
5+
password: pass
6+
options:
7+
extra: "1"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
auth_modules:
2+
foo:
3+
userpass:
4+
username: test
5+
password: pass
6+
options:
7+
extra: "1"
8+
doesNotExist: test

config/testdata/config-good.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
auth_modules:
2+
first:
3+
type: userpass
4+
userpass:
5+
username: first
6+
password: firstpass
7+
options:
8+
sslmode: disable

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
gopkg.in/alecthomas/kingpin.v2 v2.2.6
1414
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
1515
gopkg.in/yaml.v2 v2.4.0
16+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
1617
)
1718

1819
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
494494
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
495495
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
496496
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
497+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
498+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
497499
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
498500
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
499501
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

0 commit comments

Comments
 (0)