Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collect statistics from pg_stat_slru (PostgreSQL 13+) #99

Merged
merged 5 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deploy/pgscv.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ services:
# - postgres/settings
# - postgres/storage
# - postgres/stat_io
# - postgres/stat_slru
# - postgres/tables
# - postgres/wal
# - postgres/custom
Expand Down
1 change: 1 addition & 0 deletions internal/collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (f Factories) RegisterPostgresCollectors(disabled []string) {
"postgres/settings": NewPostgresSettingsCollector,
"postgres/storage": NewPostgresStorageCollector,
"postgres/stat_io": NewPostgresStatIOCollector,
"postgres/stat_slru": NewPostgresStatSlruCollector,
"postgres/tables": NewPostgresTablesCollector,
"postgres/wal": NewPostgresWalCollector,
"postgres/custom": NewPostgresCustomCollector,
Expand Down
197 changes: 197 additions & 0 deletions internal/collector/postgres_stat_slru.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package collector

import (
"strconv"
"strings"

"github.com/cherts/pgscv/internal/log"
"github.com/cherts/pgscv/internal/model"
"github.com/cherts/pgscv/internal/store"
"github.com/prometheus/client_golang/prometheus"
)

const (
postgresStatSlruQuery = "SELECT name, coalesce(blks_zeroed, 0) AS blks_zeroed, coalesce(blks_hit, 0) AS blks_hit, " +
"coalesce(blks_read, 0) AS blks_read, coalesce(blks_written, 0) AS blks_written, coalesce(blks_exists, 0) AS blks_exists, " +
"coalesce(flushes, 0) AS flushes, coalesce(truncates, 0) AS truncates FROM pg_stat_slru"
)

// postgresStatSlruCollector defines metric descriptors and stats store.
type postgresStatSlruCollector struct {
blksZeroed typedDesc
blksHit typedDesc
blksRead typedDesc
blksWritten typedDesc
blksExists typedDesc
flushes typedDesc
truncates typedDesc
labelNames []string
}

// NewPostgresStatSlruCollector returns a new Collector exposing postgres pg_stat_slru stats.
func NewPostgresStatSlruCollector(constLabels labels, settings model.CollectorSettings) (Collector, error) {
var labels = []string{"name"}

return &postgresStatSlruCollector{
blksZeroed: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "blks_zeroed", "Number of blocks zeroed during initializations.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
blksHit: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "blks_hit", "Number of times disk blocks were found already in the SLRU, so that a read was not necessary (this only includes hits in the SLRU, not the operating system's file system cache).", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
blksRead: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "blks_read", "Number of disk blocks read for this SLRU.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
blksWritten: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "blks_written", "Number of disk blocks written for this SLRU.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
blksExists: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "blks_exists", "Number of blocks checked for existence for this SLRU.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
flushes: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "flushes", "Number of flushes of dirty data for this SLRU.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
truncates: newBuiltinTypedDesc(
descOpts{"postgres", "stat_slru", "truncates", "Number of truncates for this SLRU.", 0},
prometheus.GaugeValue,
labels, constLabels,
settings.Filters,
),
}, nil
}

// Update method collects statistics, parse it and produces metrics that are sent to Prometheus.
func (c *postgresStatSlruCollector) Update(config Config, ch chan<- prometheus.Metric) error {
if config.serverVersionNum < PostgresV13 {
log.Debugln("[postgres stat_slru collector]: pg_stat_slru view are not available, required Postgres 13 or newer")
return nil
}

conn, err := store.New(config.ConnString, config.ConnTimeout)
if err != nil {
return err
}
defer conn.Close()

// Collecting pg_stat_slru since Postgres 13.
if config.serverVersionNum >= PostgresV13 {
res, err := conn.Query(postgresStatSlruQuery)
if err != nil {
log.Warnf("get pg_stat_slru failed: %s; skip", err)
} else {
stats := parsePostgresStatSlru(res, []string{"name"})

for _, stat := range stats {
ch <- c.blksZeroed.newConstMetric(stat.BlksZeroed, stat.SlruName)
ch <- c.blksHit.newConstMetric(stat.BlksHit, stat.SlruName)
ch <- c.blksRead.newConstMetric(stat.BlksRead, stat.SlruName)
ch <- c.blksWritten.newConstMetric(stat.BlksWritten, stat.SlruName)
ch <- c.blksExists.newConstMetric(stat.BlksExists, stat.SlruName)
ch <- c.flushes.newConstMetric(stat.Flushes, stat.SlruName)
ch <- c.truncates.newConstMetric(stat.Truncates, stat.SlruName)
}
}
}

return nil
}

// postgresStatSlru
type postgresStatSlru struct {
SlruName string // a name of SLRU-cache
BlksZeroed float64
BlksHit float64
BlksRead float64
BlksWritten float64
BlksExists float64
Flushes float64
Truncates float64
}

// parsePostgresStatSlru parses PGResult and returns structs with stats values.
func parsePostgresStatSlru(r *model.PGResult, labelNames []string) map[string]postgresStatSlru {
log.Debug("parse postgres stat_slru stats")

var stats = make(map[string]postgresStatSlru)

for _, row := range r.Rows {
var SlruName string

for i, colname := range r.Colnames {
switch string(colname.Name) {
case "name":
SlruName = row[i].String
}
}

// create a stat_slru name consisting of trio SlruName
statSlru := strings.Join([]string{SlruName}, "")

// Put stats with labels (but with no data values yet) into stats store.
if _, ok := stats[statSlru]; !ok {
stats[statSlru] = postgresStatSlru{SlruName: SlruName}
}

for i, colname := range r.Colnames {
// skip columns if its value used as a label
if stringsContains(labelNames, string(colname.Name)) {
continue
}

// Skip empty (NULL) values.
if !row[i].Valid {
continue
}

// Get data value and convert it to float64 used by Prometheus.
v, err := strconv.ParseFloat(row[i].String, 64)
if err != nil {
log.Errorf("invalid input, parse '%s' failed: %s; skip", row[i].String, err)
continue
}

s := stats[statSlru]

switch string(colname.Name) {
case "blks_zeroed":
s.BlksZeroed = v
case "blks_hit":
s.BlksHit = v
case "blks_read":
s.BlksRead = v
case "blks_written":
s.BlksWritten = v
case "blks_exists":
s.BlksExists = v
case "flushes":
s.Flushes = v
case "truncates":
s.Truncates = v
default:
continue
}

stats[statSlru] = s
}
}

return stats
}
67 changes: 67 additions & 0 deletions internal/collector/postgres_stat_slru_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package collector

import (
"database/sql"
"testing"

"github.com/cherts/pgscv/internal/model"
"github.com/jackc/pgproto3/v2"
"github.com/stretchr/testify/assert"
)

func TestPostgresStatSlruCollector_Update(t *testing.T) {
var input = pipelineInput{
required: []string{
"postgres_stat_slru_blks_zeroed",
"postgres_stat_slru_blks_hit",
"postgres_stat_slru_blks_read",
"postgres_stat_slru_blks_written",
"postgres_stat_slru_blks_exists",
"postgres_stat_slru_flushes",
"postgres_stat_slru_truncates",
},
collector: NewPostgresStatSlruCollector,
service: model.ServiceTypePostgresql,
}

pipeline(t, input)
}

func Test_parsePostgresStatSlru(t *testing.T) {
var testCases = []struct {
name string
res *model.PGResult
want map[string]postgresStatSlru
}{
{
name: "normal output, Postgres 13",
res: &model.PGResult{
Nrows: 1,
Ncols: 8,
Colnames: []pgproto3.FieldDescription{
{Name: []byte("name")}, {Name: []byte("blks_zeroed")}, {Name: []byte("blks_hit")}, {Name: []byte("blks_read")},
{Name: []byte("blks_written")}, {Name: []byte("blks_exists")}, {Name: []byte("flushes")}, {Name: []byte("truncates")},
},
Rows: [][]sql.NullString{
{
{String: "subtransaction", Valid: true}, {String: "2972", Valid: true}, {String: "0", Valid: true}, {String: "0", Valid: true},
{String: "2867", Valid: true}, {String: "0", Valid: true}, {String: "527", Valid: true}, {String: "527", Valid: true},
},
},
},
want: map[string]postgresStatSlru{
"subtransaction": {
SlruName: "subtransaction", BlksZeroed: 2972, BlksHit: 0, BlksRead: 0,
BlksWritten: 2867, BlksExists: 0, Flushes: 527, Truncates: 527,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got := parsePostgresStatSlru(tc.res, []string{"name"})
assert.EqualValues(t, tc.want, got)
})
}
}