├── .circleci └── config.yml ├── .github ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── README.md ├── SECURITY.md ├── VERSION ├── collector.go ├── collector_test.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── pgbouncer.ini ├── pgbouncer_exporter.go └── struct.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | 4 | orbs: 5 | prometheus: prometheus/prometheus@0.17.1 6 | 7 | executors: 8 | # This must match .promu.yml. 9 | golang: 10 | docker: 11 | - image: cimg/go:1.24 12 | 13 | jobs: 14 | test: 15 | executor: golang 16 | 17 | steps: 18 | - prometheus/setup_environment 19 | - run: make 20 | - prometheus/store_artifact: 21 | file: pgbouncer_exporter 22 | 23 | workflows: 24 | version: 2 25 | pgbouncer_exporter: 26 | jobs: 27 | - test: 28 | filters: 29 | tags: 30 | only: /.*/ 31 | - prometheus/build: 32 | name: build 33 | parallelism: 3 34 | promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386" 35 | filters: 36 | tags: 37 | ignore: /^v.*/ 38 | branches: 39 | ignore: /^(main|master|release-.*|.*build-all.*)$/ 40 | - prometheus/build: 41 | name: build_all 42 | parallelism: 12 43 | filters: 44 | branches: 45 | only: /^(main|master|release-.*|.*build-all.*)$/ 46 | tags: 47 | only: /^v.*/ 48 | - prometheus/publish_master: 49 | context: org-context 50 | docker_hub_organization: prometheuscommunity 51 | quay_io_organization: prometheuscommunity 52 | requires: 53 | - test 54 | - build_all 55 | filters: 56 | branches: 57 | only: master 58 | - prometheus/publish_release: 59 | context: org-context 60 | docker_hub_organization: prometheuscommunity 61 | quay_io_organization: prometheuscommunity 62 | requires: 63 | - test 64 | - build_all 65 | filters: 66 | tags: 67 | only: /^v.*/ 68 | branches: 69 | ignore: /.*/ 70 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | with: 23 | persist-credentials: false 24 | - name: Set docker hub repo name 25 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 26 | - name: Push README to Dockerhub 27 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 28 | env: 29 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 30 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 31 | with: 32 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 33 | provider: dockerhub 34 | short_description: ${{ env.DOCKER_REPO_NAME }} 35 | # Empty string results in README-containers.md being pushed if it 36 | # exists. Otherwise, README.md is pushed. 37 | readme_file: '' 38 | 39 | PushQuayIoReadme: 40 | runs-on: ubuntu-latest 41 | name: Push README to quay.io 42 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 43 | steps: 44 | - name: git checkout 45 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | with: 47 | persist-credentials: false 48 | - name: Set quay.io org name 49 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 50 | - name: Set quay.io repo name 51 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 52 | - name: Push README to quay.io 53 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 54 | env: 55 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 56 | with: 57 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 58 | provider: quay 59 | # Empty string results in README-containers.md being pushed if it 60 | # exists. Otherwise, README.md is pushed. 61 | readme_file: '' 62 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Install Go 29 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 30 | with: 31 | go-version: 1.24.x 32 | - name: Install snmp_exporter/generator dependencies 33 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 34 | if: github.repository == 'prometheus/snmp_exporter' 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 37 | with: 38 | args: --verbose 39 | version: v2.1.5 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | dependencies-stamp 24 | /pgbouncer_exporter 25 | /.build 26 | /.deps 27 | /.release 28 | /.tarballs 29 | /vendor 30 | 31 | # Intellij 32 | 33 | /.idea 34 | *.iml 35 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - misspell 5 | - revive 6 | - sloglint 7 | exclusions: 8 | generated: lax 9 | presets: 10 | - comments 11 | - common-false-positives 12 | - legacy 13 | - std-error-handling 14 | rules: 15 | - linters: 16 | - errcheck 17 | path: _test.go 18 | paths: 19 | - third_party$ 20 | - builtin$ 21 | - examples$ 22 | formatters: 23 | exclusions: 24 | generated: lax 25 | paths: 26 | - third_party$ 27 | - builtin$ 28 | - examples$ 29 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # This must match .circle/config.yml. 3 | version: 1.24 4 | repository: 5 | path: github.com/prometheus-community/pgbouncer_exporter 6 | build: 7 | binaries: 8 | - name: pgbouncer_exporter 9 | ldflags: | 10 | -X github.com/prometheus/common/version.Version={{.Version}} 11 | -X github.com/prometheus/common/version.Revision={{.Revision}} 12 | -X github.com/prometheus/common/version.Branch={{.Branch}} 13 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 14 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 15 | tarball: 16 | files: 17 | - LICENSE 18 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master / unreleased 2 | 3 | ## 0.10.2 / 2024-10-18 4 | 5 | * [BUGFIX] Fix wrong logging level of "Starting scrape" message #175 6 | 7 | ## 0.10.1 / 2024-10-14 8 | 9 | * [BUGFIX] Revert auth_type guage #173 10 | 11 | ## 0.10.0 / 2024-10-07 12 | 13 | * [CHANGE] Switch logging to slog #167 14 | * [ENHANCEMENT] Add auth_type to config collector #169 15 | 16 | ## 0.9.0 / 2024-08-01 17 | 18 | * [FEATURE] Allow connection config via environment variable #159 19 | 20 | ## 0.8.0 / 2024-04-02 21 | 22 | * [ENHANCEMENT] Publish server/client cancel statistics. #1144 23 | 24 | ## 0.7.0 / 2023-06-29 25 | 26 | * [CHANGE] Require Go 1.19 and update CI with Go 1.20 #120 27 | * [CHANGE] Synchronize common files from prometheus/prometheus #123 28 | 29 | ## 0.6.0 / 2023-01-27 30 | 31 | * [FEATURE] Add config metrics #93 32 | * [FEATURE] Add TLS and Basic auth to the metrics endpoint #101 33 | 34 | ## 0.5.1 / 2022-10-03 35 | 36 | * No changes, just retagging due to a VERSION fix. 37 | 38 | ## 0.5.0 / 2022-10-03 39 | 40 | * [CHANGE] Update Go to 1.18. 41 | * [CHANGE] Update upstream dependencies. 42 | 43 | ## 0.4.1 / 2022-01-27 44 | 45 | * [BUGFIX] Fix startup log message typo #50 46 | * [BUGFIX] Fix typo in reserve_pool metric #67 47 | 48 | ## 0.4.0 / 2020-07-09 49 | 50 | Counter names have been updated to match Prometheus naming conventions. 51 | * `pgbouncer_stats_queries_duration_seconds` -> `pgbouncer_stats_queries_duration_seconds_total` 52 | * `pgbouncer_stats_client_wait_seconds` -> `pgbouncer_stats_client_wait_seconds_total` 53 | * `pgbouncer_stats_server_in_transaction_seconds` -> `pgbouncer_stats_server_in_transaction_seconds_total` 54 | 55 | * [CHANGE] Cleanup exporter metrics #33 56 | * [CHANGE] Update counter metric names #35 57 | * [FEATURE] Add support for SHOW LISTS metrics #36 58 | 59 | ## 0.3.0 / 2020-05-27 60 | 61 | * [CHANGE] Switch logging to promlog #29 62 | * [FEATURE] Add pgbouncer process metrics #27 63 | 64 | ## 0.2.0 / 2020-04-29 65 | 66 | * [BUGFIX] Fix byte slice values not receiving conversion factor #18 67 | 68 | Initial prometheus-community release. 69 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/pgbouncer_exporter /bin/pgbouncer_exporter 9 | COPY LICENSE /LICENSE 10 | 11 | USER nobody 12 | ENTRYPOINT ["/bin/pgbouncer_exporter"] 13 | EXPOSE 9127 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Kristoffer K Larsen 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 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Ben Kochie @SuperQ 2 | * Stan Hu @stanhu 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 16 | DOCKER_REPO ?= prometheuscommunity 17 | 18 | include Makefile.common 19 | 20 | DOCKER_IMAGE_NAME ?= pgbouncer-exporter 21 | -------------------------------------------------------------------------------- /Makefile.common: -------------------------------------------------------------------------------- 1 | # Copyright 2018 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | 15 | # A common Makefile that includes rules to be reused in different prometheus projects. 16 | # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! 17 | 18 | # Example usage : 19 | # Create the main Makefile in the root project directory. 20 | # include Makefile.common 21 | # customTarget: 22 | # @echo ">> Running customTarget" 23 | # 24 | 25 | # Ensure GOBIN is not set during build so that promu is installed to the correct path 26 | unexport GOBIN 27 | 28 | GO ?= go 29 | GOFMT ?= $(GO)fmt 30 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 31 | GOOPTS ?= 32 | GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) 33 | GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) 34 | 35 | GO_VERSION ?= $(shell $(GO) version) 36 | GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) 37 | PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') 38 | 39 | PROMU := $(FIRST_GOPATH)/bin/promu 40 | pkgs = ./... 41 | 42 | ifeq (arm, $(GOHOSTARCH)) 43 | GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) 44 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) 45 | else 46 | GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) 47 | endif 48 | 49 | GOTEST := $(GO) test 50 | GOTEST_DIR := 51 | ifneq ($(CIRCLE_JOB),) 52 | ifneq ($(shell command -v gotestsum 2> /dev/null),) 53 | GOTEST_DIR := test-results 54 | GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- 55 | endif 56 | endif 57 | 58 | PROMU_VERSION ?= 0.17.0 59 | PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz 60 | 61 | SKIP_GOLANGCI_LINT := 62 | GOLANGCI_LINT := 63 | GOLANGCI_LINT_OPTS ?= 64 | GOLANGCI_LINT_VERSION ?= v2.1.5 65 | GOLANGCI_FMT_OPTS ?= 66 | # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. 67 | # windows isn't included here because of the path separator being different. 68 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) 69 | ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) 70 | # If we're in CI and there is an Actions file, that means the linter 71 | # is being run in Actions, so we don't need to run it here. 72 | ifneq (,$(SKIP_GOLANGCI_LINT)) 73 | GOLANGCI_LINT := 74 | else ifeq (,$(CIRCLE_JOB)) 75 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 76 | else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) 77 | GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint 78 | endif 79 | endif 80 | endif 81 | 82 | PREFIX ?= $(shell pwd) 83 | BIN_DIR ?= $(shell pwd) 84 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 85 | DOCKERFILE_PATH ?= ./Dockerfile 86 | DOCKERBUILD_CONTEXT ?= ./ 87 | DOCKER_REPO ?= prom 88 | 89 | DOCKER_ARCHS ?= amd64 90 | 91 | BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) 92 | PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) 93 | TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) 94 | 95 | SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) 96 | 97 | ifeq ($(GOHOSTARCH),amd64) 98 | ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) 99 | # Only supported on amd64 100 | test-flags := -race 101 | endif 102 | endif 103 | 104 | # This rule is used to forward a target like "build" to "common-build". This 105 | # allows a new "build" target to be defined in a Makefile which includes this 106 | # one and override "common-build" without override warnings. 107 | %: common-% ; 108 | 109 | .PHONY: common-all 110 | common-all: precheck style check_license lint yamllint unused build test 111 | 112 | .PHONY: common-style 113 | common-style: 114 | @echo ">> checking code style" 115 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 116 | if [ -n "$${fmtRes}" ]; then \ 117 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 118 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 119 | exit 1; \ 120 | fi 121 | 122 | .PHONY: common-check_license 123 | common-check_license: 124 | @echo ">> checking license header" 125 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 126 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 127 | done); \ 128 | if [ -n "$${licRes}" ]; then \ 129 | echo "license header checking failed:"; echo "$${licRes}"; \ 130 | exit 1; \ 131 | fi 132 | 133 | .PHONY: common-deps 134 | common-deps: 135 | @echo ">> getting dependencies" 136 | $(GO) mod download 137 | 138 | .PHONY: update-go-deps 139 | update-go-deps: 140 | @echo ">> updating Go dependencies" 141 | @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ 142 | $(GO) get -d $$m; \ 143 | done 144 | $(GO) mod tidy 145 | 146 | .PHONY: common-test-short 147 | common-test-short: $(GOTEST_DIR) 148 | @echo ">> running short tests" 149 | $(GOTEST) -short $(GOOPTS) $(pkgs) 150 | 151 | .PHONY: common-test 152 | common-test: $(GOTEST_DIR) 153 | @echo ">> running all tests" 154 | $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) 155 | 156 | $(GOTEST_DIR): 157 | @mkdir -p $@ 158 | 159 | .PHONY: common-format 160 | common-format: $(GOLANGCI_LINT) 161 | @echo ">> formatting code" 162 | $(GO) fmt $(pkgs) 163 | ifdef GOLANGCI_LINT 164 | @echo ">> formatting code with golangci-lint" 165 | $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) 166 | endif 167 | 168 | .PHONY: common-vet 169 | common-vet: 170 | @echo ">> vetting code" 171 | $(GO) vet $(GOOPTS) $(pkgs) 172 | 173 | .PHONY: common-lint 174 | common-lint: $(GOLANGCI_LINT) 175 | ifdef GOLANGCI_LINT 176 | @echo ">> running golangci-lint" 177 | $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) 178 | endif 179 | 180 | .PHONY: common-lint-fix 181 | common-lint-fix: $(GOLANGCI_LINT) 182 | ifdef GOLANGCI_LINT 183 | @echo ">> running golangci-lint fix" 184 | $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) 185 | endif 186 | 187 | .PHONY: common-yamllint 188 | common-yamllint: 189 | @echo ">> running yamllint on all YAML files in the repository" 190 | ifeq (, $(shell command -v yamllint 2> /dev/null)) 191 | @echo "yamllint not installed so skipping" 192 | else 193 | yamllint . 194 | endif 195 | 196 | # For backward-compatibility. 197 | .PHONY: common-staticcheck 198 | common-staticcheck: lint 199 | 200 | .PHONY: common-unused 201 | common-unused: 202 | @echo ">> running check for unused/missing packages in go.mod" 203 | $(GO) mod tidy 204 | @git diff --exit-code -- go.sum go.mod 205 | 206 | .PHONY: common-build 207 | common-build: promu 208 | @echo ">> building binaries" 209 | $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) 210 | 211 | .PHONY: common-tarball 212 | common-tarball: promu 213 | @echo ">> building release tarball" 214 | $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 215 | 216 | .PHONY: common-docker-repo-name 217 | common-docker-repo-name: 218 | @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" 219 | 220 | .PHONY: common-docker $(BUILD_DOCKER_ARCHS) 221 | common-docker: $(BUILD_DOCKER_ARCHS) 222 | $(BUILD_DOCKER_ARCHS): common-docker-%: 223 | docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ 224 | -f $(DOCKERFILE_PATH) \ 225 | --build-arg ARCH="$*" \ 226 | --build-arg OS="linux" \ 227 | $(DOCKERBUILD_CONTEXT) 228 | 229 | .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) 230 | common-docker-publish: $(PUBLISH_DOCKER_ARCHS) 231 | $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: 232 | docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" 233 | 234 | DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) 235 | .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) 236 | common-docker-tag-latest: $(TAG_DOCKER_ARCHS) 237 | $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: 238 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest" 239 | docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)" 240 | 241 | .PHONY: common-docker-manifest 242 | common-docker-manifest: 243 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG)) 244 | DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" 245 | 246 | .PHONY: promu 247 | promu: $(PROMU) 248 | 249 | $(PROMU): 250 | $(eval PROMU_TMP := $(shell mktemp -d)) 251 | curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) 252 | mkdir -p $(FIRST_GOPATH)/bin 253 | cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu 254 | rm -r $(PROMU_TMP) 255 | 256 | .PHONY: common-proto 257 | common-proto: 258 | @echo ">> generating code from proto files" 259 | @./scripts/genproto.sh 260 | 261 | ifdef GOLANGCI_LINT 262 | $(GOLANGCI_LINT): 263 | mkdir -p $(FIRST_GOPATH)/bin 264 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ 265 | | sed -e '/install -d/d' \ 266 | | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) 267 | endif 268 | 269 | .PHONY: precheck 270 | precheck:: 271 | 272 | define PRECHECK_COMMAND_template = 273 | precheck:: $(1)_precheck 274 | 275 | PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) 276 | .PHONY: $(1)_precheck 277 | $(1)_precheck: 278 | @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ 279 | echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ 280 | exit 1; \ 281 | fi 282 | endef 283 | 284 | govulncheck: install-govulncheck 285 | govulncheck ./... 286 | 287 | install-govulncheck: 288 | command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest 289 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PgBouncer exporter 2 | [![Build Status](https://circleci.com/gh/prometheus-community/pgbouncer_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/pgbouncer_exporter) 3 | 4 | Prometheus exporter for PgBouncer. 5 | Exports metrics at `9127/metrics` 6 | 7 | ## Requirements 8 | 9 | - PgBouncer 1.8 or higher, since PgBouncer exporter 0.11.0 10 | 11 | ## Building and running 12 | 13 | make build 14 | ./pgbouncer_exporter 15 | 16 | To see all available configuration flags: 17 | 18 | ./pgbouncer_exporter -h 19 | 20 | ## PGBouncer configuration 21 | 22 | The pgbouncer\_exporter requires a configuration change to pgbouncer to ignore a PostgreSQL driver connection parameter. In the `pgbouncer.ini` please include this option: 23 | 24 | ignore_startup_parameters = extra_float_digits 25 | 26 | ## Run with docker 27 | 28 | ``` 29 | docker run prometheuscommunity/pgbouncer-exporter 30 | ``` 31 | 32 | ## Metrics 33 | 34 | |PgBouncer column|Prometheus Metric|Description| 35 | |----------------|-----------------|-----------| 36 | stats_total_query_count | pgbouncer_stats_queries_pooled_total | Total number of SQL queries pooled 37 | stats.total_query_time | pgbouncer_stats_queries_duration_seconds_total | Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries 38 | stats.total_received | pgbouncer_stats_received_bytes_total | Total volume in bytes of network traffic received by pgbouncer, shown as bytes 39 | stats.total_requests | pgbouncer_stats_queries_total | Total number of SQL requests pooled by pgbouncer, shown as requests 40 | stats.total_sent | pgbouncer_stats_sent_bytes_total | Total volume in bytes of network traffic sent by pgbouncer, shown as bytes 41 | stats.total_wait_time | pgbouncer_stats_client_wait_seconds_total | Time spent by clients waiting for a server in seconds 42 | stats.total_xact_count | pgbouncer_stats_sql_transactions_pooled_total | Total number of SQL transactions pooled 43 | stats.total_xact_time | pgbouncer_stats_server_in_transaction_seconds_total | Total number of seconds spent by pgbouncer when connected to PostgreSQL in a transaction, either idle in transaction or executing queries 44 | pools.cl_active | pgbouncer_pools_client_active_connections | Client connections linked to server connection and able to process queries, shown as connection 45 | pools.cl_waiting | pgbouncer_pools_client_waiting_connections | Client connections waiting on a server connection, shown as connection 46 | pools.sv_active | pgbouncer_pools_server_active_connections | Server connections linked to a client connection, shown as connection 47 | pools.sv_idle | pgbouncer_pools_server_idle_connections | Server connections idle and ready for a client query, shown as connection 48 | pools.sv_used | pgbouncer_pools_server_used_connections | Server connections idle more than server_check_delay, needing server_check_query, shown as connection 49 | pools.sv_tested | pgbouncer_pools_server_testing_connections | Server connections currently running either server_reset_query or server_check_query, shown as connection 50 | pools.sv_login | pgbouncer_pools_server_login_connections | Server connections currently in the process of logging in, shown as connection 51 | pools.maxwait | pgbouncer_pools_client_maxwait_seconds | Age of oldest unserved client connection, shown as second 52 | config.max_client_conn | pgbouncer_config_max_client_connections | Configured maximum number of client connections 53 | config.max_user_connections | pgbouncer_config_max_user_connections | Configured maximum number of server connections per user 54 | 55 | ## TLS and basic authentication 56 | 57 | The pgbouncer exporter supports TLS and basic authentication. 58 | 59 | To use TLS and/or basic authentication, you need to pass a configuration file 60 | using the `--web.config.file` parameter. The format of the file is described 61 | [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). 62 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.10.2 2 | -------------------------------------------------------------------------------- /collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "database/sql" 18 | "errors" 19 | "fmt" 20 | "log/slog" 21 | "math" 22 | "os" 23 | "strconv" 24 | "time" 25 | "unicode/utf8" 26 | 27 | _ "github.com/lib/pq" 28 | "github.com/prometheus/client_golang/prometheus" 29 | ) 30 | 31 | var ( 32 | metricMaps = map[string]map[string]ColumnMapping{ 33 | "databases": { 34 | "name": {LABEL, "N/A", 1, "N/A"}, 35 | "host": {LABEL, "N/A", 1, "N/A"}, 36 | "port": {LABEL, "N/A", 1, "N/A"}, 37 | "database": {LABEL, "N/A", 1, "N/A"}, 38 | "force_user": {LABEL, "N/A", 1, "N/A"}, 39 | "pool_size": {GAUGE, "pool_size", 1, "Maximum number of server connections"}, 40 | "reserve_pool": {GAUGE, "reserve_pool", 1, "Maximum number of additional connections for this database"}, 41 | "pool_mode": {LABEL, "N/A", 1, "N/A"}, 42 | "max_connections": {GAUGE, "max_connections", 1, "Maximum number of allowed connections for this database"}, 43 | "current_connections": {GAUGE, "current_connections", 1, "Current number of connections for this database"}, 44 | "paused": {GAUGE, "paused", 1, "1 if this database is currently paused, else 0"}, 45 | "disabled": {GAUGE, "disabled", 1, "1 if this database is currently disabled, else 0"}, 46 | }, 47 | "stats_totals": { 48 | "database": {LABEL, "N/A", 1, "N/A"}, 49 | "query_count": {COUNTER, "queries_pooled_total", 1, "Total number of SQL queries pooled"}, 50 | "query_time": {COUNTER, "queries_duration_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries"}, 51 | "bytes_received": {COUNTER, "received_bytes_total", 1, "Total volume in bytes of network traffic received by pgbouncer, shown as bytes"}, 52 | "requests": {COUNTER, "queries_total", 1, "Total number of SQL requests pooled by pgbouncer, shown as requests"}, 53 | "bytes_sent": {COUNTER, "sent_bytes_total", 1, "Total volume in bytes of network traffic sent by pgbouncer, shown as bytes"}, 54 | "wait_time": {COUNTER, "client_wait_seconds_total", 1e-6, "Time spent by clients waiting for a server in seconds"}, 55 | "xact_count": {COUNTER, "sql_transactions_pooled_total", 1, "Total number of SQL transactions pooled"}, 56 | "xact_time": {COUNTER, "server_in_transaction_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when connected to PostgreSQL in a transaction, either idle in transaction or executing queries"}, 57 | "client_parse_count": {COUNTER, "client_parses_total", 1, "Total number of prepared statement Parse messages received from clients"}, 58 | "server_parse_count": {COUNTER, "server_parses_total", 1, "Total number of prepared statement Parse messages sent by pgbouncer to PostgreSQL"}, 59 | "bind_count": {COUNTER, "binds_total", 1, "Total number of prepared statements readied for execution with a Bind message"}, 60 | }, 61 | "pools": { 62 | "database": {LABEL, "N/A", 1, "N/A"}, 63 | "user": {LABEL, "N/A", 1, "N/A"}, 64 | "cl_active": {GAUGE, "client_active_connections", 1, "Client connections linked to server connection and able to process queries, shown as connection"}, 65 | "cl_active_cancel_req": {GAUGE, "client_active_cancel_connections", 1, "Client connections that have forwarded query cancellations to the server and are waiting for the server response"}, 66 | "cl_waiting": {GAUGE, "client_waiting_connections", 1, "Client connections waiting on a server connection, shown as connection"}, 67 | "cl_waiting_cancel_req": {GAUGE, "client_waiting_cancel_connections", 1, "Client connections that have not forwarded query cancellations to the server yet"}, 68 | "sv_active": {GAUGE, "server_active_connections", 1, "Server connections linked to a client connection, shown as connection"}, 69 | "sv_active_cancel": {GAUGE, "server_active_cancel_connections", 1, "Server connections that are currently forwarding a cancel request."}, 70 | "sv_being_canceled": {GAUGE, "server_being_canceled_connections", 1, "Servers that normally could become idle but are waiting to do so until all in-flight cancel requests have completed that were sent to cancel a query on this server."}, 71 | "sv_idle": {GAUGE, "server_idle_connections", 1, "Server connections idle and ready for a client query, shown as connection"}, 72 | "sv_used": {GAUGE, "server_used_connections", 1, "Server connections idle more than server_check_delay, needing server_check_query, shown as connection"}, 73 | "sv_tested": {GAUGE, "server_testing_connections", 1, "Server connections currently running either server_reset_query or server_check_query, shown as connection"}, 74 | "sv_login": {GAUGE, "server_login_connections", 1, "Server connections currently in the process of logging in, shown as connection"}, 75 | "maxwait": {GAUGE, "client_maxwait_seconds", 1, "Age of oldest unserved client connection, shown as second"}, 76 | }, 77 | } 78 | 79 | listsMap = map[string]*(prometheus.Desc){ 80 | "databases": prometheus.NewDesc( 81 | prometheus.BuildFQName(namespace, "", "databases"), 82 | "Count of databases", nil, nil), 83 | "users": prometheus.NewDesc( 84 | prometheus.BuildFQName(namespace, "", "users"), 85 | "Count of users", nil, nil), 86 | "pools": prometheus.NewDesc( 87 | prometheus.BuildFQName(namespace, "", "pools"), 88 | "Count of pools", nil, nil), 89 | "free_clients": prometheus.NewDesc( 90 | prometheus.BuildFQName(namespace, "", "free_clients"), 91 | "Count of free clients", nil, nil), 92 | "used_clients": prometheus.NewDesc( 93 | prometheus.BuildFQName(namespace, "", "used_clients"), 94 | "Count of used clients", nil, nil), 95 | "login_clients": prometheus.NewDesc( 96 | prometheus.BuildFQName(namespace, "", "login_clients"), 97 | "Count of clients in login state", nil, nil), 98 | "free_servers": prometheus.NewDesc( 99 | prometheus.BuildFQName(namespace, "", "free_servers"), 100 | "Count of free servers", nil, nil), 101 | "used_servers": prometheus.NewDesc( 102 | prometheus.BuildFQName(namespace, "", "used_servers"), 103 | "Count of used servers", nil, nil), 104 | "dns_names": prometheus.NewDesc( 105 | prometheus.BuildFQName(namespace, "", "cached_dns_names"), 106 | "Count of DNS names in the cache", nil, nil), 107 | "dns_zones": prometheus.NewDesc( 108 | prometheus.BuildFQName(namespace, "", "cached_dns_zones"), 109 | "Count of DNS zones in the cache", nil, nil), 110 | "dns_queries": prometheus.NewDesc( 111 | prometheus.BuildFQName(namespace, "", "in_flight_dns_queries"), 112 | "Count of in-flight DNS queries", nil, nil), 113 | } 114 | 115 | configMap = map[string]*(prometheus.Desc){ 116 | "max_client_conn": prometheus.NewDesc( 117 | prometheus.BuildFQName(namespace, "config", "max_client_connections"), 118 | "Config maximum number of client connections", nil, nil), 119 | "max_user_connections": prometheus.NewDesc( 120 | prometheus.BuildFQName(namespace, "config", "max_user_connections"), 121 | "Config maximum number of server connections per user", nil, nil), 122 | } 123 | ) 124 | 125 | // Metric descriptors. 126 | var ( 127 | bouncerVersionDesc = prometheus.NewDesc( 128 | prometheus.BuildFQName(namespace, "version", "info"), 129 | "The pgbouncer version info", 130 | []string{"version"}, nil, 131 | ) 132 | scrapeSuccessDesc = prometheus.NewDesc( 133 | prometheus.BuildFQName(namespace, "", "up"), 134 | "The pgbouncer scrape succeeded", 135 | nil, nil, 136 | ) 137 | ) 138 | 139 | func NewExporter(connectionString string, namespace string, logger *slog.Logger) *Exporter { 140 | 141 | db, err := getDB(connectionString) 142 | 143 | if err != nil { 144 | logger.Error("error setting up DB connection", "err", err.Error()) 145 | os.Exit(1) 146 | } 147 | 148 | return &Exporter{ 149 | metricMap: makeDescMap(metricMaps, namespace, logger), 150 | db: db, 151 | logger: logger, 152 | } 153 | } 154 | 155 | // Query SHOW LISTS, which has a series of rows, not columns. 156 | func queryShowLists(ch chan<- prometheus.Metric, db *sql.DB, logger *slog.Logger) error { 157 | rows, err := db.Query("SHOW LISTS;") 158 | if err != nil { 159 | return fmt.Errorf("error running SHOW LISTS on database: %w", err) 160 | } 161 | defer rows.Close() 162 | 163 | columnNames, err := rows.Columns() 164 | if err != nil || len(columnNames) != 2 { 165 | return fmt.Errorf("error retrieving columns list from SHOW LISTS: %w", err) 166 | } 167 | 168 | var list string 169 | var items sql.RawBytes 170 | for rows.Next() { 171 | if err = rows.Scan(&list, &items); err != nil { 172 | return fmt.Errorf("error retrieving SHOW LISTS rows: %w", err) 173 | } 174 | value, err := strconv.ParseFloat(string(items), 64) 175 | if err != nil { 176 | return fmt.Errorf("error parsing SHOW LISTS column: %v, error: %w", list, err) 177 | } 178 | if metric, ok := listsMap[list]; ok { 179 | ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, value) 180 | } else { 181 | logger.Debug("SHOW LISTS unknown list", "list", list) 182 | } 183 | } 184 | return nil 185 | } 186 | 187 | // Query SHOW CONFIG, which has a series of rows, not columns. 188 | func queryShowConfig(ch chan<- prometheus.Metric, db *sql.DB, logger *slog.Logger) error { 189 | rows, err := db.Query("SHOW CONFIG;") 190 | if err != nil { 191 | return fmt.Errorf("error running SHOW CONFIG on database: %w", err) 192 | } 193 | defer rows.Close() 194 | 195 | columnNames, err := rows.Columns() 196 | numColumns := len(columnNames) 197 | if err != nil { 198 | return fmt.Errorf("error retrieving columns list from SHOW CONFIG: %w", err) 199 | } 200 | 201 | exposedConfig := make(map[string]bool) 202 | for configKey := range configMap { 203 | exposedConfig[configKey] = true 204 | } 205 | 206 | var key string 207 | var values sql.RawBytes 208 | var defaultValue sql.RawBytes 209 | var changeable string 210 | for rows.Next() { 211 | switch numColumns { 212 | case 3: 213 | if err = rows.Scan(&key, &values, &changeable); err != nil { 214 | return fmt.Errorf("error retrieving SHOW CONFIG rows: %w", err) 215 | } 216 | case 4: 217 | if err = rows.Scan(&key, &values, &defaultValue, &changeable); err != nil { 218 | return fmt.Errorf("error retrieving SHOW CONFIG rows: %w", err) 219 | } 220 | default: 221 | return fmt.Errorf("invalid number of SHOW CONFIG columns: %d", numColumns) 222 | } 223 | 224 | if !exposedConfig[key] { 225 | continue 226 | } 227 | 228 | value, err := strconv.ParseFloat(string(values), 64) 229 | if err != nil { 230 | return fmt.Errorf("error parsing SHOW CONFIG column: %v, error: %w ", key, err) 231 | } 232 | if metric, ok := configMap[key]; ok { 233 | ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, value) 234 | } else { 235 | logger.Debug("SHOW CONFIG unknown config", "config", key) 236 | } 237 | } 238 | return nil 239 | } 240 | 241 | // Query within a namespace mapping and emit metrics. Returns fatal errors if 242 | // the scrape fails, and a slice of errors if they were non-fatal. 243 | func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace string, mapping MetricMapNamespace, logger *slog.Logger) ([]error, error) { 244 | query := fmt.Sprintf("SHOW %s;", namespace) 245 | 246 | // Don't fail on a bad scrape of one metric 247 | rows, err := db.Query(query) 248 | if err != nil { 249 | return []error{}, fmt.Errorf("error running query on database: %v, error: %w", namespace, err) 250 | } 251 | 252 | defer rows.Close() 253 | 254 | var columnNames []string 255 | columnNames, err = rows.Columns() 256 | if err != nil { 257 | return []error{}, fmt.Errorf("error retrieving column list for: %v, error: %w", namespace, err) 258 | } 259 | 260 | // Make a lookup map for the column indices 261 | var columnIdx = make(map[string]int, len(columnNames)) 262 | for i, n := range columnNames { 263 | columnIdx[n] = i 264 | } 265 | 266 | var columnData = make([]interface{}, len(columnNames)) 267 | var scanArgs = make([]interface{}, len(columnNames)) 268 | for i := range columnData { 269 | scanArgs[i] = &columnData[i] 270 | } 271 | 272 | nonfatalErrors := []error{} 273 | 274 | for rows.Next() { 275 | labelValues := make([]string, len(mapping.labels)) 276 | err = rows.Scan(scanArgs...) 277 | if err != nil { 278 | return []error{}, fmt.Errorf("error retrieving rows: %v, error: %w", namespace, err) 279 | } 280 | 281 | for i, label := range mapping.labels { 282 | for idx, columnName := range columnNames { 283 | if columnName == label { 284 | switch v := columnData[idx].(type) { 285 | case int: 286 | labelValues[i] = strconv.Itoa(columnData[idx].(int)) 287 | case int64: 288 | labelValues[i] = strconv.Itoa(int(columnData[idx].(int64))) 289 | case float64: 290 | labelValues[i] = fmt.Sprintf("%f", columnData[idx].(float64)) 291 | case string: 292 | labelValues[i] = columnData[idx].(string) 293 | case nil: 294 | labelValues[i] = "" 295 | default: 296 | nonfatalErrors = append(nonfatalErrors, fmt.Errorf("column %s in %s has an unhandled type %v for label: %s ", columnName, namespace, v, columnData[idx])) 297 | labelValues[i] = "" 298 | continue 299 | } 300 | 301 | // Prometheus will fail hard if the database and usernames are not UTF-8 302 | if !utf8.ValidString(labelValues[i]) { 303 | nonfatalErrors = append(nonfatalErrors, fmt.Errorf("column %s in %s has an invalid UTF-8 for a label: %s ", columnName, namespace, columnData[idx])) 304 | labelValues[i] = "" 305 | continue 306 | } 307 | } 308 | } 309 | } 310 | 311 | // Loop over column names, and match to scan data. Unknown columns 312 | // will be filled with an untyped metric number *if* they can be 313 | // converted to float64s. NULLs are allowed and treated as NaN. 314 | for idx, columnName := range columnNames { 315 | if metricMapping, ok := mapping.columnMappings[columnName]; ok { 316 | // Is this a metricy metric? 317 | if metricMapping.discard { 318 | continue 319 | } 320 | 321 | value, ok := metricMapping.conversion(columnData[idx]) 322 | if !ok { 323 | nonfatalErrors = append(nonfatalErrors, fmt.Errorf("unexpected error parsing namespace: %v, column: %v, index: %v", namespace, columnName, columnData[idx])) 324 | continue 325 | } 326 | // Generate the metric 327 | ch <- prometheus.MustNewConstMetric(metricMapping.desc, metricMapping.vtype, value, labelValues...) 328 | } 329 | } 330 | } 331 | if err := rows.Err(); err != nil { 332 | logger.Error("Failed scaning all rows", "err", err.Error()) 333 | nonfatalErrors = append(nonfatalErrors, fmt.Errorf("failed to consume all rows due to: %w", err)) 334 | } 335 | return nonfatalErrors, nil 336 | } 337 | 338 | func getDB(conn string) (*sql.DB, error) { 339 | db, err := sql.Open("postgres", conn) 340 | if err != nil { 341 | return nil, err 342 | } 343 | rows, err := db.Query("SHOW STATS") 344 | if err != nil { 345 | return nil, fmt.Errorf("error pinging pgbouncer: %w", err) 346 | } 347 | defer rows.Close() 348 | 349 | db.SetMaxOpenConns(1) 350 | db.SetMaxIdleConns(1) 351 | 352 | return db, nil 353 | } 354 | 355 | // Convert database.sql types to float64s for Prometheus consumption. Null types are mapped to NaN. string and []byte 356 | // types are mapped as NaN and !ok 357 | func dbToFloat64(t interface{}, factor float64) (float64, bool) { 358 | switch v := t.(type) { 359 | case int64: 360 | return float64(v) * factor, true 361 | case float64: 362 | return v * factor, true 363 | case time.Time: 364 | return float64(v.Unix()), true 365 | case []byte: 366 | // Try and convert to string and then parse to a float64 367 | strV := string(v) 368 | result, err := strconv.ParseFloat(strV, 64) 369 | if err != nil { 370 | return math.NaN(), false 371 | } 372 | return result * factor, true 373 | case string: 374 | result, err := strconv.ParseFloat(v, 64) 375 | if err != nil { 376 | return math.NaN(), false 377 | } 378 | return result * factor, true 379 | case nil: 380 | return math.NaN(), true 381 | default: 382 | return math.NaN(), false 383 | } 384 | } 385 | 386 | // Iterate through all the namespace mappings in the exporter and run their queries. 387 | func queryNamespaceMappings(ch chan<- prometheus.Metric, db *sql.DB, metricMap map[string]MetricMapNamespace, logger *slog.Logger) map[string]error { 388 | // Return a map of namespace -> errors 389 | namespaceErrors := make(map[string]error) 390 | 391 | for namespace, mapping := range metricMap { 392 | logger.Debug("Querying namespace", "namespace", namespace) 393 | nonFatalErrors, err := queryNamespaceMapping(ch, db, namespace, mapping, logger) 394 | // Serious error - a namespace disappeared 395 | if err != nil { 396 | namespaceErrors[namespace] = err 397 | logger.Info("namespace disappeared", "err", err.Error()) 398 | } 399 | // Non-serious errors - likely version or parsing problems. 400 | if len(nonFatalErrors) > 0 { 401 | for _, err := range nonFatalErrors { 402 | logger.Info("error parsing", "err", err.Error()) 403 | } 404 | } 405 | } 406 | 407 | return namespaceErrors 408 | } 409 | 410 | // Gather the pgbouncer version info. 411 | func queryVersion(ch chan<- prometheus.Metric, db *sql.DB) error { 412 | rows, err := db.Query("SHOW VERSION;") 413 | if err != nil { 414 | return fmt.Errorf("error getting pgbouncer version: %w", err) 415 | } 416 | defer rows.Close() 417 | 418 | var columnNames []string 419 | columnNames, err = rows.Columns() 420 | if err != nil { 421 | return fmt.Errorf("error retrieving column list for version: %w", err) 422 | } 423 | if len(columnNames) != 1 || columnNames[0] != "version" { 424 | return errors.New("show version didn't return version column") 425 | } 426 | 427 | var bouncerVersion string 428 | 429 | for rows.Next() { 430 | err := rows.Scan(&bouncerVersion) 431 | if err != nil { 432 | return err 433 | } 434 | ch <- prometheus.MustNewConstMetric( 435 | bouncerVersionDesc, 436 | prometheus.GaugeValue, 437 | 1.0, 438 | bouncerVersion, 439 | ) 440 | } 441 | 442 | return nil 443 | } 444 | 445 | // Describe implements prometheus.Collector. 446 | func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { 447 | // We cannot know in advance what metrics the exporter will generate 448 | // from Postgres. So we use the poor man's describe method: Run a collect 449 | // and send the descriptors of all the collected metrics. The problem 450 | // here is that we need to connect to the Postgres DB. If it is currently 451 | // unavailable, the descriptors will be incomplete. Since this is a 452 | // stand-alone exporter and not used as a library within other code 453 | // implementing additional metrics, the worst that can happen is that we 454 | // don't detect inconsistent metrics created by this exporter 455 | // itself. Also, a change in the monitored Postgres instance may change the 456 | // exported metrics during the runtime of the exporter. 457 | 458 | metricCh := make(chan prometheus.Metric) 459 | doneCh := make(chan struct{}) 460 | 461 | go func() { 462 | for m := range metricCh { 463 | ch <- m.Desc() 464 | } 465 | close(doneCh) 466 | }() 467 | 468 | e.Collect(metricCh) 469 | close(metricCh) 470 | <-doneCh 471 | } 472 | 473 | // Collect implements prometheus.Collector. 474 | func (e *Exporter) Collect(ch chan<- prometheus.Metric) { 475 | e.logger.Info("Starting scrape") 476 | 477 | var up = 1.0 478 | 479 | err := queryVersion(ch, e.db) 480 | if err != nil { 481 | e.logger.Error("error getting version", "err", err.Error()) 482 | up = 0 483 | } 484 | 485 | if err = queryShowLists(ch, e.db, e.logger); err != nil { 486 | e.logger.Error("error getting SHOW LISTS", "err", err.Error()) 487 | up = 0 488 | } 489 | 490 | if err = queryShowConfig(ch, e.db, e.logger); err != nil { 491 | e.logger.Error("error getting SHOW CONFIG", "err", err.Error()) 492 | up = 0 493 | } 494 | 495 | errMap := queryNamespaceMappings(ch, e.db, e.metricMap, e.logger) 496 | if len(errMap) > 0 { 497 | e.logger.Error("error querying namespace mappings", "err", errMap) 498 | up = 0 499 | } 500 | 501 | if len(errMap) == len(e.metricMap) { 502 | up = 0 503 | } 504 | ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, up) 505 | } 506 | 507 | // Turn the MetricMap column mapping into a prometheus descriptor mapping. 508 | func makeDescMap(metricMaps map[string]map[string]ColumnMapping, namespace string, logger *slog.Logger) map[string]MetricMapNamespace { 509 | var metricMap = make(map[string]MetricMapNamespace) 510 | 511 | for metricNamespace, mappings := range metricMaps { 512 | thisMap := make(map[string]MetricMap) 513 | var labels = make([]string, 0) 514 | 515 | // First collect all the labels since the metrics will need them 516 | for columnName, columnMapping := range mappings { 517 | if columnMapping.usage == LABEL { 518 | logger.Debug("Adding label", "column_name", columnName, "metric_namespace", metricNamespace) 519 | labels = append(labels, columnName) 520 | } 521 | } 522 | 523 | for columnName, columnMapping := range mappings { 524 | factor := columnMapping.factor 525 | 526 | // Determine how to convert the column based on its usage. 527 | switch columnMapping.usage { 528 | case COUNTER: 529 | thisMap[columnName] = MetricMap{ 530 | vtype: prometheus.CounterValue, 531 | desc: prometheus.NewDesc(fmt.Sprintf("%s_%s_%s", namespace, metricNamespace, columnMapping.metric), columnMapping.description, labels, nil), 532 | conversion: func(in interface{}) (float64, bool) { 533 | return dbToFloat64(in, factor) 534 | }, 535 | } 536 | case GAUGE: 537 | thisMap[columnName] = MetricMap{ 538 | vtype: prometheus.GaugeValue, 539 | desc: prometheus.NewDesc(fmt.Sprintf("%s_%s_%s", namespace, metricNamespace, columnMapping.metric), columnMapping.description, labels, nil), 540 | conversion: func(in interface{}) (float64, bool) { 541 | return dbToFloat64(in, factor) 542 | }, 543 | } 544 | } 545 | } 546 | 547 | metricMap[metricNamespace] = MetricMapNamespace{thisMap, labels} 548 | } 549 | 550 | return metricMap 551 | } 552 | -------------------------------------------------------------------------------- /collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific langu 12 | package main 13 | 14 | import ( 15 | "testing" 16 | 17 | "log/slog" 18 | 19 | "github.com/DATA-DOG/go-sqlmock" 20 | "github.com/prometheus/client_golang/prometheus" 21 | dto "github.com/prometheus/client_model/go" 22 | "github.com/smartystreets/goconvey/convey" 23 | ) 24 | 25 | type labelMap map[string]string 26 | 27 | type MetricResult struct { 28 | labels labelMap 29 | value float64 30 | metricType dto.MetricType 31 | } 32 | 33 | func readMetric(m prometheus.Metric) MetricResult { 34 | pb := &dto.Metric{} 35 | m.Write(pb) 36 | labels := make(labelMap, len(pb.Label)) 37 | for _, v := range pb.Label { 38 | labels[v.GetName()] = v.GetValue() 39 | } 40 | if pb.Gauge != nil { 41 | return MetricResult{labels: labels, value: pb.GetGauge().GetValue(), metricType: dto.MetricType_GAUGE} 42 | } 43 | if pb.Counter != nil { 44 | return MetricResult{labels: labels, value: pb.GetCounter().GetValue(), metricType: dto.MetricType_COUNTER} 45 | } 46 | if pb.Untyped != nil { 47 | return MetricResult{labels: labels, value: pb.GetUntyped().GetValue(), metricType: dto.MetricType_UNTYPED} 48 | } 49 | panic("Unsupported metric type") 50 | } 51 | 52 | func TestQueryShowList(t *testing.T) { 53 | db, mock, err := sqlmock.New() 54 | if err != nil { 55 | t.Fatalf("Error opening a stub db connection: %s", err) 56 | } 57 | defer db.Close() 58 | 59 | rows := sqlmock.NewRows([]string{"key", "value"}). 60 | AddRow("dns_queries", -1). 61 | AddRow("databases", 1). 62 | AddRow("pools", 0). 63 | AddRow("users", 2) 64 | 65 | mock.ExpectQuery("SHOW LISTS;").WillReturnRows(rows) 66 | logger := &slog.Logger{} 67 | 68 | ch := make(chan prometheus.Metric) 69 | go func() { 70 | defer close(ch) 71 | if err := queryShowLists(ch, db, logger); err != nil { 72 | t.Errorf("Error running queryShowList: %s", err) 73 | } 74 | }() 75 | 76 | expected := []MetricResult{ 77 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: -1}, 78 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 1}, 79 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 0}, 80 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 2}, 81 | } 82 | 83 | convey.Convey("Metrics comparison", t, func() { 84 | for _, expect := range expected { 85 | m := readMetric(<-ch) 86 | convey.So(expect, convey.ShouldResemble, m) 87 | } 88 | }) 89 | if err := mock.ExpectationsWereMet(); err != nil { 90 | t.Errorf("there were unfulfilled exceptions: %s", err) 91 | } 92 | } 93 | 94 | func TestQueryShowConfig(t *testing.T) { 95 | db, mock, err := sqlmock.New() 96 | if err != nil { 97 | t.Fatalf("Error opening a stub db connection: %s", err) 98 | } 99 | defer db.Close() 100 | 101 | rows := sqlmock.NewRows([]string{"key", "value", "default", "changeable"}). 102 | AddRow("max_client_conn", 1900, 100, true). 103 | AddRow("max_user_connections", 100, 100, true). 104 | AddRow("auth_type", "md5", "md5", true). 105 | AddRow("client_tls_ciphers", "default", "default", "yes") 106 | 107 | mock.ExpectQuery("SHOW CONFIG;").WillReturnRows(rows) 108 | logger := &slog.Logger{} 109 | 110 | ch := make(chan prometheus.Metric) 111 | go func() { 112 | defer close(ch) 113 | if err := queryShowConfig(ch, db, logger); err != nil { 114 | t.Errorf("Error running queryShowConfig: %s", err) 115 | } 116 | }() 117 | 118 | expected := []MetricResult{ 119 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 1900}, 120 | {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 100}, 121 | } 122 | convey.Convey("Metrics comparison", t, func() { 123 | for _, expect := range expected { 124 | m := readMetric(<-ch) 125 | convey.So(expect, convey.ShouldResemble, m) 126 | } 127 | }) 128 | if err := mock.ExpectationsWereMet(); err != nil { 129 | t.Errorf("there were unfulfilled exceptions: %s", err) 130 | } 131 | } 132 | 133 | func TestQueryShowDatabases(t *testing.T) { 134 | rows := sqlmock.NewRows([]string{"name", "host", "port", "database", "pool_size"}). 135 | AddRow("pg0_db", "10.10.10.1", "5432", "pg0", 20) 136 | 137 | expected := []MetricResult{ 138 | {labels: labelMap{"name": "pg0_db", "host": "10.10.10.1", "port": "5432", "database": "pg0", "force_user": "", "pool_mode": ""}, metricType: dto.MetricType_GAUGE, value: 20}, 139 | } 140 | 141 | testQueryNamespaceMapping(t, "databases", rows, expected) 142 | } 143 | 144 | func TestQueryShowStats(t *testing.T) { 145 | // columns are listed in the order PgBouncers exposes them, a value of -1 means pgbouncer_exporter does not expose this value as a metric 146 | rows := sqlmock.NewRows([]string{"database", 147 | "server_assignment_count", 148 | "xact_count", "query_count", "bytes_received", "bytes_sent", 149 | "xact_time", "query_time", "wait_time", "client_parse_count", "server_parse_count", "bind_count"}). 150 | AddRow("pg0", -1, 10, 40, 220, 460, 6, 8, 9, 5, 55, 555) 151 | 152 | // expected metrics are returned in the same order as the colums 153 | expected := []MetricResult{ 154 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 10}, // xact_count 155 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 40}, // query_count 156 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 220}, // bytes_received 157 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 460}, // bytes_sent 158 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 6e-6}, // xact_time 159 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 8e-6}, // query_time 160 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 9e-6}, // wait_time 161 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 5}, // client_parse_count 162 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 55}, // server_parse_count 163 | {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 555}, // bind_count 164 | } 165 | 166 | testQueryNamespaceMapping(t, "stats_totals", rows, expected) 167 | } 168 | 169 | func TestQueryShowPools(t *testing.T) { 170 | rows := sqlmock.NewRows([]string{"database", "user", "cl_active"}). 171 | AddRow("pg0", "postgres", 2) 172 | 173 | expected := []MetricResult{ 174 | {labels: labelMap{"database": "pg0", "user": "postgres"}, metricType: dto.MetricType_GAUGE, value: 2}, 175 | } 176 | 177 | testQueryNamespaceMapping(t, "pools", rows, expected) 178 | } 179 | 180 | func testQueryNamespaceMapping(t *testing.T, namespaceMapping string, rows *sqlmock.Rows, expected []MetricResult) { 181 | db, mock, err := sqlmock.New() 182 | if err != nil { 183 | t.Fatalf("Error opening a stub db connection: %s", err) 184 | } 185 | defer db.Close() 186 | 187 | mock.ExpectQuery("SHOW " + namespaceMapping + ";").WillReturnRows(rows) 188 | 189 | logger := slog.Default() 190 | 191 | metricMap := makeDescMap(metricMaps, namespace, logger) 192 | 193 | ch := make(chan prometheus.Metric) 194 | go func() { 195 | defer close(ch) 196 | if _, err := queryNamespaceMapping(ch, db, namespaceMapping, metricMap[namespaceMapping], logger); err != nil { 197 | t.Errorf("Error running queryNamespaceMapping: %s", err) 198 | } 199 | }() 200 | 201 | convey.Convey("Metrics comparison", t, func() { 202 | for _, expect := range expected { 203 | m := readMetric(<-ch) 204 | convey.So(m, convey.ShouldResemble, expect) 205 | } 206 | }) 207 | if err := mock.ExpectationsWereMet(); err != nil { 208 | t.Errorf("there were unfulfilled exceptions: %s", err) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | postgres: 5 | image: postgres:9.4 6 | ports: 7 | - "5432" 8 | 9 | pgbouncer: 10 | image: starefossen/pgbouncer:latest 11 | links: 12 | - postgres 13 | ports: 14 | - "127.0.0.1:6543:6543" 15 | volumes: 16 | - ./pgbouncer.ini:/pgbouncer.ini 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus-community/pgbouncer_exporter 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.2 7 | github.com/alecthomas/kingpin/v2 v2.4.0 8 | github.com/lib/pq v1.10.9 9 | github.com/prometheus/client_golang v1.22.0 10 | github.com/prometheus/client_model v0.6.2 11 | github.com/prometheus/common v0.64.0 12 | github.com/prometheus/exporter-toolkit v0.13.2 13 | github.com/smartystreets/goconvey v1.8.1 14 | ) 15 | 16 | require ( 17 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 21 | github.com/gopherjs/gopherjs v1.17.2 // indirect 22 | github.com/jpillora/backoff v1.0.0 // indirect 23 | github.com/jtolds/gls v4.20.0+incompatible // indirect 24 | github.com/mdlayher/socket v0.4.1 // indirect 25 | github.com/mdlayher/vsock v1.2.1 // indirect 26 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 27 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 28 | github.com/prometheus/procfs v0.15.1 // indirect 29 | github.com/smarty/assertions v1.15.0 // indirect 30 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 31 | golang.org/x/crypto v0.38.0 // indirect 32 | golang.org/x/net v0.40.0 // indirect 33 | golang.org/x/oauth2 v0.30.0 // indirect 34 | golang.org/x/sync v0.14.0 // indirect 35 | golang.org/x/sys v0.33.0 // indirect 36 | golang.org/x/text v0.25.0 // indirect 37 | google.golang.org/protobuf v1.36.6 // indirect 38 | gopkg.in/yaml.v2 v2.4.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= 2 | github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 3 | github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= 4 | github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 5 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 6 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 7 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 10 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 12 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 17 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 18 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 19 | github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= 20 | github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= 21 | github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= 22 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 23 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 24 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 25 | github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= 26 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 27 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 28 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 29 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 30 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 31 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 32 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 33 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 34 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 35 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 36 | github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= 37 | github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= 38 | github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= 39 | github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 41 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 42 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= 43 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 44 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 45 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 46 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 47 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 48 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 49 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 50 | github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 51 | github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 52 | github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= 53 | github.com/prometheus/exporter-toolkit v0.13.2/go.mod h1:tCqnfx21q6qN1KA4U3Bfb8uWzXfijIrJz3/kTIqMV7g= 54 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 55 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 56 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 57 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 58 | github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= 59 | github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= 60 | github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= 61 | github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 64 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 65 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 66 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 67 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 68 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 69 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 70 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 71 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 72 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 73 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 74 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 75 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 76 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 77 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 78 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 79 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 80 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 81 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 82 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 83 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 84 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 85 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 86 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 87 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 88 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 89 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | -------------------------------------------------------------------------------- /pgbouncer.ini: -------------------------------------------------------------------------------- 1 | [databases] 2 | postgres = host=postgres dbname=postgres 3 | 4 | [pgbouncer] 5 | user = pgbouncer 6 | pool_mode = session 7 | listen_port = 6543 8 | listen_addr = 0.0.0.0 9 | auth_type = any 10 | ignore_startup_parameters = extra_float_digits 11 | -------------------------------------------------------------------------------- /pgbouncer_exporter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "net/http" 18 | "os" 19 | 20 | "github.com/alecthomas/kingpin/v2" 21 | "github.com/prometheus/client_golang/prometheus" 22 | "github.com/prometheus/client_golang/prometheus/collectors" 23 | versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" 24 | "github.com/prometheus/client_golang/prometheus/promhttp" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/prometheus/common/promslog/flag" 27 | "github.com/prometheus/common/version" 28 | "github.com/prometheus/exporter-toolkit/web" 29 | "github.com/prometheus/exporter-toolkit/web/kingpinflag" 30 | ) 31 | 32 | const namespace = "pgbouncer" 33 | 34 | func main() { 35 | const pidFileHelpText = `Path to PgBouncer pid file. 36 | 37 | If provided, the standard process metrics get exported for the PgBouncer 38 | process, prefixed with 'pgbouncer_process_...'. The pgbouncer_process exporter 39 | needs to have read access to files owned by the PgBouncer process. Depends on 40 | the availability of /proc. 41 | 42 | https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics.` 43 | 44 | promslogConfig := &promslog.Config{} 45 | flag.AddFlags(kingpin.CommandLine, promslogConfig) 46 | 47 | var ( 48 | connectionStringPointer = kingpin.Flag("pgBouncer.connectionString", "Connection string for accessing pgBouncer.").Default("postgres://postgres:@localhost:6543/pgbouncer?sslmode=disable").Envar("PGBOUNCER_EXPORTER_CONNECTION_STRING").String() 49 | metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() 50 | pidFilePath = kingpin.Flag("pgBouncer.pid-file", pidFileHelpText).Default("").String() 51 | ) 52 | 53 | toolkitFlags := kingpinflag.AddFlags(kingpin.CommandLine, ":9127") 54 | 55 | kingpin.Version(version.Print("pgbouncer_exporter")) 56 | kingpin.HelpFlag.Short('h') 57 | kingpin.Parse() 58 | 59 | logger := promslog.New(promslogConfig) 60 | 61 | connectionString := *connectionStringPointer 62 | exporter := NewExporter(connectionString, namespace, logger) 63 | prometheus.MustRegister(exporter) 64 | prometheus.MustRegister(versioncollector.NewCollector("pgbouncer_exporter")) 65 | 66 | logger.Info("Starting pgbouncer_exporter", "version", version.Info()) 67 | logger.Info("Build context", "build_context", version.BuildContext()) 68 | 69 | if *pidFilePath != "" { 70 | procExporter := collectors.NewProcessCollector( 71 | collectors.ProcessCollectorOpts{ 72 | PidFn: prometheus.NewPidFileFn(*pidFilePath), 73 | Namespace: namespace, 74 | }, 75 | ) 76 | prometheus.MustRegister(procExporter) 77 | } 78 | 79 | http.Handle(*metricsPath, promhttp.Handler()) 80 | if *metricsPath != "/" && *metricsPath != "" { 81 | landingConfig := web.LandingConfig{ 82 | Name: "PgBouncer Exporter", 83 | Description: "Prometheus Exporter for PgBouncer servers", 84 | Version: version.Info(), 85 | Links: []web.LandingLinks{ 86 | { 87 | Address: *metricsPath, 88 | Text: "Metrics", 89 | }, 90 | }, 91 | } 92 | landingPage, err := web.NewLandingPage(landingConfig) 93 | if err != nil { 94 | logger.Error("Error creating landing page", "err", err) 95 | os.Exit(1) 96 | } 97 | http.Handle("/", landingPage) 98 | } 99 | 100 | srv := &http.Server{} 101 | if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { 102 | logger.Error("Error starting server", "err", err) 103 | os.Exit(1) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /struct.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package main 15 | 16 | // Elasticsearch Node Stats Structs 17 | import ( 18 | "database/sql" 19 | "fmt" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | type columnUsage int 26 | 27 | // convert a string to the corresponding columnUsage 28 | func stringTocolumnUsage(s string) (u columnUsage, err error) { 29 | switch s { 30 | case "DISCARD": 31 | u = DISCARD 32 | 33 | case "LABEL": 34 | u = LABEL 35 | 36 | case "COUNTER": 37 | u = COUNTER 38 | 39 | case "GAUGE": 40 | u = GAUGE 41 | 42 | case "MAPPEDMETRIC": 43 | u = MAPPEDMETRIC 44 | 45 | case "DURATION": 46 | u = DURATION 47 | 48 | default: 49 | err = fmt.Errorf("wrong columnUsage given : %s", s) 50 | } 51 | 52 | return 53 | } 54 | 55 | // Implements the yaml.Unmarshaller interface 56 | func (cu *columnUsage) UnmarshalYAML(unmarshal func(interface{}) error) error { 57 | var value string 58 | if err := unmarshal(&value); err != nil { 59 | return err 60 | } 61 | 62 | columnUsage, err := stringTocolumnUsage(value) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | *cu = columnUsage 68 | return nil 69 | } 70 | 71 | const ( 72 | DISCARD columnUsage = iota // Ignore this column 73 | LABEL columnUsage = iota // Use this column as a label 74 | COUNTER columnUsage = iota // Use this column as a counter 75 | GAUGE columnUsage = iota // Use this column as a gauge 76 | MAPPEDMETRIC columnUsage = iota // Use this column with the supplied mapping of text values 77 | DURATION columnUsage = iota // This column should be interpreted as a text duration (and converted to milliseconds) 78 | ) 79 | 80 | // Groups metric maps under a shared set of labels 81 | type MetricMapNamespace struct { 82 | columnMappings map[string]MetricMap // Column mappings in this namespace 83 | labels []string 84 | } 85 | 86 | // Stores the prometheus metric description which a given column will be mapped 87 | // to by the collector 88 | type MetricMap struct { 89 | discard bool // Should metric be discarded during mapping? 90 | vtype prometheus.ValueType // Prometheus valuetype 91 | desc *prometheus.Desc // Prometheus descriptor 92 | conversion func(interface{}) (float64, bool) // Conversion function to turn PG result into float64 93 | } 94 | 95 | type ColumnMapping struct { 96 | usage columnUsage `yaml:"usage"` 97 | metric string `yaml:"metric"` 98 | factor float64 `yaml:"factor"` 99 | description string `yaml:"description"` 100 | } 101 | 102 | // Exporter collects PgBouncer stats from the given server and exports 103 | // them using the prometheus metrics package. 104 | type Exporter struct { 105 | metricMap map[string]MetricMapNamespace 106 | 107 | db *sql.DB 108 | 109 | logger *slog.Logger 110 | } 111 | --------------------------------------------------------------------------------