Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func PoolLabel ¶
PoolLabel derives the "pool" label value from a PostgreSQL data source: the host, port and database name, with no credential of any kind - unlike a redacted DSN, which would still carry a password if the connection string put it in a field redactDataSource (postgres/notifier.go) does not expect. The identity that matters for the pool label is "which server and database", not "how was it addressed", so the normalized "host:port/db" form is used for both of PostgreSQL's two accepted DSN syntaxes, and a connection string that parses as neither falls back to a short, stable hash so a label is never raw and unredacted even in that path.
Types ¶
type Metrics ¶
type Metrics struct {
// InUse is the number of connections currently checked out of the pool,
// sampled instantaneously (not a max-since-last-sample - see Sampler).
// Compare against MaxOpen for saturation; a value pinned at MaxOpen
// together with a rising WaitCount is the pool-exhaustion signature that
// made #2474 need a goroutine dump to diagnose.
InUse metrics.Gauge
// MaxOpen is the pool's configured connection limit (sql.DBStats.MaxOpenConnections),
// exported so dashboards can compute saturation (InUse/MaxOpen) rather than
// an absolute connection count that means nothing without cross-referencing
// the node's core.yaml.
MaxOpen metrics.Gauge
// WaitCountTotal is the cumulative number of connection checkouts that had
// to wait because the pool was at its limit. Unlike InUse this is
// loss-free between samples: database/sql increments it once per blocked
// checkout regardless of how often it is scraped, so a burst too short
// for the gauge to catch still shows up here.
WaitCountTotal metrics.Counter
// WaitSecondsTotal is the cumulative time callers spent waiting for a
// connection. Divided by WaitCountTotal (as a rate ratio, not a raw
// division, since both are counters) it gives the mean wait per checkout:
// rate(wait_seconds_total[$__rate_interval]) / rate(wait_count_total[$__rate_interval]).
WaitSecondsTotal metrics.Counter
}
Metrics holds the instrumentation of a Sampler. Unlike most Panurus metrics families, these are exported under an explicitly set Namespace/Subsystem rather than letting the calling package derive the name - see "How a metric name is built" and "Metric hygiene" in docs/development/metrics.md. This is the first Panurus metric family to do so, a deliberate exception rather than an oversight: it is the fix the Metric hygiene section recommends for new metrics, and there is no existing dashboard or alert to break.
func NewMetrics ¶
NewMetrics creates the connection-pool instrumentation. A nil provider discards everything, so a Sampler built with no metrics configured needs no special case.
type Sampler ¶
type Sampler struct {
// contains filtered or unexported fields
}
Sampler periodically reads (*sql.DB).Stats() for every registered pool and reports it through Metrics.
It exists because Panurus never checks out a connection itself - the pool lives inside fabric-smart-client's postgres.Open and reaches Panurus only as a *sql.DB - so (*sql.DB).Stats() is the only available signal, and metrics.Provider has no way to register a custom collector that would be evaluated at scrape time. Something has to poll.
The InUse and MaxOpen gauges are deliberately instantaneous, not a max-since-last-sample: sampling at DefaultPoolMetricsInterval (1s) makes a sustained exhaustion episode - the kind that actually hangs Select calls, as in #2474 - visible on most samples regardless, and a transient burst too short to land on a sample still shows up in WaitCountTotal, which database/sql increments once per blocked checkout independent of how often anything scrapes it. Peak occupancy is a PromQL concern (max_over_time(db_pool_in_use[...])), not something the sampler itself needs to track. Do not raise the interval back toward something like 10s without re-reading this: at a slower poll the gauge can sit well below MaxOpen throughout an episode that was in fact permanently exhausted between samples.
func NewSampler ¶
NewSampler builds a Sampler from cfg's token.storage.db.poolMetricsInterval (DefaultPoolMetricsInterval when unset) and p. A nil p (or a cfg that fails to parse) does not prevent construction - see Start, which no-ops when the interval is 0, and NewMetrics's disabled-provider fallback.
func (*Sampler) Register ¶
Register adds db to the set of pools this sampler reports on, keyed by key (the pool's identity - callers pass the raw data source, since every store sharing a data source shares one *sql.DB and must be registered under one key) with label as the exported "pool" label value. Registering the same key again is a no-op: N stores on one data source must produce one series, not N.
func (*Sampler) Sample ¶
func (s *Sampler) Sample()
Sample reads Stats() for every registered pool once and updates Metrics. Exported so tests can drive a deterministic pass without waiting on the ticker; Start calls it on the same interval it reports.
func (*Sampler) Start ¶
Start begins the sampling loop. A zero interval disables the sampler: Start returns nil without starting a goroutine, so an operator who does not scrape Prometheus pays nothing for it. Calling Start twice is a no-op.