Skip to content

Commit 17619ff

Browse files
committed
Add support for pg_stat_checkpointer
See: https://www.dbi-services.com/blog/postgresql-17-new-catalog-view-pg_stat_checkpointer/ Signed-off-by: Nicolas Rodriguez <[email protected]>
1 parent 9eb656e commit 17619ff

File tree

2 files changed

+364
-0
lines changed

2 files changed

+364
-0
lines changed

Diff for: collector/pg_stat_checkpointer.go

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2021 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+
20+
"github.com/prometheus/client_golang/prometheus"
21+
)
22+
23+
const statCheckpointerSubsystem = "stat_checkpointer"
24+
25+
func init() {
26+
// WARNING:
27+
// Disabled by default because this set of metrics is only available from Postgres 17
28+
registerCollector(statCheckpointerSubsystem, defaultDisabled, NewPGStatCheckpointerCollector)
29+
}
30+
31+
type PGStatCheckpointerCollector struct {
32+
}
33+
34+
func NewPGStatCheckpointerCollector(collectorConfig) (Collector, error) {
35+
return &PGStatCheckpointerCollector{}, nil
36+
}
37+
38+
var (
39+
statCheckpointerNumTimedDesc = prometheus.NewDesc(
40+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_timed_total"),
41+
"Number of scheduled checkpoints due to timeout",
42+
[]string{},
43+
prometheus.Labels{},
44+
)
45+
statCheckpointerNumRequestedDesc = prometheus.NewDesc(
46+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_requested_total"),
47+
"Number of requested checkpoints that have been performed",
48+
[]string{},
49+
prometheus.Labels{},
50+
)
51+
statCheckpointerRestartpointsTimedDesc = prometheus.NewDesc(
52+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_timed_total"),
53+
"Number of scheduled restartpoints due to timeout or after a failed attempt to perform it",
54+
[]string{},
55+
prometheus.Labels{},
56+
)
57+
statCheckpointerRestartpointsReqDesc = prometheus.NewDesc(
58+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_req_total"),
59+
"Number of requested restartpoints",
60+
[]string{},
61+
prometheus.Labels{},
62+
)
63+
statCheckpointerRestartpointsDoneDesc = prometheus.NewDesc(
64+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_done_total"),
65+
"Number of restartpoints that have been performed",
66+
[]string{},
67+
prometheus.Labels{},
68+
)
69+
statCheckpointerWriteTimeDesc = prometheus.NewDesc(
70+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "write_time_total"),
71+
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are written to disk, in milliseconds",
72+
[]string{},
73+
prometheus.Labels{},
74+
)
75+
statCheckpointerSyncTimeDesc = prometheus.NewDesc(
76+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "sync_time_total"),
77+
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are synchronized to disk, in milliseconds",
78+
[]string{},
79+
prometheus.Labels{},
80+
)
81+
statCheckpointerBuffersWrittenDesc = prometheus.NewDesc(
82+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "buffers_written_total"),
83+
"Number of buffers written during checkpoints and restartpoints",
84+
[]string{},
85+
prometheus.Labels{},
86+
)
87+
statCheckpointerStatsResetDesc = prometheus.NewDesc(
88+
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "stats_reset_total"),
89+
"Time at which these statistics were last reset",
90+
[]string{},
91+
prometheus.Labels{},
92+
)
93+
94+
statCheckpointerQuery = `SELECT
95+
num_timed
96+
,num_requested
97+
,restartpoints_timed
98+
,restartpoints_req
99+
,restartpoints_done
100+
,write_time
101+
,sync_time
102+
,buffers_written
103+
,stats_reset
104+
FROM pg_stat_checkpointer;`
105+
)
106+
107+
func (PGStatCheckpointerCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
108+
db := instance.getDB()
109+
row := db.QueryRowContext(ctx, statCheckpointerQuery)
110+
111+
// num_timed = nt = bigint
112+
// num_requested = nr = bigint
113+
// restartpoints_timed = rpt = bigint
114+
// restartpoints_req = rpr = bigint
115+
// restartpoints_done = rpd = bigint
116+
// write_time = wt = double precision
117+
// sync_time = st = double precision
118+
// buffers_written = bw = bigint
119+
// stats_reset = sr = timestamp
120+
121+
var nt, nr, rpt, rpr, rpd, bw sql.NullInt64
122+
var wt, st sql.NullFloat64
123+
var sr sql.NullTime
124+
125+
err := row.Scan(&nt, &nr, &rpt, &rpr, &rpd, &wt, &st, &bw, &sr)
126+
if err != nil {
127+
return err
128+
}
129+
130+
ntMetric := 0.0
131+
if nt.Valid {
132+
ntMetric = float64(nt.Int64)
133+
}
134+
ch <- prometheus.MustNewConstMetric(
135+
statCheckpointerNumTimedDesc,
136+
prometheus.CounterValue,
137+
ntMetric,
138+
)
139+
140+
nrMetric := 0.0
141+
if nr.Valid {
142+
nrMetric = float64(nr.Int64)
143+
}
144+
ch <- prometheus.MustNewConstMetric(
145+
statCheckpointerNumRequestedDesc,
146+
prometheus.CounterValue,
147+
nrMetric,
148+
)
149+
150+
rptMetric := 0.0
151+
if rpt.Valid {
152+
rptMetric = float64(rpt.Int64)
153+
}
154+
ch <- prometheus.MustNewConstMetric(
155+
statCheckpointerRestartpointsTimedDesc,
156+
prometheus.CounterValue,
157+
rptMetric,
158+
)
159+
160+
rprMetric := 0.0
161+
if rpr.Valid {
162+
rprMetric = float64(rpr.Int64)
163+
}
164+
ch <- prometheus.MustNewConstMetric(
165+
statCheckpointerRestartpointsReqDesc,
166+
prometheus.CounterValue,
167+
rprMetric,
168+
)
169+
170+
rpdMetric := 0.0
171+
if rpd.Valid {
172+
rpdMetric = float64(rpd.Int64)
173+
}
174+
ch <- prometheus.MustNewConstMetric(
175+
statCheckpointerRestartpointsDoneDesc,
176+
prometheus.CounterValue,
177+
rpdMetric,
178+
)
179+
180+
wtMetric := 0.0
181+
if wt.Valid {
182+
wtMetric = float64(wt.Float64)
183+
}
184+
ch <- prometheus.MustNewConstMetric(
185+
statCheckpointerWriteTimeDesc,
186+
prometheus.CounterValue,
187+
wtMetric,
188+
)
189+
190+
stMetric := 0.0
191+
if st.Valid {
192+
stMetric = float64(st.Float64)
193+
}
194+
ch <- prometheus.MustNewConstMetric(
195+
statCheckpointerSyncTimeDesc,
196+
prometheus.CounterValue,
197+
stMetric,
198+
)
199+
200+
bwMetric := 0.0
201+
if bw.Valid {
202+
bwMetric = float64(bw.Int64)
203+
}
204+
ch <- prometheus.MustNewConstMetric(
205+
statCheckpointerBuffersWrittenDesc,
206+
prometheus.CounterValue,
207+
bwMetric,
208+
)
209+
210+
srMetric := 0.0
211+
if sr.Valid {
212+
srMetric = float64(sr.Time.Unix())
213+
}
214+
ch <- prometheus.MustNewConstMetric(
215+
statCheckpointerStatsResetDesc,
216+
prometheus.CounterValue,
217+
srMetric,
218+
)
219+
220+
return nil
221+
}

