Skip to content

Commit e552a37

Browse files
authored
Merge pull request prometheus-community#618 from sysadmind/multi-target
Add multi-target support
2 parents 58cc383 + 72430f8 commit e552a37

16 files changed

+494
-171
lines changed

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ docker run \
2121
quay.io/prometheuscommunity/postgres-exporter
2222
```
2323

24+
## Multi-Target Support (BETA)
25+
**This Feature is in beta and may require changes in future releases. Feedback is welcome.**
26+
27+
This exporter supports the [multi-target pattern](https://prometheus.io/docs/guides/multi-target-exporter/). This allows running a single instance of this exporter for multiple postgres targets. Using the milti-target funcationality of this exporter is **optional** and meant for users where it is impossible to install the exporter as a sidecar. For example SaaS-managed services.
28+
29+
To use the multi-target functionality, send an http request to the endpoint `/probe?target=foo:5432` where target is set to the DSN of the postgres instance to scrape metrics from.
30+
31+
To avoid putting sensitive information like username and password in the URL, preconfigured auth modules are supported via the [auth_modules](#auth_modules) section of the config file. auth_modules for DSNs can be used with the `/probe` endpoint by specifying the `?auth_module=foo` http parameter.
32+
33+
## Configuration File
34+
35+
The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postgres_exporter.yml`.
36+
37+
### auth_modules
38+
This section defines preset authentication and connection parameters for use in the [multi-target endpoint](#multi-target-support-beta). `auth_modules` is a map of modules with the key being the identifier which can be used in the `/probe` endpoint.
39+
Currently only the `userpass` type is supported.
40+
41+
Example:
42+
```yaml
43+
auth_modules:
44+
foo1: # Set this to any name you want
45+
type: userpass
46+
userpass:
47+
username: first
48+
password: firstpass
49+
options:
50+
# options become key=value parameters of the DSN
51+
sslmode: disable
52+
```
53+
2454
## Building and running
2555
2656
git clone https://github.com/prometheus-community/postgres_exporter.git

cmd/postgres_exporter/datasource.go

+6
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ func getDataSources() ([]string, error) {
162162
uri = os.Getenv("DATA_SOURCE_URI")
163163
}
164164

165+
// No datasources found. This allows us to support the multi-target pattern
166+
// withouth an explicit datasource.
167+
if uri == "" {
168+
return []string{}, nil
169+
}
170+
165171
dsn = "postgresql://" + ui + "@" + uri
166172

167173
return []string{dsn}, nil

cmd/postgres_exporter/main.go

+23-9
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", "Postgres exporter configuration file.").Default("postgres_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,14 +91,14 @@ func main() {
8591
return
8692
}
8793

88-
dsn, err := getDataSources()
89-
if err != nil {
90-
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
91-
os.Exit(1)
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)
9297
}
9398

94-
if len(dsn) == 0 {
95-
level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use")
99+
dsns, err := getDataSources()
100+
if err != nil {
101+
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
96102
os.Exit(1)
97103
}
98104

@@ -106,7 +112,7 @@ func main() {
106112
IncludeDatabases(*includeDatabases),
107113
}
108114

109-
exporter := NewExporter(dsn, opts...)
115+
exporter := NewExporter(dsns, opts...)
110116
defer func() {
111117
exporter.servers.Close()
112118
}()
@@ -115,23 +121,31 @@ func main() {
115121

116122
prometheus.MustRegister(exporter)
117123

124+
// TODO(@sysadmind): Remove this with multi-target support. We are removing multiple DSN support
125+
dsn := ""
126+
if len(dsns) > 0 {
127+
dsn = dsns[0]
128+
}
129+
118130
pe, err := collector.NewPostgresCollector(
119131
logger,
120132
dsn,
121133
[]string{},
122134
)
123135
if err != nil {
124136
level.Error(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error())
125-
os.Exit(1)
137+
} else {
138+
prometheus.MustRegister(pe)
126139
}
127-
prometheus.MustRegister(pe)
128140

129141
http.Handle(*metricPath, promhttp.Handler())
130142
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
131143
w.Header().Set("Content-Type", "text/html; charset=UTF-8") // nolint: errcheck
132144
w.Write(landingPage) // nolint: errcheck
133145
})
134146

147+
http.HandleFunc("/probe", handleProbe(logger))
148+
135149
level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress)
136150
srv := &http.Server{Addr: *listenAddress}
137151
if err := web.ListenAndServe(srv, *webConfig, logger); err != nil {

cmd/postgres_exporter/probe.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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 main
15+
16+
import (
17+
"fmt"
18+
"net/http"
19+
"time"
20+
21+
"github.com/go-kit/log"
22+
"github.com/go-kit/log/level"
23+
"github.com/prometheus-community/postgres_exporter/collector"
24+
"github.com/prometheus-community/postgres_exporter/config"
25+
"github.com/prometheus/client_golang/prometheus"
26+
"github.com/prometheus/client_golang/prometheus/promhttp"
27+
)
28+
29+
func handleProbe(logger log.Logger) http.HandlerFunc {
30+
return func(w http.ResponseWriter, r *http.Request) {
31+
ctx := r.Context()
32+
conf := c.GetConfig()
33+
params := r.URL.Query()
34+
target := params.Get("target")
35+
if target == "" {
36+
http.Error(w, "target is required", http.StatusBadRequest)
37+
return
38+
}
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+
}
62+
63+
// TODO(@sysadmind): Timeout
64+
65+
probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
66+
Name: "probe_success",
67+
Help: "Displays whether or not the probe was a success",
68+
})
69+
probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
70+
Name: "probe_duration_seconds",
71+
Help: "Returns how long the probe took to complete in seconds",
72+
})
73+
74+
tl := log.With(logger, "target", target)
75+
76+
start := time.Now()
77+
registry := prometheus.NewRegistry()
78+
registry.MustRegister(probeSuccessGauge)
79+
registry.MustRegister(probeDurationGauge)
80+
81+
// Run the probe
82+
pc, err := collector.NewProbeCollector(tl, registry, dsn)
83+
if err != nil {
84+
probeSuccessGauge.Set(0)
85+
probeDurationGauge.Set(time.Since(start).Seconds())
86+
http.Error(w, err.Error(), http.StatusInternalServerError)
87+
return
88+
}
89+
90+
// TODO(@sysadmind): Remove the registry.MustRegister() call below and instead handle the collection here. That will allow
91+
// for the passing of context, handling of timeouts, and more control over the collection.
92+
// The current NewProbeCollector() implementation relies on the MustNewConstMetric() call to create the metrics which is not
93+
// ideal to use without the registry.MustRegister() call.
94+
_ = ctx
95+
96+
registry.MustRegister(pc)
97+
98+
duration := time.Since(start).Seconds()
99+
probeDurationGauge.Set(duration)
100+
101+
// TODO check success, etc
102+
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
103+
h.ServeHTTP(w, r)
104+
}
105+
}

