1
0
mirror of https://github.com/prometheus-community/postgres_exporter.git synced 2026-10-23 12:08:57 +03:00

fix: preserve precision for large counters (#1351)

* fix: preserve precision for large counters

Wrap non-negative int64 counters at 2^53 so adjacent counter values remain distinguishable after conversion to float64. Gauges retain their existing conversion, and operators can disable wrapping through the CLI flag.

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>

* fix: keep large-counter setting CLI-only

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>

* fix: wrap counters from collector package

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>

* docs: warn about counter reset on upgrade

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>

---------

Signed-off-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
Co-authored-by: Gnani Rahul Nutakki <gnani.nutakki@gmail.com>
This commit is contained in:
Gnani Rahul Nutakki
2026-08-24 11:26:34 -05:00
committed by GitHub
parent 90a293ce96
commit 31385354ac
27 changed files with 528 additions and 198 deletions

View File

@@ -1,5 +1,16 @@
## main / (unreleased)
**WARNING: upgrading with large counters can cause a one-time false rate spike.**
Non-negative 64-bit counters are now wrapped at `2^53` by default so that
single-unit changes remain representable as `float64`. On the first scrape
after an upgrade, a previous value such as `2^53 + 1,234,567` becomes
`1,234,567`. Prometheus interprets that decrease as a counter reset and counts
the remainder as post-reset growth, so `rate()` and `increase()` can report a
large false spike for that evaluation window. Use
`--no-wrap-large-counters` to preserve the previous conversion during rollout;
enabling wrapping later will still create this one-time reset boundary.
* [CHANGE] Wrap non-negative 64-bit counters at `2^53` to preserve single-unit precision by @gnanirahulnutakki in https://github.com/prometheus-community/postgres_exporter/pull/1351
* [CHANGE] stat_replication: add `pid` label to disambiguate replication connections that otherwise share identical labels by @sysadmind in https://github.com/prometheus-community/postgres_exporter/pull/1353
## 0.20.1 / 2026-07-07

View File

@@ -229,6 +229,9 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
Do not run - print the internal representation of the metric maps. Useful when debugging a custom
queries file.
* `[no-]wrap-large-counters`
Wrap 64-bit counter values at 2^53 to avoid floating point rounding. Default is `true`.
* `constantLabels` (DEPRECATED)
Labels to set in all metrics. A list of `label=value` pairs, separated by commas.

View File

@@ -51,6 +51,7 @@ var (
includeDatabases = kingpin.Flag("include-databases", "A list of databases to include when autoDiscoverDatabases is enabled (DEPRECATED)").Default("").Envar("PG_EXPORTER_INCLUDE_DATABASES").String()
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()
collectionTimeout = kingpin.Flag("collection-timeout", "Timeout for collecting the statistics when the database is slow").Default("1m").Envar("PG_EXPORTER_COLLECTION_TIMEOUT").String()
wrapLargeCounters = kingpin.Flag("wrap-large-counters", "Wrap 64-bit counters at 2^53 to avoid floating point rounding.").Default("true").Bool()
collectorFlags = newCollectorFlags()
statStatementsFlags = newPGStatStatementsFlags()
logger = promslog.NewNopLogger()
@@ -233,6 +234,7 @@ func buildConfig(dsns []string) (config.Config, error) {
cfg.DataSourceNames = dsns
cfg.MetricPrefix = *metricPrefix
cfg.CollectionTimeout = parsedCollectionTimeout
cfg.WrapLargeCounters = *wrapLargeCounters
cfg.DisableDefaultMetrics = *disableDefaultMetrics
cfg.AutoDiscoverDatabases = *autoDiscoverDatabases
cfg.UserQueriesPath = *queriesPath

View File

@@ -23,6 +23,7 @@ import (
"time"
"github.com/prometheus-community/postgres_exporter/config"
"github.com/prometheus-community/postgres_exporter/internal/metricutil"
"github.com/prometheus/client_golang/prometheus"
)
@@ -72,6 +73,7 @@ type PostgresCollector struct {
CollectionTimeout time.Duration
collectorStates map[string]bool
pgStatStatements config.PGStatStatementsConfig
wrapLargeCounters bool
}
type Option func(*PostgresCollector) error
@@ -83,6 +85,7 @@ func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn st
collectorStates: config.DefaultCollectorConfig(),
pgStatStatements: defaultPGStatStatementsConfig(),
CollectionTimeout: time.Minute,
wrapLargeCounters: true,
}
// Apply options to customize the collector
for _, o := range options {
@@ -133,6 +136,7 @@ func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn st
if err != nil {
return nil, err
}
instance.wrapLargeCounters = p.wrapLargeCounters
p.instance = instance
return p, nil
@@ -173,6 +177,14 @@ func WithCollectionTimeout(s string) Option {
}
}
// WithWrapLargeCounters configures wrapping 64-bit counters at 2^53.
func WithWrapLargeCounters(wrap bool) Option {
return func(p *PostgresCollector) error {
p.wrapLargeCounters = wrap
return nil
}
}
// Describe implements the prometheus.Collector interface.
func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- scrapeDurationDesc
@@ -249,3 +261,11 @@ func Int32(m sql.NullInt32) float64 {
}
return mM
}
func int64CounterValue(value sql.NullInt64, wrap bool) float64 {
if !value.Valid {
return 0
}
return metricutil.Int64CounterValue(value.Int64, wrap)
}

View File

@@ -66,6 +66,39 @@ func sanitizeQuery(q string) string {
return q
}
func TestPostgresCollectorWrapLargeCounters(t *testing.T) {
tests := []struct {
name string
options []Option
want bool
}{
{name: "enabled by default", want: true},
{name: "disabled by option", options: []Option{WithWrapLargeCounters(false)}, want: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
collector, err := NewPostgresCollector(
promslog.NewNopLogger(),
nil,
"postgresql://local",
nil,
test.options...,
)
if err != nil {
t.Fatalf("creating collector: %v", err)
}
if got := collector.instance.wrapLargeCounters; got != test.want {
t.Fatalf("instance wrapLargeCounters = %t, want %t", got, test.want)
}
if got := collector.instance.copy().wrapLargeCounters; got != test.want {
t.Fatalf("copied instance wrapLargeCounters = %t, want %t", got, test.want)
}
})
}
}
// We ensure that when the database respond after a long time
// The collection process still occurs in a predictable manner
// Will avoid accumulation of queries on a completely frozen DB

