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(&current, &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() {