1
0
mirror of https://github.com/prometheus-community/postgres_exporter.git synced 2025-04-25 20:02:39 +03:00

297 lines
9.1 KiB
Go

package collector
import (
"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/common/log"
"github.com/wrouesnel/postgres_exporter/exporter/metricmaps"
)
// Metric name parts.
const (
// Namespace for all metrics.
namespace = "pg"
// Subsystems.
exporter = "exporter"
// Metric label used for static string data thats handy to send to Prometheus
// e.g. version
staticLabelName = "static"
)
// lowestSupportedVersion is the lowest version of Postgres this exporter will support.
var lowestSupportedVersion = semver.MustParse("9.1.0")
// Exporter collects Postgres metrics. It implements prometheus.Collector.
type Exporter struct {
// Holds a reference to the build in column mappings. Currently this is for testing purposes
// only, since it just points to the global.
builtinMetricMaps map[string]map[string]metricmaps.ColumnMapping
dsn string
userQueriesPath string
duration prometheus.Gauge
error prometheus.Gauge
psqlUp prometheus.Gauge
userQueriesError *prometheus.GaugeVec
totalScrapes prometheus.Counter
// dbDsn is the connection string used to establish the dbConnection
dbDsn string
// dbConnection is used to allow re-using the DB connection between scrapes
dbConnection *sql.DB
// Last version used to calculate metric map. If mismatch on scrape,
// then maps are recalculated.
lastMapVersion semver.Version
// Currently active metric map
metricMap map[string]metricmaps.MetricMapNamespace
// Currently active query overrides
queryOverrides map[string]string
mappingMtx sync.RWMutex
}
// NewExporter returns a new PostgreSQL exporter for the provided DSN.
func NewExporter(dsn string, userQueriesPath string) *Exporter {
return &Exporter{
builtinMetricMaps: builtinMetricMaps,
dsn: dsn,
userQueriesPath: userQueriesPath,
duration: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: exporter,
Name: "last_scrape_duration_seconds",
Help: "Duration of the last scrape of metrics from PostgresSQL.",
}),
totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: exporter,
Name: "scrapes_total",
Help: "Total number of times PostgresSQL was scraped for metrics.",
}),
error: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: exporter,
Name: "last_scrape_error",
Help: "Whether the last scrape of metrics from PostgreSQL resulted in an error (1 for error, 0 for success).",
}),
psqlUp: prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: namespace,
Name: "up",
Help: "Whether the last scrape of metrics from PostgreSQL was able to connect to the server (1 for yes, 0 for no).",
}),
userQueriesError: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: exporter,
Name: "user_queries_load_error",
Help: "Whether the user queries file was loaded and parsed successfully (1 for error, 0 for success).",
}, []string{"filename", "hashsum"}),
metricMap: nil,
queryOverrides: nil,
}
}
// Describe implements prometheus.Collector.
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
// We cannot know in advance what metrics the exporter will generate
// from Postgres. So we use the poor man's describe method: Run a collect
// and send the descriptors of all the collected metrics. The problem
// here is that we need to connect to the Postgres DB. If it is currently
// unavailable, the descriptors will be incomplete. Since this is a
// stand-alone exporter and not used as a library within other code
// implementing additional metrics, the worst that can happen is that we
// don't detect inconsistent metrics created by this exporter
// itself. Also, a change in the monitored Postgres instance may change the
// exported metrics during the runtime of the exporter.
metricCh := make(chan prometheus.Metric)
doneCh := make(chan struct{})
go func() {
for m := range metricCh {
ch <- m.Desc()
}
close(doneCh)
}()
e.Collect(metricCh)
close(metricCh)
<-doneCh
}
// Collect implements prometheus.Collector.
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
e.scrape(ch)
ch <- e.duration
ch <- e.totalScrapes
ch <- e.error
ch <- e.psqlUp
e.userQueriesError.Collect(ch)
}
// Check and update the exporters query maps if the version has changed.
func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, db *sql.DB) error {
log.Debugln("Querying Postgres Version")
versionRow := db.QueryRow("SELECT version();")
var versionString string
err := versionRow.Scan(&versionString)
if err != nil {
return fmt.Errorf("Error scanning version string: %v", err)
}
semanticVersion, err := parseVersion(versionString)
if err != nil {
return fmt.Errorf("Error parsing version string: %v", err)
}
if semanticVersion.LT(lowestSupportedVersion) {
log.Warnln("PostgreSQL version is lower then our lowest supported version! Got", semanticVersion.String(), "minimum supported is", lowestSupportedVersion.String())
}
// Check if semantic version changed and recalculate maps if needed.
if semanticVersion.NE(e.lastMapVersion) || e.metricMap == nil {
log.Infoln("Semantic Version Changed:", e.lastMapVersion.String(), "->", semanticVersion.String())
e.mappingMtx.Lock()
e.metricMap = makeDescMap(semanticVersion, e.builtinMetricMaps)
e.queryOverrides = makeQueryOverrideMap(semanticVersion, queryOverrides)
e.lastMapVersion = semanticVersion
if e.userQueriesPath != "" {
// Clear the metric while a reload is happening
e.userQueriesError.Reset()
// Calculate the hashsum of the useQueries
userQueriesData, err := ioutil.ReadFile(e.userQueriesPath)
if err != nil {
log.Errorln("Failed to reload user queries:", e.userQueriesPath, err)
e.userQueriesError.WithLabelValues(e.userQueriesPath, "").Set(1)
} else {
hashsumStr := fmt.Sprintf("%x", sha256.Sum256(userQueriesData))
if err := addQueries(userQueriesData, semanticVersion, e.metricMap, e.queryOverrides); err != nil {
log.Errorln("Failed to reload user queries:", e.userQueriesPath, err)
e.userQueriesError.WithLabelValues(e.userQueriesPath, hashsumStr).Set(1)
} else {
// Mark user queries as successfully loaded
e.userQueriesError.WithLabelValues(e.userQueriesPath, hashsumStr).Set(0)
}
}
}
e.mappingMtx.Unlock()
}
// Output the version as a special metric
versionDesc := prometheus.NewDesc(fmt.Sprintf("%s_%s", namespace, staticLabelName),
"Version string as reported by postgres", []string{"version", "short_version"}, nil)
ch <- prometheus.MustNewConstMetric(versionDesc,
prometheus.UntypedValue, 1, versionString, semanticVersion.String())
return nil
}
func (e *Exporter) getDB(conn string) (*sql.DB, error) {
// Has dsn changed?
if (e.dbConnection != nil) && (e.dsn != e.dbDsn) {
err := e.dbConnection.Close()
log.Warnln("Error while closing obsolete DB connection:", err)
e.dbConnection = nil
e.dbDsn = ""
}
if e.dbConnection == nil {
d, err := sql.Open("postgres", conn)
if err != nil {
return nil, err
}
d.SetMaxOpenConns(1)
d.SetMaxIdleConns(1)
e.dbConnection = d
e.dbDsn = e.dsn
log.Infoln("Established new database connection.")
}
// Always send a ping and possibly invalidate the connection if it fails
if err := e.dbConnection.Ping(); err != nil {
cerr := e.dbConnection.Close()
log.Infoln("Error while closing non-pinging DB connection:", cerr)
e.dbConnection = nil
e.psqlUp.Set(0)
return nil, err
}
return e.dbConnection, nil
}
func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
defer func(begun time.Time) {
e.duration.Set(time.Since(begun).Seconds())
}(time.Now())
e.error.Set(0)
e.totalScrapes.Inc()
db, err := e.getDB(e.dsn)
if err != nil {
loggableDsn := "could not parse DATA_SOURCE_NAME"
// If the DSN is parseable, log it with a blanked out password
pDsn, pErr := url.Parse(e.dsn)
if pErr == nil {
// Blank user info if not nil
if pDsn.User != nil {
pDsn.User = url.UserPassword(pDsn.User.Username(), "PASSWORD_REMOVED")
}
loggableDsn = pDsn.String()
}
log.Infof("Error opening connection to database (%s): %s", loggableDsn, err)
e.psqlUp.Set(0)
e.error.Set(1)
return
}
// Didn't fail, can mark connection as up for this scrape.
e.psqlUp.Set(1)
// Check if map versions need to be updated
if err := e.checkMapVersions(ch, db); err != nil {
log.Warnln("Proceeding with outdated query maps, as the Postgres version could not be determined:", err)
e.error.Set(1)
}
// Lock the exporter maps
e.mappingMtx.RLock()
defer e.mappingMtx.RUnlock()
if err := querySettings(ch, db); err != nil {
log.Infof("Error retrieving settings: %s", err)
e.error.Set(1)
}
errMap := queryNamespaceMappings(ch, db, e.metricMap, e.queryOverrides)
if len(errMap) > 0 {
e.error.Set(1)
}
}