View File

@@ -22,14 +22,16 @@ import (
)
type instance struct {
dsn string
db *sql.DB
version semver.Version
dsn string
db *sql.DB
version semver.Version
wrapLargeCounters bool
}
func newInstance(dsn string) (*instance, error) {
i := &instance{
dsn: dsn,
dsn: dsn,
wrapLargeCounters: true,
}
// "Create" a database handle to verify the DSN provided is valid.
@@ -46,7 +48,8 @@ func newInstance(dsn string) (*instance, error) {
// copy returns a copy of the instance.
func (i *instance) copy() *instance {
return &instance{
dsn: i.dsn,
dsn: i.dsn,
wrapLargeCounters: i.wrapLargeCounters,
}
}

View File

@@ -77,7 +77,7 @@ func (PGStatArchiverCollector) Update(ctx context.Context, instance *instance, c
ch <- prometheus.MustNewConstMetric(
statArchiverArchivedCountDesc,
prometheus.CounterValue,
float64(archivedCount.Int64),
int64CounterValue(archivedCount, instance.wrapLargeCounters),
)
}
@@ -85,7 +85,7 @@ func (PGStatArchiverCollector) Update(ctx context.Context, instance *instance, c
ch <- prometheus.MustNewConstMetric(
statArchiverFailedCountDesc,
prometheus.CounterValue,
float64(failedCount.Int64),
int64CounterValue(failedCount, instance.wrapLargeCounters),
)
}

View File

@@ -135,28 +135,19 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, c
return err
}
bcMetric := 0.0
if bc.Valid {
bcMetric = float64(bc.Int64)
}
bcMetric := int64CounterValue(bc, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCleanDesc,
prometheus.CounterValue,
bcMetric,
)
mwcMetric := 0.0
if mwc.Valid {
mwcMetric = float64(mwc.Int64)
}
mwcMetric := int64CounterValue(mwc, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterMaxwrittenCleanDesc,
prometheus.CounterValue,
mwcMetric,
)
baMetric := 0.0
if ba.Valid {
baMetric = float64(ba.Int64)
}
baMetric := int64CounterValue(ba, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersAllocDesc,
prometheus.CounterValue,
@@ -184,19 +175,13 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, c
return err
}
cptMetric := 0.0
if cpt.Valid {
cptMetric = float64(cpt.Int64)
}
cptMetric := int64CounterValue(cpt, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsTimedDesc,
prometheus.CounterValue,
cptMetric,
)
cprMetric := 0.0
if cpr.Valid {
cprMetric = float64(cpr.Int64)
}
cprMetric := int64CounterValue(cpr, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsReqDesc,
prometheus.CounterValue,
@@ -220,55 +205,37 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, c
prometheus.CounterValue,
cpstMetric,
)
bcpMetric := 0.0
if bcp.Valid {
bcpMetric = float64(bcp.Int64)
}
bcpMetric := int64CounterValue(bcp, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCheckpointDesc,
prometheus.CounterValue,
bcpMetric,
)
bcMetric := 0.0
if bc.Valid {
bcMetric = float64(bc.Int64)
}
bcMetric := int64CounterValue(bc, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCleanDesc,
prometheus.CounterValue,
bcMetric,
)
mwcMetric := 0.0
if mwc.Valid {
mwcMetric = float64(mwc.Int64)
}
mwcMetric := int64CounterValue(mwc, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterMaxwrittenCleanDesc,
prometheus.CounterValue,
mwcMetric,
)
bbMetric := 0.0
if bb.Valid {
bbMetric = float64(bb.Int64)
}
bbMetric := int64CounterValue(bb, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendDesc,
prometheus.CounterValue,
bbMetric,
)
bbfMetric := 0.0
if bbf.Valid {
bbfMetric = float64(bbf.Int64)
}
bbfMetric := int64CounterValue(bbf, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendFsyncDesc,
prometheus.CounterValue,
bbfMetric,
)
baMetric := 0.0
if ba.Valid {
baMetric = float64(ba.Int64)
}
baMetric := int64CounterValue(ba, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersAllocDesc,
prometheus.CounterValue,

View File

@@ -18,6 +18,7 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
@@ -149,3 +150,55 @@ func TestPGStatBGWriterCollectorNullValues(t *testing.T) {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatBGWriterCollectorWrapsLargeCounters(t *testing.T) {
const largeCounter = int64(1<<53) + 1
tests := []struct {
name string
wrap bool
value float64
}{
{name: "enabled", wrap: true, value: 1},
{name: "disabled", wrap: false, value: float64(largeCounter)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("opening stub database: %s", err)
}
t.Cleanup(func() { _ = db.Close() })
inst := &instance{
db: db,
version: semver.MustParse("17.0.0"),
wrapLargeCounters: test.wrap,
}
columns := []string{"buffers_clean", "maxwritten_clean", "buffers_alloc", "stats_reset"}
rows := sqlmock.NewRows(columns).AddRow(largeCounter, 0, 0, nil)
mock.ExpectQuery(sanitizeQuery(statBGWriterQueryAfter17)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
if err := (PGStatBGWriterCollector{}).Update(context.Background(), inst, ch); err != nil {
t.Errorf("updating collector: %s", err)
}
}()
metric := readMetric(<-ch)
if metric.value != test.value {
t.Fatalf("buffers_clean value = %v, want %v", metric.value, test.value)
}
for range 3 {
<-ch
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet database expectations: %v", err)
}
})
}
}

View File

@@ -135,50 +135,35 @@ func (c PGStatCheckpointerCollector) Update(ctx context.Context, instance *insta
return err
}
ntMetric := 0.0
if nt.Valid {
ntMetric = float64(nt.Int64)
}
ntMetric := int64CounterValue(nt, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerNumTimedDesc,
prometheus.CounterValue,
ntMetric,
)
nrMetric := 0.0
if nr.Valid {
nrMetric = float64(nr.Int64)
}
nrMetric := int64CounterValue(nr, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerNumRequestedDesc,
prometheus.CounterValue,
nrMetric,
)
rptMetric := 0.0
if rpt.Valid {
rptMetric = float64(rpt.Int64)
}
rptMetric := int64CounterValue(rpt, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsTimedDesc,
prometheus.CounterValue,
rptMetric,
)
rprMetric := 0.0
if rpr.Valid {
rprMetric = float64(rpr.Int64)
}
rprMetric := int64CounterValue(rpr, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsReqDesc,
prometheus.CounterValue,
rprMetric,
)
rpdMetric := 0.0
if rpd.Valid {
rpdMetric = float64(rpd.Int64)
}
rpdMetric := int64CounterValue(rpd, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsDoneDesc,
prometheus.CounterValue,
@@ -205,10 +190,7 @@ func (c PGStatCheckpointerCollector) Update(ctx context.Context, instance *insta
stMetric,
)
bwMetric := 0.0
if bw.Valid {
bwMetric = float64(bw.Int64)
}
bwMetric := int64CounterValue(bw, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statCheckpointerBuffersWrittenDesc,
prometheus.CounterValue,

View File

@@ -261,7 +261,8 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
for rows.Next() {
var datid, datname sql.NullString
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime sql.NullFloat64
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks sql.NullInt64
var blkReadTime, blkWriteTime, activeTime sql.NullFloat64
var statsReset sql.NullTime
r := []any{
@@ -385,98 +386,98 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
ch <- prometheus.MustNewConstMetric(
statDatabaseNumbackends,
prometheus.GaugeValue,
numBackends.Float64,
float64(numBackends.Int64),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseXactCommit,
prometheus.CounterValue,
xactCommit.Float64,
int64CounterValue(xactCommit, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseXactRollback,
prometheus.CounterValue,
xactRollback.Float64,
int64CounterValue(xactRollback, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseBlksRead,
prometheus.CounterValue,
blksRead.Float64,
int64CounterValue(blksRead, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseBlksHit,
prometheus.CounterValue,
blksHit.Float64,
int64CounterValue(blksHit, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTupReturned,
prometheus.CounterValue,
tupReturned.Float64,
int64CounterValue(tupReturned, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTupFetched,
prometheus.CounterValue,
tupFetched.Float64,
int64CounterValue(tupFetched, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTupInserted,
prometheus.CounterValue,
tupInserted.Float64,
int64CounterValue(tupInserted, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTupUpdated,
prometheus.CounterValue,
tupUpdated.Float64,
int64CounterValue(tupUpdated, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTupDeleted,
prometheus.CounterValue,
tupDeleted.Float64,
int64CounterValue(tupDeleted, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseConflicts,
prometheus.CounterValue,
conflicts.Float64,
int64CounterValue(conflicts, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTempFiles,
prometheus.CounterValue,
tempFiles.Float64,
int64CounterValue(tempFiles, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseTempBytes,
prometheus.CounterValue,
tempBytes.Float64,
int64CounterValue(tempBytes, instance.wrapLargeCounters),
labels...,
)
ch <- prometheus.MustNewConstMetric(
statDatabaseDeadlocks,
prometheus.CounterValue,
deadlocks.Float64,
int64CounterValue(deadlocks, instance.wrapLargeCounters),
labels...,
)

View File

@@ -258,10 +258,7 @@ func (c PGStatStatementsCollector) Update(ctx context.Context, instance *instanc
}
seen[key] = struct{}{}
callsTotalMetric := 0.0
if callsTotal.Valid {
callsTotalMetric = float64(callsTotal.Int64)
}
callsTotalMetric := int64CounterValue(callsTotal, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statStatementsCallsTotal,
prometheus.CounterValue,
@@ -280,10 +277,7 @@ func (c PGStatStatementsCollector) Update(ctx context.Context, instance *instanc
userLabel, datnameLabel, queryidLabel,
)
rowsTotalMetric := 0.0
if rowsTotal.Valid {
rowsTotalMetric = float64(rowsTotal.Int64)
}
rowsTotalMetric := int64CounterValue(rowsTotal, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statStatementsRowsTotal,
prometheus.CounterValue,

View File

@@ -223,10 +223,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
relnameLabel = relname.String
}
seqScanMetric := 0.0
if seqScan.Valid {
seqScanMetric = float64(seqScan.Int64)
}
seqScanMetric := int64CounterValue(seqScan, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesSeqScan,
prometheus.CounterValue,
@@ -234,10 +231,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
seqTupReadMetric := 0.0
if seqTupRead.Valid {
seqTupReadMetric = float64(seqTupRead.Int64)
}
seqTupReadMetric := int64CounterValue(seqTupRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesSeqTupRead,
prometheus.CounterValue,
@@ -245,10 +239,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
idxScanMetric := 0.0
if idxScan.Valid {
idxScanMetric = float64(idxScan.Int64)
}
idxScanMetric := int64CounterValue(idxScan, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesIdxScan,
prometheus.CounterValue,
@@ -256,10 +247,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
idxTupFetchMetric := 0.0
if idxTupFetch.Valid {
idxTupFetchMetric = float64(idxTupFetch.Int64)
}
idxTupFetchMetric := int64CounterValue(idxTupFetch, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesIdxTupFetch,
prometheus.CounterValue,
@@ -267,10 +255,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
nTupInsMetric := 0.0
if nTupIns.Valid {
nTupInsMetric = float64(nTupIns.Int64)
}
nTupInsMetric := int64CounterValue(nTupIns, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesNTupIns,
prometheus.CounterValue,
@@ -278,10 +263,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
nTupUpdMetric := 0.0
if nTupUpd.Valid {
nTupUpdMetric = float64(nTupUpd.Int64)
}
nTupUpdMetric := int64CounterValue(nTupUpd, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesNTupUpd,
prometheus.CounterValue,
@@ -289,10 +271,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
nTupDelMetric := 0.0
if nTupDel.Valid {
nTupDelMetric = float64(nTupDel.Int64)
}
nTupDelMetric := int64CounterValue(nTupDel, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesNTupDel,
prometheus.CounterValue,
@@ -300,10 +279,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
nTupHotUpdMetric := 0.0
if nTupHotUpd.Valid {
nTupHotUpdMetric = float64(nTupHotUpd.Int64)
}
nTupHotUpdMetric := int64CounterValue(nTupHotUpd, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesNTupHotUpd,
prometheus.CounterValue,
@@ -388,10 +364,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
vacuumCountMetric := 0.0
if vacuumCount.Valid {
vacuumCountMetric = float64(vacuumCount.Int64)
}
vacuumCountMetric := int64CounterValue(vacuumCount, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesVacuumCount,
prometheus.CounterValue,
@@ -399,10 +372,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
autovacuumCountMetric := 0.0
if autovacuumCount.Valid {
autovacuumCountMetric = float64(autovacuumCount.Int64)
}
autovacuumCountMetric := int64CounterValue(autovacuumCount, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesAutovacuumCount,
prometheus.CounterValue,
@@ -410,10 +380,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
analyzeCountMetric := 0.0
if analyzeCount.Valid {
analyzeCountMetric = float64(analyzeCount.Int64)
}
analyzeCountMetric := int64CounterValue(analyzeCount, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesAnalyzeCount,
prometheus.CounterValue,
@@ -421,10 +388,7 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
datnameLabel, schemanameLabel, relnameLabel,
)
autoanalyzeCountMetric := 0.0
if autoanalyzeCount.Valid {
autoanalyzeCountMetric = float64(autoanalyzeCount.Int64)
}
autoanalyzeCountMetric := int64CounterValue(autoanalyzeCount, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statUserTablesAutoanalyzeCount,
prometheus.CounterValue,

View File

@@ -204,14 +204,14 @@ func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *insta
ch <- prometheus.MustNewConstMetric(
statWalReceiverReceiveStartLsn,
prometheus.CounterValue,
float64(receiveStartLsn.Int64),
int64CounterValue(receiveStartLsn, instance.wrapLargeCounters),
labels...)
if hasFlushedLSN {
ch <- prometheus.MustNewConstMetric(
statWalReceiverFlushedLSN,
prometheus.CounterValue,
float64(flushedLsn.Int64),
int64CounterValue(flushedLsn, instance.wrapLargeCounters),
labels...)
}
@@ -242,7 +242,7 @@ func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *insta
ch <- prometheus.MustNewConstMetric(
statWalReceiverLatestEndLsn,
prometheus.CounterValue,
float64(latestEndLsn.Int64),
int64CounterValue(latestEndLsn, instance.wrapLargeCounters),
labels...)
ch <- prometheus.MustNewConstMetric(

View File

@@ -68,7 +68,7 @@ func (c *PGStatioUserIndexesCollector) Update(ctx context.Context, instance *ins
defer rows.Close()
for rows.Next() {
var schemaname, relname, indexrelname sql.NullString
var idxBlksRead, idxBlksHit sql.NullFloat64
var idxBlksRead, idxBlksHit sql.NullInt64
if err := rows.Scan(&schemaname, &relname, &indexrelname, &idxBlksRead, &idxBlksHit); err != nil {
return err
@@ -87,10 +87,7 @@ func (c *PGStatioUserIndexesCollector) Update(ctx context.Context, instance *ins
}
labels := []string{schemanameLabel, relnameLabel, indexrelnameLabel}
idxBlksReadMetric := 0.0
if idxBlksRead.Valid {
idxBlksReadMetric = idxBlksRead.Float64
}
idxBlksReadMetric := int64CounterValue(idxBlksRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserIndexesIdxBlksRead,
prometheus.CounterValue,
@@ -98,10 +95,7 @@ func (c *PGStatioUserIndexesCollector) Update(ctx context.Context, instance *ins
labels...,
)
idxBlksHitMetric := 0.0
if idxBlksHit.Valid {
idxBlksHitMetric = idxBlksHit.Float64
}
idxBlksHitMetric := int64CounterValue(idxBlksHit, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserIndexesIdxBlksHit,
prometheus.CounterValue,

View File

@@ -128,10 +128,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
relnameLabel = relname.String
}
heapBlksReadMetric := 0.0
if heapBlksRead.Valid {
heapBlksReadMetric = float64(heapBlksRead.Int64)
}
heapBlksReadMetric := int64CounterValue(heapBlksRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesHeapBlksRead,
prometheus.CounterValue,
@@ -139,10 +136,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
heapBlksHitMetric := 0.0
if heapBlksHit.Valid {
heapBlksHitMetric = float64(heapBlksHit.Int64)
}
heapBlksHitMetric := int64CounterValue(heapBlksHit, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesHeapBlksHit,
prometheus.CounterValue,
@@ -150,10 +144,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
idxBlksReadMetric := 0.0
if idxBlksRead.Valid {
idxBlksReadMetric = float64(idxBlksRead.Int64)
}
idxBlksReadMetric := int64CounterValue(idxBlksRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesIdxBlksRead,
prometheus.CounterValue,
@@ -161,10 +152,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
idxBlksHitMetric := 0.0
if idxBlksHit.Valid {
idxBlksHitMetric = float64(idxBlksHit.Int64)
}
idxBlksHitMetric := int64CounterValue(idxBlksHit, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesIdxBlksHit,
prometheus.CounterValue,
@@ -172,10 +160,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
toastBlksReadMetric := 0.0
if toastBlksRead.Valid {
toastBlksReadMetric = float64(toastBlksRead.Int64)
}
toastBlksReadMetric := int64CounterValue(toastBlksRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesToastBlksRead,
prometheus.CounterValue,
@@ -183,10 +168,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
toastBlksHitMetric := 0.0
if toastBlksHit.Valid {
toastBlksHitMetric = float64(toastBlksHit.Int64)
}
toastBlksHitMetric := int64CounterValue(toastBlksHit, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesToastBlksHit,
prometheus.CounterValue,
@@ -194,10 +176,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
tidxBlksReadMetric := 0.0
if tidxBlksRead.Valid {
tidxBlksReadMetric = float64(tidxBlksRead.Int64)
}
tidxBlksReadMetric := int64CounterValue(tidxBlksRead, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesTidxBlksRead,
prometheus.CounterValue,
@@ -205,10 +184,7 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
tidxBlksHitMetric := 0.0
if tidxBlksHit.Valid {
tidxBlksHitMetric = float64(tidxBlksHit.Int64)
}
tidxBlksHitMetric := int64CounterValue(tidxBlksHit, instance.wrapLargeCounters)
ch <- prometheus.MustNewConstMetric(
statioUserTablesTidxBlksHit,
prometheus.CounterValue,

View File

@@ -55,6 +55,7 @@ func NewRuntime(validatedConfig config.ValidatedConfig, logger *slog.Logger) (*R
WithCollectionTimeout(cfg.CollectionTimeout.String()),
WithCollectorStates(cfg.Collectors),
WithPGStatStatementsConfig(cfg.PGStatStatements),
WithWrapLargeCounters(cfg.WrapLargeCounters),
)
if err != nil {
runtime.Close()
@@ -93,5 +94,6 @@ func exporterOptions(cfg config.Config) []exporter.ExporterOpt {
exporter.ExcludeDatabases(cfg.ExcludeDatabases),
exporter.IncludeDatabases(strings.Join(cfg.IncludeDatabases, ",")),
exporter.WithMetricPrefix(cfg.MetricPrefix),
exporter.WrapLargeCounters(cfg.WrapLargeCounters),
}
}

View File

@@ -74,3 +74,23 @@ func TestNewRuntimeCollectorsWithDataSource(t *testing.T) {
t.Fatalf("len(Collectors()) = %d, want %d", got, want)
}
}
func TestNewRuntimePropagatesWrapLargeCounters(t *testing.T) {
cfg := config.NewConfigWithDefaults()
cfg.DataSourceNames = []string{"postgresql://localhost:5432/postgres?sslmode=disable"}
cfg.WrapLargeCounters = false
validated, err := cfg.Validate()
if err != nil {
t.Fatalf("Validate() error = %v", err)
}
runtime, err := NewRuntime(validated, promslog.NewNopLogger())
if err != nil {
t.Fatalf("NewRuntime() error = %v", err)
}
defer runtime.Close()
if runtime.postgresCollector.instance.wrapLargeCounters {
t.Fatal("postgres collector wrapLargeCounters = true, want false")
}
}

View File

@@ -70,6 +70,7 @@ type Config struct {
DataSourceNames []string
MetricPrefix string
CollectionTimeout time.Duration
WrapLargeCounters bool
DisableDefaultMetrics bool
AutoDiscoverDatabases bool
UserQueriesPath string
@@ -115,6 +116,7 @@ func NewConfigWithDefaults() Config {
return Config{
MetricPrefix: DefaultMetricPrefix,
CollectionTimeout: DefaultCollectionTimeout,
WrapLargeCounters: true,
Collectors: DefaultCollectorConfig(),
PGStatStatements: PGStatStatementsConfig{
IncludeQuery: DefaultPGStatStatementsIncludeQuery,

View File

@@ -30,6 +30,9 @@ func TestNewConfigWithDefaults(t *testing.T) {
if got, want := cfg.CollectionTimeout, DefaultCollectionTimeout; got != want {
t.Fatalf("CollectionTimeout = %v, want %v", got, want)
}
if !cfg.WrapLargeCounters {
t.Fatal("WrapLargeCounters = false, want true")
}
if cfg.DisableDefaultMetrics {
t.Fatal("DisableDefaultMetrics = true, want false")
}

View File

@@ -152,7 +152,11 @@ func queryNamespaceMapping(server *Server, namespace string, mapping MetricMapNa
labels...,
)
} else {
value, ok := dbToFloat64(columnData[idx], server.logger)
conversion := dbToFloat64
if server.wrapLargeCounters && metricMapping.vtype == prometheus.CounterValue {
conversion = dbToFloat64Counter
}
value, ok := conversion(columnData[idx], server.logger)
if !ok {
nonfatalErrors = append(nonfatalErrors, errors.New(fmt.Sprintln("Unexpected error parsing column: ", namespace, columnName, columnData[idx])))
continue

165
exporter/namespace_test.go Normal file
View File

@@ -0,0 +1,165 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package exporter
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/promslog"
)
func TestDBToFloat64Counter(t *testing.T) {
tests := []struct {
name string
input interface{}
want float64
}{
{
name: "largest exactly represented integer",
input: int64(1<<53) - 1,
want: float64(int64(1<<53) - 1),
},
{
name: "wrap boundary",
input: int64(1 << 53),
want: 0,
},
{
name: "above wrap boundary",
input: int64(1<<53) + 1,
want: 1,
},
{
name: "negative value remains unchanged",
input: int64(-1),
want: -1,
},
{
name: "non-integer conversion remains unchanged",
input: 42.5,
want: 42.5,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, ok := dbToFloat64Counter(test.input, promslog.NewNopLogger())
if !ok {
t.Fatal("conversion failed")
}
if got != test.want {
t.Errorf("counter value = %v, want %v", got, test.want)
}
})
}
}
func TestNewExporterWrapLargeCounters(t *testing.T) {
if exporter := NewExporter(nil, promslog.NewNopLogger()); !exporter.wrapLargeCounters {
t.Error("large counter wrapping should be enabled by default")
}
if exporter := NewExporter(nil, promslog.NewNopLogger(), WrapLargeCounters(false)); exporter.wrapLargeCounters {
t.Error("large counter wrapping should be disabled by option")
}
}
func TestQueryNamespaceMappingWrapsLargeCounters(t *testing.T) {
const largeInteger = int64(1<<53) + 1
tests := []struct {
name string
wrapLargeCounters bool
expectedCounterValue float64
}{
{
name: "enabled",
wrapLargeCounters: true,
expectedCounterValue: 1,
},
{
name: "disabled",
wrapLargeCounters: false,
expectedCounterValue: float64(largeInteger),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("creating mock database: %v", err)
}
t.Cleanup(func() {
_ = db.Close()
})
mock.ExpectQuery(`SELECT \* FROM test_namespace;`).WillReturnRows(
sqlmock.NewRows([]string{"counter", "gauge"}).AddRow(largeInteger, largeInteger),
)
server := &Server{
db: db,
logger: promslog.NewNopLogger(),
wrapLargeCounters: test.wrapLargeCounters,
}
mapping := MetricMapNamespace{
columnMappings: map[string]MetricMap{
"counter": {
vtype: prometheus.CounterValue,
desc: prometheus.NewDesc("test_counter", "Test counter.", nil, nil),
},
"gauge": {
vtype: prometheus.GaugeValue,
desc: prometheus.NewDesc("test_gauge", "Test gauge.", nil, nil),
},
},
}
metrics, nonfatalErrors, err := queryNamespaceMapping(server, "test_namespace", mapping)
if err != nil {
t.Fatalf("querying namespace: %v", err)
}
if len(nonfatalErrors) != 0 {
t.Fatalf("unexpected nonfatal errors: %v", nonfatalErrors)
}
if len(metrics) != 2 {
t.Fatalf("got %d metrics, want 2", len(metrics))
}
counter := &dto.Metric{}
if err := metrics[0].Write(counter); err != nil {
t.Fatalf("writing counter metric: %v", err)
}
if got := counter.GetCounter().GetValue(); got != test.expectedCounterValue {
t.Errorf("counter value = %v, want %v", got, test.expectedCounterValue)
}
gauge := &dto.Metric{}
if err := metrics[1].Write(gauge); err != nil {
t.Fatalf("writing gauge metric: %v", err)
}
if got, want := gauge.GetGauge().GetValue(), float64(largeInteger); got != want {
t.Errorf("gauge value = %v, want %v", got, want)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet database expectations: %v", err)
}
})
}
}

View File

@@ -331,6 +331,7 @@ type Exporter struct {
builtinMetricMaps map[string]intermediateMetricMap
disableDefaultMetrics, autoDiscoverDatabases bool
wrapLargeCounters bool
excludeDatabases []string
includeDatabases []string
@@ -405,6 +406,13 @@ func WithMetricPrefix(prefix string) ExporterOpt {
}
}
// WrapLargeCounters configures wrapping counters at 2^53.
func WrapLargeCounters(wrap bool) ExporterOpt {
return func(e *Exporter) {
e.wrapLargeCounters = wrap
}
}
func parseConstLabels(s string, logger *slog.Logger) prometheus.Labels {
labels := make(prometheus.Labels)
@@ -437,6 +445,7 @@ func NewExporter(dsn []string, logger *slog.Logger, opts ...ExporterOpt) *Export
dsn: dsn,
builtinMetricMaps: builtinMetricMaps,
logger: logger,
wrapLargeCounters: true,
}
for _, opt := range opts {
@@ -444,7 +453,11 @@ func NewExporter(dsn []string, logger *slog.Logger, opts ...ExporterOpt) *Export
}
e.setupInternalMetrics()
e.servers = NewServers(ServerWithLabels(e.constantLabels), ServerWithLogger(e.logger))
e.servers = NewServers(
ServerWithLabels(e.constantLabels),
ServerWithLogger(e.logger),
ServerWithWrapLargeCounters(e.wrapLargeCounters),
)
return e
}

View File

@@ -28,10 +28,11 @@ import (
// Server describes a connection to Postgres.
// Also it contains metrics map and query overrides.
type Server struct {
db *sql.DB
labels prometheus.Labels
master bool
runonserver string
db *sql.DB
labels prometheus.Labels
master bool
runonserver string
wrapLargeCounters bool
// Last version used to calculate metric map. If mismatch on scrape,
// then maps are recalculated.
@@ -65,6 +66,13 @@ func ServerWithLogger(logger *slog.Logger) ServerOpt {
}
}
// ServerWithWrapLargeCounters configures wrapping counters at 2^53.
func ServerWithWrapLargeCounters(wrap bool) ServerOpt {
return func(s *Server) {
s.wrapLargeCounters = wrap
}
}
// NewServer establishes a new connection using DSN.
func NewServer(dsn string, opts ...ServerOpt) (*Server, error) {
fingerprint, err := parseFingerprint(dsn)
@@ -80,8 +88,9 @@ func NewServer(dsn string, opts ...ServerOpt) (*Server, error) {
db.SetMaxIdleConns(1)
s := &Server{
db: db,
master: false,
db: db,
master: false,
wrapLargeCounters: true,
labels: prometheus.Labels{
serverLabelName: fingerprint,
},

View File

@@ -21,6 +21,8 @@ import (
"strconv"
"strings"
"time"
"github.com/prometheus-community/postgres_exporter/internal/metricutil"
)
// convert a string to the corresponding ColumnUsage
@@ -94,6 +96,17 @@ func dbToFloat64(t interface{}, logger *slog.Logger) (float64, bool) {
}
}
// dbToFloat64Counter converts PostgreSQL counter values while preserving
// single-unit precision for positive int64 values above float64's exact
// integer range. Prometheus handles the modulo boundary as a counter reset.
func dbToFloat64Counter(t interface{}, logger *slog.Logger) (float64, bool) {
if v, ok := t.(int64); ok {
return metricutil.Int64CounterValue(v, true), true
}
return dbToFloat64(t, logger)
}
// Convert database.sql types to uint64 for Prometheus consumption. Null types are mapped to 0. string and []byte
// types are mapped as 0 and !ok
func dbToUint64(t interface{}, logger *slog.Logger) (uint64, bool) {

View File

@@ -0,0 +1,26 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metricutil
const float64ExactIntegerLimit uint64 = 1 << 53
// Int64CounterValue converts a PostgreSQL counter while preserving single-unit
// precision above float64's exact integer range when wrapping is enabled.
func Int64CounterValue(value int64, wrap bool) float64 {
if wrap && value >= 0 {
return float64(uint64(value) % float64ExactIntegerLimit)
}
return float64(value)
}

View File

@@ -0,0 +1,70 @@
// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package metricutil
import "testing"
func TestInt64CounterValue(t *testing.T) {
tests := []struct {
name string
value int64
wrap bool
want float64
}{
{
name: "largest exactly represented integer",
value: int64(1<<53) - 1,
wrap: true,
want: float64(int64(1<<53) - 1),
},
{
name: "wrap boundary",
value: int64(1 << 53),
wrap: true,
want: 0,
},
{
name: "above wrap boundary",
value: int64(1<<53) + 1,
wrap: true,
want: 1,
},
{
name: "upgrade warning example",
value: int64(1<<53) + 1_234_567,
wrap: true,
want: 1_234_567,
},
{
name: "wrapping disabled",
value: int64(1<<53) + 1,
wrap: false,
want: float64(int64(1<<53) + 1),
},
{
name: "negative value",
value: -1,
wrap: true,
want: -1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := Int64CounterValue(test.value, test.wrap); got != test.want {
t.Fatalf("Int64CounterValue(%d, %t) = %v, want %v", test.value, test.wrap, got, test.want)
}
})
}
}