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)
CGO_ENABLED=0 go test -c -tags integration \
-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
@mkdir -p $(COVERDIR)

View File

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

View File

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

View File

@ -1,41 +1,25 @@
package main
package collector
import (
"crypto/sha256"
"database/sql"
"errors"
"fmt"
"io/ioutil"
"math"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
"gopkg.in/alecthomas/kingpin.v2"
"gopkg.in/yaml.v2"
"crypto/sha256"
"github.com/blang/semver"
_ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/log"
)
// 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()
"gopkg.in/yaml.v2"
)
// Metric name parts.
@ -124,7 +108,7 @@ type MetricMap struct {
}
// TODO: revisit this with the semver system
func dumpMaps() {
func DumpMaps() {
// TODO: make this function part of the exporter
for name, cmap := range builtinMetricMaps {
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.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// 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 {
d, err := sql.Open("postgres", conn)
if err != nil {
e.psqlUp.Set(0)
return nil, err
}
err = d.Ping()
if err != nil {
e.psqlUp.Set(0)
return nil, err
}
e.psqlUp.Set(1)
d.SetMaxOpenConns(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)
e.error.Set(1)
e.psqlUp.Set(0) // Force "up" to 0 here.
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
// reading secrets from files wins over secrets in environment variables
// 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")
if len(dsn) == 0 {
var user string
@ -1068,48 +1060,3 @@ func getDataSource() string {
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.
// +build integration
package main
package collector
import (
"database/sql"
"fmt"
"os"
"testing"
. "gopkg.in/check.v1"
"database/sql"
"fmt"
_ "github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
. "gopkg.in/check.v1"
)
// 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
// although not a replication of the scenario.
func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) {
queriesPath := os.Getenv("PG_EXPORTER_EXTEND_QUERY_PATH")
// Setup a dummy channel to consume metrics
ch := make(chan prometheus.Metric, 100)
go func() {
@ -86,12 +86,12 @@ func (s *IntegrationSuite) TestInvalidDsnDoesntCrash(c *C) {
}()
// Send a bad DSN
exporter := NewExporter("invalid dsn", *queriesPath)
exporter := NewExporter("invalid dsn", queriesPath)
c.Assert(exporter, NotNil)
exporter.scrape(ch)
// 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)
exporter.scrape(ch)
}

View File

@ -1,6 +1,6 @@
// +build !integration
package main
package collector
import (
. "gopkg.in/check.v1"
@ -89,11 +89,11 @@ func (s *FunctionalSuite) TestSemanticVersionColumnDiscard(c *C) {
// test read username and password from file
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)
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)
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"
dsn := getDataSource()
dsn := GetDataSource()
if 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)
defer UnsetEnvironment(c, "DATA_SOURCE_NAME")
dsn := getDataSource()
dsn := GetDataSource()
if 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)
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)
defer UnsetEnvironment(c, "DATA_SOURCE_USER_FILE")
@ -139,7 +139,7 @@ func (s *FunctionalSuite) TestEnvironmentSettingWithDnsAndSecrets(c *C) {
c.Assert(err, IsNil)
defer UnsetEnvironment(c, "DATA_SOURCE_PASS")
dsn := getDataSource()
dsn := GetDataSource()
if 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))
}