Skip to content

Commit

Permalink
chore: add pool.Pooler interface to pgbouncer/metricsserver (cloudnat…
Browse files Browse the repository at this point in the history
…ive-pg#2480)

This patch leverages the pool.Pooler interface to add unit tests
for pgbouncer metrics server.

Partially Close cloudnative-pg#2358

Signed-off-by: Armando Ruocco <[email protected]>
Signed-off-by: Jaime Silvela <[email protected]>
Co-authored-by: Jaime Silvela <[email protected]>
  • Loading branch information
armru and jsilvela authored Aug 2, 2023
1 parent 5402903 commit 11e051c
Show file tree
Hide file tree
Showing 5 changed files with 399 additions and 2 deletions.
50 changes: 50 additions & 0 deletions pkg/management/pgbouncer/metricsserver/metricsserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright The CloudNativePG Contributors
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 metricsserver

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("MetricsServer", func() {
Describe("Setup", func() {
BeforeEach(func() {
server = nil
registry = nil
exporter = nil
})

It("should register exporters and collectors successfully", func() {
err := Setup()
Expect(err).NotTo(HaveOccurred())

mfs, err := registry.Gather()
Expect(err).NotTo(HaveOccurred())
Expect(mfs).NotTo(BeEmpty())

Expect(exporter.Metrics.CollectionsTotal).NotTo(BeNil())
Expect(exporter.Metrics.PgCollectionErrors).NotTo(BeNil())
Expect(exporter.Metrics.Error).NotTo(BeNil())
Expect(exporter.Metrics.CollectionDuration).NotTo(BeNil())
Expect(exporter.Metrics.PgbouncerUp).NotTo(BeNil())
Expect(exporter.Metrics.ShowLists).NotTo(BeNil())
Expect(exporter.Metrics.ShowPools).NotTo(BeNil())
Expect(exporter.Metrics.ShowStats).NotTo(BeNil())
})
})
})
4 changes: 2 additions & 2 deletions pkg/management/pgbouncer/metricsserver/pgbouncer_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const PrometheusNamespace = "cnpg"
// Exporter exports a set of metrics and collectors on a given postgres instance
type Exporter struct {
Metrics *metrics
pool *pool.ConnectionPool
pool pool.Pooler
}

// metrics here are related to the exporter itself, which is instrumented to
Expand Down Expand Up @@ -145,7 +145,7 @@ func (e *Exporter) GetPgBouncerDB() (*sql.DB, error) {
}

// ConnectionPool gets or initializes the connection pool for this instance
func (e *Exporter) ConnectionPool() *pool.ConnectionPool {
func (e *Exporter) ConnectionPool() pool.Pooler {
if e.pool == nil {
dsn := fmt.Sprintf(
"host=%s port=%v user=%s sslmode=disable",
Expand Down
139 changes: 139 additions & 0 deletions pkg/management/pgbouncer/metricsserver/pools_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
Copyright The CloudNativePG Contributors
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 metricsserver

import (
"database/sql"

"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Exporter", func() {
var (
registry *prometheus.Registry
db *sql.DB
mock sqlmock.Sqlmock
exp *Exporter
ch chan prometheus.Metric
columns16 = []string{
"database",
"user",
"cl_active",
"cl_waiting",
"cl_active_cancel_req",
"cl_waiting_cancel_req",
"sv_active",
"sv_active_cancel",
"sv_being_canceled",
"sv_idle",
"sv_used",
"sv_tested",
"sv_login",
"maxwait",
"maxwait_us",
"pool_mode",
}
)

BeforeEach(func() {
var err error
db, mock, err = sqlmock.New()
Expect(err).ShouldNot(HaveOccurred())

exp = &Exporter{
Metrics: newMetrics(),
pool: fakePooler{db: db},
}

registry = prometheus.NewRegistry()
registry.MustRegister(exp.Metrics.PgbouncerUp)
registry.MustRegister(exp.Metrics.Error)

ch = make(chan prometheus.Metric, 1000)
})

AfterEach(func() {
Expect(mock.ExpectationsWereMet()).To(Succeed())
})

Context("collectShowPools", func() {
It("should react properly if SQL shows no pools", func() {
mock.ExpectQuery("SHOW POOLS;").WillReturnError(sql.ErrNoRows)
exp.collectShowPools(ch, db)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgBouncerUpValue := getMetric(metrics, pgBouncerUpKey).GetMetric()[0].GetGauge().GetValue()
Expect(pgBouncerUpValue).Should(BeEquivalentTo(0))

errorValue := getMetric(metrics, lastCollectionErrorKey).GetMetric()[0].GetGauge().GetValue()
Expect(errorValue).To(BeEquivalentTo(1))
})

It("should handle SQL rows scanning properly", func() {
mock.ExpectQuery("SHOW POOLS;").
WillReturnRows(sqlmock.NewRows(columns16).
AddRow("db1", "user1", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, "session"))

exp.collectShowPools(ch, db)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgBouncerUpValue := getMetric(metrics, pgBouncerUpKey).GetMetric()[0].GetGauge().GetValue()
Expect(pgBouncerUpValue).Should(BeEquivalentTo(1))

errorValue := getMetric(metrics, lastCollectionErrorKey).GetMetric()[0].GetGauge().GetValue()
Expect(errorValue).To(BeEquivalentTo(0))
})

It("should handle error during SQL rows scanning", func() {
mock.ExpectQuery("SHOW POOLS;").
WillReturnRows(sqlmock.NewRows(columns16).
AddRow("db1", "user1", "error", 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, "session"))

exp.collectShowPools(ch, db)

registry.MustRegister(exp.Metrics.PgCollectionErrors)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgBouncerUpValue := getMetric(metrics, pgBouncerUpKey).GetMetric()[0].GetGauge().GetValue()
Expect(pgBouncerUpValue).Should(BeEquivalentTo(1))

errorsMetric := getMetric(metrics, collectionErrorsTotalKey).GetMetric()[0]
label := errorsMetric.GetLabel()[0]
Expect(*label.Name).To(BeEquivalentTo("collector"))
Expect(*label.Value).To(BeEquivalentTo("sql: Scan error on column index 2, name \"cl_active\": " +
"converting driver.Value type string (\"error\") to a int: invalid syntax"))
Expect(errorsMetric.GetCounter().GetValue()).To(BeEquivalentTo(1))
})

It("should return the correct integer value", func() {
Expect(poolModeToInt("session")).To(Equal(1))
Expect(poolModeToInt("transaction")).To(Equal(2))
Expect(poolModeToInt("statement")).To(Equal(3))
Expect(poolModeToInt("random")).To(Equal(-1))
})
})
})
138 changes: 138 additions & 0 deletions pkg/management/pgbouncer/metricsserver/stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Copyright The CloudNativePG Contributors
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 metricsserver

import (
"database/sql"
"errors"

"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("MetricsServer", func() {
const (
showStatsQuery = "SHOW STATS;"
)

var (
db *sql.DB
mock sqlmock.Sqlmock
exp *Exporter
registry *prometheus.Registry
ch chan prometheus.Metric
)

BeforeEach(func() {
var err error
db, mock, err = sqlmock.New()
Expect(err).NotTo(HaveOccurred())

exp = &Exporter{
Metrics: newMetrics(),
pool: fakePooler{db: db},
}
registry = prometheus.NewRegistry()
registry.MustRegister(exp.Metrics.Error)
registry.MustRegister(exp.Metrics.ShowStats.TotalXactCount)
registry.MustRegister(exp.Metrics.ShowStats.TotalQueryCount)
registry.MustRegister(exp.Metrics.PgbouncerUp)

ch = make(chan prometheus.Metric, 1000)
})

Context("collectShowStats", func() {
It("should handle successful SQL query execution", func() {
mock.ExpectQuery(showStatsQuery).WillReturnRows(getShowStatsRows())

exp.collectShowStats(ch, db)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgbouncerUpMetric := getMetric(metrics, "cnpg_pgbouncer_up")
Expect(pgbouncerUpMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(1))

lastCollectionErrorMetric := getMetric(metrics, "cnpg_pgbouncer_last_collection_error")
Expect(lastCollectionErrorMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(0))

totalXactCountMetric := getMetric(metrics, "cnpg_pgbouncer_stats_total_xact_count")
Expect(totalXactCountMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(1))

totalQueryCountMetric := getMetric(metrics, "cnpg_pgbouncer_stats_total_query_count")
Expect(totalQueryCountMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(2))
})

It("should handle error during SQL query execution", func() {
mock.ExpectQuery(showStatsQuery).WillReturnError(errors.New("database error"))

exp.collectShowStats(ch, db)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgbouncerUpMetric := getMetric(metrics, "cnpg_pgbouncer_up")
Expect(pgbouncerUpMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(0))

lastCollectionErrorMetric := getMetric(metrics, "cnpg_pgbouncer_last_collection_error")
Expect(lastCollectionErrorMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(1))
})

It("should handle error during rows scanning", func() {
mock.ExpectQuery(showStatsQuery).
WillReturnRows(sqlmock.NewRows([]string{"total_xact_count"}).
AddRow("invalid"))

exp.collectShowStats(ch, db)

metrics, err := registry.Gather()
Expect(err).ToNot(HaveOccurred())

pgbouncerUpMetric := getMetric(metrics, "cnpg_pgbouncer_up")
Expect(pgbouncerUpMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(1))

lastCollectionErrorMetric := getMetric(metrics, "cnpg_pgbouncer_last_collection_error")
Expect(lastCollectionErrorMetric.GetMetric()[0].GetGauge().GetValue()).To(BeEquivalentTo(1))
})
})
})

func getShowStatsRows() *sqlmock.Rows {
columns := []string{
"database",
"total_xact_count",
"total_query_count",
"total_received",
"total_sent",
"total_xact_time",
"total_query_time",
"total_wait_time",
"avg_xact_count",
"avg_query_count",
"avg_recv",
"avg_sent",
"avg_xact_time",
"avg_query_time",
"avg_wait_time",
}

return sqlmock.NewRows(columns).
AddRow("db1", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)
}
Loading

0 comments on commit 11e051c

Please sign in to comment.