Skip to content

Commit f3762e3

Browse files
committed
Database wraparound collector and test
Signed-off-by: Felix Yuan <[email protected]>
1 parent dcf498e commit f3762e3

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

collector/pg_database_wraparound.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
"database/sql"
19+
20+
"github.com/go-kit/log"
21+
"github.com/prometheus/client_golang/prometheus"
22+
)
23+
24+
const databaseWraparoundSubsystem = "database_wraparound"
25+
26+
func init() {
27+
registerCollector(databaseWraparoundSubsystem, defaultDisabled, NewPGDatabaseWraparoundCollector)
28+
}
29+
30+
type PGDatabaseWraparoundCollector struct {
31+
log log.Logger
32+
}
33+
34+
func NewPGDatabaseWraparoundCollector(config collectorConfig) (Collector, error) {
35+
return &PGDatabaseWraparoundCollector{log: config.logger}, nil
36+
}
37+
38+
var (
39+
databaseWraparoundAgeDatfrozenxid = prometheus.NewDesc(
40+
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datfrozenxid_seconds"),
41+
"Age of the oldest transaction ID that has not been frozen.",
42+
[]string{"datname"},
43+
prometheus.Labels{},
44+
)
45+
databaseWraparoundAgeDatminmxid = prometheus.NewDesc(
46+
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datminmxid"),
47+
"Age of the oldest multi-transaction ID that has been replaced with a transaction ID.",
48+
[]string{"datname"},
49+
prometheus.Labels{},
50+
)
51+
52+
databaseWraparoundQuery = `
53+
SELECT
54+
datname,
55+
age(d.datfrozenxid) as age_datfrozenxid,
56+
mxid_age(d.datminmxid) as age_datminmxid
57+
FROM
58+
pg_catalog.pg_database d
59+
WHERE
60+
d.datallowconn
61+
`
62+
)
63+
64+
func (PGDatabaseWraparoundCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
65+
db := instance.getDB()
66+
rows, err := db.QueryContext(ctx,
67+
databaseWraparoundQuery)
68+
69+
if err != nil {
70+
return err
71+
}
72+
defer rows.Close()
73+
74+
for rows.Next() {
75+
var datname sql.NullString
76+
var ageDatfrozenxid, ageDatminmxid sql.NullFloat64
77+
78+
if err := rows.Scan(&datname, &ageDatfrozenxid, &ageDatminmxid); err != nil {
79+
return err
80+
}
81+
82+
datnameLabel := "unknown"
83+
if datname.Valid {
84+
datnameLabel = datname.String
85+
}
86+
87+
ageDatfrozenxidMetric := 0.0
88+
if ageDatfrozenxid.Valid {
89+
ageDatfrozenxidMetric = ageDatfrozenxid.Float64
90+
}
91+
92+
ch <- prometheus.MustNewConstMetric(
93+
databaseWraparoundAgeDatfrozenxid,
94+
prometheus.GaugeValue,
95+
ageDatfrozenxidMetric, datnameLabel,
96+
)
97+
98+
ageDatminmxidMetric := 0.0
99+
if ageDatminmxid.Valid {
100+
ageDatminmxidMetric = ageDatminmxid.Float64
101+
}
102+
ch <- prometheus.MustNewConstMetric(
103+
databaseWraparoundAgeDatminmxid,
104+
prometheus.GaugeValue,
105+
ageDatminmxidMetric, datnameLabel,
106+
)
107+
}
108+
if err := rows.Err(); err != nil {
109+
return err
110+
}
111+
return nil
112+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
19+
"github.com/DATA-DOG/go-sqlmock"
20+
"github.com/prometheus/client_golang/prometheus"
21+
dto "github.com/prometheus/client_model/go"
22+
"github.com/smartystreets/goconvey/convey"
23+
)
24+
25+
func TestPGDatabaseWraparoundCollector(t *testing.T) {
26+
db, mock, err := sqlmock.New()
27+
if err != nil {
28+
t.Fatalf("Error opening a stub db connection: %s", err)
29+
}
30+
defer db.Close()
31+
inst := &instance{db: db}
32+
columns := []string{
33+
"datname",
34+
"age_datfrozenxid",
35+
"age_datminmxid",
36+
}
37+
rows := sqlmock.NewRows(columns).
38+
AddRow("newreddit", 87126426, 0)
39+
40+
mock.ExpectQuery(sanitizeQuery(databaseWraparoundQuery)).WillReturnRows(rows)
41+
42+
ch := make(chan prometheus.Metric)
43+
go func() {
44+
defer close(ch)
45+
c := PGDatabaseWraparoundCollector{}
46+
47+
if err := c.Update(context.Background(), inst, ch); err != nil {
48+
t.Errorf("Error calling PGDatabaseWraparoundCollector.Update: %s", err)
49+
}
50+
}()
51+
expected := []MetricResult{
52+
{labels: labelMap{"datname": "newreddit"}, value: 87126426, metricType: dto.MetricType_GAUGE},
53+
{labels: labelMap{"datname": "newreddit"}, value: 0, metricType: dto.MetricType_GAUGE},
54+
}
55+
convey.Convey("Metrics comparison", t, func() {
56+
for _, expect := range expected {
57+
m := readMetric(<-ch)
58+
convey.So(expect, convey.ShouldResemble, m)
59+
}
60+
})
61+
if err := mock.ExpectationsWereMet(); err != nil {
62+
t.Errorf("there were unfulfilled exceptions: %s", err)
63+
}
64+
}
65+
66+
func TestPGDatabaseWraparoundCollectorNull(t *testing.T) {
67+
db, mock, err := sqlmock.New()
68+
if err != nil {
69+
t.Fatalf("Error opening a stub db connection: %s", err)
70+
}
71+
defer db.Close()
72+
inst := &instance{db: db}
73+
columns := []string{
74+
"datname",
75+
"age_datfrozenxid",
76+
"age_datminmxid",
77+
}
78+
rows := sqlmock.NewRows(columns).
79+
AddRow(nil, nil, nil)
80+
81+
mock.ExpectQuery(sanitizeQuery(databaseWraparoundQuery)).WillReturnRows(rows)
82+
83+
ch := make(chan prometheus.Metric)
84+
go func() {
85+
defer close(ch)
86+
c := PGDatabaseWraparoundCollector{}
87+
88+
if err := c.Update(context.Background(), inst, ch); err != nil {
89+
t.Errorf("Error calling PGDatabaseWraparoundCollector.Update: %s", err)
90+
}
91+
}()
92+
expected := []MetricResult{
93+
{labels: labelMap{"datname": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
94+
{labels: labelMap{"datname": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
95+
}
96+
convey.Convey("Metrics comparison", t, func() {
97+
for _, expect := range expected {
98+
m := readMetric(<-ch)
99+
convey.So(expect, convey.ShouldResemble, m)
100+
}
101+
})
102+
if err := mock.ExpectationsWereMet(); err != nil {
103+
t.Errorf("there were unfulfilled exceptions: %s", err)
104+
}
105+
}

0 commit comments

Comments
 (0)