Skip to content

Commit d8ba628

Browse files
Add a collector for pg_buffercache_summary. (#1165)
* Add a collector for pg_buffercache_summary() * Requires PostgreSQL >= 16. --------- Signed-off-by: Peter Nuttall <[email protected]> Co-authored-by: Ben Kochie <[email protected]>
1 parent 94e8399 commit d8ba628

File tree

3 files changed

+217
-0
lines changed

3 files changed

+217
-0
lines changed

collector/collector.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package collector
1515

1616
import (
1717
"context"
18+
"database/sql"
1819
"errors"
1920
"fmt"
2021
"log/slog"
@@ -228,3 +229,11 @@ var ErrNoData = errors.New("collector returned no data")
228229
func IsNoDataError(err error) bool {
229230
return err == ErrNoData
230231
}
232+
233+
func Int32(m sql.NullInt32) float64 {
234+
mM := 0.0
235+
if m.Valid {
236+
mM = float64(m.Int32)
237+
}
238+
return mM
239+
}

collector/pg_buffercache_summary.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package collector
15+
16+
import (
17+
"context"
18+
"database/sql"
19+
"log/slog"
20+
21+
"github.com/blang/semver/v4"
22+
"github.com/prometheus/client_golang/prometheus"
23+
)
24+
25+
const buffercacheSummarySubsystem = "buffercache_summary"
26+
27+
func init() {
28+
registerCollector(buffercacheSummarySubsystem, defaultDisabled, NewBuffercacheSummaryCollector)
29+
}
30+
31+
// BuffercacheSummaryCollector collects stats from pg_buffercache: https://www.postgresql.org/docs/current/pgbuffercache.html.
32+
//
33+
// It depends on the extension being loaded with
34+
//
35+
// create extension pg_buffercache;
36+
//
37+
// It does not take locks, see the PG docs above.
38+
type BuffercacheSummaryCollector struct {
39+
log *slog.Logger
40+
}
41+
42+
func NewBuffercacheSummaryCollector(config collectorConfig) (Collector, error) {
43+
return &BuffercacheSummaryCollector{
44+
log: config.logger,
45+
}, nil
46+
}
47+
48+
var (
49+
buffersUsedDesc = prometheus.NewDesc(
50+
prometheus.BuildFQName(namespace, buffercacheSummarySubsystem, "buffers_used"),
51+
"Number of used shared buffers",
52+
[]string{},
53+
prometheus.Labels{},
54+
)
55+
buffersUnusedDesc = prometheus.NewDesc(
56+
prometheus.BuildFQName(namespace, buffercacheSummarySubsystem, "buffers_unused"),
57+
"Number of unused shared buffers",
58+
[]string{},
59+
prometheus.Labels{},
60+
)
61+
buffersDirtyDesc = prometheus.NewDesc(
62+
prometheus.BuildFQName(namespace, buffercacheSummarySubsystem, "buffers_dirty"),
63+
"Number of dirty shared buffers",
64+
[]string{},
65+
prometheus.Labels{},
66+
)
67+
buffersPinnedDesc = prometheus.NewDesc(
68+
prometheus.BuildFQName(namespace, buffercacheSummarySubsystem, "buffers_pinned"),
69+
"Number of pinned shared buffers",
70+
[]string{},
71+
prometheus.Labels{},
72+
)
73+
usageCountAvgDesc = prometheus.NewDesc(
74+
prometheus.BuildFQName(namespace, buffercacheSummarySubsystem, "usagecount_avg"),
75+
"Average usage count of used shared buffers",
76+
[]string{},
77+
prometheus.Labels{},
78+
)
79+
80+
buffercacheQuery = `
81+
SELECT
82+
buffers_used,
83+
buffers_unused,
84+
buffers_dirty,
85+
buffers_pinned,
86+
usagecount_avg
87+
FROM
88+
pg_buffercache_summary()
89+
`
90+
)
91+
92+
// Update implements Collector
93+
// It is called by the Prometheus registry when collecting metrics.
94+
func (c BuffercacheSummaryCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
95+
// pg_buffercache_summary is only in v16, and we don't need support for earlier currently.
96+
if !instance.version.GE(semver.MustParse("16.0.0")) {
97+
return nil
98+
}
99+
db := instance.getDB()
100+
rows, err := db.QueryContext(ctx, buffercacheQuery)
101+
if err != nil {
102+
return err
103+
}
104+
defer rows.Close()
105+
106+
var used, unused, dirty, pinned sql.NullInt32
107+
var usagecountAvg sql.NullFloat64
108+
109+
for rows.Next() {
110+
if err := rows.Scan(
111+
&used,
112+
&unused,
113+
&dirty,
114+
&pinned,
115+
&usagecountAvg,
116+
); err != nil {
117+
return err
118+
}
119+
120+
usagecountAvgMetric := 0.0
121+
if usagecountAvg.Valid {
122+
usagecountAvgMetric = usagecountAvg.Float64
123+
}
124+
ch <- prometheus.MustNewConstMetric(
125+
usageCountAvgDesc,
126+
prometheus.GaugeValue,
127+
usagecountAvgMetric)
128+
ch <- prometheus.MustNewConstMetric(buffersUsedDesc, prometheus.GaugeValue, Int32(used))
129+
ch <- prometheus.MustNewConstMetric(buffersUnusedDesc, prometheus.GaugeValue, Int32(unused))
130+
ch <- prometheus.MustNewConstMetric(buffersDirtyDesc, prometheus.GaugeValue, Int32(dirty))
131+
ch <- prometheus.MustNewConstMetric(buffersPinnedDesc, prometheus.GaugeValue, Int32(pinned))
132+
}
133+
134+
return rows.Err()
135+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package collector
14+
15+
import (
16+
"context"
17+
"testing"
18+
19+
"github.com/DATA-DOG/go-sqlmock"
20+
"github.com/blang/semver/v4"
21+
"github.com/prometheus/client_golang/prometheus"
22+
dto "github.com/prometheus/client_model/go"
23+
"github.com/smartystreets/goconvey/convey"
24+
)
25+
26+
func TestBuffercacheSummaryCollector(t *testing.T) {
27+
db, mock, err := sqlmock.New()
28+
if err != nil {
29+
t.Fatalf("Error opening a stub db connection: %s", err)
30+
}
31+
defer db.Close()
32+
33+
inst := &instance{db: db, version: semver.MustParse("16.0.0")}
34+
35+
columns := []string{
36+
"buffers_used",
37+
"buffers_unused",
38+
"buffers_dirty",
39+
"buffers_pinned",
40+
"usagecount_avg"}
41+
42+
rows := sqlmock.NewRows(columns).AddRow(123, 456, 789, 234, 56.6778)
43+
44+
mock.ExpectQuery(sanitizeQuery(buffercacheQuery)).WillReturnRows(rows)
45+
46+
ch := make(chan prometheus.Metric)
47+
go func() {
48+
defer close(ch)
49+
c := BuffercacheSummaryCollector{}
50+
51+
if err := c.Update(context.Background(), inst, ch); err != nil {
52+
t.Errorf("Error calling PGStatStatementsCollector.Update: %s", err)
53+
}
54+
}()
55+
56+
expected := []MetricResult{
57+
{labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 56.6778},
58+
{labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 123},
59+
{labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 456},
60+
{labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 789},
61+
{labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 234},
62+
}
63+
64+
convey.Convey("Metrics comparison", t, func() {
65+
for _, expect := range expected {
66+
m := readMetric(<-ch)
67+
convey.So(expect, convey.ShouldResemble, m)
68+
}
69+
})
70+
if err := mock.ExpectationsWereMet(); err != nil {
71+
t.Errorf("there were unfulfilled exceptions: %s", err)
72+
}
73+
}

0 commit comments

Comments
 (0)