You've already forked postgres_exporter
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:
committed by
GitHub
parent
90a293ce96
commit
31385354ac
11
CHANGELOG.md
11
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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...,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
165
exporter/namespace_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
26
internal/metricutil/counter.go
Normal file
26
internal/metricutil/counter.go
Normal 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)
|
||||
}
|
||||
70
internal/metricutil/counter_test.go
Normal file
70
internal/metricutil/counter_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user