From 713461df9847bd50d47b0df2b5a4f1f01206bdb8 Mon Sep 17 00:00:00 2001
From: Joe Adams <github@joeadams.io>
Date: Fri, 25 Feb 2022 11:45:15 -0500
Subject: [PATCH 1/4] WIP: Add prelim multi-target support

- Remove multi server support from new collector package
- Add http handler for multi-target support

Signed-off-by: Joe Adams <github@joeadams.io>
---
 cmd/postgres_exporter/datasource.go |   6 ++
 cmd/postgres_exporter/main.go       |  25 ++++---
 cmd/postgres_exporter/probe.go      |  91 +++++++++++++++++++++++++
 collector/collector.go              |  44 +++++-------
 collector/pg_database.go            |  11 ++-
 collector/pg_stat_bgwriter.go       |  43 ++++--------
 collector/probe.go                  |  90 +++++++++++++++++++++++++
 collector/server.go                 | 100 ----------------------------
 8 files changed, 239 insertions(+), 171 deletions(-)
 create mode 100644 cmd/postgres_exporter/probe.go
 create mode 100644 collector/probe.go
 delete mode 100644 collector/server.go

diff --git a/cmd/postgres_exporter/datasource.go b/cmd/postgres_exporter/datasource.go
index 90d797b6..716138f3 100644
--- a/cmd/postgres_exporter/datasource.go
+++ b/cmd/postgres_exporter/datasource.go
@@ -162,6 +162,12 @@ func getDataSources() ([]string, error) {
 		uri = os.Getenv("DATA_SOURCE_URI")
 	}
 
+	// No datasources found. This allows us to support the multi-target pattern
+	// withouth an explicit datasource.
+	if uri == "" {
+		return []string{}, nil
+	}
+
 	dsn = "postgresql://" + ui + "@" + uri
 
 	return []string{dsn}, nil
diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go
index 35db723a..2ec1bf26 100644
--- a/cmd/postgres_exporter/main.go
+++ b/cmd/postgres_exporter/main.go
@@ -85,16 +85,17 @@ func main() {
 		return
 	}
 
-	dsn, err := getDataSources()
+	dsns, err := getDataSources()
 	if err != nil {
 		level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
 		os.Exit(1)
 	}
 
