Skip to content

Commit b745e60

Browse files
committed
Refactor code into logical files
Moves code into more manageable, logical files to group behavior together. This should help improve a developer's ability to navigate the code. Signed-off-by: Joe Adams <[email protected]>
1 parent 134e908 commit b745e60

File tree

7 files changed

+1222
-1147
lines changed

7 files changed

+1222
-1147
lines changed

cmd/postgres_exporter/datasource.go

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"net/url"
7+
"os"
8+
"regexp"
9+
"strings"
10+
11+
"github.com/go-kit/kit/log/level"
12+
"github.com/prometheus/client_golang/prometheus"
13+
)
14+
15+
func (e *Exporter) discoverDatabaseDSNs() []string {
16+
// connstring syntax is complex (and not sure if even regular).
17+
// we don't need to parse it, so just superficially validate that it starts
18+
// with a valid-ish keyword pair
19+
connstringRe := regexp.MustCompile(`^ *[a-zA-Z0-9]+ *= *[^= ]+`)
20+
21+
dsns := make(map[string]struct{})
22+
for _, dsn := range e.dsn {
23+
var dsnURI *url.URL
24+
var dsnConnstring string
25+
26+
if strings.HasPrefix(dsn, "postgresql://") {
27+
var err error
28+
dsnURI, err = url.Parse(dsn)
29+
if err != nil {
30+
level.Error(logger).Log("msg", "Unable to parse DSN as URI", "dsn", loggableDSN(dsn), "err", err)
31+
continue
32+
}
33+
} else if connstringRe.MatchString(dsn) {
34+
dsnConnstring = dsn
35+
} else {
36+
level.Error(logger).Log("msg", "Unable to parse DSN as either URI or connstring", "dsn", loggableDSN(dsn))
37+
continue
38+
}
39+
40+
server, err := e.servers.GetServer(dsn)
41+
if err != nil {
42+
level.Error(logger).Log("msg", "Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
43+
continue
44+
}
45+
dsns[dsn] = struct{}{}
46+
47+
// If autoDiscoverDatabases is true, set first dsn as master database (Default: false)
48+
server.master = true
49+
50+
databaseNames, err := queryDatabases(server)
51+
if err != nil {
52+
level.Error(logger).Log("msg", "Error querying databases", "dsn", loggableDSN(dsn), "err", err)
53+
continue
54+
}
55+
for _, databaseName := range databaseNames {
56+
if contains(e.excludeDatabases, databaseName) {
57+
continue
58+
}
59+
60+
if len(e.includeDatabases) != 0 && !contains(e.includeDatabases, databaseName) {
61+
continue
62+
}
63+
64+
if dsnURI != nil {
65+
dsnURI.Path = databaseName
66+
dsn = dsnURI.String()
67+
} else {
68+
// replacing one dbname with another is complicated.
69+
// just append new dbname to override.
70+
dsn = fmt.Sprintf("%s dbname=%s", dsnConnstring, databaseName)
71+
}
72+
dsns[dsn] = struct{}{}
73+
}
74+
}
75+
76+
result := make([]string, len(dsns))
77+
index := 0
78+
for dsn := range dsns {
79+
result[index] = dsn
80+
index++
81+
}
82+
83+
return result
84+
}
85+
86+
func (e *Exporter) scrapeDSN(ch chan<- prometheus.Metric, dsn string) error {
87+
server, err := e.servers.GetServer(dsn)
88+
89+
if err != nil {
90+
return &ErrorConnectToServer{fmt.Sprintf("Error opening connection to database (%s): %s", loggableDSN(dsn), err.Error())}
91+
}
92+
93+
// Check if autoDiscoverDatabases is false, set dsn as master database (Default: false)
94+
if !e.autoDiscoverDatabases {
95+
server.master = true
96+
}
97+
98+
// Check if map versions need to be updated
99+
if err := e.checkMapVersions(ch, server); err != nil {
100+
level.Warn(logger).Log("msg", "Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
101+
}
102+
103+
return server.Scrape(ch, e.disableSettingsMetrics)
104+
}
105+
106+
// try to get the DataSource
107+
// DATA_SOURCE_NAME always wins so we do not break older versions
108+
// reading secrets from files wins over secrets in environment variables
109+
// DATA_SOURCE_NAME > DATA_SOURCE_{USER|PASS}_FILE > DATA_SOURCE_{USER|PASS}
110+
func getDataSources() ([]string, error) {
111+
var dsn = os.Getenv("DATA_SOURCE_NAME")
112+
if len(dsn) != 0 {
113+
return strings.Split(dsn, ","), nil
114+
}
115+
116+
var user, pass, uri string
117+
118+
dataSourceUserFile := os.Getenv("DATA_SOURCE_USER_FILE")
119+
if len(dataSourceUserFile) != 0 {
120+
fileContents, err := ioutil.ReadFile(dataSourceUserFile)
121+
if err != nil {
122+
return nil, fmt.Errorf("failed loading data source user file %s: %s", dataSourceUserFile, err.Error())
123+
}
124+
user = strings.TrimSpace(string(fileContents))
125+
} else {
126+
user = os.Getenv("DATA_SOURCE_USER")
127+
}
128+
129+
dataSourcePassFile := os.Getenv("DATA_SOURCE_PASS_FILE")
130+
if len(dataSourcePassFile) != 0 {
131+
fileContents, err := ioutil.ReadFile(dataSourcePassFile)
132+
if err != nil {
133+
return nil, fmt.Errorf("failed loading data source pass file %s: %s", dataSourcePassFile, err.Error())
134+
}
135+
pass = strings.TrimSpace(string(fileContents))
136+
} else {
137+
pass = os.Getenv("DATA_SOURCE_PASS")
138+
}
139+
140+
ui := url.UserPassword(user, pass).String()
141+
dataSrouceURIFile := os.Getenv("DATA_SOURCE_URI_FILE")
142+
if len(dataSrouceURIFile) != 0 {
143+
fileContents, err := ioutil.ReadFile(dataSrouceURIFile)
144+
if err != nil {
145+
return nil, fmt.Errorf("failed loading data source URI file %s: %s", dataSrouceURIFile, err.Error())
146+
}
147+
uri = strings.TrimSpace(string(fileContents))
148+
} else {
149+
uri = os.Getenv("DATA_SOURCE_URI")
150+
}
151+
152+
dsn = "postgresql://" + ui + "@" + uri
153+
154+
return []string{dsn}, nil
155+
}

cmd/postgres_exporter/main.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"os"
6+
7+
"github.com/go-kit/kit/log/level"
8+
"github.com/go-kit/log"
9+
"github.com/prometheus/client_golang/prometheus"
10+
"github.com/prometheus/client_golang/prometheus/promhttp"
11+
"github.com/prometheus/common/promlog"
12+
"github.com/prometheus/common/promlog/flag"
13+
"github.com/prometheus/common/version"
14+
"github.com/prometheus/exporter-toolkit/web"
15+
webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag"
16+
"gopkg.in/alecthomas/kingpin.v2"
17+
)
18+
19+
var (
20+
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
21+
webConfig = webflag.AddFlags(kingpin.CommandLine)
22+
metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("PG_EXPORTER_WEB_TELEMETRY_PATH").String()
23+
disableDefaultMetrics = kingpin.Flag("disable-default-metrics", "Do not include default metrics.").Default("false").Envar("PG_EXPORTER_DISABLE_DEFAULT_METRICS").Bool()
24+
disableSettingsMetrics = kingpin.Flag("disable-settings-metrics", "Do not include pg_settings metrics.").Default("false").Envar("PG_EXPORTER_DISABLE_SETTINGS_METRICS").Bool()
25+
autoDiscoverDatabases = kingpin.Flag("auto-discover-databases", "Whether to discover the databases on a server dynamically.").Default("false").Envar("PG_EXPORTER_AUTO_DISCOVER_DATABASES").Bool()
26+
queriesPath = kingpin.Flag("extend.query-path", "Path to custom queries to run.").Default("").Envar("PG_EXPORTER_EXTEND_QUERY_PATH").String()
27+
onlyDumpMaps = kingpin.Flag("dumpmaps", "Do not run, simply dump the maps.").Bool()
28+
constantLabelsList = kingpin.Flag("constantLabels", "A list of label=value separated by comma(,).").Default("").Envar("PG_EXPORTER_CONSTANT_LABELS").String()
29+
excludeDatabases = kingpin.Flag("exclude-databases", "A list of databases to remove when autoDiscoverDatabases is enabled").Default("").Envar("PG_EXPORTER_EXCLUDE_DATABASES").String()
30+
includeDatabases = kingpin.Flag("include-databases", "A list of databases to include when autoDiscoverDatabases is enabled").Default("").Envar("PG_EXPORTER_INCLUDE_DATABASES").String()
31+
metricPrefix = kingpin.Flag("metric-prefix", "A metric prefix can be used to have non-default (not \"pg\") prefixes for each of the metrics").Default("pg").Envar("PG_EXPORTER_METRIC_PREFIX").String()
32+
logger = log.NewNopLogger()
33+
)
34+
35+
// Metric name parts.
36+
const (
37+
// Namespace for all metrics.
38+
namespace = "pg"
39+
// Subsystems.
40+
exporter = "exporter"
41+
// The name of the exporter.
42+
exporterName = "postgres_exporter"
43+
// Metric label used for static string data thats handy to send to Prometheus
44+
// e.g. version
45+
staticLabelName = "static"
46+
// Metric label used for server identification.
47+
serverLabelName = "server"
48+
)
49+
50+
func main() {
51+
kingpin.Version(version.Print(exporterName))
52+
promlogConfig := &promlog.Config{}
53+
flag.AddFlags(kingpin.CommandLine, promlogConfig)
54+
kingpin.HelpFlag.Short('h')
55+
kingpin.Parse()
56+
logger = promlog.New(promlogConfig)
57+
58+
// landingPage contains the HTML served at '/'.
59+
// TODO: Make this nicer and more informative.
60+
var landingPage = []byte(`<html>
61+
<head><title>Postgres exporter</title></head>
62+
<body>
63+
<h1>Postgres exporter</h1>
64+
<p><a href='` + *metricPath + `'>Metrics</a></p>
65+
</body>
66+
</html>
67+
`)
68+
69+
if *onlyDumpMaps {
70+
dumpMaps()
71+
return
72+
}
73+
74+
dsn, err := getDataSources()
75+
if err != nil {
76+
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
77+
os.Exit(1)
78+
}
79+
80+
if len(dsn) == 0 {
81+
level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use")
82+
os.Exit(1)
83+
}
84+
85+
opts := []ExporterOpt{
86+
DisableDefaultMetrics(*disableDefaultMetrics),
87+
DisableSettingsMetrics(*disableSettingsMetrics),
88+
AutoDiscoverDatabases(*autoDiscoverDatabases),
89+
WithUserQueriesPath(*queriesPath),
90+
WithConstantLabels(*constantLabelsList),
91+
ExcludeDatabases(*excludeDatabases),
92+
IncludeDatabases(*includeDatabases),
93+
}
94+
95+
exporter := NewExporter(dsn, opts...)
96+
defer func() {
97+
exporter.servers.Close()
98+
}()
99+
100+
prometheus.MustRegister(version.NewCollector(exporterName))
101+
102+
prometheus.MustRegister(exporter)
103+
104+
http.Handle(*metricPath, promhttp.Handler())
105+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
106+
w.Header().Set("Content-Type", "text/html; charset=UTF-8") // nolint: errcheck
107+
w.Write(landingPage) // nolint: errcheck
108+
})
109+
110+
level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress)
111+
srv := &http.Server{Addr: *listenAddress}
112+
if err := web.ListenAndServe(srv, *webConfig, logger); err != nil {
113+
level.Error(logger).Log("msg", "Error running HTTP server", "err", err)
114+
os.Exit(1)
115+
}
116+
}

0 commit comments

Comments
 (0)