You've already forked postgres_exporter
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:
committed by
Will Rouesnel
parent
4117fb2afc
commit
2ab8f10935
2
Makefile
2
Makefile
@ -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)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
@ -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"
|
@ -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))
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
@ -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
66
main.go
Normal 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))
|
||||||
|
}
|
Reference in New Issue
Block a user