pax_global_header00006660000000000000000000000064151520476410014517gustar00rootroot0000000000000052 comment=6d546512ddab698296d574b4912a6b28bb101c76 pgbouncer_exporter-0.12.0/000077500000000000000000000000001515204764100155135ustar00rootroot00000000000000pgbouncer_exporter-0.12.0/.github/000077500000000000000000000000001515204764100170535ustar00rootroot00000000000000pgbouncer_exporter-0.12.0/.github/dependabot.yml000066400000000000000000000003211515204764100216770ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "monthly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" pgbouncer_exporter-0.12.0/.github/workflows/000077500000000000000000000000001515204764100211105ustar00rootroot00000000000000pgbouncer_exporter-0.12.0/.github/workflows/ci.yml000066400000000000000000000103121515204764100222230ustar00rootroot00000000000000--- name: CI on: pull_request: push: branches: [master, 'release-*'] tags: ['v*'] jobs: test_go: name: Go tests runs-on: ubuntu-latest container: # Whenever the Go version is updated here, .promu.yml # should also be updated. image: quay.io/prometheus/golang-builder:1.26-base steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4 - uses: ./.github/promci/actions/setup_environment - run: make build: name: Build Prometheus for common architectures runs-on: ubuntu-latest if: | !(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) && !(github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-')) && !(github.event_name == 'push' && github.event.ref == 'refs/heads/main') && !(github.event_name == 'push' && github.event.ref == 'refs/heads/master') strategy: matrix: thread: [ 0, 1, 2 ] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4 - uses: ./.github/promci/actions/build with: promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386" parallelism: 3 thread: ${{ matrix.thread }} build_all: name: Build Prometheus for all architectures runs-on: ubuntu-latest if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'pull_request' && startsWith(github.event.pull_request.base.ref, 'release-')) || (github.event_name == 'push' && github.event.ref == 'refs/heads/main') || (github.event_name == 'push' && github.event.ref == 'refs/heads/master') strategy: matrix: thread: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ] # Whenever the Go version is updated here, .promu.yml # should also be updated. steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4 - uses: ./.github/promci/actions/build with: parallelism: 12 thread: ${{ matrix.thread }} publish_main: # https://github.com/prometheus/promci/blob/52c7012f5f0070d7281b8db4a119e21341d43c91/actions/publish_main/action.yml name: Publish main branch artifacts runs-on: ubuntu-latest needs: [test_go, build_all] if: | (github.event_name == 'push' && github.event.ref == 'refs/heads/main') || (github.event_name == 'push' && github.event.ref == 'refs/heads/master') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4 - uses: ./.github/promci/actions/publish_main with: docker_hub_organization: prometheuscommunity docker_hub_login: ${{ secrets.docker_hub_login }} docker_hub_password: ${{ secrets.docker_hub_password }} quay_io_organization: prometheuscommunity quay_io_login: ${{ secrets.quay_io_login }} quay_io_password: ${{ secrets.quay_io_password }} publish_release: name: Publish release artefacts runs-on: ubuntu-latest needs: [test_go, build_all] if: | (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: prometheus/promci@fc721ff8497a70a93a881cd552b71af7fb3a9d53 # v0.5.4 - uses: ./.github/promci/actions/publish_release with: docker_hub_organization: prometheuscommunity docker_hub_login: ${{ secrets.docker_hub_login }} docker_hub_password: ${{ secrets.docker_hub_password }} quay_io_organization: prometheuscommunity quay_io_login: ${{ secrets.quay_io_login }} quay_io_password: ${{ secrets.quay_io_password }} github_token: ${{ secrets.PROMBOT_GITHUB_TOKEN }} pgbouncer_exporter-0.12.0/.github/workflows/container_description.yml000066400000000000000000000046351515204764100262300ustar00rootroot00000000000000--- name: Push README to Docker Hub on: push: paths: - "README.md" - "README-containers.md" - ".github/workflows/container_description.yml" branches: [ main, master ] permissions: contents: read jobs: PushDockerHubReadme: runs-on: ubuntu-latest name: Push README to Docker Hub if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Set docker hub repo name run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to Dockerhub uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} with: destination_container_repo: ${{ env.DOCKER_REPO_NAME }} provider: dockerhub short_description: ${{ env.DOCKER_REPO_NAME }} # Empty string results in README-containers.md being pushed if it # exists. Otherwise, README.md is pushed. readme_file: '' PushQuayIoReadme: runs-on: ubuntu-latest name: Push README to quay.io if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. steps: - name: git checkout uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Set quay.io org name run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV - name: Set quay.io repo name run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV - name: Push README to quay.io uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 env: DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} with: destination_container_repo: ${{ env.DOCKER_REPO_NAME }} provider: quay # Empty string results in README-containers.md being pushed if it # exists. Otherwise, README.md is pushed. readme_file: '' pgbouncer_exporter-0.12.0/.github/workflows/golangci-lint.yml000066400000000000000000000030321515204764100243600ustar00rootroot00000000000000--- # This action is synced from https://github.com/prometheus/prometheus name: golangci-lint on: push: branches: [main, master, 'release-*'] paths: - "go.sum" - "go.mod" - "**.go" - "scripts/errcheck_excludes.txt" - ".github/workflows/golangci-lint.yml" - ".golangci.yml" tags: ['v*'] pull_request: permissions: # added using https://github.com/step-security/secure-repo contents: read jobs: golangci: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Install Go uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 with: go-version: 1.26.x - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' - name: Get golangci-lint version id: golangci-lint-version run: echo "version=$(make print-golangci-lint-version)" >> $GITHUB_OUTPUT - name: Lint uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 with: args: --verbose version: ${{ steps.golangci-lint-version.outputs.version }} pgbouncer_exporter-0.12.0/.gitignore000066400000000000000000000005511515204764100175040ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe dependencies-stamp /pgbouncer_exporter /.build /.deps /.release /.tarballs /vendor # Intellij /.idea *.iml pgbouncer_exporter-0.12.0/.golangci.yml000066400000000000000000000005101515204764100200730ustar00rootroot00000000000000version: "2" linters: enable: - misspell - revive - sloglint exclusions: generated: lax presets: - comments - common-false-positives - legacy - std-error-handling rules: - linters: - errcheck path: _test.go formatters: exclusions: generated: lax pgbouncer_exporter-0.12.0/.promu.yml000066400000000000000000000011061515204764100174540ustar00rootroot00000000000000go: # This must match .github/workflows/ci.yml. version: 1.26 repository: path: github.com/prometheus-community/pgbouncer_exporter build: binaries: - name: pgbouncer_exporter ldflags: | -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.Revision={{.Revision}} -X github.com/prometheus/common/version.Branch={{.Branch}} -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} tarball: files: - LICENSE pgbouncer_exporter-0.12.0/.yamllint000066400000000000000000000007731515204764100173540ustar00rootroot00000000000000--- extends: default ignore: | **/node_modules web/api/v1/testdata/openapi_*_golden.yaml rules: braces: max-spaces-inside: 1 level: error brackets: max-spaces-inside: 1 level: error commas: disable comments: disable comments-indentation: disable document-start: disable indentation: spaces: consistent indent-sequences: consistent key-duplicates: ignore: | config/testdata/section_key_dup.bad.yml line-length: disable truthy: check-keys: false pgbouncer_exporter-0.12.0/CHANGELOG.md000066400000000000000000000044511515204764100173300ustar00rootroot00000000000000## master / unreleased ## 0.12.0 / 2026-03-04 * [FEATURE] Add SHOW CLIENTS metrics #250 ## 0.11.1 / 2026-02-09 * [BUGFIX] Fix `pgbouncer_stats_totals_server_assignments_total` metric #228 ## 0.11.0 / 2025-06-13 Notes: - This version changes the connection behavior to create a new client connection for each scrape. Previously a connection was created at startup. - This version now requires PgBouncer >= 1.8. * [CHANGE] Make connections per-scrape #214 * [FEATURE] Add prepared statement metrics #194 * [FEATURE] Add total_server_assignment_count metric #208 ## 0.10.2 / 2024-10-18 * [BUGFIX] Fix wrong logging level of "Starting scrape" message #175 ## 0.10.1 / 2024-10-14 * [BUGFIX] Revert auth_type guage #173 ## 0.10.0 / 2024-10-07 * [CHANGE] Switch logging to slog #167 * [ENHANCEMENT] Add auth_type to config collector #169 ## 0.9.0 / 2024-08-01 * [FEATURE] Allow connection config via environment variable #159 ## 0.8.0 / 2024-04-02 * [ENHANCEMENT] Publish server/client cancel statistics. #1144 ## 0.7.0 / 2023-06-29 * [CHANGE] Require Go 1.19 and update CI with Go 1.20 #120 * [CHANGE] Synchronize common files from prometheus/prometheus #123 ## 0.6.0 / 2023-01-27 * [FEATURE] Add config metrics #93 * [FEATURE] Add TLS and Basic auth to the metrics endpoint #101 ## 0.5.1 / 2022-10-03 * No changes, just retagging due to a VERSION fix. ## 0.5.0 / 2022-10-03 * [CHANGE] Update Go to 1.18. * [CHANGE] Update upstream dependencies. ## 0.4.1 / 2022-01-27 * [BUGFIX] Fix startup log message typo #50 * [BUGFIX] Fix typo in reserve_pool metric #67 ## 0.4.0 / 2020-07-09 Counter names have been updated to match Prometheus naming conventions. * `pgbouncer_stats_queries_duration_seconds` -> `pgbouncer_stats_queries_duration_seconds_total` * `pgbouncer_stats_client_wait_seconds` -> `pgbouncer_stats_client_wait_seconds_total` * `pgbouncer_stats_server_in_transaction_seconds` -> `pgbouncer_stats_server_in_transaction_seconds_total` * [CHANGE] Cleanup exporter metrics #33 * [CHANGE] Update counter metric names #35 * [FEATURE] Add support for SHOW LISTS metrics #36 ## 0.3.0 / 2020-05-27 * [CHANGE] Switch logging to promlog #29 * [FEATURE] Add pgbouncer process metrics #27 ## 0.2.0 / 2020-04-29 * [BUGFIX] Fix byte slice values not receiving conversion factor #18 Initial prometheus-community release. pgbouncer_exporter-0.12.0/CODE_OF_CONDUCT.md000066400000000000000000000002301515204764100203050ustar00rootroot00000000000000# Prometheus Community Code of Conduct Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). pgbouncer_exporter-0.12.0/Dockerfile000066400000000000000000000006151515204764100175070ustar00rootroot00000000000000ARG ARCH="amd64" ARG OS="linux" FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest LABEL maintainer="The Prometheus Authors " ARG ARCH="amd64" ARG OS="linux" COPY .build/${OS}-${ARCH}/pgbouncer_exporter /bin/pgbouncer_exporter COPY LICENSE /LICENSE USER nobody ENTRYPOINT ["/bin/pgbouncer_exporter"] EXPOSE 9127 pgbouncer_exporter-0.12.0/LICENSE000066400000000000000000000021251515204764100165200ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2017 Kristoffer K Larsen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pgbouncer_exporter-0.12.0/MAINTAINERS.md000066400000000000000000000001151515204764100176040ustar00rootroot00000000000000* Ben Kochie @SuperQ * Stan Hu @stanhu pgbouncer_exporter-0.12.0/Makefile000066400000000000000000000014431515204764100171550ustar00rootroot00000000000000# Copyright 2020 The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Needs to be defined before including Makefile.common to auto-generate targets DOCKER_ARCHS ?= amd64 armv7 arm64 DOCKER_REPO ?= prometheuscommunity include Makefile.common DOCKER_IMAGE_NAME ?= pgbouncer-exporter pgbouncer_exporter-0.12.0/Makefile.common000066400000000000000000000470351515204764100204530ustar00rootroot00000000000000# Copyright The Prometheus Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # A common Makefile that includes rules to be reused in different prometheus projects. # !!! Open PRs only against the prometheus/prometheus/Makefile.common repository! # Example usage : # Create the main Makefile in the root project directory. # include Makefile.common # customTarget: # @echo ">> Running customTarget" # # Ensure GOBIN is not set during build so that promu is installed to the correct path unexport GOBIN GO ?= go GOFMT ?= $(GO)fmt FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) GOOPTS ?= GOHOSTOS ?= $(shell $(GO) env GOHOSTOS) GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH) GO_VERSION ?= $(shell $(GO) version) GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') PROMU := $(FIRST_GOPATH)/bin/promu pkgs = ./... ifeq (arm, $(GOHOSTARCH)) GOHOSTARM ?= $(shell GOARM= $(GO) env GOARM) GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH)v$(GOHOSTARM) else GO_BUILD_PLATFORM ?= $(GOHOSTOS)-$(GOHOSTARCH) endif GOTEST := $(GO) test GOTEST_DIR := ifneq ($(CIRCLE_JOB),) ifneq ($(shell command -v gotestsum 2> /dev/null),) GOTEST_DIR := test-results GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml -- endif endif PROMU_VERSION ?= 0.18.0 PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= GOLANGCI_LINT_VERSION ?= v2.10.1 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64)) # If we're in CI and there is an Actions file, that means the linter # is being run in Actions, so we don't need to run it here. ifneq (,$(SKIP_GOLANGCI_LINT)) GOLANGCI_LINT := else ifeq (,$(CIRCLE_JOB)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint endif endif endif PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) DOCKERBUILD_CONTEXT ?= ./ DOCKER_REPO ?= prom # Check if deprecated DOCKERFILE_PATH is set ifdef DOCKERFILE_PATH $(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile) endif DOCKER_ARCHS ?= amd64 DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*) # Function to extract variant from Dockerfile label. # Returns the variant name from io.prometheus.image.variant label, or "default" if not found. define dockerfile_variant $(strip $(or $(shell sed -n 's/.*io\.prometheus\.image\.variant="\([^"]*\)".*/\1/p' $(1)),default)) endef # Check for duplicate variant names (including default for Dockerfiles without labels). DOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df))) DOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES)) ifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED))) $(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default)) endif # Build variant:dockerfile pairs for shell iteration. DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df)) # Shell helper to check whether a dockerfile/arch pair is excluded. define dockerfile_arch_is_excluded case " $(DOCKERFILE_ARCH_EXCLUSIONS) " in \ *" $$dockerfile:$(1) "*) true ;; \ *) false ;; \ esac endef # Shell helper to check whether a registry/arch pair is excluded. # Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io) define registry_arch_is_excluded registry=$$(echo "$(DOCKER_REPO)" | cut -d'/' -f1); \ case " $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) " in \ *" $$registry:$(1) "*) true ;; \ *) false ;; \ esac endef BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS)) PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS)) TAG_DOCKER_ARCHS = $(addprefix common-docker-tag-latest-,$(DOCKER_ARCHS)) SANITIZED_DOCKER_IMAGE_TAG := $(subst +,-,$(DOCKER_IMAGE_TAG)) ifeq ($(GOHOSTARCH),amd64) ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux freebsd darwin windows)) # Only supported on amd64 test-flags := -race endif endif # This rule is used to forward a target like "build" to "common-build". This # allows a new "build" target to be defined in a Makefile which includes this # one and override "common-build" without override warnings. %: common-% ; .PHONY: common-all common-all: precheck style check_license lint yamllint unused build test .PHONY: common-style common-style: @echo ">> checking code style" @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \ if [ -n "$${fmtRes}" ]; then \ echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ echo "Please ensure you are using $$($(GO) version) for formatting code."; \ exit 1; \ fi .PHONY: common-check_license common-check_license: @echo ">> checking license header" @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \ awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ done); \ if [ -n "$${licRes}" ]; then \ echo "license header checking failed:"; echo "$${licRes}"; \ exit 1; \ fi @echo ">> checking for copyright years 2026 or later" @futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \ if [ -n "$${futureYearRes}" ]; then \ echo "Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):"; echo "$${futureYearRes}"; \ exit 1; \ fi .PHONY: common-deps common-deps: @echo ">> getting dependencies" $(GO) mod download .PHONY: update-go-deps update-go-deps: @echo ">> updating Go dependencies" @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ $(GO) get $$m; \ done $(GO) mod tidy .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" $(GOTEST) -short $(GOOPTS) $(pkgs) .PHONY: common-test common-test: $(GOTEST_DIR) @echo ">> running all tests" $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) $(GOTEST_DIR): @mkdir -p $@ .PHONY: common-format common-format: $(GOLANGCI_LINT) @echo ">> formatting code" $(GO) fmt $(pkgs) ifdef GOLANGCI_LINT @echo ">> formatting code with golangci-lint" $(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS) endif .PHONY: common-vet common-vet: @echo ">> vetting code" $(GO) vet $(GOOPTS) $(pkgs) .PHONY: common-lint common-lint: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint" $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-lint-fix common-lint-fix: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint fix" $(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs) endif .PHONY: common-yamllint common-yamllint: @echo ">> running yamllint on all YAML files in the repository" ifeq (, $(shell command -v yamllint 2> /dev/null)) @echo "yamllint not installed so skipping" else yamllint . endif # For backward-compatibility. .PHONY: common-staticcheck common-staticcheck: lint .PHONY: common-unused common-unused: @echo ">> running check for unused/missing packages in go.mod" $(GO) mod tidy @git diff --exit-code -- go.sum go.mod .PHONY: common-build common-build: promu @echo ">> building binaries" $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) .PHONY: common-tarball common-tarball: promu @echo ">> building release tarball" $(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) .PHONY: common-docker-repo-name common-docker-repo-name: @echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)" .PHONY: common-docker $(BUILD_DOCKER_ARCHS) common-docker: $(BUILD_DOCKER_ARCHS) $(BUILD_DOCKER_ARCHS): common-docker-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if $(call dockerfile_arch_is_excluded,$*); then \ echo "Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ distroless_arch="$*"; \ if [ "$*" = "armv7" ]; then \ distroless_arch="arm"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ -f $$dockerfile \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ $(DOCKERBUILD_CONTEXT); \ if [ "$$variant_name" != "default" ]; then \ echo "Tagging default variant with $$variant_name suffix"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \ "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ else \ echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \ -f $$dockerfile \ --build-arg ARCH="$*" \ --build-arg OS="linux" \ --build-arg DISTROLESS_ARCH="$$distroless_arch" \ $(DOCKERBUILD_CONTEXT); \ fi; \ done .PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS) common-docker-publish: $(PUBLISH_DOCKER_ARCHS) $(PUBLISH_DOCKER_ARCHS): common-docker-publish-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if $(call dockerfile_arch_is_excluded,$*); then \ echo "Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$*); then \ echo "Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Pushing $$variant_name variant for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Pushing default variant ($$variant_name) for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ fi; \ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Pushing $$variant_name variant version tags for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Pushing default variant version tag for linux-$*"; \ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ fi; \ done DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION))) .PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS) common-docker-tag-latest: $(TAG_DOCKER_ARCHS) $(TAG_DOCKER_ARCHS): common-docker-tag-latest-%: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if $(call dockerfile_arch_is_excluded,$*); then \ echo "Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$*); then \ echo "Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Tagging $$variant_name variant for linux-$* as latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Tagging default variant ($$variant_name) for linux-$* as latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"; \ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ done .PHONY: common-docker-manifest common-docker-manifest: @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \ dockerfile=$${variant#*:}; \ variant_name=$${variant%%:*}; \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Creating manifest for $$variant_name variant"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ if $(call dockerfile_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping manifest for $$variant_name variant (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Creating default variant ($$variant_name) manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ if $(call dockerfile_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping default variant manifest (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"; \ fi; \ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \ echo "Creating manifest for $$variant_name variant version tag"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ if $(call dockerfile_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping version-tag manifest for $$variant_name variant (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \ fi; \ if [ "$$dockerfile" = "Dockerfile" ]; then \ echo "Creating default variant version tag manifest"; \ refs=""; \ for arch in $(DOCKER_ARCHS); do \ if $(call dockerfile_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ if $(call registry_arch_is_excluded,$$arch); then \ echo " Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \ continue; \ fi; \ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \ done; \ if [ -z "$$refs" ]; then \ echo "Skipping default variant version-tag manifest (no supported architectures)"; \ continue; \ fi; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $$refs; \ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \ fi; \ fi; \ done .PHONY: promu promu: $(PROMU) $(PROMU): $(eval PROMU_TMP := $(shell mktemp -d)) curl -s -L $(PROMU_URL) | tar -xvzf - -C $(PROMU_TMP) mkdir -p $(FIRST_GOPATH)/bin cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu rm -r $(PROMU_TMP) .PHONY: common-proto common-proto: @echo ">> generating code from proto files" @./scripts/genproto.sh ifdef GOLANGCI_LINT $(GOLANGCI_LINT): mkdir -p $(FIRST_GOPATH)/bin curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_LINT_VERSION)/install.sh \ | sed -e '/install -d/d' \ | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif .PHONY: common-print-golangci-lint-version common-print-golangci-lint-version: @echo $(GOLANGCI_LINT_VERSION) .PHONY: precheck precheck:: define PRECHECK_COMMAND_template = precheck:: $(1)_precheck PRECHECK_COMMAND_$(1) ?= $(1) $$(strip $$(PRECHECK_OPTIONS_$(1))) .PHONY: $(1)_precheck $(1)_precheck: @if ! $$(PRECHECK_COMMAND_$(1)) 1>/dev/null 2>&1; then \ echo "Execution of '$$(PRECHECK_COMMAND_$(1))' command failed. Is $(1) installed?"; \ exit 1; \ fi endef govulncheck: install-govulncheck govulncheck ./... install-govulncheck: command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest pgbouncer_exporter-0.12.0/README.md000066400000000000000000000073771515204764100170100ustar00rootroot00000000000000# PgBouncer exporter [![Build Status](https://github.com/prometheus-community/pgbouncer_exporter/actions/workflows/ci.yml/badge.svg)](https://github.com/prometheus-community/pgbouncer_exporter/actions/workflows/ci.yml) Prometheus exporter for PgBouncer. Exports metrics at `9127/metrics` ## Requirements - PgBouncer 1.8 or higher, since PgBouncer exporter 0.11.0 ## Building and running make build ./pgbouncer_exporter To see all available configuration flags: ./pgbouncer_exporter -h ## PGBouncer configuration The pgbouncer\_exporter requires a configuration change to pgbouncer to ignore a PostgreSQL driver connection parameter. In the `pgbouncer.ini` please include this option: ignore_startup_parameters = extra_float_digits ## Run with docker ``` docker run prometheuscommunity/pgbouncer-exporter ``` ## Metrics |PgBouncer column|Prometheus Metric|Description| |----------------|-----------------|-----------| stats_total_query_count | pgbouncer_stats_queries_pooled_total | Total number of SQL queries pooled stats.total_query_time | pgbouncer_stats_queries_duration_seconds_total | Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries stats.total_received | pgbouncer_stats_received_bytes_total | Total volume in bytes of network traffic received by pgbouncer, shown as bytes stats.total_requests | pgbouncer_stats_queries_total | Total number of SQL requests pooled by pgbouncer, shown as requests stats.total_sent | pgbouncer_stats_sent_bytes_total | Total volume in bytes of network traffic sent by pgbouncer, shown as bytes stats.total_wait_time | pgbouncer_stats_client_wait_seconds_total | Time spent by clients waiting for a server in seconds stats.total_xact_count | pgbouncer_stats_sql_transactions_pooled_total | Total number of SQL transactions pooled 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 pools.cl_active | pgbouncer_pools_client_active_connections | Client connections linked to server connection and able to process queries, shown as connection pools.cl_waiting | pgbouncer_pools_client_waiting_connections | Client connections waiting on a server connection, shown as connection pools.sv_active | pgbouncer_pools_server_active_connections | Server connections linked to a client connection, shown as connection pools.sv_idle | pgbouncer_pools_server_idle_connections | Server connections idle and ready for a client query, shown as connection pools.sv_used | pgbouncer_pools_server_used_connections | Server connections idle more than server_check_delay, needing server_check_query, shown as connection pools.sv_tested | pgbouncer_pools_server_testing_connections | Server connections currently running either server_reset_query or server_check_query, shown as connection pools.sv_login | pgbouncer_pools_server_login_connections | Server connections currently in the process of logging in, shown as connection pools.maxwait | pgbouncer_pools_client_maxwait_seconds | Age of oldest unserved client connection, shown as second config.max_client_conn | pgbouncer_config_max_client_connections | Configured maximum number of client connections config.max_user_connections | pgbouncer_config_max_user_connections | Configured maximum number of server connections per user ## TLS and basic authentication The pgbouncer exporter supports TLS and basic authentication. To use TLS and/or basic authentication, you need to pass a configuration file using the `--web.config.file` parameter. The format of the file is described [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). pgbouncer_exporter-0.12.0/SECURITY.md000066400000000000000000000002541515204764100173050ustar00rootroot00000000000000# Reporting a security issue The Prometheus security policy, including how to report vulnerabilities, can be found here: pgbouncer_exporter-0.12.0/VERSION000066400000000000000000000000071515204764100165600ustar00rootroot000000000000000.12.0 pgbouncer_exporter-0.12.0/collector.go000066400000000000000000000560061515204764100200370ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "database/sql" "errors" "fmt" "log/slog" "math" "strconv" "time" "unicode/utf8" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" ) var ( metricMaps = map[string]map[string]ColumnMapping{ "databases": { "name": {LABEL, "N/A", 1, "N/A"}, "host": {LABEL, "N/A", 1, "N/A"}, "port": {LABEL, "N/A", 1, "N/A"}, "database": {LABEL, "N/A", 1, "N/A"}, "force_user": {LABEL, "N/A", 1, "N/A"}, "pool_size": {GAUGE, "pool_size", 1, "Maximum number of server connections"}, "reserve_pool": {GAUGE, "reserve_pool", 1, "Maximum number of additional connections for this database"}, "pool_mode": {LABEL, "N/A", 1, "N/A"}, "max_connections": {GAUGE, "max_connections", 1, "Maximum number of allowed connections for this database"}, "current_connections": {GAUGE, "current_connections", 1, "Current number of connections for this database"}, "paused": {GAUGE, "paused", 1, "1 if this database is currently paused, else 0"}, "disabled": {GAUGE, "disabled", 1, "1 if this database is currently disabled, else 0"}, }, "stats_totals": { "database": {LABEL, "N/A", 1, "N/A"}, "query_count": {COUNTER, "queries_pooled_total", 1, "Total number of SQL queries pooled"}, "query_time": {COUNTER, "queries_duration_seconds_total", 1e-6, "Total number of seconds spent by pgbouncer when actively connected to PostgreSQL, executing queries"}, "bytes_received": {COUNTER, "received_bytes_total", 1, "Total volume in bytes of network traffic received by pgbouncer, shown as bytes"}, "requests": {COUNTER, "queries_total", 1, "Total number of SQL requests pooled by pgbouncer, shown as requests"}, "bytes_sent": {COUNTER, "sent_bytes_total", 1, "Total volume in bytes of network traffic sent by pgbouncer, shown as bytes"}, "wait_time": {COUNTER, "client_wait_seconds_total", 1e-6, "Time spent by clients waiting for a server in seconds"}, "xact_count": {COUNTER, "sql_transactions_pooled_total", 1, "Total number of SQL transactions pooled"}, "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"}, "client_parse_count": {COUNTER, "client_parses_total", 1, "Total number of prepared statement Parse messages received from clients"}, "server_parse_count": {COUNTER, "server_parses_total", 1, "Total number of prepared statement Parse messages sent by pgbouncer to PostgreSQL"}, "bind_count": {COUNTER, "binds_total", 1, "Total number of prepared statements readied for execution with a Bind message"}, "server_assignment_count": {COUNTER, "server_assignments_total", 1, "Total number of client connections which have been served since process start"}, }, "pools": { "database": {LABEL, "N/A", 1, "N/A"}, "user": {LABEL, "N/A", 1, "N/A"}, "cl_active": {GAUGE, "client_active_connections", 1, "Client connections linked to server connection and able to process queries, shown as connection"}, "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"}, "cl_waiting": {GAUGE, "client_waiting_connections", 1, "Client connections waiting on a server connection, shown as connection"}, "cl_waiting_cancel_req": {GAUGE, "client_waiting_cancel_connections", 1, "Client connections that have not forwarded query cancellations to the server yet"}, "sv_active": {GAUGE, "server_active_connections", 1, "Server connections linked to a client connection, shown as connection"}, "sv_active_cancel": {GAUGE, "server_active_cancel_connections", 1, "Server connections that are currently forwarding a cancel request."}, "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."}, "sv_idle": {GAUGE, "server_idle_connections", 1, "Server connections idle and ready for a client query, shown as connection"}, "sv_used": {GAUGE, "server_used_connections", 1, "Server connections idle more than server_check_delay, needing server_check_query, shown as connection"}, "sv_tested": {GAUGE, "server_testing_connections", 1, "Server connections currently running either server_reset_query or server_check_query, shown as connection"}, "sv_login": {GAUGE, "server_login_connections", 1, "Server connections currently in the process of logging in, shown as connection"}, "maxwait": {GAUGE, "client_maxwait_seconds", 1, "Age of oldest unserved client connection, shown as second"}, }, } listsMap = map[string]*(prometheus.Desc){ "databases": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "databases"), "Count of databases", nil, nil), "users": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "users"), "Count of users", nil, nil), "pools": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "pools"), "Count of pools", nil, nil), "free_clients": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "free_clients"), "Count of free clients", nil, nil), "used_clients": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "used_clients"), "Count of used clients", nil, nil), "login_clients": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "login_clients"), "Count of clients in login state", nil, nil), "free_servers": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "free_servers"), "Count of free servers", nil, nil), "used_servers": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "used_servers"), "Count of used servers", nil, nil), "dns_names": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "cached_dns_names"), "Count of DNS names in the cache", nil, nil), "dns_zones": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "cached_dns_zones"), "Count of DNS zones in the cache", nil, nil), "dns_queries": prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "in_flight_dns_queries"), "Count of in-flight DNS queries", nil, nil), } configMap = map[string]*(prometheus.Desc){ "max_client_conn": prometheus.NewDesc( prometheus.BuildFQName(namespace, "config", "max_client_connections"), "Config maximum number of client connections", nil, nil), "max_user_connections": prometheus.NewDesc( prometheus.BuildFQName(namespace, "config", "max_user_connections"), "Config maximum number of server connections per user", nil, nil), } ) var ( clientConnectionsDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, "client", "connections"), "Number of client connections grouped by database, user, application name, and state", []string{"database", "user", "application_name", "state"}, nil, ) ) // Metric descriptors. var ( bouncerVersionDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, "version", "info"), "The pgbouncer version info", []string{"version"}, nil, ) scrapeSuccessDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, "", "up"), "The pgbouncer scrape succeeded", nil, nil, ) ) func NewExporter(connectionString string, namespace string, logger *slog.Logger) *Exporter { conn, err := pq.NewConnector(connectionString) if err != nil { logger.Error("failed to create connector", "error", err) return nil } return &Exporter{ conn: conn, metricMap: makeDescMap(metricMaps, namespace, logger), logger: logger, } } // Query SHOW LISTS, which has a series of rows, not columns. func queryShowLists(ch chan<- prometheus.Metric, db *sql.DB, logger *slog.Logger) error { rows, err := db.Query("SHOW LISTS;") if err != nil { return fmt.Errorf("error running SHOW LISTS on database: %w", err) } defer rows.Close() columnNames, err := rows.Columns() if err != nil || len(columnNames) != 2 { return fmt.Errorf("error retrieving columns list from SHOW LISTS: %w", err) } var list string var items sql.RawBytes for rows.Next() { if err = rows.Scan(&list, &items); err != nil { return fmt.Errorf("error retrieving SHOW LISTS rows: %w", err) } value, err := strconv.ParseFloat(string(items), 64) if err != nil { return fmt.Errorf("error parsing SHOW LISTS column: %v, error: %w", list, err) } if metric, ok := listsMap[list]; ok { ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, value) } else { logger.Debug("SHOW LISTS unknown list", "list", list) } } return nil } // Query SHOW CONFIG, which has a series of rows, not columns. func queryShowConfig(ch chan<- prometheus.Metric, db *sql.DB, logger *slog.Logger) error { rows, err := db.Query("SHOW CONFIG;") if err != nil { return fmt.Errorf("error running SHOW CONFIG on database: %w", err) } defer rows.Close() columnNames, err := rows.Columns() numColumns := len(columnNames) if err != nil { return fmt.Errorf("error retrieving columns list from SHOW CONFIG: %w", err) } exposedConfig := make(map[string]bool) for configKey := range configMap { exposedConfig[configKey] = true } var key string var values sql.RawBytes var defaultValue sql.RawBytes var changeable string for rows.Next() { switch numColumns { case 3: if err = rows.Scan(&key, &values, &changeable); err != nil { return fmt.Errorf("error retrieving SHOW CONFIG rows: %w", err) } case 4: if err = rows.Scan(&key, &values, &defaultValue, &changeable); err != nil { return fmt.Errorf("error retrieving SHOW CONFIG rows: %w", err) } default: return fmt.Errorf("invalid number of SHOW CONFIG columns: %d", numColumns) } if !exposedConfig[key] { continue } value, err := strconv.ParseFloat(string(values), 64) if err != nil { return fmt.Errorf("error parsing SHOW CONFIG column: %v, error: %w ", key, err) } if metric, ok := configMap[key]; ok { ch <- prometheus.MustNewConstMetric(metric, prometheus.GaugeValue, value) } else { logger.Debug("SHOW CONFIG unknown config", "config", key) } } return nil } // Query SHOW CLIENTS, aggregate by (database, user, application_name, state), and emit counts. func queryShowClients(ch chan<- prometheus.Metric, db *sql.DB, _ *slog.Logger) error { rows, err := db.Query("SHOW CLIENTS;") if err != nil { return fmt.Errorf("error running SHOW CLIENTS on database: %w", err) } defer rows.Close() columnNames, err := rows.Columns() if err != nil { return fmt.Errorf("error retrieving columns from SHOW CLIENTS: %w", err) } colIdx := make(map[string]int, len(columnNames)) for i, name := range columnNames { colIdx[name] = i } for _, required := range []string{"database", "user", "state"} { if _, ok := colIdx[required]; !ok { return fmt.Errorf("SHOW CLIENTS missing required column: %s", required) } } type groupKey struct{ database, user, applicationName, state string } counts := make(map[groupKey]float64) var ( dbCol sql.RawBytes userCol sql.RawBytes stateCol sql.RawBytes appCol sql.RawBytes discard sql.RawBytes ) hasAppName := false scanArgs := make([]any, len(columnNames)) for i, name := range columnNames { switch name { case "database": scanArgs[i] = &dbCol case "user": scanArgs[i] = &userCol case "state": scanArgs[i] = &stateCol case "application_name": hasAppName = true scanArgs[i] = &appCol default: scanArgs[i] = &discard } } sanitize := func(s string) string { if !utf8.ValidString(s) { return "" } return s } for rows.Next() { if err := rows.Scan(scanArgs...); err != nil { return fmt.Errorf("error scanning SHOW CLIENTS row: %w", err) } appName := "" if hasAppName { appName = string(appCol) } key := groupKey{ database: sanitize(string(dbCol)), user: sanitize(string(userCol)), applicationName: sanitize(appName), state: sanitize(string(stateCol)), } counts[key]++ } if err := rows.Err(); err != nil { return fmt.Errorf("error iterating SHOW CLIENTS rows: %w", err) } for key, count := range counts { ch <- prometheus.MustNewConstMetric( clientConnectionsDesc, prometheus.GaugeValue, count, key.database, key.user, key.applicationName, key.state, ) } return nil } // Query within a namespace mapping and emit metrics. Returns fatal errors if // the scrape fails, and a slice of errors if they were non-fatal. func queryNamespaceMapping(ch chan<- prometheus.Metric, db *sql.DB, namespace string, mapping MetricMapNamespace, logger *slog.Logger) ([]error, error) { query := fmt.Sprintf("SHOW %s;", namespace) // Don't fail on a bad scrape of one metric rows, err := db.Query(query) if err != nil { return []error{}, fmt.Errorf("error running query on database: %v, error: %w", namespace, err) } defer rows.Close() var columnNames []string columnNames, err = rows.Columns() if err != nil { return []error{}, fmt.Errorf("error retrieving column list for: %v, error: %w", namespace, err) } // Make a lookup map for the column indices var columnIdx = make(map[string]int, len(columnNames)) for i, n := range columnNames { columnIdx[n] = i } var columnData = make([]interface{}, len(columnNames)) var scanArgs = make([]interface{}, len(columnNames)) for i := range columnData { scanArgs[i] = &columnData[i] } nonfatalErrors := []error{} for rows.Next() { labelValues := make([]string, len(mapping.labels)) err = rows.Scan(scanArgs...) if err != nil { return []error{}, fmt.Errorf("error retrieving rows: %v, error: %w", namespace, err) } for i, label := range mapping.labels { for idx, columnName := range columnNames { if columnName == label { switch v := columnData[idx].(type) { case int: labelValues[i] = strconv.Itoa(columnData[idx].(int)) case int64: labelValues[i] = strconv.Itoa(int(columnData[idx].(int64))) case float64: labelValues[i] = fmt.Sprintf("%f", columnData[idx].(float64)) case string: labelValues[i] = columnData[idx].(string) case nil: labelValues[i] = "" default: nonfatalErrors = append(nonfatalErrors, fmt.Errorf("column %s in %s has an unhandled type %v for label: %s ", columnName, namespace, v, columnData[idx])) labelValues[i] = "" continue } // Prometheus will fail hard if the database and usernames are not UTF-8 if !utf8.ValidString(labelValues[i]) { nonfatalErrors = append(nonfatalErrors, fmt.Errorf("column %s in %s has an invalid UTF-8 for a label: %s ", columnName, namespace, columnData[idx])) labelValues[i] = "" continue } } } } // Loop over column names, and match to scan data. Unknown columns // will be filled with an untyped metric number *if* they can be // converted to float64s. NULLs are allowed and treated as NaN. for idx, columnName := range columnNames { if metricMapping, ok := mapping.columnMappings[columnName]; ok { // Is this a metricy metric? if metricMapping.discard { continue } value, ok := metricMapping.conversion(columnData[idx]) if !ok { nonfatalErrors = append(nonfatalErrors, fmt.Errorf("unexpected error parsing namespace: %v, column: %v, index: %v", namespace, columnName, columnData[idx])) continue } // Generate the metric ch <- prometheus.MustNewConstMetric(metricMapping.desc, metricMapping.vtype, value, labelValues...) } } } if err := rows.Err(); err != nil { logger.Error("Failed scaning all rows", "err", err.Error()) nonfatalErrors = append(nonfatalErrors, fmt.Errorf("failed to consume all rows due to: %w", err)) } return nonfatalErrors, nil } func getDB(conn *pq.Connector) (*sql.DB, error) { db := sql.OpenDB(conn) if db == nil { return nil, errors.New("error opening DB") } rows, err := db.Query("SHOW STATS") if err != nil { return nil, fmt.Errorf("error pinging pgbouncer: %w", err) } defer rows.Close() db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) return db, nil } // Convert database.sql types to float64s for Prometheus consumption. Null types are mapped to NaN. string and []byte // types are mapped as NaN and !ok func dbToFloat64(t interface{}, factor float64) (float64, bool) { switch v := t.(type) { case int64: return float64(v) * factor, true case float64: return v * factor, true case time.Time: return float64(v.Unix()), true case []byte: // Try and convert to string and then parse to a float64 strV := string(v) result, err := strconv.ParseFloat(strV, 64) if err != nil { return math.NaN(), false } return result * factor, true case string: result, err := strconv.ParseFloat(v, 64) if err != nil { return math.NaN(), false } return result * factor, true case nil: return math.NaN(), true default: return math.NaN(), false } } // Iterate through all the namespace mappings in the exporter and run their queries. func queryNamespaceMappings(ch chan<- prometheus.Metric, db *sql.DB, metricMap map[string]MetricMapNamespace, logger *slog.Logger) map[string]error { // Return a map of namespace -> errors namespaceErrors := make(map[string]error) for namespace, mapping := range metricMap { logger.Debug("Querying namespace", "namespace", namespace) nonFatalErrors, err := queryNamespaceMapping(ch, db, namespace, mapping, logger) // Serious error - a namespace disappeared if err != nil { namespaceErrors[namespace] = err logger.Info("namespace disappeared", "err", err.Error()) } // Non-serious errors - likely version or parsing problems. if len(nonFatalErrors) > 0 { for _, err := range nonFatalErrors { logger.Info("error parsing", "err", err.Error()) } } } return namespaceErrors } // Gather the pgbouncer version info. func queryVersion(ch chan<- prometheus.Metric, db *sql.DB) error { rows, err := db.Query("SHOW VERSION;") if err != nil { return fmt.Errorf("error getting pgbouncer version: %w", err) } defer rows.Close() var columnNames []string columnNames, err = rows.Columns() if err != nil { return fmt.Errorf("error retrieving column list for version: %w", err) } if len(columnNames) != 1 || columnNames[0] != "version" { return errors.New("show version didn't return version column") } var bouncerVersion string for rows.Next() { err := rows.Scan(&bouncerVersion) if err != nil { return err } ch <- prometheus.MustNewConstMetric( bouncerVersionDesc, prometheus.GaugeValue, 1.0, bouncerVersion, ) } return nil } // Describe implements prometheus.Collector. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { // We cannot know in advance what metrics the exporter will generate // from Postgres. So we use the poor man's describe method: Run a collect // and send the descriptors of all the collected metrics. The problem // here is that we need to connect to the Postgres DB. If it is currently // unavailable, the descriptors will be incomplete. Since this is a // stand-alone exporter and not used as a library within other code // implementing additional metrics, the worst that can happen is that we // don't detect inconsistent metrics created by this exporter // itself. Also, a change in the monitored Postgres instance may change the // exported metrics during the runtime of the exporter. metricCh := make(chan prometheus.Metric) doneCh := make(chan struct{}) go func() { for m := range metricCh { ch <- m.Desc() } close(doneCh) }() e.Collect(metricCh) close(metricCh) <-doneCh } // Collect implements prometheus.Collector. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { e.logger.Debug("Starting scrape") var up = 1.0 defer func() { ch <- prometheus.MustNewConstMetric(scrapeSuccessDesc, prometheus.GaugeValue, up) }() db, err := getDB(e.conn) if err != nil { e.logger.Warn("error setting up DB connection", "err", err.Error()) up = 0 return } defer db.Close() err = queryVersion(ch, db) if err != nil { e.logger.Warn("error getting version", "err", err.Error()) up = 0 } if err = queryShowLists(ch, db, e.logger); err != nil { e.logger.Warn("error getting SHOW LISTS", "err", err.Error()) up = 0 } if err = queryShowConfig(ch, db, e.logger); err != nil { e.logger.Warn("error getting SHOW CONFIG", "err", err.Error()) up = 0 } if err = queryShowClients(ch, db, e.logger); err != nil { e.logger.Warn("error getting SHOW CLIENTS", "err", err.Error()) up = 0 } errMap := queryNamespaceMappings(ch, db, e.metricMap, e.logger) if len(errMap) > 0 { e.logger.Warn("error querying namespace mappings", "err", errMap) up = 0 } if len(errMap) == len(e.metricMap) { up = 0 } } // Turn the MetricMap column mapping into a prometheus descriptor mapping. func makeDescMap(metricMaps map[string]map[string]ColumnMapping, namespace string, logger *slog.Logger) map[string]MetricMapNamespace { var metricMap = make(map[string]MetricMapNamespace) for metricNamespace, mappings := range metricMaps { thisMap := make(map[string]MetricMap) var labels = make([]string, 0) // First collect all the labels since the metrics will need them for columnName, columnMapping := range mappings { if columnMapping.usage == LABEL { logger.Debug("Adding label", "column_name", columnName, "metric_namespace", metricNamespace) labels = append(labels, columnName) } } for columnName, columnMapping := range mappings { factor := columnMapping.factor // Determine how to convert the column based on its usage. switch columnMapping.usage { case COUNTER: thisMap[columnName] = MetricMap{ vtype: prometheus.CounterValue, desc: prometheus.NewDesc(fmt.Sprintf("%s_%s_%s", namespace, metricNamespace, columnMapping.metric), columnMapping.description, labels, nil), conversion: func(in interface{}) (float64, bool) { return dbToFloat64(in, factor) }, } case GAUGE: thisMap[columnName] = MetricMap{ vtype: prometheus.GaugeValue, desc: prometheus.NewDesc(fmt.Sprintf("%s_%s_%s", namespace, metricNamespace, columnMapping.metric), columnMapping.description, labels, nil), conversion: func(in interface{}) (float64, bool) { return dbToFloat64(in, factor) }, } } } metricMap[metricNamespace] = MetricMapNamespace{thisMap, labels} } return metricMap } pgbouncer_exporter-0.12.0/collector_test.go000066400000000000000000000251321515204764100210720ustar00rootroot00000000000000// Copyright 2024 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific langu package main import ( "testing" "log/slog" "github.com/DATA-DOG/go-sqlmock" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "github.com/smartystreets/goconvey/convey" ) type labelMap map[string]string type MetricResult struct { labels labelMap value float64 metricType dto.MetricType } func readMetric(m prometheus.Metric) MetricResult { pb := &dto.Metric{} m.Write(pb) labels := make(labelMap, len(pb.Label)) for _, v := range pb.Label { labels[v.GetName()] = v.GetValue() } if pb.Gauge != nil { return MetricResult{labels: labels, value: pb.GetGauge().GetValue(), metricType: dto.MetricType_GAUGE} } if pb.Counter != nil { return MetricResult{labels: labels, value: pb.GetCounter().GetValue(), metricType: dto.MetricType_COUNTER} } if pb.Untyped != nil { return MetricResult{labels: labels, value: pb.GetUntyped().GetValue(), metricType: dto.MetricType_UNTYPED} } panic("Unsupported metric type") } func TestQueryShowList(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Error opening a stub db connection: %s", err) } defer db.Close() rows := sqlmock.NewRows([]string{"key", "value"}). AddRow("dns_queries", -1). AddRow("databases", 1). AddRow("pools", 0). AddRow("users", 2) mock.ExpectQuery("SHOW LISTS;").WillReturnRows(rows) logger := &slog.Logger{} ch := make(chan prometheus.Metric) go func() { defer close(ch) if err := queryShowLists(ch, db, logger); err != nil { t.Errorf("Error running queryShowList: %s", err) } }() expected := []MetricResult{ {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: -1}, {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 1}, {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 0}, {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 2}, } convey.Convey("Metrics comparison", t, func() { for _, expect := range expected { m := readMetric(<-ch) convey.So(expect, convey.ShouldResemble, m) } }) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled exceptions: %s", err) } } func TestQueryShowConfig(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Error opening a stub db connection: %s", err) } defer db.Close() rows := sqlmock.NewRows([]string{"key", "value", "default", "changeable"}). AddRow("max_client_conn", 1900, 100, true). AddRow("max_user_connections", 100, 100, true). AddRow("auth_type", "md5", "md5", true). AddRow("client_tls_ciphers", "default", "default", "yes") mock.ExpectQuery("SHOW CONFIG;").WillReturnRows(rows) logger := &slog.Logger{} ch := make(chan prometheus.Metric) go func() { defer close(ch) if err := queryShowConfig(ch, db, logger); err != nil { t.Errorf("Error running queryShowConfig: %s", err) } }() expected := []MetricResult{ {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 1900}, {labels: labelMap{}, metricType: dto.MetricType_GAUGE, value: 100}, } convey.Convey("Metrics comparison", t, func() { for _, expect := range expected { m := readMetric(<-ch) convey.So(expect, convey.ShouldResemble, m) } }) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled exceptions: %s", err) } } func TestQueryShowClients(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Error opening a stub db connection: %s", err) } defer db.Close() rows := sqlmock.NewRows([]string{"type", "user", "database", "state", "addr", "port", "local_addr", "local_port", "connect_time", "request_time", "wait", "wait_us", "close_needed", "ptr", "link", "remote_pid", "tls", "application_name"}). AddRow("C", "alice", "mydb", "active", "10.0.0.1", 5432, "10.0.0.2", 6432, "2024-01-01", "2024-01-01", 0, 0, 0, "0x0", "", 0, "", "myapp"). AddRow("C", "alice", "mydb", "active", "10.0.0.3", 5432, "10.0.0.2", 6432, "2024-01-01", "2024-01-01", 0, 0, 0, "0x1", "", 0, "", "myapp"). AddRow("C", "bob", "mydb", "idle", "10.0.0.4", 5432, "10.0.0.2", 6432, "2024-01-01", "2024-01-01", 0, 0, 0, "0x2", "", 0, "", "otherapp") mock.ExpectQuery("SHOW CLIENTS;").WillReturnRows(rows) logger := slog.Default() ch := make(chan prometheus.Metric) go func() { defer close(ch) if err := queryShowClients(ch, db, logger); err != nil { t.Errorf("Error running queryShowClients: %s", err) } }() results := []MetricResult{} for m := range ch { results = append(results, readMetric(m)) } convey.Convey("Clients metrics aggregated correctly", t, func() { convey.So(len(results), convey.ShouldEqual, 2) found := map[string]float64{} for _, r := range results { key := r.labels["user"] + "/" + r.labels["application_name"] + "/" + r.labels["state"] found[key] = r.value } convey.So(found["alice/myapp/active"], convey.ShouldEqual, 2) convey.So(found["bob/otherapp/idle"], convey.ShouldEqual, 1) }) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } } func TestQueryShowClientsNoApplicationName(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Error opening a stub db connection: %s", err) } defer db.Close() // Simulate PgBouncer < 1.18 which does not expose application_name rows := sqlmock.NewRows([]string{"type", "user", "database", "state", "addr", "port", "local_addr", "local_port", "connect_time", "request_time", "wait", "wait_us", "close_needed", "ptr", "link", "remote_pid", "tls"}). AddRow("C", "alice", "mydb", "active", "10.0.0.1", 5432, "10.0.0.2", 6432, "2024-01-01", "2024-01-01", 0, 0, 0, "0x0", "", 0, ""). AddRow("C", "alice", "mydb", "active", "10.0.0.3", 5432, "10.0.0.2", 6432, "2024-01-01", "2024-01-01", 0, 0, 0, "0x1", "", 0, "") mock.ExpectQuery("SHOW CLIENTS;").WillReturnRows(rows) logger := slog.Default() ch := make(chan prometheus.Metric) go func() { defer close(ch) if err := queryShowClients(ch, db, logger); err != nil { t.Errorf("Error running queryShowClients without application_name: %s", err) } }() results := []MetricResult{} for m := range ch { results = append(results, readMetric(m)) } convey.Convey("Clients metrics work without application_name column", t, func() { convey.So(len(results), convey.ShouldEqual, 1) convey.So(results[0].value, convey.ShouldEqual, 2) convey.So(results[0].labels["application_name"], convey.ShouldEqual, "") convey.So(results[0].labels["user"], convey.ShouldEqual, "alice") convey.So(results[0].labels["state"], convey.ShouldEqual, "active") }) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } } func TestQueryShowDatabases(t *testing.T) { rows := sqlmock.NewRows([]string{"name", "host", "port", "database", "pool_size"}). AddRow("pg0_db", "10.10.10.1", "5432", "pg0", 20) expected := []MetricResult{ {labels: labelMap{"name": "pg0_db", "host": "10.10.10.1", "port": "5432", "database": "pg0", "force_user": "", "pool_mode": ""}, metricType: dto.MetricType_GAUGE, value: 20}, } testQueryNamespaceMapping(t, "databases", rows, expected) } func TestQueryShowStats(t *testing.T) { // columns are listed in the order PgBouncers exposes them, a value of -1 means pgbouncer_exporter does not expose this value as a metric rows := sqlmock.NewRows([]string{"database", "server_assignment_count", "xact_count", "query_count", "bytes_received", "bytes_sent", "xact_time", "query_time", "wait_time", "client_parse_count", "server_parse_count", "bind_count"}). AddRow("pg0", -1, 10, 40, 220, 460, 6, 8, 9, 5, 55, 555) // expected metrics are returned in the same order as the colums expected := []MetricResult{ {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: -1}, // server_assignment_count {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 10}, // xact_count {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 40}, // query_count {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 220}, // bytes_received {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 460}, // bytes_sent {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 6e-6}, // xact_time {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 8e-6}, // query_time {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 9e-6}, // wait_time {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 5}, // client_parse_count {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 55}, // server_parse_count {labels: labelMap{"database": "pg0"}, metricType: dto.MetricType_COUNTER, value: 555}, // bind_count } testQueryNamespaceMapping(t, "stats_totals", rows, expected) } func TestQueryShowPools(t *testing.T) { rows := sqlmock.NewRows([]string{"database", "user", "cl_active"}). AddRow("pg0", "postgres", 2) expected := []MetricResult{ {labels: labelMap{"database": "pg0", "user": "postgres"}, metricType: dto.MetricType_GAUGE, value: 2}, } testQueryNamespaceMapping(t, "pools", rows, expected) } func testQueryNamespaceMapping(t *testing.T, namespaceMapping string, rows *sqlmock.Rows, expected []MetricResult) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("Error opening a stub db connection: %s", err) } defer db.Close() mock.ExpectQuery("SHOW " + namespaceMapping + ";").WillReturnRows(rows) logger := slog.Default() metricMap := makeDescMap(metricMaps, namespace, logger) ch := make(chan prometheus.Metric) go func() { defer close(ch) if _, err := queryNamespaceMapping(ch, db, namespaceMapping, metricMap[namespaceMapping], logger); err != nil { t.Errorf("Error running queryNamespaceMapping: %s", err) } }() convey.Convey("Metrics comparison", t, func() { for _, expect := range expected { m := readMetric(<-ch) convey.So(m, convey.ShouldResemble, expect) } }) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled exceptions: %s", err) } } pgbouncer_exporter-0.12.0/docker-compose.yml000066400000000000000000000004051515204764100211470ustar00rootroot00000000000000version: '2' services: postgres: image: postgres:9.4 ports: - "5432" pgbouncer: image: starefossen/pgbouncer:latest links: - postgres ports: - "127.0.0.1:6543:6543" volumes: - ./pgbouncer.ini:/pgbouncer.ini pgbouncer_exporter-0.12.0/go.mod000066400000000000000000000031671515204764100166300ustar00rootroot00000000000000module github.com/prometheus-community/pgbouncer_exporter go 1.25.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/lib/pq v1.11.2 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/prometheus/common v0.67.5 github.com/prometheus/exporter-toolkit v0.15.1 github.com/smartystreets/goconvey v1.8.1 ) require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/vsock v1.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.14.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) pgbouncer_exporter-0.12.0/go.sum000066400000000000000000000215221515204764100166500ustar00rootroot00000000000000github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ= github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/exporter-toolkit v0.15.1 h1:XrGGr/qWl8Gd+pqJqTkNLww9eG8vR/CoRk0FubOKfLE= github.com/prometheus/exporter-toolkit v0.15.1/go.mod h1:P/NR9qFRGbCFgpklyhix9F6v6fFr/VQB/CVsrMDGKo4= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pgbouncer_exporter-0.12.0/pgbouncer.ini000066400000000000000000000003171515204764100202010ustar00rootroot00000000000000[databases] postgres = host=postgres dbname=postgres [pgbouncer] user = pgbouncer pool_mode = session listen_port = 6543 listen_addr = 0.0.0.0 auth_type = any ignore_startup_parameters = extra_float_digits pgbouncer_exporter-0.12.0/pgbouncer_exporter.go000066400000000000000000000073371515204764100217700ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "net/http" "os" "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promslog" "github.com/prometheus/common/promslog/flag" "github.com/prometheus/common/version" "github.com/prometheus/exporter-toolkit/web" "github.com/prometheus/exporter-toolkit/web/kingpinflag" ) const namespace = "pgbouncer" func main() { const pidFileHelpText = `Path to PgBouncer pid file. If provided, the standard process metrics get exported for the PgBouncer process, prefixed with 'pgbouncer_process_...'. The pgbouncer_process exporter needs to have read access to files owned by the PgBouncer process. Depends on the availability of /proc. https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics.` promslogConfig := &promslog.Config{} flag.AddFlags(kingpin.CommandLine, promslogConfig) var ( connectionStringPointer = kingpin.Flag("pgBouncer.connectionString", "Connection string for accessing pgBouncer.").Default("postgres://postgres:@localhost:6543/pgbouncer?sslmode=disable").Envar("PGBOUNCER_EXPORTER_CONNECTION_STRING").String() metricsPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").String() pidFilePath = kingpin.Flag("pgBouncer.pid-file", pidFileHelpText).Default("").String() ) toolkitFlags := kingpinflag.AddFlags(kingpin.CommandLine, ":9127") kingpin.Version(version.Print("pgbouncer_exporter")) kingpin.HelpFlag.Short('h') kingpin.Parse() logger := promslog.New(promslogConfig) logger.Info("Starting pgbouncer_exporter", "version", version.Info()) logger.Info("Build context", "build_context", version.BuildContext()) connectionString := *connectionStringPointer exporter := NewExporter(connectionString, namespace, logger) if exporter == nil { logger.Error("Failed to create exporter") os.Exit(1) } prometheus.MustRegister(exporter) prometheus.MustRegister(versioncollector.NewCollector("pgbouncer_exporter")) if *pidFilePath != "" { procExporter := collectors.NewProcessCollector( collectors.ProcessCollectorOpts{ PidFn: prometheus.NewPidFileFn(*pidFilePath), Namespace: namespace, }, ) prometheus.MustRegister(procExporter) } http.Handle(*metricsPath, promhttp.Handler()) if *metricsPath != "/" && *metricsPath != "" { landingConfig := web.LandingConfig{ Name: "PgBouncer Exporter", Description: "Prometheus Exporter for PgBouncer servers", Version: version.Info(), Links: []web.LandingLinks{ { Address: *metricsPath, Text: "Metrics", }, }, } landingPage, err := web.NewLandingPage(landingConfig) if err != nil { logger.Error("Error creating landing page", "err", err) os.Exit(1) } http.Handle("/", landingPage) } srv := &http.Server{} if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { logger.Error("Error starting server", "err", err) os.Exit(1) } } pgbouncer_exporter-0.12.0/struct.go000066400000000000000000000057441515204764100174000ustar00rootroot00000000000000// Copyright 2020 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main // Elasticsearch Node Stats Structs import ( "fmt" "log/slog" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" ) type columnUsage int // convert a string to the corresponding columnUsage func stringTocolumnUsage(s string) (u columnUsage, err error) { switch s { case "DISCARD": u = DISCARD case "LABEL": u = LABEL case "COUNTER": u = COUNTER case "GAUGE": u = GAUGE case "MAPPEDMETRIC": u = MAPPEDMETRIC case "DURATION": u = DURATION default: err = fmt.Errorf("wrong columnUsage given : %s", s) } return } // Implements the yaml.Unmarshaller interface func (cu *columnUsage) UnmarshalYAML(unmarshal func(interface{}) error) error { var value string if err := unmarshal(&value); err != nil { return err } columnUsage, err := stringTocolumnUsage(value) if err != nil { return err } *cu = columnUsage return nil } const ( DISCARD columnUsage = iota // Ignore this column LABEL columnUsage = iota // Use this column as a label COUNTER columnUsage = iota // Use this column as a counter GAUGE columnUsage = iota // Use this column as a gauge MAPPEDMETRIC columnUsage = iota // Use this column with the supplied mapping of text values DURATION columnUsage = iota // This column should be interpreted as a text duration (and converted to milliseconds) ) // Groups metric maps under a shared set of labels type MetricMapNamespace struct { columnMappings map[string]MetricMap // Column mappings in this namespace labels []string } // Stores the prometheus metric description which a given column will be mapped // to by the collector type MetricMap struct { discard bool // Should metric be discarded during mapping? vtype prometheus.ValueType // Prometheus valuetype desc *prometheus.Desc // Prometheus descriptor conversion func(interface{}) (float64, bool) // Conversion function to turn PG result into float64 } type ColumnMapping struct { usage columnUsage `yaml:"usage"` metric string `yaml:"metric"` factor float64 `yaml:"factor"` description string `yaml:"description"` } // Exporter collects PgBouncer stats from the given server and exports // them using the prometheus metrics package. type Exporter struct { conn *pq.Connector metricMap map[string]MetricMapNamespace logger *slog.Logger }