1
0
mirror of https://github.com/prometheus-community/postgres_exporter.git synced 2025-07-31 20:44:25 +03:00

Refactor exporter so that it can be used as a library.

This commit is contained in:
Tamal Saha
2017-05-19 18:35:30 -07:00
committed by Will Rouesnel
parent 4117fb2afc
commit 2ab8f10935
7 changed files with 98 additions and 85 deletions

View File

@ -89,7 +89,7 @@ fmt: tools
postgres_exporter_integration_test: $(GO_SRC) postgres_exporter_integration_test: $(GO_SRC)
CGO_ENABLED=0 go test -c -tags integration \ CGO_ENABLED=0 go test -c -tags integration \
-a -ldflags "-extldflags '-static' -X main.Version=$(VERSION)" \ -a -ldflags "-extldflags '-static' -X main.Version=$(VERSION)" \
-o postgres_exporter_integration_test -cover -covermode count . -o postgres_exporter_integration_test -cover -covermode count ./collector/...
test: tools test: tools
@mkdir -p $(COVERDIR) @mkdir -p $(COVERDIR)

View File

@ -1,4 +1,4 @@
package main package collector
import ( import (
"database/sql" "database/sql"

View File

@ -1,6 +1,6 @@
// +build !integration // +build !integration
package main package collector
import ( import (
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"

View File

@ -1,41 +1,25 @@
package main package collector
import ( import (
"crypto/sha256"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math" "math"
"net/http"
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
"crypto/sha256"
"github.com/blang/semver" "github.com/blang/semver"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log" "github.com/prometheus/common/log"
) "gopkg.in/yaml.v2"
// Version is set during build to the git describe version
// (semantic version)-(commitish) form.
var Version = "0.0.1"
var (
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").OverrideDefaultFromEnvar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").OverrideDefaultFromEnvar("PG_EXPORTER_WEB_TELEMETRY_PATH").String()
queriesPath = kingpin.Flag("extend.query-path", "Path to custom queries to run.").Default("").OverrideDefaultFromEnvar("PG_EXPORTER_EXTEND_QUERY_PATH").String()
onlyDumpMaps = kingpin.Flag("dumpmaps", "Do not run, simply dump the maps.").Bool()
) )
// Metric name parts. // Metric name parts.
@ -124,7 +108,7 @@ type MetricMap struct {
} }
// TODO: revisit this with the semver system // TODO: revisit this with the semver system
func dumpMaps() { func DumpMaps() {
// TODO: make this function part of the exporter // TODO: make this function part of the exporter
for name, cmap := range builtinMetricMaps { for name, cmap := range builtinMetricMaps {
query, ok := queryOverrides[name] query, ok := queryOverrides[name]
@ -727,6 +711,12 @@ func NewExporter(dsn string, userQueriesPath string) *Exporter {
} }
} }
func (e *Exporter) Close() {
if e.dbConnection != nil {
e.dbConnection.Close() // nolint: errcheck
}
}
// Describe implements prometheus.Collector. // Describe implements prometheus.Collector.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// We cannot know in advance what metrics the exporter will generate // We cannot know in advance what metrics the exporter will generate
@ -969,12 +959,15 @@ func (e *Exporter) getDB(conn string) (*sql.DB, error) {
if e.dbConnection == nil { if e.dbConnection == nil {
d, err := sql.Open("postgres", conn) d, err := sql.Open("postgres", conn)
if err != nil { if err != nil {
e.psqlUp.Set(0)
return nil, err return nil, err
} }
err = d.Ping() err = d.Ping()
if err != nil { if err != nil {
e.psqlUp.Set(0)
return nil, err return nil, err
} }
e.psqlUp.Set(1)
d.SetMaxOpenConns(1) d.SetMaxOpenConns(1)
d.SetMaxIdleConns(1) d.SetMaxIdleConns(1)
@ -1008,7 +1001,6 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
} }
log.Infof("Error opening connection to database (%s): %s", loggableDsn, err) log.Infof("Error opening connection to database (%s): %s", loggableDsn, err)
e.error.Set(1) e.error.Set(1)
e.psqlUp.Set(0) // Force "up" to 0 here.
return return
} }
@ -1036,7 +1028,7 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
// DATA_SOURCE_NAME always wins so we do not break older versions // DATA_SOURCE_NAME always wins so we do not break older versions
// reading secrets from files wins over secrets in environment variables // reading secrets from files wins over secrets in environment variables
// DATA_SOURCE_NAME > DATA_SOURCE_{USER|FILE}_FILE > DATA_SOURCE_{USER|FILE} // DATA_SOURCE_NAME > DATA_SOURCE_{USER|FILE}_FILE > DATA_SOURCE_{USER|FILE}
func getDataSource() string { func GetDataSource() string {
var dsn = os.Getenv("DATA_SOURCE_NAME") var dsn = os.Getenv("DATA_SOURCE_NAME")
if len(dsn) == 0 { if len(dsn) == 0 {
var user string var user string
@ -1068,48 +1060,3 @@ func getDataSource() string {
return dsn return dsn
} }
func main() {
kingpin.Version(fmt.Sprintf("postgres_exporter %s (built with %s)\n", Version, runtime.Version()))
log.AddFlags(kingpin.CommandLine)
kingpin.Parse()
// landingPage contains the HTML served at '/'.
// TODO: Make this nicer and more informative.
var landingPage = []byte(`<html>
<head><title>Postgres exporter</title></head>
<body>
<h1>Postgres exporter</h1>
<p><a href='` + *metricPath + `'>Metrics</a></p>
</body>
</html>
`)
if *onlyDumpMaps {
dumpMaps()
return
}
dsn := getDataSource()
if len(dsn) == 0 {
log.Fatal("couldn't find environment variables describing the datasource to use")
}
exporter := NewExporter(dsn, *queriesPath)
defer func() {
if exporter.dbConnection != nil {
exporter.dbConnection.Close() // nolint: errcheck
}
}()
prometheus.MustRegister(exporter)
http.Handle(*metricPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "Content-Type:text/plain; charset=UTF-8") // nolint: errcheck
w.Write(landingPage) // nolint: errcheck
})
log.Infof("Starting Server: %s", *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
}

View File

@ -3,19 +3,17 @@
// working. // working.
// +build integration // +build integration
package main package collector
import ( import (
"database/sql"
"fmt"
"os" "os"
"testing" "testing"
. "gopkg.in/check.v1"
"database/sql"
"fmt"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
. "gopkg.in/check.v1"
) )
// Hook up gocheck into the "go test" runner. // Hook up gocheck into the "go test" runner.
@ -78,6 +76,8 @@ func (s *IntegrationSuite) TestAllNamespacesReturnResults(c *C) {
// the exporter. Related to https://github.com/wrouesnel/postgres_exporter/issues/93 // the exporter. Related to https://github.com/wrouesnel/postgres_exporter/issues/93
// although not a replication of the scenario. // although not a replication of the scenario.
func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) { func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) {
queriesPath := os.Getenv("PG_EXPORTER_EXTEND_QUERY_PATH")
// Setup a dummy channel to consume metrics // Setup a dummy channel to consume metrics
ch := make(chan prometheus.Metric, 100) ch := make(chan prometheus.Metric, 100)
go func() { go func() {
@ -86,12 +86,12 @@ func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) {
}() }()
// Send a bad DSN // Send a bad DSN
exporter := NewExporter("invalid dsn", *queriesPath) exporter := NewExporter("invalid dsn", queriesPath)
c.Assert(exporter, NotNil) c.Assert(exporter, NotNil)
exporter.scrape(ch) exporter.scrape(ch)
// Send a DSN to a non-listening port. // Send a DSN to a non-listening port.
exporter = NewExporter("postgresql://nothing:nothing@127.0.0.1:1/nothing", *queriesPath) exporter = NewExporter("postgresql://nothing:nothing@127.0.0.1:1/nothing", queriesPath)
c.Assert(exporter, NotNil) c.Assert(exporter, NotNil)
exporter.scrape(ch) exporter.scrape(ch)
} }

View File

@ -1,6 +1,6 @@
// +build !integration // +build !integration
package main package collector
import ( import (
. "gopkg.in/check.v1" . "gopkg.in/check.v1"
@ -89,11 +89,11 @@ func (s *FunctionalSuite) TestSemanticVersionColumnDiscard(c *C) {
// test read username and password from file // test read username and password from file
func (s *FunctionalSuite) TestEnvironmentSettingWithSecretsFiles(c *C) { func (s *FunctionalSuite) TestEnvironmentSettingWithSecretsFiles(c *C) {
err := os.Setenv("DATA_SOURCE_USER_FILE", "./tests/username_file") err := os.Setenv("DATA_SOURCE_USER_FILE", "../tests/username_file")
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_USER_FILE") defer UnsetEnvironment(c, "DATA_SOURCE_USER_FILE")
err = os.Setenv("DATA_SOURCE_PASS_FILE", "./tests/userpass_file") err = os.Setenv("DATA_SOURCE_PASS_FILE", "../tests/userpass_file")
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_PASS_FILE") defer UnsetEnvironment(c, "DATA_SOURCE_PASS_FILE")
@ -103,7 +103,7 @@ func (s *FunctionalSuite) TestEnvironmentSettingWithSecretsFiles(c *C) {
var expected = "postgresql://custom_username:custom_password@localhost:5432/?sslmode=disable" var expected = "postgresql://custom_username:custom_password@localhost:5432/?sslmode=disable"
dsn := getDataSource() dsn := GetDataSource()
if dsn != expected { if dsn != expected {
c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, expected) c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, expected)
} }
@ -117,7 +117,7 @@ func (s *FunctionalSuite) TestEnvironmentSettingWithDns(c *C) {
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_NAME") defer UnsetEnvironment(c, "DATA_SOURCE_NAME")
dsn := getDataSource() dsn := GetDataSource()
if dsn != envDsn { if dsn != envDsn {
c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, envDsn) c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, envDsn)
} }
@ -131,7 +131,7 @@ func (s *FunctionalSuite) TestEnvironmentSettingWithDnsAndSecrets(c *C) {
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_NAME") defer UnsetEnvironment(c, "DATA_SOURCE_NAME")
err = os.Setenv("DATA_SOURCE_USER_FILE", "./tests/username_file") err = os.Setenv("DATA_SOURCE_USER_FILE", "../tests/username_file")
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_USER_FILE") defer UnsetEnvironment(c, "DATA_SOURCE_USER_FILE")
@ -139,7 +139,7 @@ func (s *FunctionalSuite) TestEnvironmentSettingWithDnsAndSecrets(c *C) {
c.Assert(err, IsNil) c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_PASS") defer UnsetEnvironment(c, "DATA_SOURCE_PASS")
dsn := getDataSource() dsn := GetDataSource()
if dsn != envDsn { if dsn != envDsn {
c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, envDsn) c.Errorf("Expected Username to be read from file. Found=%v, expected=%v", dsn, envDsn)
} }

66
main.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"fmt"
"net/http"
"runtime"
_ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
"github.com/wrouesnel/postgres_exporter/collector"
"gopkg.in/alecthomas/kingpin.v2"
)
// Version is set during build to the git describe version
// (semantic version)-(commitish) form.
var Version = "0.0.1"
var (
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").OverrideDefaultFromEnvar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String()
metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").OverrideDefaultFromEnvar("PG_EXPORTER_WEB_TELEMETRY_PATH").String()
queriesPath = kingpin.Flag("extend.query-path", "Path to custom queries to run.").Default("").OverrideDefaultFromEnvar("PG_EXPORTER_EXTEND_QUERY_PATH").String()
onlyDumpMaps = kingpin.Flag("dumpmaps", "Do not run, simply dump the maps.").Bool()
)
func main() {
kingpin.Version(fmt.Sprintf("postgres_exporter %s (built with %s)\n", Version, runtime.Version()))
log.AddFlags(kingpin.CommandLine)
kingpin.Parse()
// landingPage contains the HTML served at '/'.
// TODO: Make this nicer and more informative.
var landingPage = []byte(`<html>
<head><title>Postgres exporter</title></head>
<body>
<h1>Postgres exporter</h1>
<p><a href='` + *metricPath + `'>Metrics</a></p>
</body>
</html>
`)
if *onlyDumpMaps {
collector.DumpMaps()
return
}
dsn := collector.GetDataSource()
if len(dsn) == 0 {
log.Fatal("couldn't find environment variables describing the datasource to use")
}
exporter := collector.NewExporter(dsn, *queriesPath)
defer exporter.Close()
prometheus.MustRegister(exporter)
http.Handle(*metricPath, promhttp.Handler())
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "Content-Type:text/plain; charset=UTF-8") // nolint: errcheck
w.Write(landingPage) // nolint: errcheck
})
log.Infof("Starting Server: %s", *listenAddress)
log.Fatal(http.ListenAndServe(*listenAddress, nil))
}