Diff for: collector/pg_stat_checkpointer_test.go

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2023 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+
"time"
19+
20+
"github.com/DATA-DOG/go-sqlmock"
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 TestPGStatCheckpointerCollector(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}
34+
35+
columns := []string{
36+
"num_timed",
37+
"num_requested",
38+
"restartpoints_timed",
39+
"restartpoints_req",
40+
"restartpoints_done",
41+
"write_time",
42+
"sync_time",
43+
"buffers_written",
44+
"stats_reset"}
45+
46+
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
47+
if err != nil {
48+
t.Fatalf("Error parsing time: %s", err)
49+
}
50+
51+
rows := sqlmock.NewRows(columns).
52+
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, srT)
53+
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
54+
55+
ch := make(chan prometheus.Metric)
56+
go func() {
57+
defer close(ch)
58+
c := PGStatCheckpointerCollector{}
59+
60+
if err := c.Update(context.Background(), inst, ch); err != nil {
61+
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
62+
}
63+
}()
64+
65+
expected := []MetricResult{
66+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 354},
67+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 4945},
68+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 289097744},
69+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1242257},
70+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 3275602074},
71+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 89320867},
72+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 450139},
73+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 2034563757},
74+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1685059842},
75+
}
76+
77+
convey.Convey("Metrics comparison", t, func() {
78+
for _, expect := range expected {
79+
m := readMetric(<-ch)
80+
convey.So(expect, convey.ShouldResemble, m)
81+
}
82+
})
83+
if err := mock.ExpectationsWereMet(); err != nil {
84+
t.Errorf("there were unfulfilled exceptions: %s", err)
85+
}
86+
}
87+
88+
func TestPGStatCheckpointerCollectorNullValues(t *testing.T) {
89+
db, mock, err := sqlmock.New()
90+
if err != nil {
91+
t.Fatalf("Error opening a stub db connection: %s", err)
92+
}
93+
defer db.Close()
94+
95+
inst := &instance{db: db}
96+
97+
columns := []string{
98+
"num_timed",
99+
"num_requested",
100+
"restartpoints_timed",
101+
"restartpoints_req",
102+
"restartpoints_done",
103+
"write_time",
104+
"sync_time",
105+
"buffers_written",
106+
"stats_reset"}
107+
108+
rows := sqlmock.NewRows(columns).
109+
AddRow(nil, nil, nil, nil, nil, nil, nil, nil, nil)
110+
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
111+
112+
ch := make(chan prometheus.Metric)
113+
go func() {
114+
defer close(ch)
115+
c := PGStatCheckpointerCollector{}
116+
117+
if err := c.Update(context.Background(), inst, ch); err != nil {
118+
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
119+
}
120+
}()
121+
122+
expected := []MetricResult{
123+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
124+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
125+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
126+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
127+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
128+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
129+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
130+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
131+
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
132+
}
133+
134+
convey.Convey("Metrics comparison", t, func() {
135+
for _, expect := range expected {
136+
m := readMetric(<-ch)
137+
convey.So(expect, convey.ShouldResemble, m)
138+
}
139+
})
140+
if err := mock.ExpectationsWereMet(); err != nil {
141+
t.Errorf("there were unfulfilled exceptions: %s", err)
142+
}
143+
}

0 commit comments

Comments
 (0)