Skip to content

Commit 2b6b78e

Browse files
feat: scrape timeout
Signed-off-by: Johannes Würbach <[email protected]>
1 parent 57719ba commit 2b6b78e

File tree

8 files changed

+58
-39
lines changed

8 files changed

+58
-39
lines changed

cmd/postgres_exporter/datasource.go

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

1616
import (
17+
"context"
1718
"fmt"
1819
"io/ioutil"
1920
"net/url"
@@ -25,7 +26,7 @@ import (
2526
"github.com/prometheus/client_golang/prometheus"
2627
)
2728

28-
func (e *Exporter) discoverDatabaseDSNs() []string {
29+
func (e *Exporter) discoverDatabaseDSNs(ctx context.Context) []string {
2930
// connstring syntax is complex (and not sure if even regular).
3031
// we don't need to parse it, so just superficially validate that it starts
3132
// with a valid-ish keyword pair
@@ -50,7 +51,7 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
5051
continue
5152
}
5253

53-
server, err := e.servers.GetServer(dsn)
54+
server, err := e.servers.GetServer(ctx, dsn)
5455
if err != nil {
5556
level.Error(logger).Log("msg", "Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
5657
continue
@@ -60,7 +61,7 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
6061
// If autoDiscoverDatabases is true, set first dsn as master database (Default: false)
6162
server.master = true
6263

63-
databaseNames, err := queryDatabases(server)
64+
databaseNames, err := queryDatabases(ctx, server)
6465
if err != nil {
6566
level.Error(logger).Log("msg", "Error querying databases", "dsn", loggableDSN(dsn), "err", err)
6667
continue
@@ -96,8 +97,8 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
9697
return result
9798
}
9899

99-
func (e *Exporter) scrapeDSN(ch chan<- prometheus.Metric, dsn string) error {
100-
server, err := e.servers.GetServer(dsn)
100+
func (e *Exporter) scrapeDSN(ctx context.Context, ch chan<- prometheus.Metric, dsn string) error {
101+
server, err := e.servers.GetServer(ctx, dsn)
101102

102103
if err != nil {
103104
return &ErrorConnectToServer{fmt.Sprintf("Error opening connection to database (%s): %s", loggableDSN(dsn), err.Error())}
@@ -109,11 +110,11 @@ func (e *Exporter) scrapeDSN(ch chan<- prometheus.Metric, dsn string) error {
109110
}
110111

111112
// Check if map versions need to be updated
112-
if err := e.checkMapVersions(ch, server); err != nil {
113+
if err := e.checkMapVersions(ctx, ch, server); err != nil {
113114
level.Warn(logger).Log("msg", "Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
114115
}
115116

116-
return server.Scrape(ch, e.disableSettingsMetrics)
117+
return server.Scrape(ctx, ch, e.disableSettingsMetrics)
117118
}
118119

119120
// try to get the DataSource

cmd/postgres_exporter/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ var (
4242
excludeDatabases = kingpin.Flag("exclude-databases", "A list of databases to remove when autoDiscoverDatabases is enabled").Default("").Envar("PG_EXPORTER_EXCLUDE_DATABASES").String()
4343
includeDatabases = kingpin.Flag("include-databases", "A list of databases to include when autoDiscoverDatabases is enabled").Default("").Envar("PG_EXPORTER_INCLUDE_DATABASES").String()
4444
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()
45+
scrapeTimeout = kingpin.Flag("scrape-timeout", "Maximum duration of a scrape").Default("60s").Envar("PG_EXPORTER_SCRAPE_TIMEOUT").Duration()
4546
logger = log.NewNopLogger()
4647
)
4748

@@ -105,7 +106,7 @@ func main() {
105106
IncludeDatabases(*includeDatabases),
106107
}
107108

108-
exporter := NewExporter(dsn, opts...)
109+
exporter := NewExporter(dsn, scrapeTimeout, opts...)
109110
defer func() {
110111
exporter.servers.Close()
111112
}()

cmd/postgres_exporter/namespace.go

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

1616
import (
17+
"context"
1718
"database/sql"
1819
"errors"
1920
"fmt"
@@ -27,7 +28,7 @@ import (
2728

2829
// Query within a namespace mapping and emit metrics. Returns fatal errors if
2930
// the scrape fails, and a slice of errors if they were non-fatal.
30-
func queryNamespaceMapping(server *Server, namespace string, mapping MetricMapNamespace) ([]prometheus.Metric, []error, error) {
31+
func queryNamespaceMapping(ctx context.Context, server *Server, namespace string, mapping MetricMapNamespace) ([]prometheus.Metric, []error, error) {
3132
// Check for a query override for this namespace
3233
query, found := server.queryOverrides[namespace]
3334

@@ -45,9 +46,9 @@ func queryNamespaceMapping(server *Server, namespace string, mapping MetricMapNa
4546
if !found {
4647
// I've no idea how to avoid this properly at the moment, but this is
4748
// an admin tool so you're not injecting SQL right?
48-
rows, err = server.db.Query(fmt.Sprintf("SELECT * FROM %s;", namespace)) // nolint: gas
49+
rows, err = server.db.QueryContext(ctx, fmt.Sprintf("SELECT * FROM %s;", namespace)) // nolint: gas
4950
} else {
50-
rows, err = server.db.Query(query)
51+
rows, err = server.db.QueryContext(ctx, query)
5152
}
5253
if err != nil {
5354
return []prometheus.Metric{}, []error{}, fmt.Errorf("Error running query on database %q: %s %v", server, namespace, err)
@@ -183,7 +184,7 @@ func queryNamespaceMapping(server *Server, namespace string, mapping MetricMapNa
183184

184185
// Iterate through all the namespace mappings in the exporter and run their
185186
// queries.
186-
func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[string]error {
187+
func queryNamespaceMappings(ctx context.Context, ch chan<- prometheus.Metric, server *Server) map[string]error {
187188
// Return a map of namespace -> errors
188189
namespaceErrors := make(map[string]error)
189190

@@ -225,7 +226,7 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
225226
var nonFatalErrors []error
226227
var err error
227228
if scrapeMetric {
228-
metrics, nonFatalErrors, err = queryNamespaceMapping(server, namespace, mapping)
229+
metrics, nonFatalErrors, err = queryNamespaceMapping(ctx, server, namespace, mapping)
229230
} else {
230231
metrics = cachedMetric.metrics
231232
}

cmd/postgres_exporter/pg_setting.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package main
1515

1616
import (
17+
"context"
1718
"fmt"
1819
"math"
1920
"strconv"
@@ -24,7 +25,7 @@ import (
2425
)
2526

2627
// Query the pg_settings view containing runtime variables
27-
func querySettings(ch chan<- prometheus.Metric, server *Server) error {
28+
func querySettings(ctx context.Context, ch chan<- prometheus.Metric, server *Server) error {
2829
level.Debug(logger).Log("msg", "Querying pg_setting view", "server", server)
2930

3031
// pg_settings docs: https://www.postgresql.org/docs/current/static/view-pg-settings.html
@@ -33,7 +34,7 @@ func querySettings(ch chan<- prometheus.Metric, server *Server) error {
3334
// types in normaliseUnit() below
3435
query := "SELECT name, setting, COALESCE(unit, ''), short_desc, vartype FROM pg_settings WHERE vartype IN ('bool', 'integer', 'real');"
3536

36-
rows, err := server.db.Query(query)
37+
rows, err := server.db.QueryContext(ctx, query)
3738
if err != nil {
3839
return fmt.Errorf("Error running query on database %q: %s %v", server, namespace, err)
3940
}

cmd/postgres_exporter/postgres_exporter.go

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package main
1515

1616
import (
17+
"context"
1718
"crypto/sha256"
1819
"database/sql"
1920
"errors"
@@ -468,6 +469,7 @@ type Exporter struct {
468469
psqlUp prometheus.Gauge
469470
userQueriesError *prometheus.GaugeVec
470471
totalScrapes prometheus.Counter
472+
scrapeTimeout *time.Duration
471473

472474
// servers are used to allow re-using the DB connection between scrapes.
473475
// servers contains metrics map and query overrides.
@@ -555,7 +557,7 @@ func parseConstLabels(s string) prometheus.Labels {
555557
}
556558

557559
// NewExporter returns a new PostgreSQL exporter for the provided DSN.
558-
func NewExporter(dsn []string, opts ...ExporterOpt) *Exporter {
560+
func NewExporter(dsn []string, scrapeTimeout *time.Duration, opts ...ExporterOpt) *Exporter {
559561
e := &Exporter{
560562
dsn: dsn,
561563
builtinMetricMaps: builtinMetricMaps,
@@ -567,6 +569,7 @@ func NewExporter(dsn []string, opts ...ExporterOpt) *Exporter {
567569

568570
e.setupInternalMetrics()
569571
e.servers = NewServers(ServerWithLabels(e.constantLabels))
572+
e.scrapeTimeout = scrapeTimeout
570573

571574
return e
572575
}
@@ -614,7 +617,16 @@ func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
614617

615618
// Collect implements prometheus.Collector.
616619
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
617-
e.scrape(ch)
620+
var ctx context.Context
621+
var cancel context.CancelFunc
622+
if e.scrapeTimeout != nil {
623+
ctx, cancel = context.WithTimeout(context.Background(), *e.scrapeTimeout)
624+
} else {
625+
ctx, cancel = context.WithCancel(context.Background())
626+
}
627+
defer cancel()
628+
629+
e.scrape(ctx, ch)
618630

619631
ch <- e.duration
620632
ch <- e.totalScrapes
@@ -630,9 +642,9 @@ func newDesc(subsystem, name, help string, labels prometheus.Labels) *prometheus
630642
)
631643
}
632644

633-
func checkPostgresVersion(db *sql.DB, server string) (semver.Version, string, error) {
645+
func checkPostgresVersion(ctx context.Context, db *sql.DB, server string) (semver.Version, string, error) {
634646
level.Debug(logger).Log("msg", "Querying PostgreSQL version", "server", server)
635-
versionRow := db.QueryRow("SELECT version();")
647+
versionRow := db.QueryRowContext(ctx, "SELECT version();")
636648
var versionString string
637649
err := versionRow.Scan(&versionString)
638650
if err != nil {
@@ -647,8 +659,8 @@ func checkPostgresVersion(db *sql.DB, server string) (semver.Version, string, er
647659
}
648660

649661
// Check and update the exporters query maps if the version has changed.
650-
func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server) error {
651-
semanticVersion, versionString, err := checkPostgresVersion(server.db, server.String())
662+
func (e *Exporter) checkMapVersions(ctx context.Context, ch chan<- prometheus.Metric, server *Server) error {
663+
semanticVersion, versionString, err := checkPostgresVersion(ctx, server.db, server.String())
652664
if err != nil {
653665
return fmt.Errorf("Error fetching version string on %q: %v", server, err)
654666
}
@@ -709,7 +721,7 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
709721
return nil
710722
}
711723

712-
func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
724+
func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) {
713725
defer func(begun time.Time) {
714726
e.duration.Set(time.Since(begun).Seconds())
715727
}(time.Now())
@@ -718,14 +730,14 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
718730

719731
dsns := e.dsn
720732
if e.autoDiscoverDatabases {
721-
dsns = e.discoverDatabaseDSNs()
733+
dsns = e.discoverDatabaseDSNs(ctx)
722734
}
723735

724736
var errorsCount int
725737
var connectionErrorsCount int
726738

727739
for _, dsn := range dsns {
728-
if err := e.scrapeDSN(ch, dsn); err != nil {
740+
if err := e.scrapeDSN(ctx, ch, dsn); err != nil {
729741
errorsCount++
730742

731743
level.Error(logger).Log("err", err)

cmd/postgres_exporter/postgres_exporter_integration_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (s *IntegrationSuite) SetUpSuite(c *C) {
4242
dsn := os.Getenv("DATA_SOURCE_NAME")
4343
c.Assert(dsn, Not(Equals), "")
4444

45-
exporter := NewExporter(strings.Split(dsn, ","))
45+
exporter := NewExporter(strings.Split(dsn, ","), 10*time.Duration)
4646
c.Assert(exporter, NotNil)
4747
// Assign the exporter to the suite
4848
s.e = exporter
@@ -66,7 +66,7 @@ func (s *IntegrationSuite) TestAllNamespacesReturnResults(c *C) {
6666
c.Assert(err, IsNil)
6767

6868
// Do a version update
69-
err = s.e.checkMapVersions(ch, server)
69+
err = s.e.checkMapVersions(context.Background(), ch, server)
7070
c.Assert(err, IsNil)
7171

7272
err = querySettings(ch, server)
@@ -99,12 +99,12 @@ func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) {
9999
}()
100100

101101
// Send a bad DSN
102-
exporter := NewExporter([]string{"invalid dsn"})
102+
exporter := NewExporter([]string{"invalid dsn"}, 10*time.Duration)
103103
c.Assert(exporter, NotNil)
104104
exporter.scrape(ch)
105105

106106
// Send a DSN to a non-listening port.
107-
exporter = NewExporter([]string{"postgresql://nothing:[email protected]:1/nothing"})
107+
exporter = NewExporter([]string{"postgresql://nothing:[email protected]:1/nothing"}, 10*time.Duration)
108108
c.Assert(exporter, NotNil)
109109
exporter.scrape(ch)
110110
}
@@ -122,7 +122,7 @@ func (s *IntegrationSuite) TestUnknownMetricParsingDoesntCrash(c *C) {
122122
dsn := os.Getenv("DATA_SOURCE_NAME")
123123
c.Assert(dsn, Not(Equals), "")
124124

125-
exporter := NewExporter(strings.Split(dsn, ","))
125+
exporter := NewExporter(strings.Split(dsn, ","), 10*time.Duration)
126126
c.Assert(exporter, NotNil)
127127

128128
// Convert the default maps into a list of empty maps.
@@ -154,7 +154,7 @@ func (s *IntegrationSuite) TestExtendQueriesDoesntCrash(c *C) {
154154
c.Assert(dsn, Not(Equals), "")
155155

156156
exporter := NewExporter(
157-
strings.Split(dsn, ","),
157+
strings.Split(dsn, ","), 10*time.Duration,
158158
WithUserQueriesPath("../user_queries_test.yaml"),
159159
)
160160
c.Assert(exporter, NotNil)
@@ -168,6 +168,7 @@ func (s *IntegrationSuite) TestAutoDiscoverDatabases(c *C) {
168168

169169
exporter := NewExporter(
170170
strings.Split(dsn, ","),
171+
10*time.Duration,
171172
)
172173
c.Assert(exporter, NotNil)
173174

cmd/postgres_exporter/queries.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
package main
1515

1616
import (
17+
"context"
1718
"errors"
1819
"fmt"
1920

@@ -282,8 +283,8 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
282283
return nil
283284
}
284285

285-
func queryDatabases(server *Server) ([]string, error) {
286-
rows, err := server.db.Query("SELECT datname FROM pg_database WHERE datallowconn = true AND datistemplate = false AND datname != current_database()")
286+
func queryDatabases(ctx context.Context, server *Server) ([]string, error) {
287+
rows, err := server.db.QueryContext(ctx, "SELECT datname FROM pg_database WHERE datallowconn = true AND datistemplate = false AND datname != current_database()")
287288
if err != nil {
288289
return nil, fmt.Errorf("Error retrieving databases: %v", err)
289290
}

cmd/postgres_exporter/server.go

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

1616
import (
17+
"context"
1718
"database/sql"
1819
"fmt"
1920
"sync"
@@ -95,8 +96,8 @@ func (s *Server) Close() error {
9596
}
9697

9798
// Ping checks connection availability and possibly invalidates the connection if it fails.
98-
func (s *Server) Ping() error {
99-
if err := s.db.Ping(); err != nil {
99+
func (s *Server) Ping(ctx context.Context) error {
100+
if err := s.db.PingContext(ctx); err != nil {
100101
if cerr := s.Close(); cerr != nil {
101102
level.Error(logger).Log("msg", "Error while closing non-pinging DB connection", "server", s, "err", cerr)
102103
}
@@ -111,19 +112,19 @@ func (s *Server) String() string {
111112
}
112113

113114
// Scrape loads metrics.
114-
func (s *Server) Scrape(ch chan<- prometheus.Metric, disableSettingsMetrics bool) error {
115+
func (s *Server) Scrape(ctx context.Context, ch chan<- prometheus.Metric, disableSettingsMetrics bool) error {
115116
s.mappingMtx.RLock()
116117
defer s.mappingMtx.RUnlock()
117118

118119
var err error
119120

120121
if !disableSettingsMetrics && s.master {
121-
if err = querySettings(ch, s); err != nil {
122+
if err = querySettings(ctx, ch, s); err != nil {
122123
err = fmt.Errorf("error retrieving settings: %s", err)
123124
}
124125
}
125126

126-
errMap := queryNamespaceMappings(ch, s)
127+
errMap := queryNamespaceMappings(ctx, ch, s)
127128
if len(errMap) > 0 {
128129
err = fmt.Errorf("queryNamespaceMappings returned %d errors", len(errMap))
129130
}
@@ -147,7 +148,7 @@ func NewServers(opts ...ServerOpt) *Servers {
147148
}
148149

149150
// GetServer returns established connection from a collection.
150-
func (s *Servers) GetServer(dsn string) (*Server, error) {
151+
func (s *Servers) GetServer(ctx context.Context, dsn string) (*Server, error) {
151152
s.m.Lock()
152153
defer s.m.Unlock()
153154
var err error
@@ -168,7 +169,7 @@ func (s *Servers) GetServer(dsn string) (*Server, error) {
168169
}
169170
s.servers[dsn] = server
170171
}
171-
if err = server.Ping(); err != nil {
172+
if err = server.Ping(ctx); err != nil {
172173
delete(s.servers, dsn)
173174
time.Sleep(time.Duration(errCount) * time.Second)
174175
continue

0 commit comments

Comments
 (0)