From a66da1df4a7e12cb9f84cf5ae3c7adec4539ed27 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <25817501+johejo@users.noreply.github.com> Date: Fri, 21 May 2021 05:22:17 +0900 Subject: [PATCH] Add collector for database/sql#DBStats (#866) * Add collector for database/sql#DBStats Signed-off-by: Mitsuo Heijo --- prometheus/collectors/dbstats_collector.go | 102 ++++++++++++++++++ .../collectors/dbstats_collector_go115.go | 43 ++++++++ .../collectors/dbstats_collector_pre_go115.go | 41 +++++++ .../collectors/dbstats_collector_test.go | 97 +++++++++++++++++ 4 files changed, 283 insertions(+) create mode 100644 prometheus/collectors/dbstats_collector.go create mode 100644 prometheus/collectors/dbstats_collector_go115.go create mode 100644 prometheus/collectors/dbstats_collector_pre_go115.go create mode 100644 prometheus/collectors/dbstats_collector_test.go diff --git a/prometheus/collectors/dbstats_collector.go b/prometheus/collectors/dbstats_collector.go new file mode 100644 index 000000000..d7a37ebe1 --- /dev/null +++ b/prometheus/collectors/dbstats_collector.go @@ -0,0 +1,102 @@ +// Copyright 2021 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 collectors + +import ( + "database/sql" + + "github.com/prometheus/client_golang/prometheus" +) + +type dbStatsCollector struct { + db *sql.DB + + maxOpenConnections *prometheus.Desc + + openConnections *prometheus.Desc + inUseConnections *prometheus.Desc + idleConnections *prometheus.Desc + + waitCount *prometheus.Desc + waitDuration *prometheus.Desc + maxIdleClosed *prometheus.Desc + maxIdleTimeClosed *prometheus.Desc + maxLifetimeClosed *prometheus.Desc +} + +// NewDBStatsCollector returns a collector that exports metrics about the given *sql.DB. +// See https://golang.org/pkg/database/sql/#DBStats for more information on stats. +func NewDBStatsCollector(db *sql.DB, dbName string) prometheus.Collector { + fqName := func(name string) string { + return "go_sql_" + name + } + return &dbStatsCollector{ + db: db, + maxOpenConnections: prometheus.NewDesc( + fqName("max_open_connections"), + "Maximum number of open connections to the database.", + nil, prometheus.Labels{"db_name": dbName}, + ), + openConnections: prometheus.NewDesc( + fqName("open_connections"), + "The number of established connections both in use and idle.", + nil, prometheus.Labels{"db_name": dbName}, + ), + inUseConnections: prometheus.NewDesc( + fqName("in_use_connections"), + "The number of connections currently in use.", + nil, prometheus.Labels{"db_name": dbName}, + ), + idleConnections: prometheus.NewDesc( + fqName("idle_connections"), + "The number of idle connections.", + nil, prometheus.Labels{"db_name": dbName}, + ), + waitCount: prometheus.NewDesc( + fqName("wait_count_total"), + "The total number of connections waited for.", + nil, prometheus.Labels{"db_name": dbName}, + ), + waitDuration: prometheus.NewDesc( + fqName("wait_duration_seconds_total"), + "The total time blocked waiting for a new connection.", + nil, prometheus.Labels{"db_name": dbName}, + ), + maxIdleClosed: prometheus.NewDesc( + fqName("max_idle_closed_total"), + "The total number of connections closed due to SetMaxIdleConns.", + nil, prometheus.Labels{"db_name": dbName}, + ), + maxIdleTimeClosed: prometheus.NewDesc( + fqName("max_idle_time_closed_total"), + "The total number of connections closed due to SetConnMaxIdleTime.", + nil, prometheus.Labels{"db_name": dbName}, + ), + maxLifetimeClosed: prometheus.NewDesc( + fqName("max_lifetime_closed_total"), + "The total number of connections closed due to SetConnMaxLifetime.", + nil, prometheus.Labels{"db_name": dbName}, + ), + } +} + +// Describe implements Collector. +func (c *dbStatsCollector) Describe(ch chan<- *prometheus.Desc) { + c.describe(ch) +} + +// Collect implements Collector. +func (c *dbStatsCollector) Collect(ch chan<- prometheus.Metric) { + c.collect(ch) +} diff --git a/prometheus/collectors/dbstats_collector_go115.go b/prometheus/collectors/dbstats_collector_go115.go new file mode 100644 index 000000000..c5324c64a --- /dev/null +++ b/prometheus/collectors/dbstats_collector_go115.go @@ -0,0 +1,43 @@ +// Copyright 2021 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. + +// +build go1.15 + +package collectors + +import "github.com/prometheus/client_golang/prometheus" + +func (c *dbStatsCollector) describe(ch chan<- *prometheus.Desc) { + ch <- c.maxOpenConnections + ch <- c.openConnections + ch <- c.inUseConnections + ch <- c.idleConnections + ch <- c.waitCount + ch <- c.waitDuration + ch <- c.maxIdleClosed + ch <- c.maxIdleTimeClosed + ch <- c.maxLifetimeClosed +} + +func (c *dbStatsCollector) collect(ch chan<- prometheus.Metric) { + stats := c.db.Stats() + ch <- prometheus.MustNewConstMetric(c.maxOpenConnections, prometheus.GaugeValue, float64(stats.MaxOpenConnections)) + ch <- prometheus.MustNewConstMetric(c.openConnections, prometheus.GaugeValue, float64(stats.OpenConnections)) + ch <- prometheus.MustNewConstMetric(c.inUseConnections, prometheus.GaugeValue, float64(stats.InUse)) + ch <- prometheus.MustNewConstMetric(c.idleConnections, prometheus.GaugeValue, float64(stats.Idle)) + ch <- prometheus.MustNewConstMetric(c.waitCount, prometheus.CounterValue, float64(stats.WaitCount)) + ch <- prometheus.MustNewConstMetric(c.waitDuration, prometheus.CounterValue, stats.WaitDuration.Seconds()) + ch <- prometheus.MustNewConstMetric(c.maxIdleClosed, prometheus.CounterValue, float64(stats.MaxIdleClosed)) + ch <- prometheus.MustNewConstMetric(c.maxIdleTimeClosed, prometheus.CounterValue, float64(stats.MaxIdleTimeClosed)) + ch <- prometheus.MustNewConstMetric(c.maxLifetimeClosed, prometheus.CounterValue, float64(stats.MaxLifetimeClosed)) +} diff --git a/prometheus/collectors/dbstats_collector_pre_go115.go b/prometheus/collectors/dbstats_collector_pre_go115.go new file mode 100644 index 000000000..8eb5153ff --- /dev/null +++ b/prometheus/collectors/dbstats_collector_pre_go115.go @@ -0,0 +1,41 @@ +// Copyright 2021 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. + +// +build !go1.15 + +package collectors + +import "github.com/prometheus/client_golang/prometheus" + +func (c *dbStatsCollector) describe(ch chan<- *prometheus.Desc) { + ch <- c.maxOpenConnections + ch <- c.openConnections + ch <- c.inUseConnections + ch <- c.idleConnections + ch <- c.waitCount + ch <- c.waitDuration + ch <- c.maxIdleClosed + ch <- c.maxLifetimeClosed +} + +func (c *dbStatsCollector) collect(ch chan<- prometheus.Metric) { + stats := c.db.Stats() + ch <- prometheus.MustNewConstMetric(c.maxOpenConnections, prometheus.GaugeValue, float64(stats.MaxOpenConnections)) + ch <- prometheus.MustNewConstMetric(c.openConnections, prometheus.GaugeValue, float64(stats.OpenConnections)) + ch <- prometheus.MustNewConstMetric(c.inUseConnections, prometheus.GaugeValue, float64(stats.InUse)) + ch <- prometheus.MustNewConstMetric(c.idleConnections, prometheus.GaugeValue, float64(stats.Idle)) + ch <- prometheus.MustNewConstMetric(c.waitCount, prometheus.CounterValue, float64(stats.WaitCount)) + ch <- prometheus.MustNewConstMetric(c.waitDuration, prometheus.CounterValue, stats.WaitDuration.Seconds()) + ch <- prometheus.MustNewConstMetric(c.maxIdleClosed, prometheus.CounterValue, float64(stats.MaxIdleClosed)) + ch <- prometheus.MustNewConstMetric(c.maxLifetimeClosed, prometheus.CounterValue, float64(stats.MaxLifetimeClosed)) +} diff --git a/prometheus/collectors/dbstats_collector_test.go b/prometheus/collectors/dbstats_collector_test.go new file mode 100644 index 000000000..4cfdda541 --- /dev/null +++ b/prometheus/collectors/dbstats_collector_test.go @@ -0,0 +1,97 @@ +// Copyright 2021 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 collectors + +import ( + "database/sql" + "runtime" + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestDBStatsCollector(t *testing.T) { + reg := prometheus.NewRegistry() + { + db := new(sql.DB) + if err := reg.Register(NewDBStatsCollector(db, "db_A")); err != nil { + t.Fatal(err) + } + } + { + db := new(sql.DB) + if err := reg.Register(NewDBStatsCollector(db, "db_B")); err != nil { + t.Fatal(err) + } + } + + mfs, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + + names := []string{ + "go_sql_max_open_connections", + "go_sql_open_connections", + "go_sql_in_use_connections", + "go_sql_idle_connections", + "go_sql_wait_count_total", + "go_sql_wait_duration_seconds_total", + "go_sql_max_idle_closed_total", + "go_sql_max_lifetime_closed_total", + } + if runtime.Version() >= "go1.15" { + names = append(names, "go_sql_max_idle_time_closed_total") + } + type result struct { + found bool + } + results := make(map[string]result) + for _, name := range names { + results[name] = result{found: false} + } + for _, mf := range mfs { + m := mf.GetMetric() + if len(m) != 2 { + t.Errorf("expected 2 metrics bug got %d", len(m)) + } + labelA := m[0].GetLabel()[0] + if name := labelA.GetName(); name != "db_name" { + t.Errorf("expected to get label \"db_name\" but got %s", name) + } + if value := labelA.GetValue(); value != "db_A" { + t.Errorf("expected to get value \"db_A\" but got %s", value) + } + labelB := m[1].GetLabel()[0] + if name := labelB.GetName(); name != "db_name" { + t.Errorf("expected to get label \"db_name\" but got %s", name) + } + if value := labelB.GetValue(); value != "db_B" { + t.Errorf("expected to get value \"db_B\" but got %s", value) + } + + for _, name := range names { + if name == mf.GetName() { + results[name] = result{found: true} + break + } + } + } + + for name, result := range results { + if !result.found { + t.Errorf("%s not found", name) + } + } +}