-	if len(dsn) == 0 {
-		level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use")
-		os.Exit(1)
-	}
+	// TODO(@sysadmind): Remove this with multi-target support
+	// if len(dsn) == 0 {
+	// 	level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use")
+	// 	os.Exit(1)
+	// }
 
 	opts := []ExporterOpt{
 		DisableDefaultMetrics(*disableDefaultMetrics),
@@ -106,7 +107,7 @@ func main() {
 		IncludeDatabases(*includeDatabases),
 	}
 
-	exporter := NewExporter(dsn, opts...)
+	exporter := NewExporter(dsns, opts...)
 	defer func() {
 		exporter.servers.Close()
 	}()
@@ -115,6 +116,12 @@ func main() {
 
 	prometheus.MustRegister(exporter)
 
+	// TODO(@sysadmind): Remove this with multi-target support. We are removing multiple DSN support
+	dsn := ""
+	if len(dsns) > 0 {
+		dsn = dsns[0]
+	}
+
 	pe, err := collector.NewPostgresCollector(
 		logger,
 		dsn,
@@ -122,9 +129,9 @@ func main() {
 	)
 	if err != nil {
 		level.Error(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error())
-		os.Exit(1)
+	} else {
+		prometheus.MustRegister(pe)
 	}
-	prometheus.MustRegister(pe)
 
 	http.Handle(*metricPath, promhttp.Handler())
 	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
@@ -132,6 +139,8 @@ func main() {
 		w.Write(landingPage)                                       // nolint: errcheck
 	})
 
+	http.HandleFunc("/probe", handleProbe(logger))
+
 	level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress)
 	srv := &http.Server{Addr: *listenAddress}
 	if err := web.ListenAndServe(srv, *webConfig, logger); err != nil {
diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go
new file mode 100644
index 00000000..c23777b4
--- /dev/null
+++ b/cmd/postgres_exporter/probe.go
@@ -0,0 +1,91 @@
+// Copyright 2022 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 (
+	"net/http"
+	"time"
+
+	"github.com/go-kit/log"
+	"github.com/prometheus-community/postgres_exporter/collector"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+func handleProbe(logger log.Logger) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		ctx := r.Context()
+		params := r.URL.Query()
+		target := params.Get("target")
+		if target == "" {
+			http.Error(w, "target is required", http.StatusBadRequest)
+			return
+		}
+
+		// TODO: Timeout
+		// TODO: Auth Module
+
+		probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "probe_success",
+			Help: "Displays whether or not the probe was a success",
+		})
+		probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{
+			Name: "probe_duration_seconds",
+			Help: "Returns how long the probe took to complete in seconds",
+		})
+
+		tl := log.With(logger, "target", target)
+		_ = tl
+
+		start := time.Now()
+		registry := prometheus.NewRegistry()
+		registry.MustRegister(probeSuccessGauge)
+		registry.MustRegister(probeDurationGauge)
+
+		// TODO(@sysadmind): this is a temp hack until we have a proper auth module
+		target = "postgres://postgres:test@localhost:5432/circle_test?sslmode=disable"
+
+		// Run the probe
+		pc, err := collector.NewProbeCollector(tl, registry, target)
+		if err != nil {
+			probeSuccessGauge.Set(0)
+			probeDurationGauge.Set(time.Since(start).Seconds())
+			http.Error(w, err.Error(), http.StatusInternalServerError)
+			return
+		}
+		_ = ctx
+
+		// TODO: Which way should this be? Register or handle the collection manually?
+		// Also, what about the context?
+
+		// Option 1: Register the collector
+		registry.MustRegister(pc)
+
+		// Option 2: Handle the collection manually. This allows us to collect duration metrics.
+		// The collectors themselves already support their own duration metrics.
+		// err = pc.Update(ctx)
+		// if err != nil {
+		// 	probeSuccessGauge.Set(0)
+		// } else {
+		// 	probeSuccessGauge.Set(1)
+		// }
+
+		duration := time.Since(start).Seconds()
+		probeDurationGauge.Set(duration)
+
+		// TODO check success, etc
+		h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
+		h.ServeHTTP(w, r)
+	}
+}
diff --git a/collector/collector.go b/collector/collector.go
index 58765b65..6d1a4dd1 100644
--- a/collector/collector.go
+++ b/collector/collector.go
@@ -15,6 +15,7 @@ package collector
 
 import (
 	"context"
+	"database/sql"
 	"errors"
 	"fmt"
 	"sync"
@@ -58,7 +59,7 @@ var (
 )
 
 type Collector interface {
-	Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error
+	Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error
 }
 
 func registerCollector(name string, isDefaultEnabled bool, createFunc func(logger log.Logger) (Collector, error)) {
@@ -86,13 +87,13 @@ type PostgresCollector struct {
 	Collectors map[string]Collector
 	logger     log.Logger
 
-	servers map[string]*server
+	db *sql.DB
 }
 
 type Option func(*PostgresCollector) error
 
 // NewPostgresCollector creates a new PostgresCollector.
-func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, options ...Option) (*PostgresCollector, error) {
+func NewPostgresCollector(logger log.Logger, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
 	p := &PostgresCollector{
 		logger: logger,
 	}
@@ -136,17 +137,18 @@ func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, op
 
 	p.Collectors = collectors
 
-	servers := make(map[string]*server)
-	for _, dsn := range dsns {
-		s, err := makeServer(dsn)
-		if err != nil {
-			return nil, err
-		}
-
-		servers[dsn] = s
+	if dsn == "" {
+		return nil, errors.New("empty dsn")
 	}
 
-	p.servers = servers
+	db, err := sql.Open("postgres", dsn)
+	if err != nil {
+		return nil, err
+	}
+	db.SetMaxOpenConns(1)
+	db.SetMaxIdleConns(1)
+
+	p.db = db
 
 	return p, nil
 }
@@ -160,32 +162,20 @@ func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) {
 // Collect implements the prometheus.Collector interface.
 func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
 	ctx := context.TODO()
-	wg := sync.WaitGroup{}
-	wg.Add(len(p.servers))
-	for _, s := range p.servers {
-		go func(s *server) {
-			p.subCollect(ctx, s, ch)
-			wg.Done()
-		}(s)
-	}
-	wg.Wait()
-}
-
-func (p PostgresCollector) subCollect(ctx context.Context, server *server, ch chan<- prometheus.Metric) {
 	wg := sync.WaitGroup{}
 	wg.Add(len(p.Collectors))
 	for name, c := range p.Collectors {
 		go func(name string, c Collector) {
-			execute(ctx, name, c, server, ch, p.logger)
+			execute(ctx, name, c, p.db, ch, p.logger)
 			wg.Done()
 		}(name, c)
 	}
 	wg.Wait()
 }
 
-func execute(ctx context.Context, name string, c Collector, s *server, ch chan<- prometheus.Metric, logger log.Logger) {
+func execute(ctx context.Context, name string, c Collector, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) {
 	begin := time.Now()
-	err := c.Update(ctx, s, ch)
+	err := c.Update(ctx, db, ch)
 	duration := time.Since(begin)
 	var success float64
 
diff --git a/collector/pg_database.go b/collector/pg_database.go
index 5868f66d..8fa4dab8 100644
--- a/collector/pg_database.go
+++ b/collector/pg_database.go
@@ -15,6 +15,7 @@ package collector
 
 import (
 	"context"
+	"database/sql"
 
 	"github.com/go-kit/log"
 	"github.com/prometheus/client_golang/prometheus"
@@ -36,15 +37,11 @@ var pgDatabase = map[string]*prometheus.Desc{
 	"size_bytes": prometheus.NewDesc(
 		"pg_database_size_bytes",
 		"Disk space used by the database",
-		[]string{"datname", "server"}, nil,
+		[]string{"datname"}, nil,
 	),
 }
 
-func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error {
-	db, err := server.GetDB()
-	if err != nil {
-		return err
-	}
+func (PGDatabaseCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error {
 	rows, err := db.QueryContext(ctx,
 		`SELECT pg_database.datname
 		,pg_database_size(pg_database.datname)
@@ -63,7 +60,7 @@ func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<-
 
 		ch <- prometheus.MustNewConstMetric(
 			pgDatabase["size_bytes"],
-			prometheus.GaugeValue, float64(size), datname, server.GetName(),
+			prometheus.GaugeValue, float64(size), datname,
 		)
 	}
 	if err := rows.Err(); err != nil {
diff --git a/collector/pg_stat_bgwriter.go b/collector/pg_stat_bgwriter.go
index 7e7d09c7..f897dbe2 100644
--- a/collector/pg_stat_bgwriter.go
+++ b/collector/pg_stat_bgwriter.go
@@ -15,6 +15,7 @@ package collector
 
 import (
 	"context"
+	"database/sql"
 	"time"
 
 	"github.com/go-kit/log"
@@ -38,77 +39,72 @@ var statBGWriter = map[string]*prometheus.Desc{
 	"checkpoints_timed": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_timed_total"),
 		"Number of scheduled checkpoints that have been performed",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"checkpoints_req": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_req_total"),
 		"Number of requested checkpoints that have been performed",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"checkpoint_write_time": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_write_time_total"),
 		"Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk, in milliseconds",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"checkpoint_sync_time": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_sync_time_total"),
 		"Total amount of time that has been spent in the portion of checkpoint processing where files are synchronized to disk, in milliseconds",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"buffers_checkpoint": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_checkpoint_total"),
 		"Number of buffers written during checkpoints",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"buffers_clean": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_clean_total"),
 		"Number of buffers written by the background writer",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"maxwritten_clean": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "maxwritten_clean_total"),
 		"Number of times the background writer stopped a cleaning scan because it had written too many buffers",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"buffers_backend": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_backend_total"),
 		"Number of buffers written directly by a backend",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"buffers_backend_fsync": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_backend_fsync_total"),
 		"Number of times a backend had to execute its own fsync call (normally the background writer handles those even when the backend does its own write)",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"buffers_alloc": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_alloc_total"),
 		"Number of buffers allocated",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 	"stats_reset": prometheus.NewDesc(
 		prometheus.BuildFQName(namespace, bgWriterSubsystem, "stats_reset_total"),
 		"Time at which these statistics were last reset",
-		[]string{"server"},
+		[]string{},
 		prometheus.Labels{},
 	),
 }
 
-func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error {
-	db, err := server.GetDB()
-	if err != nil {
-		return err
-	}
-
+func (PGStatBGWriterCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error {
 	row := db.QueryRowContext(ctx,
 		`SELECT
 			 checkpoints_timed
@@ -136,7 +132,7 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch ch
 	var ba int
 	var sr time.Time
 
-	err = row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
+	err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
 	if err != nil {
 		return err
 	}
@@ -145,67 +141,56 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch ch
 		statBGWriter["checkpoints_timed"],
 		prometheus.CounterValue,
 		float64(cpt),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["checkpoints_req"],
 		prometheus.CounterValue,
 		float64(cpr),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["checkpoint_write_time"],
 		prometheus.CounterValue,
 		float64(cpwt),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["checkpoint_sync_time"],
 		prometheus.CounterValue,
 		float64(cpst),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["buffers_checkpoint"],
 		prometheus.CounterValue,
 		float64(bcp),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["buffers_clean"],
 		prometheus.CounterValue,
 		float64(bc),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["maxwritten_clean"],
 		prometheus.CounterValue,
 		float64(mwc),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["buffers_backend"],
 		prometheus.CounterValue,
 		float64(bb),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["buffers_backend_fsync"],
 		prometheus.CounterValue,
 		float64(bbf),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["buffers_alloc"],
 		prometheus.CounterValue,
 		float64(ba),
-		server.GetName(),
 	)
 	ch <- prometheus.MustNewConstMetric(
 		statBGWriter["stats_reset"],
 		prometheus.CounterValue,
 		float64(sr.Unix()),
-		server.GetName(),
 	)
 
 	return nil
diff --git a/collector/probe.go b/collector/probe.go
new file mode 100644
index 00000000..8aa18b92
--- /dev/null
+++ b/collector/probe.go
@@ -0,0 +1,90 @@
+// Copyright 2022 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 collector
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/go-kit/log"
+	"github.com/prometheus/client_golang/prometheus"
+)
+
+type ProbeCollector struct {
+	registry   *prometheus.Registry
+	collectors map[string]Collector
+	logger     log.Logger
+	db         *sql.DB
+}
+
+func NewProbeCollector(logger log.Logger, registry *prometheus.Registry, dsn string) (*ProbeCollector, error) {
+	collectors := make(map[string]Collector)
+	initiatedCollectorsMtx.Lock()
+	defer initiatedCollectorsMtx.Unlock()
+	for key, enabled := range collectorState {
+		// TODO: Handle filters
+		// if !*enabled || (len(f) > 0 && !f[key]) {
+		// 	continue
+		// }
+		if !*enabled {
+			continue
+		}
+		if collector, ok := initiatedCollectors[key]; ok {
+			collectors[key] = collector
+		} else {
+			collector, err := factories[key](log.With(logger, "collector", key))
+			if err != nil {
+				return nil, err
+			}
+			collectors[key] = collector
+			initiatedCollectors[key] = collector
+		}
+	}
+
+	if !strings.HasPrefix(dsn, "postgres://") {
+		dsn = fmt.Sprintf("postgres://%s", dsn)
+	}
+
+	db, err := sql.Open("postgres", dsn)
+	if err != nil {
+		return nil, err
+	}
+	db.SetMaxOpenConns(1)
+	db.SetMaxIdleConns(1)
+
+	return &ProbeCollector{
+		registry:   registry,
+		collectors: collectors,
+		logger:     logger,
+		db:         db,
+	}, nil
+}
+
+func (pc *ProbeCollector) Describe(ch chan<- *prometheus.Desc) {
+}
+
+func (pc *ProbeCollector) Collect(ch chan<- prometheus.Metric) {
+	wg := sync.WaitGroup{}
+	wg.Add(len(pc.collectors))
+	for name, c := range pc.collectors {
+		go func(name string, c Collector) {
+			execute(context.TODO(), name, c, pc.db, ch, pc.logger)
+			wg.Done()
+		}(name, c)
+	}
+	wg.Wait()
+}
diff --git a/collector/server.go b/collector/server.go
deleted file mode 100644
index fa490a2c..00000000
--- a/collector/server.go
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright 2022 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 collector
-
-import (
-	"database/sql"
-	"fmt"
-	"strings"
-
-	"github.com/lib/pq"
-)
-
-type server struct {
-	dsn  string
-	name string
-	db   *sql.DB
-}
-
-func makeServer(dsn string) (*server, error) {
-	name, err := parseServerName(dsn)
-	if err != nil {
-		return nil, err
-	}
-	return &server{
-		dsn:  dsn,
-		name: name,
-	}, nil
-}
-
-func (s *server) GetDB() (*sql.DB, error) {
-	if s.db != nil {
-		return s.db, nil
-	}
-
-	db, err := sql.Open("postgres", s.dsn)
-	if err != nil {
-		return nil, err
-	}
-	db.SetMaxOpenConns(1)
-	db.SetMaxIdleConns(1)
-
-	s.db = db
-
-	return s.db, nil
-}
-
-func (s *server) GetName() string {
-	return s.name
-}
-
-func (s *server) String() string {
-	return s.name
-}
-
-func parseServerName(url string) (string, error) {
-	dsn, err := pq.ParseURL(url)
-	if err != nil {
-		dsn = url
-	}
-
-	pairs := strings.Split(dsn, " ")
-	kv := make(map[string]string, len(pairs))
-	for _, pair := range pairs {
-		splitted := strings.SplitN(pair, "=", 2)
-		if len(splitted) != 2 {
-			return "", fmt.Errorf("malformed dsn %q", dsn)
-		}
-		// Newer versions of pq.ParseURL quote values so trim them off if they exist
-		key := strings.Trim(splitted[0], "'\"")
-		value := strings.Trim(splitted[1], "'\"")
-		kv[key] = value
-	}
-
-	var fingerprint string
-
-	if host, ok := kv["host"]; ok {
-		fingerprint += host
-	} else {
-		fingerprint += "localhost"
-	}
-
-	if port, ok := kv["port"]; ok {
-		fingerprint += ":" + port
-	} else {
-		fingerprint += ":5432"
-	}
-
-	return fingerprint, nil
-}

From cc751b7966f66a60512609c7497379d32bc2847b Mon Sep 17 00:00:00 2001
From: Joe Adams <github@joeadams.io>
Date: Fri, 4 Mar 2022 16:39:48 -0500
Subject: [PATCH 2/4] 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 <github@joeadams.io>
---
 cmd/postgres_exporter/main.go               |  11 ++
 cmd/postgres_exporter/probe.go              |  34 +++++-
 config/config.go                            | 126 ++++++++++++++++++++
 config/config_test.go                       |  58 +++++++++
 config/testdata/config-bad-auth-module.yaml |   7 ++
 config/testdata/config-bad-extra-field.yaml |   8 ++
 config/testdata/config-good.yaml            |   8 ++
 go.mod                                      |   1 +
 go.sum                                      |   2 +
 9 files changed, 249 insertions(+), 6 deletions(-)
 create mode 100644 config/config.go
 create mode 100644 config/config_test.go
 create mode 100644 config/testdata/config-bad-auth-module.yaml
 create mode 100644 config/testdata/config-bad-extra-field.yaml
 create mode 100644 config/testdata/config-good.yaml

diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go
index 2ec1bf26..aee32503 100644
--- a/cmd/postgres_exporter/main.go
+++ b/cmd/postgres_exporter/main.go
@@ -20,6 +20,7 @@ import (
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
 	"github.com/prometheus-community/postgres_exporter/collector"
+	"github.com/prometheus-community/postgres_exporter/config"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/prometheus/common/promlog"
@@ -31,6 +32,11 @@ import (
 )
 
 var (
+	c = config.ConfigHandler{
+		Config: &config.Config{},
+	}
+
+	configFile             = kingpin.Flag("config.file", "Promehteus exporter configuration file.").Default("postres_exporter.yml").String()
 	listenAddress          = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
 	webConfig              = webflag.AddFlags(kingpin.CommandLine)
 	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() {
 		return
 	}
 
+	if err := c.ReloadConfig(*configFile, logger); err != nil {
+		// This is not fatal, but it means that auth must be provided for every dsn.
+		level.Error(logger).Log("msg", "Error loading config", "err", err)
+	}
+
 	dsns, err := getDataSources()
 	if err != nil {
 		level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go
index c23777b4..813f4ea8 100644
--- a/cmd/postgres_exporter/probe.go
+++ b/cmd/postgres_exporter/probe.go
@@ -14,11 +14,14 @@
 package main
 
 import (
+	"fmt"
 	"net/http"
 	"time"
 
 	"github.com/go-kit/log"
+	"github.com/go-kit/log/level"
 	"github.com/prometheus-community/postgres_exporter/collector"
+	"github.com/prometheus-community/postgres_exporter/config"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 )
@@ -26,15 +29,38 @@ import (
 func handleProbe(logger log.Logger) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		ctx := r.Context()
+		conf := c.GetConfig()
 		params := r.URL.Query()
 		target := params.Get("target")
 		if target == "" {
 			http.Error(w, "target is required", http.StatusBadRequest)
 			return
 		}
+		var authModule config.AuthModule
+		authModuleName := params.Get("auth_module")
+		if authModuleName == "" {
+			level.Info(logger).Log("msg", "no auth_module specified, using default")
+		} else {
+			var ok bool
+			authModule, ok = conf.AuthModules[authModuleName]
+			if !ok {
+				http.Error(w, fmt.Sprintf("auth_module %s not found", authModuleName), http.StatusBadRequest)
+				return
+			}
+			if authModule.UserPass.Username == "" || authModule.UserPass.Password == "" {
+				http.Error(w, fmt.Sprintf("auth_module %s has no username or password", authModuleName), http.StatusBadRequest)
+				return
+			}
+		}
+
+		dsn, err := authModule.ConfigureTarget(target)
+		if err != nil {
+			level.Error(logger).Log("msg", "failed to configure target", "err", err)
+			http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest)
+			return
+		}
 
 		// TODO: Timeout
-		// TODO: Auth Module
 
 		probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
 			Name: "probe_success",
@@ -46,18 +72,14 @@ func handleProbe(logger log.Logger) http.HandlerFunc {
 		})
 
 		tl := log.With(logger, "target", target)
-		_ = tl
 
 		start := time.Now()
 		registry := prometheus.NewRegistry()
 		registry.MustRegister(probeSuccessGauge)
 		registry.MustRegister(probeDurationGauge)
 
-		// TODO(@sysadmind): this is a temp hack until we have a proper auth module
-		target = "postgres://postgres:test@localhost:5432/circle_test?sslmode=disable"
-
 		// Run the probe
-		pc, err := collector.NewProbeCollector(tl, registry, target)
+		pc, err := collector.NewProbeCollector(tl, registry, dsn)
 		if err != nil {
 			probeSuccessGauge.Set(0)
 			probeDurationGauge.Set(time.Since(start).Seconds())
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 00000000..49a2dbd6
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,126 @@
+// Copyright 2022 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 config
+
+import (
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+	"sync"
+
+	"github.com/go-kit/log"
+	"github.com/prometheus/client_golang/prometheus"
+	"gopkg.in/yaml.v3"
+)
+
+var (
+	configReloadSuccess = prometheus.NewGauge(prometheus.GaugeOpts{
+		Namespace: "postgres_exporter",
+		Name:      "config_last_reload_successful",
+		Help:      "Postgres exporter config loaded successfully.",
+	})
+
+	configReloadSeconds = prometheus.NewGauge(prometheus.GaugeOpts{
+		Namespace: "postgres_exporter",
+		Name:      "config_last_reload_success_timestamp_seconds",
+		Help:      "Timestamp of the last successful configuration reload.",
+	})
+)
+
+func init() {
+	prometheus.MustRegister(configReloadSuccess)
+	prometheus.MustRegister(configReloadSeconds)
+}
+
+type Config struct {
+	AuthModules map[string]AuthModule `yaml:"auth_modules"`
+}
+
+type AuthModule struct {
+	Type     string   `yaml:"type"`
+	UserPass UserPass `yaml:"userpass,omitempty"`
+	// Add alternative auth modules here
+	Options map[string]string `yaml:"options"`
+}
+
+type UserPass struct {
+	Username string `yaml:"username"`
+	Password string `yaml:"password"`
+}
+
+type ConfigHandler struct {
+	sync.RWMutex
+	Config *Config
+}
+
+func (ch *ConfigHandler) GetConfig() *Config {
+	ch.RLock()
+	defer ch.RUnlock()
+	return ch.Config
+}
+
+func (ch *ConfigHandler) ReloadConfig(f string, logger log.Logger) error {
+	config := &Config{}
+	var err error
+	defer func() {
+		if err != nil {
+			configReloadSuccess.Set(0)
+		} else {
+			configReloadSuccess.Set(1)
+			configReloadSeconds.SetToCurrentTime()
+		}
+	}()
+
+	yamlReader, err := os.Open(f)
+	if err != nil {
+		return fmt.Errorf("Error opening config file %q: %s", f, err)
+	}
+	defer yamlReader.Close()
+	decoder := yaml.NewDecoder(yamlReader)
+	decoder.KnownFields(true)
+
+	if err = decoder.Decode(config); err != nil {
+		return fmt.Errorf("Error parsing config file %q: %s", f, err)
+	}
+
+	ch.Lock()
+	ch.Config = config
+	ch.Unlock()
+	return nil
+}
+
+func (m AuthModule) ConfigureTarget(target string) (string, error) {
+	// ip:port urls do not parse properly and that is the typical way users interact with postgres
+	t := fmt.Sprintf("exporter://%s", target)
+	u, err := url.Parse(t)
+	if err != nil {
+		return "", err
+	}
+
+	if m.Type == "userpass" {
+		u.User = url.UserPassword(m.UserPass.Username, m.UserPass.Password)
+	}
+
+	query := u.Query()
+	for k, v := range m.Options {
+		query.Set(k, v)
+	}
+	u.RawQuery = query.Encode()
+
+	parsed := u.String()
+	trim := strings.TrimPrefix(parsed, "exporter://")
+
+	return trim, nil
+}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 00000000..63b932ad
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,58 @@
+// Copyright 2022 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 config
+
+import (
+	"testing"
+)
+
+func TestLoadConfig(t *testing.T) {
+	ch := &ConfigHandler{
+		Config: &Config{},
+	}
+
+	err := ch.ReloadConfig("testdata/config-good.yaml", nil)
+	if err != nil {
+		t.Errorf("Error loading config: %s", err)
+	}
+}
+
+func TestLoadBadConfigs(t *testing.T) {
+	ch := &ConfigHandler{
+		Config: &Config{},
+	}
+
+	tests := []struct {
+		input string
+		want  string
+	}{
+		{
+			input: "testdata/config-bad-auth-module.yaml",
+			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",
+		},
+		{
+			input: "testdata/config-bad-extra-field.yaml",
+			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",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.input, func(t *testing.T) {
+			got := ch.ReloadConfig(test.input, nil)
+			if got == nil || got.Error() != test.want {
+				t.Fatalf("ReloadConfig(%q) = %v, want %s", test.input, got, test.want)
+			}
+		})
+	}
+}
diff --git a/config/testdata/config-bad-auth-module.yaml b/config/testdata/config-bad-auth-module.yaml
new file mode 100644
index 00000000..8f718dd5
--- /dev/null
+++ b/config/testdata/config-bad-auth-module.yaml
@@ -0,0 +1,7 @@
+auth_modules:
+  foo:
+    pretendauth:
+      username: test
+      password: pass
+    options:
+      extra: "1"
diff --git a/config/testdata/config-bad-extra-field.yaml b/config/testdata/config-bad-extra-field.yaml
new file mode 100644
index 00000000..f6ff6d6c
--- /dev/null
+++ b/config/testdata/config-bad-extra-field.yaml
@@ -0,0 +1,8 @@
+auth_modules:
+  foo:
+    userpass:
+      username: test
+      password: pass
+    options:
+      extra: "1"
+    doesNotExist: test
diff --git a/config/testdata/config-good.yaml b/config/testdata/config-good.yaml
new file mode 100644
index 00000000..13453e26
--- /dev/null
+++ b/config/testdata/config-good.yaml
@@ -0,0 +1,8 @@
+auth_modules:
+  first:
+    type: userpass
+    userpass:
+      username: first
+      password: firstpass
+    options:
+      sslmode: disable
diff --git a/go.mod b/go.mod
index f27c896b..f2ccd64e 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	gopkg.in/alecthomas/kingpin.v2 v2.2.6
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
 	gopkg.in/yaml.v2 v2.4.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )
 
 require (
diff --git a/go.sum b/go.sum
index 3f9003a2..1ad1ca5b 100644
--- a/go.sum
+++ b/go.sum
@@ -494,6 +494,8 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

From 8f8d2208f593056b0f159466625b42d2c8a956c9 Mon Sep 17 00:00:00 2001
From: Joe Adams <github@joeadams.io>
Date: Tue, 28 Jun 2022 22:22:14 -0400
Subject: [PATCH 3/4] cleanup and README

Signed-off-by: Joe Adams <github@joeadams.io>

Co-authored-by: Ben Kochie <superq@gmail.com>
---
 README.md                      | 30 ++++++++++++++++++++++++++++++
 cmd/postgres_exporter/main.go  |  6 ------
 cmd/postgres_exporter/probe.go | 20 ++++++--------------
 config/config.go               | 10 +++-------
 4 files changed, 39 insertions(+), 27 deletions(-)

diff --git a/README.md b/README.md
index 4d23603a..2f50fba0 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,36 @@ docker run \
   quay.io/prometheuscommunity/postgres-exporter
 ```
 
+## Multi-Target Support (BETA)
+**This Feature is in beta and may require changes in future releases. Feedback is welcome.**
+
+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.
+
+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.
+
+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.
+
+## Configuration File
+
+The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postres_exporter.yml`.
+
+### auth_modules
+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.
+Currently only the `userpass` type is supported.
+
+Example:
+```yaml
+auth_modules:
+  foo1: # Set this to any name you want
+    type: userpass
+    userpass:
+      username: first
+      password: firstpass
+    options:
+      # options become key=value parameters of the DSN
+      sslmode: disable
+```
+
 ## Building and running
 
     git clone https://github.com/prometheus-community/postgres_exporter.git
diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go
index aee32503..d8f61295 100644
--- a/cmd/postgres_exporter/main.go
+++ b/cmd/postgres_exporter/main.go
@@ -102,12 +102,6 @@ func main() {
 		os.Exit(1)
 	}
 
-	// TODO(@sysadmind): Remove this with multi-target support
-	// if len(dsn) == 0 {
-	// 	level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use")
-	// 	os.Exit(1)
-	// }
-
 	opts := []ExporterOpt{
 		DisableDefaultMetrics(*disableDefaultMetrics),
 		DisableSettingsMetrics(*disableSettingsMetrics),
diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go
index 813f4ea8..7b215431 100644
--- a/cmd/postgres_exporter/probe.go
+++ b/cmd/postgres_exporter/probe.go
@@ -60,7 +60,7 @@ func handleProbe(logger log.Logger) http.HandlerFunc {
 			return
 		}
 
-		// TODO: Timeout
+		// TODO(@sysadmind): Timeout
 
 		probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{
 			Name: "probe_success",
@@ -86,23 +86,15 @@ func handleProbe(logger log.Logger) http.HandlerFunc {
 			http.Error(w, err.Error(), http.StatusInternalServerError)
 			return
 		}
+
+		// TODO(@sysadmind): Remove the registry.MustRegister() call below and instead handle the collection here. That will allow
+		// for the passing of context, handling of timeouts, and more control over the collection.
+		// The current NewProbeCollector() implementation relies on the MustNewConstMetric() call to create the metrics which is not
+		// ideal to use without the registry.MustRegister() call.
 		_ = ctx
 
-		// TODO: Which way should this be? Register or handle the collection manually?
-		// Also, what about the context?
-
-		// Option 1: Register the collector
 		registry.MustRegister(pc)
 
-		// Option 2: Handle the collection manually. This allows us to collect duration metrics.
-		// The collectors themselves already support their own duration metrics.
-		// err = pc.Update(ctx)
-		// if err != nil {
-		// 	probeSuccessGauge.Set(0)
-		// } else {
-		// 	probeSuccessGauge.Set(1)
-		// }
-
 		duration := time.Since(start).Seconds()
 		probeDurationGauge.Set(duration)
 
diff --git a/config/config.go b/config/config.go
index 49a2dbd6..10e7b733 100644
--- a/config/config.go
+++ b/config/config.go
@@ -22,28 +22,24 @@ import (
 
 	"github.com/go-kit/log"
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promauto"
 	"gopkg.in/yaml.v3"
 )
 
 var (
-	configReloadSuccess = prometheus.NewGauge(prometheus.GaugeOpts{
+	configReloadSuccess = promauto.NewGauge(prometheus.GaugeOpts{
 		Namespace: "postgres_exporter",
 		Name:      "config_last_reload_successful",
 		Help:      "Postgres exporter config loaded successfully.",
 	})
 
-	configReloadSeconds = prometheus.NewGauge(prometheus.GaugeOpts{
+	configReloadSeconds = promauto.NewGauge(prometheus.GaugeOpts{
 		Namespace: "postgres_exporter",
 		Name:      "config_last_reload_success_timestamp_seconds",
 		Help:      "Timestamp of the last successful configuration reload.",
 	})
 )
 
-func init() {
-	prometheus.MustRegister(configReloadSuccess)
-	prometheus.MustRegister(configReloadSeconds)
-}
-
 type Config struct {
 	AuthModules map[string]AuthModule `yaml:"auth_modules"`
 }

From 72430f8d2a7e541526a45a275b6615adabbdef48 Mon Sep 17 00:00:00 2001
From: Joe Adams <adams10301@gmail.com>
Date: Thu, 28 Jul 2022 10:13:47 -0400
Subject: [PATCH 4/4] Update cmd/postgres_exporter/main.go

Signed-off-by: Joe Adams <github@joeadams.io>
---
 README.md                     | 2 +-
 cmd/postgres_exporter/main.go | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index 2f50fba0..c7cb4621 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ To avoid putting sensitive information like username and password in the URL, pr
 
 ## Configuration File
 
-The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postres_exporter.yml`.
+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`.
 
 ### auth_modules
 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.
diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go
index d8f61295..8759f437 100644
--- a/cmd/postgres_exporter/main.go
+++ b/cmd/postgres_exporter/main.go
@@ -36,7 +36,7 @@ var (
 		Config: &config.Config{},
 	}
 
-	configFile             = kingpin.Flag("config.file", "Promehteus exporter configuration file.").Default("postres_exporter.yml").String()
+	configFile             = kingpin.Flag("config.file", "Postgres exporter configuration file.").Default("postgres_exporter.yml").String()
 	listenAddress          = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
 	webConfig              = webflag.AddFlags(kingpin.CommandLine)
 	metricPath             = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("PG_EXPORTER_WEB_TELEMETRY_PATH").String()