Skip to content

Commit 8720c53

Browse files
committed
Xid collector and test
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
1 parent dcf498e commit 8720c53

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-0
lines changed

collector/pg_xid.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
14+
package collector
15+
16+
import (
17+
"context"
18+
19+
"github.com/go-kit/log"
20+
"github.com/prometheus/client_golang/prometheus"
21+
)
22+
23+
const xidSubsystem = "xid"
24+
25+
func init() {
26+
registerCollector(xidSubsystem, defaultDisabled, NewPGXidCollector)
27+
}
28+
29+
type PGXidCollector struct {
30+
log log.Logger
31+
}
32+
33+
func NewPGXidCollector(config collectorConfig) (Collector, error) {
34+
return &PGXidCollector{log: config.logger}, nil
35+
}
36+
37+
var (
38+
xidCurrent = prometheus.NewDesc(
39+
prometheus.BuildFQName(namespace, xidSubsystem, "current"),
40+
"Current 64-bit transaction id of the query used to collect this metric (truncated to low 52 bits)",
41+
[]string{}, prometheus.Labels{},
42+
)
43+
xidXmin = prometheus.NewDesc(
44+
prometheus.BuildFQName(namespace, xidSubsystem, "xmin"),
45+
"Oldest transaction id of a transaction still in progress, i.e. not known committed or aborted (truncated to low 52 bits)",
46+
[]string{}, prometheus.Labels{},
47+
)
48+
xidXminAge = prometheus.NewDesc(
49+
prometheus.BuildFQName(namespace, xidSubsystem, "xmin_age"),
50+
"Age of oldest transaction still not committed or aborted measured in transaction ids",
51+
[]string{}, prometheus.Labels{},
52+
)
53+
54+
xidQuery = `
55+
SELECT
56+
CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_current() % (2^52)::bigint END AS current,
57+
CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_snapshot_xmin(txid_current_snapshot()) % (2^52)::bigint END AS xmin,
58+
CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_current() - txid_snapshot_xmin(txid_current_snapshot()) END AS xmin_age
59+
`
60+
)
61+
62+
func (PGXidCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
63+
db := instance.getDB()
64+
rows, err := db.QueryContext(ctx,
65+
xidQuery)
66+
67+
if err != nil {
68+
return err
69+
}
70+
defer rows.Close()
71+
72+
for rows.Next() {
73+
var current, xmin, xminAge float64
74+
75+
if err := rows.Scan(&current, &xmin, &xminAge); err != nil {
76+
return err
77+
}
78+
79+
ch <- prometheus.MustNewConstMetric(
80+
xidCurrent,
81+
prometheus.CounterValue,
82+
current,
83+
)
84+
ch <- prometheus.MustNewConstMetric(
85+
xidXmin,
86+
prometheus.CounterValue,
87+
xmin,
88+
)
89+
ch <- prometheus.MustNewConstMetric(
90+
xidXminAge,
91+
prometheus.GaugeValue,
92+
xminAge,
93+
)
94+
}
95+
if err := rows.Err(); err != nil {
96+
return err
97+
}
98+
return nil
99+
}

collector/pg_xid_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
"math"
18+
"testing"
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 TestPgXidCollector(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+
inst := &instance{db: db}
33+
columns := []string{
34+
"current",
35+
"xmin",
36+
"xmin_age",
37+
}
38+
rows := sqlmock.NewRows(columns).
39+
AddRow(22, 25, 30)
40+
41+
mock.ExpectQuery(sanitizeQuery(xidQuery)).WillReturnRows(rows)
42+
43+
ch := make(chan prometheus.Metric)
44+
go func() {
45+
defer close(ch)
46+
c := PGXidCollector{}
47+
48+
if err := c.Update(context.Background(), inst, ch); err != nil {
49+
t.Errorf("Error calling PGXidCollector.Update: %s", err)
50+
}
51+
}()
52+
expected := []MetricResult{
53+
{labels: labelMap{}, value: 22, metricType: dto.MetricType_COUNTER},
54+
{labels: labelMap{}, value: 25, metricType: dto.MetricType_COUNTER},
55+
{labels: labelMap{}, value: 30, metricType: dto.MetricType_GAUGE},
56+
}
57+
convey.Convey("Metrics comparison", t, func() {
58+
for _, expect := range expected {
59+
m := readMetric(<-ch)
60+
convey.So(expect, convey.ShouldResemble, m)
61+
}
62+
})
63+
if err := mock.ExpectationsWereMet(); err != nil {
64+
t.Errorf("there were unfulfilled exceptions: %s", err)
65+
}
66+
}
67+
68+
func TestPgNanCollector(t *testing.T) {
69+
db, mock, err := sqlmock.New()
70+
if err != nil {
71+
t.Fatalf("Error opening a stub db connection: %s", err)
72+
}
73+
defer db.Close()
74+
inst := &instance{db: db}
75+
columns := []string{
76+
"current",
77+
"xmin",
78+
"xmin_age",
79+
}
80+
rows := sqlmock.NewRows(columns).
81+
AddRow(math.NaN(), math.NaN(), math.NaN())
82+
83+
mock.ExpectQuery(sanitizeQuery(xidQuery)).WillReturnRows(rows)
84+
85+
ch := make(chan prometheus.Metric)
86+
go func() {
87+
defer close(ch)
88+
c := PGXidCollector{}
89+
90+
if err := c.Update(context.Background(), inst, ch); err != nil {
91+
t.Errorf("Error calling PGXidCollector.Update: %s", err)
92+
}
93+
}()
94+
expected := []MetricResult{
95+
{labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER},
96+
{labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER},
97+
{labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_GAUGE},
98+
}
99+
convey.Convey("Metrics comparison", t, func() {
100+
for _, expect := range expected {
101+
m := readMetric(<-ch)
102+
103+
convey.So(expect.labels, convey.ShouldResemble, m.labels)
104+
convey.So(math.IsNaN(m.value), convey.ShouldResemble, math.IsNaN(expect.value))
105+
convey.So(expect.metricType, convey.ShouldEqual, m.metricType)
106+
}
107+
})
108+
if err := mock.ExpectationsWereMet(); err != nil {
109+
t.Errorf("there were unfulfilled exceptions: %s", err)
110+
}
111+
}

0 commit comments

Comments
 (0)