collector/collector.go

+16-26
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package collector
1515

1616
import (
1717
"context"
18+
"database/sql"
1819
"errors"
1920
"fmt"
2021
"sync"
@@ -58,7 +59,7 @@ var (
5859
)
5960

6061
type Collector interface {
61-
Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error
62+
Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error
6263
}
6364

6465
func registerCollector(name string, isDefaultEnabled bool, createFunc func(logger log.Logger) (Collector, error)) {
@@ -86,13 +87,13 @@ type PostgresCollector struct {
8687
Collectors map[string]Collector
8788
logger log.Logger
8889

89-
servers map[string]*server
90+
db *sql.DB
9091
}
9192

9293
type Option func(*PostgresCollector) error
9394

9495
// NewPostgresCollector creates a new PostgresCollector.
95-
func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, options ...Option) (*PostgresCollector, error) {
96+
func NewPostgresCollector(logger log.Logger, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
9697
p := &PostgresCollector{
9798
logger: logger,
9899
}
@@ -136,17 +137,18 @@ func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, op
136137

137138
p.Collectors = collectors
138139

139-
servers := make(map[string]*server)
140-
for _, dsn := range dsns {
141-
s, err := makeServer(dsn)
142-
if err != nil {
143-
return nil, err
144-
}
140+
if dsn == "" {
141+
return nil, errors.New("empty dsn")
142+
}
145143

146-
servers[dsn] = s
144+
db, err := sql.Open("postgres", dsn)
145+
if err != nil {
146+
return nil, err
147147
}
148+
db.SetMaxOpenConns(1)
149+
db.SetMaxIdleConns(1)
148150

149-
p.servers = servers
151+
p.db = db
150152

151153
return p, nil
152154
}
@@ -160,32 +162,20 @@ func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) {
160162
// Collect implements the prometheus.Collector interface.
161163
func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
162164
ctx := context.TODO()
163-
wg := sync.WaitGroup{}
164-
wg.Add(len(p.servers))
165-
for _, s := range p.servers {
166-
go func(s *server) {
167-
p.subCollect(ctx, s, ch)
168-
wg.Done()
169-
}(s)
170-
}
171-
wg.Wait()
172-
}
173-
174-
func (p PostgresCollector) subCollect(ctx context.Context, server *server, ch chan<- prometheus.Metric) {
175165
wg := sync.WaitGroup{}
176166
wg.Add(len(p.Collectors))
177167
for name, c := range p.Collectors {
178168
go func(name string, c Collector) {
179-
execute(ctx, name, c, server, ch, p.logger)
169+
execute(ctx, name, c, p.db, ch, p.logger)
180170
wg.Done()
181171
}(name, c)
182172
}
183173
wg.Wait()
184174
}
185175

186-
func execute(ctx context.Context, name string, c Collector, s *server, ch chan<- prometheus.Metric, logger log.Logger) {
176+
func execute(ctx context.Context, name string, c Collector, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) {
187177
begin := time.Now()
188-
err := c.Update(ctx, s, ch)
178+
err := c.Update(ctx, db, ch)
189179
duration := time.Since(begin)
190180
var success float64
191181

collector/pg_database.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package collector
1515

1616
import (
1717
"context"
18+
"database/sql"
1819

1920
"github.com/go-kit/log"
2021
"github.com/prometheus/client_golang/prometheus"
@@ -36,15 +37,11 @@ var pgDatabase = map[string]*prometheus.Desc{
3637
"size_bytes": prometheus.NewDesc(
3738
"pg_database_size_bytes",
3839
"Disk space used by the database",
39-
[]string{"datname", "server"}, nil,
40+
[]string{"datname"}, nil,
4041
),
4142
}
4243

43-
func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error {
44-
db, err := server.GetDB()
45-
if err != nil {
46-
return err
47-
}
44+
func (PGDatabaseCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error {
4845
rows, err := db.QueryContext(ctx,
4946
`SELECT pg_database.datname
5047
,pg_database_size(pg_database.datname)
@@ -63,7 +60,7 @@ func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<-
6360

6461
ch <- prometheus.MustNewConstMetric(
6562
pgDatabase["size_bytes"],
66-
prometheus.GaugeValue, float64(size), datname, server.GetName(),
63+
prometheus.GaugeValue, float64(size), datname,
6764
)
6865
}
6966
if err := rows.Err(); err != nil {

0 commit comments

Comments
 (0)