├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── go.mod ├── example_test.go ├── go.sum ├── LICENSE ├── README.md ├── sqlmetrics_test.go └── sqlmetrics.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: "/" 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cristalhq/sqlmetrics 2 | 3 | go 1.17 4 | 5 | require github.com/VictoriaMetrics/metrics v1.24.0 6 | 7 | require ( 8 | github.com/valyala/fastrand v1.1.0 // indirect 9 | github.com/valyala/histogram v1.2.0 // indirect 10 | golang.org/x/sys v0.9.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: '0 0 * * 0' # run "At 00:00 on Sunday" 10 | 11 | # See https://github.com/cristalhq/.github/.github/workflows 12 | jobs: 13 | build: 14 | uses: cristalhq/.github/.github/workflows/build.yml@v0.5.0 15 | 16 | vuln: 17 | uses: cristalhq/.github/.github/workflows/vuln.yml@v0.5.0 18 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sqlmetrics_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "time" 8 | 9 | "github.com/VictoriaMetrics/metrics" 10 | "github.com/cristalhq/sqlmetrics" 11 | ) 12 | 13 | func ExampleCollector() { 14 | db, err := sql.Open("driver", "") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | ctx := context.Background() // or any other context you have 20 | every := 3 * time.Second 21 | 22 | sqlmetrics.NewCollector(ctx, db, every, "label1", "value1", "another", "etc") 23 | 24 | // done, db metrics are registered 25 | // you can see them here 26 | w := &bytes.Buffer{} 27 | metrics.WritePrometheus(w, true) 28 | } 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw= 2 | github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys= 3 | github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= 4 | github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= 5 | github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= 6 | github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= 7 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 8 | golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= 9 | golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 cristaltech 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlmetrics 2 | 3 | [![build-img]][build-url] 4 | [![pkg-img]][pkg-url] 5 | [![reportcard-img]][reportcard-url] 6 | [![coverage-img]][coverage-url] 7 | [![version-img]][version-url] 8 | 9 | Prometheus metrics for Go `database/sql` via [VictoriaMetrics/metrics](https://github.com/VictoriaMetrics/metrics) 10 | 11 | ## Features 12 | 13 | * Simple API. 14 | * Easy to integrate. 15 | 16 | ## Install 17 | 18 | Go version 1.16+ 19 | 20 | ``` 21 | go get github.com/cristalhq/sqlmetrics 22 | ``` 23 | 24 | ## Example 25 | 26 | ```go 27 | import ( 28 | "github.com/VictoriaMetrics/metrics" 29 | "github.com/cristalhq/sqlmetrics" 30 | ) 31 | 32 | // ... 33 | 34 | db, err := sql.Open("") 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | ctx := context.Background() // or any other context you have 40 | every := 3 * time.Second 41 | 42 | sqlmetrics.NewCollector(ctx, db, every, "label1", "value1", "another", "etc") 43 | 44 | // done, db metrics are registered 45 | // you can see them here 46 | w := &bytes.Buffer{} 47 | metrics.WritePrometheus(w, true) 48 | ``` 49 | 50 | See this examples: [example_test.go](https://github.com/cristalhq/sqlmetrics/blob/main/example_test.go). 51 | 52 | ## Documentation 53 | 54 | See [these docs][pkg-url]. 55 | 56 | ## License 57 | 58 | [MIT License](LICENSE). 59 | 60 | [build-img]: https://github.com/cristalhq/sqlmetrics/workflows/build/badge.svg 61 | [build-url]: https://github.com/cristalhq/sqlmetrics/actions 62 | [pkg-img]: https://pkg.go.dev/badge/cristalhq/sqlmetrics 63 | [pkg-url]: https://pkg.go.dev/github.com/cristalhq/sqlmetrics 64 | [reportcard-img]: https://goreportcard.com/badge/cristalhq/sqlmetrics 65 | [reportcard-url]: https://goreportcard.com/report/cristalhq/sqlmetrics 66 | [coverage-img]: https://codecov.io/gh/cristalhq/sqlmetrics/branch/main/graph/badge.svg 67 | [coverage-url]: https://codecov.io/gh/cristalhq/sqlmetrics 68 | [version-img]: https://img.shields.io/github/v/release/cristalhq/sqlmetrics 69 | [version-url]: https://github.com/cristalhq/sqlmetrics/releases 70 | -------------------------------------------------------------------------------- /sqlmetrics_test.go: -------------------------------------------------------------------------------- 1 | package sqlmetrics 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | "testing" 8 | "time" 9 | 10 | "github.com/VictoriaMetrics/metrics" 11 | ) 12 | 13 | func TestCollector(t *testing.T) { 14 | _ = makeCollector(t, &mockStatser{}, time.Nanosecond, "db", "mydb", "table", "mytable") 15 | 16 | time.Sleep(time.Second) // get some time to collect metrics 17 | 18 | b := &bytes.Buffer{} 19 | metrics.WritePrometheus(b, false) 20 | 21 | got := b.String() 22 | want := `go_sql_idle{db="mydb",table="mytable"} 4 23 | go_sql_in_use{db="mydb",table="mytable"} 3 24 | go_sql_max_idle_closed{db="mydb",table="mytable"} 7 25 | go_sql_max_idletime_closed{db="mydb",table="mytable"} 8 26 | go_sql_max_lifetime_closed{db="mydb",table="mytable"} 9 27 | go_sql_max_open{db="mydb",table="mytable"} 1 28 | go_sql_open{db="mydb",table="mytable"} 2 29 | go_sql_wait_count{db="mydb",table="mytable"} 5 30 | go_sql_wait_duration_seconds{db="mydb",table="mytable"} 6 31 | ` 32 | if got != want { 33 | t.Fatalf("got %s\n want %s", got, want) 34 | } 35 | } 36 | 37 | func TestPassSQL(t *testing.T) { 38 | _ = makeCollector(t, &sql.DB{}, time.Second, "sql", "best", "label", "value") 39 | } 40 | 41 | func TestBadLabels(t *testing.T) { 42 | defer func() { 43 | if r := recover(); r == nil { 44 | t.Fatal("must panic") 45 | } 46 | }() 47 | 48 | _ = makeCollector(t, &mockStatser{}, time.Second, "mock", "stub", "onlyone") 49 | } 50 | 51 | type mockStatser struct{} 52 | 53 | func (m *mockStatser) Stats() sql.DBStats { 54 | return sql.DBStats{ 55 | MaxOpenConnections: 1, 56 | OpenConnections: 2, 57 | InUse: 3, 58 | Idle: 4, 59 | WaitCount: 5, 60 | WaitDuration: 6 * time.Second, 61 | MaxIdleClosed: 7, 62 | MaxIdleTimeClosed: 8, 63 | MaxLifetimeClosed: 9, 64 | } 65 | } 66 | 67 | func makeCollector(t testing.TB, db Statser, every time.Duration, labels ...string) *Collector { 68 | t.Helper() 69 | ctx, cancel := context.WithCancel(context.Background()) 70 | c := NewCollector(ctx, db, every, labels...) 71 | 72 | t.Cleanup(func() { 73 | cancel() 74 | unregisterCollectorMetrics(labels...) 75 | }) 76 | return c 77 | } 78 | 79 | func unregisterCollectorMetrics(labels ...string) { 80 | names := []string{ 81 | "max_open", 82 | "open", 83 | "in_use", 84 | "idle", 85 | "wait_count", 86 | "wait_duration_seconds", 87 | "max_idle_closed", 88 | "max_idletime_closed", 89 | "max_lifetime_closed", 90 | } 91 | 92 | allLabels := buildLabels(labels...) 93 | for _, name := range names { 94 | metrics.UnregisterMetric(buildName(name, allLabels)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /sqlmetrics.go: -------------------------------------------------------------------------------- 1 | package sqlmetrics 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "strings" 7 | "time" 8 | 9 | "github.com/VictoriaMetrics/metrics" 10 | ) 11 | 12 | const namespace = "go_sql" 13 | 14 | // Statser is an interface that gets sql.DBStats. 15 | // Most of the DB clients support this. 16 | type Statser interface { 17 | Stats() sql.DBStats 18 | } 19 | 20 | // Collector for the sql.DBStats for Prometheus (via VictoriaMetrics client). 21 | type Collector struct { 22 | maxOpen *metrics.Gauge 23 | open *metrics.Gauge 24 | inUse *metrics.Gauge 25 | idle *metrics.Gauge 26 | waitCount *metrics.Counter 27 | waitDuration *metrics.Counter 28 | maxIdleClosed *metrics.Counter 29 | maxIdleTimeClosed *metrics.Counter 30 | maxLifetimeClosed *metrics.Counter 31 | } 32 | 33 | // NewCollector creates a new Collector. 34 | func NewCollector(ctx context.Context, stats Statser, every time.Duration, labels ...string) *Collector { 35 | allLabels := buildLabels(labels...) 36 | 37 | c := &Collector{ 38 | maxOpen: newGauge("max_open", allLabels, func() float64 { 39 | return float64(stats.Stats().MaxOpenConnections) 40 | }), 41 | open: newGauge("open", allLabels, func() float64 { 42 | return float64(stats.Stats().OpenConnections) 43 | }), 44 | inUse: newGauge("in_use", allLabels, func() float64 { 45 | return float64(stats.Stats().InUse) 46 | }), 47 | idle: newGauge("idle", allLabels, func() float64 { 48 | return float64(stats.Stats().Idle) 49 | }), 50 | 51 | waitCount: newCounter("wait_count", allLabels), 52 | waitDuration: newCounter("wait_duration_seconds", allLabels), 53 | maxIdleClosed: newCounter("max_idle_closed", allLabels), 54 | maxIdleTimeClosed: newCounter("max_idletime_closed", allLabels), 55 | maxLifetimeClosed: newCounter("max_lifetime_closed", allLabels), 56 | } 57 | 58 | go func() { 59 | ticker := time.NewTicker(every) 60 | defer ticker.Stop() 61 | 62 | for { 63 | select { 64 | case <-ctx.Done(): 65 | return 66 | case <-ticker.C: 67 | s := stats.Stats() 68 | c.waitCount.Set(uint64(s.WaitCount)) 69 | c.waitDuration.Set(uint64(s.WaitDuration.Seconds())) 70 | c.maxIdleClosed.Set(uint64(s.MaxIdleClosed)) 71 | c.maxIdleTimeClosed.Set(uint64(s.MaxIdleTimeClosed)) 72 | c.maxLifetimeClosed.Set(uint64(s.MaxLifetimeClosed)) 73 | } 74 | } 75 | }() 76 | return c 77 | } 78 | 79 | func newGauge(name, labels string, f func() float64) *metrics.Gauge { 80 | return metrics.NewGauge(buildName(name, labels), f) 81 | } 82 | 83 | func newCounter(name, labels string) *metrics.Counter { 84 | return metrics.NewCounter(buildName(name, labels)) 85 | } 86 | 87 | func buildName(name, labels string) string { 88 | return namespace + "_" + name + labels 89 | } 90 | 91 | func buildLabels(labels ...string) string { 92 | if len(labels) == 0 { 93 | return "" 94 | } 95 | if len(labels)%2 != 0 { 96 | panic("sqlmetrics: incorrect label pairs") 97 | } 98 | 99 | var b strings.Builder 100 | b.WriteByte('{') 101 | for i := 0; i < len(labels); i += 2 { 102 | if i != 0 { 103 | b.WriteString(`",`) 104 | } 105 | b.WriteString(labels[i]) 106 | b.WriteString(`="`) 107 | b.WriteString(labels[i+1]) 108 | } 109 | b.WriteString(`"}`) 110 | return b.String() 111 | } 112 | --------------------------------------------------------------------------------