From c0be446b4ff0935c8f34de46fbd8890d9c46a774 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 12 May 2021 20:39:53 +0900 Subject: [PATCH] Add collector for database/sql#DBStats Signed-off-by: Mitsuo Heijo --- prometheus/collectors/dbstats_collector.go | 137 ++++++++++++++++++ .../collectors/dbstats_collector_go115.go | 129 +++++++++++++++++ .../collectors/dbstats_collector_test.go | 71 +++++++++ 3 files changed, 337 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_test.go diff --git a/prometheus/collectors/dbstats_collector.go b/prometheus/collectors/dbstats_collector.go new file mode 100644 index 000000000..4e10eb73b --- /dev/null +++ b/prometheus/collectors/dbstats_collector.go @@ -0,0 +1,137 @@ +// 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. + +//go:build go1.15 +// +build go1.15 + +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 +} + +// DBStatsCollectorOpts defines the behavior of a db stats collector +// created with NewDBStatsCollector. +type DBStatsCollectorOpts struct { + // DriverName holds the name of driver. + // It will not used for empty strings. + DriverName string +} + +// 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, opts DBStatsCollectorOpts) prometheus.Collector { + var fqName func(name string) string + if opts.DriverName == "" { + fqName = func(name string) string { + return "go_db_stats_" + name + } + } else { + fqName = func(name string) string { + return "go_" + opts.DriverName + "_db_stats_" + name + } + } + return &dbStatsCollector{ + db: db, + maxOpenConnections: prometheus.NewDesc( + fqName("max_open_connections"), + "Maximum number of open connections to the database.", + nil, nil, + ), + openConnections: prometheus.NewDesc( + fqName("open_connections"), + "The number of established connections both in use and idle.", + nil, nil, + ), + inUseConnections: prometheus.NewDesc( + fqName("in_use_connections"), + "The number of connections currently in use.", + nil, nil, + ), + idleConnections: prometheus.NewDesc( + fqName("idle_connections"), + "The number of idle connections.", + nil, nil, + ), + waitCount: prometheus.NewDesc( + fqName("wait_count_total"), + "The total number of connections waited for.", + nil, nil, + ), + waitDuration: prometheus.NewDesc( + fqName("wait_duration_seconds_total"), + "The total time blocked waiting for a new connection.", + nil, nil, + ), + maxIdleClosed: prometheus.NewDesc( + fqName("max_idle_closed_total"), + "The total number of connections closed due to SetMaxIdleConns.", + nil, nil, + ), + maxIdleTimeClosed: prometheus.NewDesc( + fqName("max_idle_time_closed_total"), + "The total number of connections closed due to SetConnMaxIdleTime.", + nil, nil, + ), + maxLifetimeClosed: prometheus.NewDesc( + fqName("max_lifetime_closed_total"), + "The total number of connections closed due to SetConnMaxLifetime.", + nil, nil, + ), + } +} + +// Describe implements Collector. +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 +} + +// Collect implements Collector. +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_go115.go b/prometheus/collectors/dbstats_collector_go115.go new file mode 100644 index 000000000..ed482acbd --- /dev/null +++ b/prometheus/collectors/dbstats_collector_go115.go @@ -0,0 +1,129 @@ +// 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. + +//go:build !go1.15 +// +build !go1.15 + +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 + maxLifetimeClosed *prometheus.Desc +} + +// DBStatsCollectorOpts defines the behavior of a db stats collector +// created with NewDBStatsCollector. +type DBStatsCollectorOpts struct { + // DriverName holds the name of driver. + // It will not used for empty strings. + DriverName string +} + +// 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, opts DBStatsCollectorOpts) prometheus.Collector { + var fqName func(name string) string + if opts.DriverName == "" { + fqName = func(name string) string { + return "go_db_stats_" + name + } + } else { + fqName = func(name string) string { + return "go_" + opts.DriverName + "_db_stats_" + name + } + } + return &dbStatsCollector{ + db: db, + maxOpenConnections: prometheus.NewDesc( + fqName("max_open_connections"), + "Maximum number of open connections to the database.", + nil, nil, + ), + openConnections: prometheus.NewDesc( + fqName("open_connections"), + "The number of established connections both in use and idle.", + nil, nil, + ), + inUseConnections: prometheus.NewDesc( + fqName("in_use_connections"), + "The number of connections currently in use.", + nil, nil, + ), + idleConnections: prometheus.NewDesc( + fqName("idle_connections"), + "The number of idle connections.", + nil, nil, + ), + waitCount: prometheus.NewDesc( + fqName("wait_count_total"), + "The total number of connections waited for.", + nil, nil, + ), + waitDuration: prometheus.NewDesc( + fqName("wait_duration_seconds_total"), + "The total time blocked waiting for a new connection.", + nil, nil, + ), + maxIdleClosed: prometheus.NewDesc( + fqName("max_idle_closed_total"), + "The total number of connections closed due to SetMaxIdleConns.", + nil, nil, + ), + maxLifetimeClosed: prometheus.NewDesc( + fqName("max_lifetime_closed_total"), + "The total number of connections closed due to SetConnMaxLifetime.", + nil, nil, + ), + } +} + +// Describe implements Collector. +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 +} + +// Collect implements Collector. +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..2d83c964a --- /dev/null +++ b/prometheus/collectors/dbstats_collector_test.go @@ -0,0 +1,71 @@ +// 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) + opts := DBStatsCollectorOpts{DriverName: "test"} + if err := reg.Register(NewDBStatsCollector(db, opts)); err != nil { + t.Fatal(err) + } + + mfs, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + + names := []string{ + "go_test_db_stats_max_open_connections", + "go_test_db_stats_open_connections", + "go_test_db_stats_in_use_connections", + "go_test_db_stats_idle_connections", + "go_test_db_stats_wait_count_total", + "go_test_db_stats_wait_duration_seconds_total", + "go_test_db_stats_max_idle_closed_total", + "go_test_db_stats_max_lifetime_closed_total", + } + if runtime.Version() >= "go1.15" { + names = append(names, "go_test_db_stats_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 { + 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) + } + } +}