├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── collector.go ├── collector_test.go ├── go.mod ├── go.sum └── scripts └── travis_checks.sh /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | - GO_VERSION="module" # The oldest version we claim to support 5 | - GO_VERSION="stable" # The latest version 6 | 7 | dist: jammy 8 | 9 | notifications: 10 | email: true 11 | 12 | before_install: 13 | - sudo apt-get update 14 | 15 | install: 16 | # Travis's docs claim to use gimme to install the go version but they in fact use go install. This does not support 17 | # the 1.18.x syntax or any equivalent. Travis's version of gimme is also unmaintained and broken. Use a maintained 18 | # version from the urfave org in its place. 19 | - curl -sL -o ~/bin/gimme https://raw.githubusercontent.com/urfave/gimme/main/gimme 20 | - chmod +x ~/bin/gimme 21 | - gimme version 22 | - eval "$(gimme "$GO_VERSION")" 23 | - go env 24 | - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.64.6 25 | 26 | script: 27 | - ./scripts/travis_checks.sh 28 | 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Issues 2 | If you encounter an issue with the project, you are welcome to submit a [bug report](https://github.com/IBM/pgxpoolprometheus/issues). Before that, please search for similar issues. It's possible that someone has already reported the problem. 3 | 4 | # Coding style 5 | 6 | Write your code in Idiomatic Go. Check out [Effective Go](https://golang.org/doc/effective_go.html) for some tips about how to write Go effectively. 7 | 8 | # Testing changes 9 | 10 | Your code must pass a Travis check before it can be merged. Before you submit a PR, ensure that the tests pass. You can run the tests on your workstation by using the `./scripts/travis_checks.sh` command. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://app.travis-ci.com/github/IBM/pgxpoolprometheus.svg?branch=main)](https://app.travis-ci.com/github/IBM/pgxpoolprometheus) 2 | [![Release](https://img.shields.io/github/v/release/IBM/pgxpoolprometheus)](https://github.com/IBM/pgxpoolprometheus/releases/latest) 3 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/IBM/pgxpoolprometheus) 4 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 5 | 6 | # Prometheus Collector for PGX Pool 7 | 8 | This is a [Prometheus Collector](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#Collector) for [PGX Pool](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool). 9 | 10 | ## Example Usage 11 | 12 | ```go 13 | package main 14 | 15 | import ( 16 | "context" 17 | "log" 18 | "net/http" 19 | "os" 20 | 21 | "github.com/jackc/pgx/v5/pgxpool" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | 25 | "github.com/IBM/pgxpoolprometheus" 26 | ) 27 | 28 | func main() { 29 | pool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL")) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | collector := pgxpoolprometheus.NewCollector(pool, map[string]string{"db_name": "my_db"}) 35 | prometheus.MustRegister(collector) 36 | 37 | http.Handle("/metrics", promhttp.Handler()) 38 | log.Fatal(http.ListenAndServe(":8080", nil)) 39 | } 40 | ``` 41 | 42 | ## Metrics Collected 43 | 44 | This collector provides metrics for all the stats produced by [pgxpool.Stat](https://pkg.go.dev/github.com/jackc/pgx/v5/pgxpool#Stat) all prefixed with `pgxpool`: 45 | 46 | | Name | Description | 47 | |--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| 48 | | pgxpool_acquire_count | Cumulative count of successful acquires from the pool. | 49 | | pgxpool_acquire_duration_ns | Total duration of all successful acquires from the pool in nanoseconds. | 50 | | pgxpool_acquired_conns | Number of currently acquired connections in the pool. | 51 | | pgxpool_canceled_acquire_count | Cumulative count of acquires from the pool that were canceled by a context. | 52 | | pgxpool_constructing_conns | Number of conns with construction in progress in the pool. | 53 | | pgxpool_empty_acquire | Cumulative count of successful acquires from the pool that waited for a resource to be released or constructed because the pool was empty. | 54 | | pgxpool_idle_conns | Number of currently idle conns in the pool. | 55 | | pgxpool_max_conns | Maximum size of the pool. | 56 | | pgxpool_total_conns | Total number of resources currently in the pool. The value is the sum of ConstructingConns, AcquiredConns, and IdleConns. | 57 | 58 | -------------------------------------------------------------------------------- /collector.go: -------------------------------------------------------------------------------- 1 | package pgxpoolprometheus 2 | 3 | /** 4 | * (C) Copyright IBM Corp. 2021. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/jackc/pgx/v5/pgxpool" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | var ( 27 | _ prometheus.Collector = (*Collector)(nil) 28 | _ pgxStat = (*pgxpool.Stat)(nil) 29 | ) 30 | 31 | // pgxStat is an interface implemented by pgxpool.Stat. 32 | type pgxStat interface { 33 | AcquireCount() int64 34 | AcquireDuration() time.Duration 35 | AcquiredConns() int32 36 | CanceledAcquireCount() int64 37 | ConstructingConns() int32 38 | EmptyAcquireCount() int64 39 | IdleConns() int32 40 | MaxConns() int32 41 | TotalConns() int32 42 | NewConnsCount() int64 43 | MaxLifetimeDestroyCount() int64 44 | MaxIdleDestroyCount() int64 45 | } 46 | 47 | // staterFunc should return a struct that implements pgxStat. 48 | type staterFunc func() pgxStat 49 | 50 | // Collector is a prometheus.Collector that will collect the nine statistics produced by pgxpool.Stat. 51 | type Collector struct { 52 | stat staterFunc 53 | 54 | acquireCountDesc *prometheus.Desc 55 | acquireDurationDesc *prometheus.Desc 56 | acquiredConnsDesc *prometheus.Desc 57 | canceledAcquireCountDesc *prometheus.Desc 58 | constructingConnsDesc *prometheus.Desc 59 | emptyAcquireCountDesc *prometheus.Desc 60 | idleConnsDesc *prometheus.Desc 61 | maxConnsDesc *prometheus.Desc 62 | totalConnsDesc *prometheus.Desc 63 | newConnsCount *prometheus.Desc 64 | maxLifetimeDestroyCount *prometheus.Desc 65 | maxIdleDestroyCount *prometheus.Desc 66 | } 67 | 68 | // Stater is a provider of the Stat() function. Implemented by pgxpool.Pool. 69 | type Stater interface { 70 | Stat() *pgxpool.Stat 71 | } 72 | 73 | // NewCollector creates a new Collector to collect stats from pgxpool. 74 | func NewCollector(stater Stater, labels map[string]string) *Collector { 75 | fn := func() pgxStat { return stater.Stat() } 76 | return newCollector(fn, labels) 77 | } 78 | 79 | // newCollector is an internal only constructor for a Collecter. It accepts 80 | // a staterFunc which provides a closure for requesting pgxpool.Stat metrics. 81 | // Labels to each metric and may be nil. A label is recommended when an 82 | // application uses more than one pgxpool.Pool to enable differentiation between them. 83 | func newCollector(fn staterFunc, labels map[string]string) *Collector { 84 | return &Collector{ 85 | stat: fn, 86 | acquireCountDesc: prometheus.NewDesc( 87 | "pgxpool_acquire_count", 88 | "Cumulative count of successful acquires from the pool.", 89 | nil, labels), 90 | acquireDurationDesc: prometheus.NewDesc( 91 | "pgxpool_acquire_duration_ns", 92 | "Total duration of all successful acquires from the pool in nanoseconds.", 93 | nil, labels), 94 | acquiredConnsDesc: prometheus.NewDesc( 95 | "pgxpool_acquired_conns", 96 | "Number of currently acquired connections in the pool.", 97 | nil, labels), 98 | canceledAcquireCountDesc: prometheus.NewDesc( 99 | "pgxpool_canceled_acquire_count", 100 | "Cumulative count of acquires from the pool that were canceled by a context.", 101 | nil, labels), 102 | constructingConnsDesc: prometheus.NewDesc( 103 | "pgxpool_constructing_conns", 104 | "Number of conns with construction in progress in the pool.", 105 | nil, labels), 106 | emptyAcquireCountDesc: prometheus.NewDesc( 107 | "pgxpool_empty_acquire", 108 | "Cumulative count of successful acquires from the pool that waited for a resource to be released or constructed because the pool was empty.", 109 | nil, labels), 110 | idleConnsDesc: prometheus.NewDesc( 111 | "pgxpool_idle_conns", 112 | "Number of currently idle conns in the pool.", 113 | nil, labels), 114 | maxConnsDesc: prometheus.NewDesc( 115 | "pgxpool_max_conns", 116 | "Maximum size of the pool.", 117 | nil, labels), 118 | totalConnsDesc: prometheus.NewDesc( 119 | "pgxpool_total_conns", 120 | "Total number of resources currently in the pool. The value is the sum of ConstructingConns, AcquiredConns, and IdleConns.", 121 | nil, labels), 122 | newConnsCount: prometheus.NewDesc( 123 | "pgxpool_new_conns_count", 124 | "Cumulative count of new connections opened.", 125 | nil, labels), 126 | maxLifetimeDestroyCount: prometheus.NewDesc( 127 | "pgxpool_max_lifetime_destroy_count", 128 | "Cumulative count of connections destroyed because they exceeded MaxConnLifetime. ", 129 | nil, labels), 130 | maxIdleDestroyCount: prometheus.NewDesc( 131 | "pgxpool_max_idle_destroy_count", 132 | "Cumulative count of connections destroyed because they exceeded MaxConnIdleTime.", 133 | nil, labels), 134 | } 135 | } 136 | 137 | // Describe implements the prometheus.Collector interface. 138 | func (c *Collector) Describe(ch chan<- *prometheus.Desc) { 139 | prometheus.DescribeByCollect(c, ch) 140 | } 141 | 142 | // Collect implements the prometheus.Collector interface. 143 | func (c *Collector) Collect(metrics chan<- prometheus.Metric) { 144 | stats := &statWrapper{c.stat()} 145 | metrics <- prometheus.MustNewConstMetric( 146 | c.acquireCountDesc, 147 | prometheus.CounterValue, 148 | stats.acquireCount(), 149 | ) 150 | metrics <- prometheus.MustNewConstMetric( 151 | c.acquireDurationDesc, 152 | prometheus.CounterValue, 153 | stats.acquireDuration(), 154 | ) 155 | metrics <- prometheus.MustNewConstMetric( 156 | c.acquiredConnsDesc, 157 | prometheus.GaugeValue, 158 | stats.acquiredConns(), 159 | ) 160 | metrics <- prometheus.MustNewConstMetric( 161 | c.canceledAcquireCountDesc, 162 | prometheus.CounterValue, 163 | stats.canceledAcquireCount(), 164 | ) 165 | metrics <- prometheus.MustNewConstMetric( 166 | c.constructingConnsDesc, 167 | prometheus.GaugeValue, 168 | stats.constructingConns(), 169 | ) 170 | metrics <- prometheus.MustNewConstMetric( 171 | c.emptyAcquireCountDesc, 172 | prometheus.CounterValue, 173 | stats.emptyAcquireCount(), 174 | ) 175 | metrics <- prometheus.MustNewConstMetric( 176 | c.idleConnsDesc, 177 | prometheus.GaugeValue, 178 | stats.idleConns(), 179 | ) 180 | metrics <- prometheus.MustNewConstMetric( 181 | c.maxConnsDesc, 182 | prometheus.GaugeValue, 183 | stats.maxConns(), 184 | ) 185 | metrics <- prometheus.MustNewConstMetric( 186 | c.totalConnsDesc, 187 | prometheus.GaugeValue, 188 | stats.totalConns(), 189 | ) 190 | metrics <- prometheus.MustNewConstMetric( 191 | c.newConnsCount, 192 | prometheus.CounterValue, 193 | stats.newConnsCount(), 194 | ) 195 | metrics <- prometheus.MustNewConstMetric( 196 | c.maxLifetimeDestroyCount, 197 | prometheus.CounterValue, 198 | stats.maxLifetimeDestroyCount(), 199 | ) 200 | metrics <- prometheus.MustNewConstMetric( 201 | c.maxIdleDestroyCount, 202 | prometheus.CounterValue, 203 | stats.maxIdleDestroyCount(), 204 | ) 205 | } 206 | 207 | // statWrapper is convenience struct that deals with converting 208 | // pgxpool.Stat values to float64 for use by prometheus. 209 | type statWrapper struct { 210 | stats pgxStat 211 | } 212 | 213 | func (w *statWrapper) acquireCount() float64 { 214 | return float64(w.stats.AcquireCount()) 215 | } 216 | func (w *statWrapper) acquireDuration() float64 { 217 | return float64(w.stats.AcquireDuration()) 218 | } 219 | func (w *statWrapper) acquiredConns() float64 { 220 | return float64(w.stats.AcquiredConns()) 221 | } 222 | func (w *statWrapper) canceledAcquireCount() float64 { 223 | return float64(w.stats.CanceledAcquireCount()) 224 | } 225 | func (w *statWrapper) constructingConns() float64 { 226 | return float64(w.stats.ConstructingConns()) 227 | } 228 | func (w *statWrapper) emptyAcquireCount() float64 { 229 | return float64(w.stats.EmptyAcquireCount()) 230 | } 231 | func (w *statWrapper) idleConns() float64 { 232 | return float64(w.stats.IdleConns()) 233 | } 234 | func (w *statWrapper) maxConns() float64 { 235 | return float64(w.stats.MaxConns()) 236 | } 237 | func (w *statWrapper) totalConns() float64 { 238 | return float64(w.stats.TotalConns()) 239 | } 240 | func (w *statWrapper) newConnsCount() float64 { 241 | return float64(w.stats.NewConnsCount()) 242 | } 243 | func (w *statWrapper) maxLifetimeDestroyCount() float64 { 244 | return float64(w.stats.MaxLifetimeDestroyCount()) 245 | } 246 | func (w *statWrapper) maxIdleDestroyCount() float64 { 247 | return float64(w.stats.MaxIdleDestroyCount()) 248 | } 249 | -------------------------------------------------------------------------------- /collector_test.go: -------------------------------------------------------------------------------- 1 | package pgxpoolprometheus 2 | 3 | /** 4 | * (C) Copyright IBM Corp. 2021. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import ( 20 | "strings" 21 | "testing" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus" 25 | dto "github.com/prometheus/client_model/go" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/mock" 28 | ) 29 | 30 | type mockStater struct { 31 | mock.Mock 32 | } 33 | 34 | func (m *mockStater) Stat() pgxStat { 35 | return m.Called().Get(0).(pgxStat) 36 | } 37 | 38 | var ( 39 | _ pgxStat = (*pgxStatMock)(nil) 40 | _ pgxStat = (*noOpStat)(nil) 41 | ) 42 | 43 | type pgxStatMock struct { 44 | mock.Mock 45 | } 46 | 47 | func (m *pgxStatMock) AcquireCount() int64 { 48 | return m.Called().Get(0).(int64) 49 | } 50 | func (m *pgxStatMock) AcquireDuration() time.Duration { 51 | return m.Called().Get(0).(time.Duration) 52 | } 53 | func (m *pgxStatMock) AcquiredConns() int32 { 54 | return m.Called().Get(0).(int32) 55 | } 56 | func (m *pgxStatMock) CanceledAcquireCount() int64 { 57 | return m.Called().Get(0).(int64) 58 | } 59 | func (m *pgxStatMock) ConstructingConns() int32 { 60 | return m.Called().Get(0).(int32) 61 | } 62 | func (m *pgxStatMock) EmptyAcquireCount() int64 { 63 | return m.Called().Get(0).(int64) 64 | } 65 | func (m *pgxStatMock) IdleConns() int32 { 66 | return m.Called().Get(0).(int32) 67 | } 68 | func (m *pgxStatMock) MaxConns() int32 { 69 | return m.Called().Get(0).(int32) 70 | } 71 | func (m *pgxStatMock) TotalConns() int32 { 72 | return m.Called().Get(0).(int32) 73 | } 74 | func (m *pgxStatMock) NewConnsCount() int64 { 75 | return m.Called().Get(0).(int64) 76 | } 77 | func (m *pgxStatMock) MaxLifetimeDestroyCount() int64 { 78 | return m.Called().Get(0).(int64) 79 | } 80 | func (m *pgxStatMock) MaxIdleDestroyCount() int64 { 81 | return m.Called().Get(0).(int64) 82 | } 83 | 84 | type noOpStat struct{} 85 | 86 | func (m noOpStat) AcquireCount() int64 { 87 | return 0 88 | } 89 | func (m noOpStat) AcquireDuration() time.Duration { 90 | return time.Second * 0 91 | } 92 | func (m noOpStat) AcquiredConns() int32 { 93 | return 0 94 | } 95 | func (m noOpStat) CanceledAcquireCount() int64 { 96 | return 0 97 | } 98 | func (m noOpStat) ConstructingConns() int32 { 99 | return 0 100 | } 101 | func (m noOpStat) EmptyAcquireCount() int64 { 102 | return 0 103 | } 104 | func (m noOpStat) IdleConns() int32 { 105 | return 0 106 | } 107 | func (m noOpStat) MaxConns() int32 { 108 | return 0 109 | } 110 | func (m noOpStat) TotalConns() int32 { 111 | return 0 112 | } 113 | func (m noOpStat) NewConnsCount() int64 { 114 | return 0 115 | } 116 | func (m noOpStat) MaxLifetimeDestroyCount() int64 { 117 | return 0 118 | } 119 | func (m noOpStat) MaxIdleDestroyCount() int64 { 120 | return 0 121 | } 122 | 123 | func TestDescribeDescribesAllAvailableStats(t *testing.T) { 124 | labelName := "testLabel" 125 | labelValue := "testLabelValue" 126 | labels := map[string]string{labelName: labelValue} 127 | expectedDescriptorCount := 12 128 | timeout := time.After(time.Second * 5) 129 | stater := &mockStater{} 130 | stater.On("Stat").Return(noOpStat{}) 131 | statFn := func() pgxStat { return stater.Stat() } 132 | testObject := newCollector(statFn, labels) 133 | 134 | ch := make(chan *prometheus.Desc) 135 | go testObject.Describe(ch) 136 | 137 | expectedDescriptorCountRemaining := expectedDescriptorCount 138 | uniqueDescriptors := make(map[string]struct{}) 139 | for { 140 | if expectedDescriptorCountRemaining == 0 { 141 | break 142 | } 143 | select { 144 | case desc := <-ch: 145 | assert.Contains(t, desc.String(), labelName) 146 | assert.Contains(t, desc.String(), labelValue) 147 | uniqueDescriptors[desc.String()] = struct{}{} 148 | expectedDescriptorCountRemaining-- 149 | case <-timeout: 150 | t.Fatalf("Test timed out while there were still %d descriptors expected", expectedDescriptorCountRemaining) 151 | } 152 | } 153 | assert.Equal(t, 0, expectedDescriptorCountRemaining) 154 | assert.Len(t, uniqueDescriptors, expectedDescriptorCount) 155 | } 156 | 157 | func TestCollectCollectsAllAvailableStats(t *testing.T) { 158 | expectedAcquireCount := float64(1) 159 | expectedAcquireDuration := float64(2e+09) 160 | expectedAcquiredConns := float64(3) 161 | expectedCanceledAcquireCount := float64(4) 162 | expectedConstructingConns := float64(5) 163 | expectedEmptyAcquireCount := float64(6) 164 | expectedIdleConns := float64(7) 165 | expectedMaxConns := float64(8) 166 | expectedTotalConns := float64(9) 167 | expectedNewConnsCount := float64(10) 168 | expectedMaxLifetimeDestroyCount := float64(11) 169 | expectedMaxIdleDestroyCount := float64(12) 170 | 171 | mockStats := &pgxStatMock{} 172 | mockStats.On("AcquireCount").Return(int64(1)) 173 | mockStats.On("AcquireDuration").Return(time.Second * 2) 174 | mockStats.On("AcquiredConns").Return(int32(3)) 175 | mockStats.On("CanceledAcquireCount").Return(int64(4)) 176 | mockStats.On("ConstructingConns").Return(int32(5)) 177 | mockStats.On("EmptyAcquireCount").Return(int64(6)) 178 | mockStats.On("IdleConns").Return(int32(7)) 179 | mockStats.On("MaxConns").Return(int32(8)) 180 | mockStats.On("TotalConns").Return(int32(9)) 181 | mockStats.On("NewConnsCount").Return(int64(10)) 182 | mockStats.On("MaxLifetimeDestroyCount").Return(int64(11)) 183 | mockStats.On("MaxIdleDestroyCount").Return(int64(12)) 184 | expectedMetricCount := 12 185 | timeout := time.After(time.Second * 5) 186 | stater := &mockStater{} 187 | stater.On("Stat").Return(mockStats) 188 | staterfn := func() pgxStat { return stater.Stat() } 189 | testObject := newCollector(staterfn, nil) 190 | 191 | ch := make(chan prometheus.Metric) 192 | go testObject.Collect(ch) 193 | 194 | expectedMetricCountRemaining := expectedMetricCount 195 | for { 196 | if expectedMetricCountRemaining == 0 { 197 | break 198 | } 199 | select { 200 | case metric := <-ch: 201 | pb := &dto.Metric{} 202 | metric.Write(pb) 203 | description := metric.Desc().String() 204 | switch { 205 | case strings.Contains(description, "pgxpool_acquire_count"): 206 | assert.Equal(t, expectedAcquireCount, *pb.GetCounter().Value) 207 | case strings.Contains(description, "pgxpool_acquire_duration_ns"): 208 | assert.Equal(t, expectedAcquireDuration, *pb.GetCounter().Value) 209 | case strings.Contains(description, "pgxpool_acquired_conns"): 210 | assert.Equal(t, expectedAcquiredConns, *pb.GetGauge().Value) 211 | case strings.Contains(description, "pgxpool_canceled_acquire_count"): 212 | assert.Equal(t, expectedCanceledAcquireCount, *pb.GetCounter().Value) 213 | case strings.Contains(description, "pgxpool_constructing_conns"): 214 | assert.Equal(t, expectedConstructingConns, *pb.GetGauge().Value) 215 | case strings.Contains(description, "pgxpool_empty_acquire"): 216 | assert.Equal(t, expectedEmptyAcquireCount, *pb.GetCounter().Value) 217 | case strings.Contains(description, "pgxpool_idle_conns"): 218 | assert.Equal(t, expectedIdleConns, *pb.GetGauge().Value) 219 | case strings.Contains(description, "pgxpool_max_conns"): 220 | assert.Equal(t, expectedMaxConns, *pb.GetGauge().Value) 221 | case strings.Contains(description, "pgxpool_total_conns"): 222 | assert.Equal(t, expectedTotalConns, *pb.GetGauge().Value) 223 | case strings.Contains(description, "pgxpool_new_conns_count"): 224 | assert.Equal(t, expectedNewConnsCount, *pb.GetCounter().Value) 225 | case strings.Contains(description, "pgxpool_max_lifetime_destroy_count"): 226 | assert.Equal(t, expectedMaxLifetimeDestroyCount, *pb.GetCounter().Value) 227 | case strings.Contains(description, "pgxpool_max_idle_destroy_count"): 228 | assert.Equal(t, expectedMaxIdleDestroyCount, *pb.GetCounter().Value) 229 | default: 230 | t.Errorf("Unexpected description: %s", description) 231 | } 232 | expectedMetricCountRemaining-- 233 | case <-timeout: 234 | t.Fatalf("Test timed out while there were still %d descriptors expected", expectedMetricCountRemaining) 235 | } 236 | } 237 | assert.Equal(t, 0, expectedMetricCountRemaining) 238 | } 239 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/IBM/pgxpoolprometheus 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/jackc/pgx/v5 v5.7.4 7 | github.com/prometheus/client_golang v1.22.0 8 | github.com/prometheus/client_model v0.6.2 9 | github.com/stretchr/testify v1.10.0 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/jackc/pgpassfile v1.0.0 // indirect 17 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 18 | github.com/jackc/puddle/v2 v2.2.2 // indirect 19 | github.com/kr/text v0.2.0 // indirect 20 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/prometheus/common v0.62.0 // indirect 23 | github.com/prometheus/procfs v0.15.1 // indirect 24 | github.com/stretchr/objx v0.5.2 // indirect 25 | golang.org/x/crypto v0.37.0 // indirect 26 | golang.org/x/sync v0.13.0 // indirect 27 | golang.org/x/sys v0.32.0 // indirect 28 | golang.org/x/text v0.24.0 // indirect 29 | google.golang.org/protobuf v1.36.6 // indirect 30 | gopkg.in/yaml.v3 v3.0.1 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 10 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 11 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 12 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 13 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 14 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 15 | github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= 16 | github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= 17 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 18 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 22 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 23 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 24 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 28 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 29 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 30 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 31 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 32 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 33 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 34 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 35 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 36 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 39 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 40 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 41 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 42 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 43 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 44 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 45 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 46 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 47 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 48 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 49 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 50 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 51 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 52 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 53 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 56 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 57 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 58 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 59 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 60 | -------------------------------------------------------------------------------- /scripts/travis_checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go test 4 | golangci-lint run -E gosec -E revive -E goconst --tests=false 5 | --------------------------------------------------------------------------------