From 4e53bdf6a2b9dc2257253ca458d103086ecfc6d4 Mon Sep 17 00:00:00 2001 From: Felix Yuan <felix.yuan@reddit.com> Date: Wed, 28 Jun 2023 11:19:30 -0700 Subject: [PATCH 1/3] Xid collector and test Signed-off-by: Felix Yuan <felix.yuan@reddit.com> --- collector/pg_xid.go | 99 ++++++++++++++++++++++++++++++++++ collector/pg_xid_test.go | 111 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 collector/pg_xid.go create mode 100644 collector/pg_xid_test.go diff --git a/collector/pg_xid.go b/collector/pg_xid.go new file mode 100644 index 00000000..c5db6432 --- /dev/null +++ b/collector/pg_xid.go @@ -0,0 +1,99 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "context" + + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" +) + +const xidSubsystem = "xid" + +func init() { + registerCollector(xidSubsystem, defaultDisabled, NewPGXidCollector) +} + +type PGXidCollector struct { + log log.Logger +} + +func NewPGXidCollector(config collectorConfig) (Collector, error) { + return &PGXidCollector{log: config.logger}, nil +} + +var ( + xidCurrent = prometheus.NewDesc( + prometheus.BuildFQName(namespace, xidSubsystem, "current"), + "Current 64-bit transaction id of the query used to collect this metric (truncated to low 52 bits)", + []string{}, prometheus.Labels{}, + ) + xidXmin = prometheus.NewDesc( + prometheus.BuildFQName(namespace, xidSubsystem, "xmin"), + "Oldest transaction id of a transaction still in progress, i.e. not known committed or aborted (truncated to low 52 bits)", + []string{}, prometheus.Labels{}, + ) + xidXminAge = prometheus.NewDesc( + prometheus.BuildFQName(namespace, xidSubsystem, "xmin_age"), + "Age of oldest transaction still not committed or aborted measured in transaction ids", + []string{}, prometheus.Labels{}, + ) + + xidQuery = ` + SELECT + CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_current() % (2^52)::bigint END AS current, + CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_snapshot_xmin(txid_current_snapshot()) % (2^52)::bigint END AS xmin, + CASE WHEN pg_is_in_recovery() THEN 'NaN'::float ELSE txid_current() - txid_snapshot_xmin(txid_current_snapshot()) END AS xmin_age + ` +) + +func (PGXidCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { + db := instance.getDB() + rows, err := db.QueryContext(ctx, + xidQuery) + + if err != nil { + return err + } + defer rows.Close() + + for rows.Next() { + var current, xmin, xminAge float64 + + if err := rows.Scan(¤t, &xmin, &xminAge); err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric( + xidCurrent, + prometheus.CounterValue, + current, + ) + ch <- prometheus.MustNewConstMetric( + xidXmin, + prometheus.CounterValue, + xmin, + ) + ch <- prometheus.MustNewConstMetric( + xidXminAge, + prometheus.GaugeValue, + xminAge, + ) + } + if err := rows.Err(); err != nil { + return err + } + return nil +} diff --git a/collector/pg_xid_test.go b/collector/pg_xid_test.go new file mode 100644 index 00000000..6de90fcf --- /dev/null +++ b/collector/pg_xid_test.go @@ -0,0 +1,111 @@ +// Copyright 2023 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package collector + +import ( + "context" + "math" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/smartystreets/goconvey/convey" +) + +func TestPgXidCollector(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub db connection: %s", err) + } + defer db.Close() + inst := &instance{db: db} + columns := []string{ + "current", + "xmin", + "xmin_age", + } + rows := sqlmock.NewRows(columns). + AddRow(22, 25, 30) + + mock.ExpectQuery(sanitizeQuery(xidQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGXidCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGXidCollector.Update: %s", err) + } + }() + expected := []MetricResult{ + {labels: labelMap{}, value: 22, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 25, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 30, metricType: dto.MetricType_GAUGE}, + } + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, m) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} + +func TestPgNanCollector(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Error opening a stub db connection: %s", err) + } + defer db.Close() + inst := &instance{db: db} + columns := []string{ + "current", + "xmin", + "xmin_age", + } + rows := sqlmock.NewRows(columns). + AddRow(math.NaN(), math.NaN(), math.NaN()) + + mock.ExpectQuery(sanitizeQuery(xidQuery)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + defer close(ch) + c := PGXidCollector{} + + if err := c.Update(context.Background(), inst, ch); err != nil { + t.Errorf("Error calling PGXidCollector.Update: %s", err) + } + }() + expected := []MetricResult{ + {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_GAUGE}, + } + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + m := readMetric(<-ch) + + convey.So(expect.labels, convey.ShouldResemble, m.labels) + convey.So(math.IsNaN(m.value), convey.ShouldResemble, math.IsNaN(expect.value)) + convey.So(expect.metricType, convey.ShouldEqual, m.metricType) + } + }) + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} From 5214aae34dd328b67af9b785cb9469ddcc47ef9b Mon Sep 17 00:00:00 2001 From: Felix Yuan <felix.yuan@reddit.com> Date: Wed, 28 Jun 2023 11:23:49 -0700 Subject: [PATCH 2/3] Add more escapes Signed-off-by: Felix Yuan <felix.yuan@reddit.com> --- collector/collector_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/collector/collector_test.go b/collector/collector_test.go index 00c21ed2..18101f00 100644 --- a/collector/collector_test.go +++ b/collector/collector_test.go @@ -49,6 +49,7 @@ func readMetric(m prometheus.Metric) MetricResult { func sanitizeQuery(q string) string { q = strings.Join(strings.Fields(q), " ") q = strings.Replace(q, "(", "\\(", -1) + q = strings.Replace(q, "?", "\\?", -1) q = strings.Replace(q, ")", "\\)", -1) q = strings.Replace(q, "[", "\\[", -1) q = strings.Replace(q, "]", "\\]", -1) From 4d68e2df6880262c7f99aa650e6c314e190be341 Mon Sep 17 00:00:00 2001 From: Felix Yuan <felix.yuan@reddit.com> Date: Mon, 10 Jul 2023 10:42:19 -0700 Subject: [PATCH 3/3] Change to Gauge Signed-off-by: Felix Yuan <felix.yuan@reddit.com> --- collector/pg_xid.go | 4 ++-- collector/pg_xid_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/collector/pg_xid.go b/collector/pg_xid.go index c5db6432..8746f470 100644 --- a/collector/pg_xid.go +++ b/collector/pg_xid.go @@ -78,12 +78,12 @@ func (PGXidCollector) Update(ctx context.Context, instance *instance, ch chan<- ch <- prometheus.MustNewConstMetric( xidCurrent, - prometheus.CounterValue, + prometheus.GaugeValue, current, ) ch <- prometheus.MustNewConstMetric( xidXmin, - prometheus.CounterValue, + prometheus.GaugeValue, xmin, ) ch <- prometheus.MustNewConstMetric( diff --git a/collector/pg_xid_test.go b/collector/pg_xid_test.go index 6de90fcf..7546122b 100644 --- a/collector/pg_xid_test.go +++ b/collector/pg_xid_test.go @@ -50,8 +50,8 @@ func TestPgXidCollector(t *testing.T) { } }() expected := []MetricResult{ - {labels: labelMap{}, value: 22, metricType: dto.MetricType_COUNTER}, - {labels: labelMap{}, value: 25, metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: 22, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: 25, metricType: dto.MetricType_GAUGE}, {labels: labelMap{}, value: 30, metricType: dto.MetricType_GAUGE}, } convey.Convey("Metrics comparison", t, func() { @@ -92,8 +92,8 @@ func TestPgNanCollector(t *testing.T) { } }() expected := []MetricResult{ - {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER}, - {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_COUNTER}, + {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_GAUGE}, + {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_GAUGE}, {labels: labelMap{}, value: math.NaN(), metricType: dto.MetricType_GAUGE}, } convey.Convey("Metrics comparison", t, func() {