mirror of
https://github.com/prometheus-community/postgres_exporter
synced 2025-04-07 09:41:52 +00:00
Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f8b7139174 | ||
|
43576acc76 | ||
|
8d5ec4b3ea | ||
|
9e86f1ee38 | ||
|
fca2ad84cd | ||
|
2ce65c324c | ||
|
b0e61bf263 | ||
|
602302ffe2 | ||
|
457b6fa8cd | ||
|
1e574cf4fd | ||
|
2869087f3c | ||
|
51006aba2f | ||
|
8bb1a41abf | ||
|
4c170ed564 | ||
|
99e1b5118c | ||
|
c3885e840a | ||
|
2ee2a8fa7c | ||
|
9e42fc0145 | ||
|
072864d179 | ||
|
d85a7710bf | ||
|
3acc4793fc | ||
|
7d4c278221 | ||
|
9de4f19d43 | ||
|
ecb5ec5dff | ||
|
bea2609519 | ||
|
5145620988 | ||
|
5bb1702321 | ||
|
6f36adfadf | ||
|
a324fe37bc | ||
|
4abdfa5bfd | ||
|
0045c4f93e | ||
|
340a104d25 | ||
|
c52405ab48 | ||
|
552ff92f8b | ||
|
f9c74570ed | ||
|
071ebb6244 | ||
|
e8540767e4 | ||
|
3743987494 | ||
|
3be4edccd4 | ||
|
98f75c7e7e | ||
|
3c5ef40e2b | ||
|
49f66e1bfb | ||
|
a4ac0e6747 | ||
|
cc0fd2eda5 | ||
|
ddd51368a1 | ||
|
5ffc58cd28 | ||
|
b126e621db | ||
|
89087f1744 | ||
|
838f09c97f | ||
|
8f39f5b114 | ||
|
f98834a678 | ||
|
9cfa132115 | ||
|
825cc8af13 | ||
|
f5b613aba7 | ||
|
5ceae7f414 | ||
|
34f5443ca0 | ||
|
ae1375b28e | ||
|
f0ea0163bb | ||
|
94b0651246 | ||
|
68c176b883 | ||
|
e2892a7976 | ||
|
2a5692c028 | ||
|
f0f051cb9a | ||
|
69fc35b0ec | ||
|
5e24d43e3e | ||
|
51415a0e5b | ||
|
30d7d25a7e | ||
|
e3eaa91c0b | ||
|
c06e57db4e | ||
|
add5b86cff | ||
|
4e521d460e | ||
|
31ef4ed5a2 | ||
|
0b6d9860ab | ||
|
dbc7b0b229 | ||
|
68ea167866 | ||
|
a181fba674 | ||
|
5890879126 | ||
|
ce4ee0507f | ||
|
ce74daee92 | ||
|
2402783205 | ||
|
b74852a535 | ||
|
04bb60ce31 | ||
|
716ac23f20 | ||
|
f9277b04b7 | ||
|
74800f483a | ||
|
2d7e152751 | ||
|
dc3e813f43 | ||
|
24a45f2fe3 | ||
|
c3eec6263b | ||
|
12c12cf368 | ||
|
4aa8cd4996 | ||
|
4ac5481917 | ||
|
9a9a4294c4 | ||
|
c514fcad2d | ||
|
d7766801fd | ||
|
5f917ccdd9 | ||
|
a8b86cf7da | ||
|
6b56e2f057 | ||
|
401711b2e3 | ||
|
2477aba363 | ||
|
a6012e0b54 | ||
|
cf67a472d0 | ||
|
2ca1798188 | ||
|
099d3ddb6f | ||
|
d01184f28d | ||
|
d920553227 | ||
|
dcf498e709 | ||
|
e6ce2ecba9 | ||
|
030a2a9bc7 | ||
|
1a4e8993f6 | ||
|
6a1bb59efb |
.circleci
.github/workflows
.golangci.yml.promu.yml.yamllintCHANGELOG.mdMakefile.commonREADME.mdVERSIONcmd/postgres_exporter
datasource.gomain.gonamespace.gopg_setting.gopg_setting_test.gopostgres_exporter.goprobe.goqueries.goserver.goutil.go
collector
collector.gocollector_test.goinstance.gopg_database.gopg_database_test.gopg_database_wraparound.gopg_database_wraparound_test.gopg_locks.gopg_locks_test.gopg_long_running_transactions.gopg_long_running_transactions_test.gopg_postmaster.gopg_process_idle.gopg_replication.gopg_replication_slot.gopg_replication_slot_test.gopg_replication_test.gopg_roles.gopg_roles_test.gopg_stat_activity_autovacuum.gopg_stat_activity_autovacuum_test.gopg_stat_bgwriter.gopg_stat_bgwriter_test.gopg_stat_checkpointer.gopg_stat_checkpointer_test.gopg_stat_database.gopg_stat_database_test.gopg_stat_progress_vacuum.gopg_stat_progress_vacuum_test.gopg_stat_statements.gopg_stat_statements_test.gopg_stat_user_tables.gopg_stat_user_tables_test.gopg_stat_walreceiver.gopg_stat_walreceiver_test.gopg_statio_user_indexes.gopg_statio_user_indexes_test.gopg_statio_user_tables.gopg_wal.gopg_wal_test.gopg_xlog_location.gopg_xlog_location_test.goprobe.go
config
go.modgo.sum@ -8,7 +8,7 @@ executors:
|
||||
# This must match .promu.yml.
|
||||
golang:
|
||||
docker:
|
||||
- image: cimg/go:1.20
|
||||
- image: cimg/go:1.24
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@ -16,13 +16,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- prometheus/setup_environment
|
||||
- run: GOHOSTARCH=386 GOARCH=386 make test
|
||||
- run: make
|
||||
- prometheus/store_artifact:
|
||||
file: postgres_exporter
|
||||
|
||||
integration:
|
||||
docker:
|
||||
- image: cimg/go:1.20
|
||||
- image: cimg/go:1.24
|
||||
- image: << parameters.postgres_image >>
|
||||
environment:
|
||||
POSTGRES_DB: circle_test
|
||||
@ -56,12 +57,13 @@ workflows:
|
||||
matrix:
|
||||
parameters:
|
||||
postgres_image:
|
||||
- circleci/postgres:10
|
||||
- circleci/postgres:11
|
||||
- circleci/postgres:12
|
||||
- circleci/postgres:13
|
||||
- cimg/postgres:14.1
|
||||
- cimg/postgres:15.1
|
||||
- cimg/postgres:14.9
|
||||
- cimg/postgres:15.4
|
||||
- cimg/postgres:16.0
|
||||
- cimg/postgres:17.0
|
||||
- prometheus/build:
|
||||
name: build
|
||||
parallelism: 3
|
||||
|
57
.github/workflows/container_description.yml
vendored
Normal file
57
.github/workflows/container_description.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
---
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- 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: ''
|
21
.github/workflows/golangci-lint.yml
vendored
21
.github/workflows/golangci-lint.yml
vendored
@ -1,3 +1,5 @@
|
||||
---
|
||||
# This action is synced from https://github.com/prometheus/prometheus
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
@ -10,21 +12,28 @@ on:
|
||||
- ".golangci.yml"
|
||||
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@v3
|
||||
- name: install Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
|
||||
with:
|
||||
go-version: 1.20.x
|
||||
go-version: 1.24.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: Lint
|
||||
uses: golangci/golangci-lint-action@v3.4.0
|
||||
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
|
||||
with:
|
||||
version: v1.51.2
|
||||
args: --verbose
|
||||
version: v2.0.2
|
||||
|
@ -1,16 +1,36 @@
|
||||
---
|
||||
version: "2"
|
||||
linters:
|
||||
enable:
|
||||
- revive
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test.go
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
# Never check for logger errors.
|
||||
- (github.com/go-kit/log.Logger).Log
|
||||
- misspell
|
||||
- revive
|
||||
settings:
|
||||
errcheck:
|
||||
exclude-functions:
|
||||
- (github.com/go-kit/log.Logger).Log
|
||||
revive:
|
||||
rules:
|
||||
- name: unused-parameter
|
||||
severity: warning
|
||||
disabled: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- errcheck
|
||||
path: _test.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
@ -1,13 +1,12 @@
|
||||
go:
|
||||
# This must match .circle/config.yml.
|
||||
version: 1.20
|
||||
version: 1.24
|
||||
repository:
|
||||
path: github.com/prometheus-community/postgres_exporter
|
||||
build:
|
||||
binaries:
|
||||
- name: postgres_exporter
|
||||
path: ./cmd/postgres_exporter
|
||||
flags: -a -tags 'netgo static_build'
|
||||
ldflags: |
|
||||
-X github.com/prometheus/common/version.Version={{.Version}}
|
||||
-X github.com/prometheus/common/version.Revision={{.Revision}}
|
||||
|
@ -1,5 +1,7 @@
|
||||
---
|
||||
extends: default
|
||||
ignore: |
|
||||
**/node_modules
|
||||
|
||||
rules:
|
||||
braces:
|
||||
@ -20,5 +22,4 @@ rules:
|
||||
config/testdata/section_key_dup.bad.yml
|
||||
line-length: disable
|
||||
truthy:
|
||||
ignore: |
|
||||
.github/workflows/*.yml
|
||||
check-keys: false
|
||||
|
72
CHANGELOG.md
72
CHANGELOG.md
@ -1,3 +1,75 @@
|
||||
## 0.17.1 / 2025-02-26
|
||||
|
||||
* [BUGFIX] Fix: Handle incoming labels with invalid UTF-8 #1131
|
||||
|
||||
## 0.17.0 / 2025-02-16
|
||||
|
||||
## What's Changed
|
||||
* [ENHANCEMENT] Add Postgres 17 for CI test by @khiemdoan in https://github.com/prometheus-community/postgres_exporter/pull/1105
|
||||
* [ENHANCEMENT] Add wait/backend to pg_stat_activity by @fgalind1 in https://github.com/prometheus-community/postgres_exporter/pull/1106
|
||||
* [ENHANCEMENT] Export last replay age in replication collector by @bitfehler in https://github.com/prometheus-community/postgres_exporter/pull/1085
|
||||
* [BUGFIX] Fix pg_long_running_transactions time by @jyothikirant-sayukth in https://github.com/prometheus-community/postgres_exporter/pull/1092
|
||||
* [BUGFIX] Fix to replace dashes with underscore in the metric names by @aagarwalla-fx in https://github.com/prometheus-community/postgres_exporter/pull/1103
|
||||
* [BIGFIX] Checkpoint related columns in PG 17 have been moved from pg_stat_bgwriter to pg_stat_checkpointer by @n-rodriguez in https://github.com/prometheus-community/postgres_exporter/pull/1072
|
||||
* [BUGFIX] Fix pg_stat_statements for PG17 by @NevermindZ4 in https://github.com/prometheus-community/postgres_exporter/pull/1114
|
||||
* [BUGFIX] Handle pg_replication_slots on pg<13 by @michael-todorovic in https://github.com/prometheus-community/postgres_exporter/pull/1098
|
||||
* [BUGFIX] Fix missing dsn sanitization for logging by @sysadmind in https://github.com/prometheus-community/postgres_exporter/pull/1104
|
||||
|
||||
## New Contributors
|
||||
* @jyothikirant-sayukth made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1092
|
||||
* @aagarwalla-fx made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1103
|
||||
* @NevermindZ4 made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1114
|
||||
* @michael-todorovic made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1098
|
||||
* @fgalind1 made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1106
|
||||
|
||||
**Full Changelog**: https://github.com/prometheus-community/postgres_exporter/compare/v0.16.0...v0.17.0
|
||||
|
||||
## 0.16.0 / 2024-11-10
|
||||
|
||||
BREAKING CHANGES:
|
||||
|
||||
The logging system has been replaced with log/slog from the stdlib. This change is being made across the prometheus ecosystem. The logging output has changed, but the messages and levels remain the same. The `ts` label for the timestamp has bewen replaced with `time`, the accuracy is less, and the timezone is not forced to UTC. The `caller` field has been replaced by the `source` field, which now includes the full path to the source file. The `level` field now exposes the log level in capital letters.
|
||||
|
||||
* [CHANGE] Replace logging system #1073
|
||||
* [ENHANCEMENT] Add save_wal_size and wal_status to replication_slot collector #1027
|
||||
* [ENHANCEMENT] Add roles collector and connection limit metrics to database collector #997
|
||||
* [ENHANCEMENT] Excluded databases log messgae is now info level #1003
|
||||
* [ENHANCEMENT] Add active_time to stat_database collector #961
|
||||
* [ENHANCEMENT] Add slot_type label to replication_slot collector #960
|
||||
* [BUGFIX] Fix walreceiver collectore when no repmgr #1086
|
||||
* [BUGFIX] Remove logging errors on replicas #1048
|
||||
* [BUGFIX] Fix active_time query on postgres>=14 #1045
|
||||
|
||||
## 0.15.0 / 2023-10-27
|
||||
|
||||
* [ENHANCEMENT] Add 1kB and 2kB units #915
|
||||
* [BUGFIX] Add error log when probe collector creation fails #918
|
||||
* [BUGFIX] Fix test build failures on 32-bit arch #919
|
||||
* [BUGFIX] Adjust collector to use separate connection per scrape #936
|
||||
|
||||
## 0.14.0 / 2023-09-11
|
||||
|
||||
* [CHANGE] Add `state` label to pg_process_idle_seconds #862
|
||||
* [CHANGE] Change database connections to one per scrape #882 #902
|
||||
* [ENHANCEMENT] Add wal collector #858
|
||||
* [ENHANCEMENT] Add database_wraparound collector #834
|
||||
* [ENHANCEMENT] Add stat_activity_autovacuum collector #840
|
||||
* [ENHANCEMENT] Add stat_wal_receiver collector #844
|
||||
* [ENHANCEMENT] Add xlog_location collector #849
|
||||
* [ENHANCEMENT] Add statio_user_indexes collector #845
|
||||
* [ENHANCEMENT] Add long_running_transactions collector #836
|
||||
* [ENHANCEMENT] Add pg_stat_user_tables_size_bytes metric #904
|
||||
* [BUGFIX] Fix tests on 32-bit systems #857
|
||||
* [BUGFIX] Fix pg_stat_statements metrics on Postgres 13+ #874 #876
|
||||
* [BUGFIX] Fix pg_stat_database metrics for NULL stats_reset #877
|
||||
* [BUGFIX] Fix pg_replication_lag_seconds on Postgres 10+ when master is idle #895
|
||||
|
||||
## 0.13.2 / 2023-07-21
|
||||
|
||||
* [BUGFIX] Fix type issues on pg_postmaster metrics #828
|
||||
* [BUGFIX] Fix pg_replication collector instantiation #854
|
||||
* [BUGFIX] Fix pg_process_idle metrics #855
|
||||
|
||||
## 0.13.1 / 2023-06-27
|
||||
|
||||
* [BUGFIX] Make collectors not fail on null values #823
|
||||
|
@ -49,23 +49,23 @@ endif
|
||||
GOTEST := $(GO) test
|
||||
GOTEST_DIR :=
|
||||
ifneq ($(CIRCLE_JOB),)
|
||||
ifneq ($(shell command -v gotestsum > /dev/null),)
|
||||
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.14.0
|
||||
PROMU_VERSION ?= 0.17.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 ?= v1.51.2
|
||||
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
|
||||
GOLANGCI_LINT_VERSION ?= v2.0.2
|
||||
# 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))
|
||||
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))
|
||||
@ -169,16 +169,20 @@ common-vet:
|
||||
common-lint: $(GOLANGCI_LINT)
|
||||
ifdef GOLANGCI_LINT
|
||||
@echo ">> running golangci-lint"
|
||||
# 'go list' needs to be executed before staticcheck to prepopulate the modules cache.
|
||||
# Otherwise staticcheck might fail randomly for some reason not yet explained.
|
||||
$(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null
|
||||
$(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 > /dev/null))
|
||||
ifeq (, $(shell command -v yamllint 2> /dev/null))
|
||||
@echo "yamllint not installed so skipping"
|
||||
else
|
||||
yamllint .
|
||||
@ -204,6 +208,10 @@ 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-%:
|
||||
@ -267,3 +275,9 @@ $(1)_precheck:
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
|
||||
govulncheck: install-govulncheck
|
||||
govulncheck ./...
|
||||
|
||||
install-govulncheck:
|
||||
command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
84
README.md
84
README.md
@ -7,7 +7,7 @@
|
||||
|
||||
Prometheus exporter for PostgreSQL server metrics.
|
||||
|
||||
CI Tested PostgreSQL versions: `10`, `11`, `12`, `13`, `14`, `15`
|
||||
CI Tested PostgreSQL versions: `11`, `12`, `13`, `14`, `15`, `16`, `17`.
|
||||
|
||||
## Quick Start
|
||||
This package is available for Docker:
|
||||
@ -17,10 +17,29 @@ docker run --net=host -it --rm -e POSTGRES_PASSWORD=password postgres
|
||||
# Connect to it
|
||||
docker run \
|
||||
--net=host \
|
||||
-e DATA_SOURCE_NAME="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" \
|
||||
-e DATA_SOURCE_URI="localhost:5432/postgres?sslmode=disable" \
|
||||
-e DATA_SOURCE_USER=postgres \
|
||||
-e DATA_SOURCE_PASS=password \
|
||||
quay.io/prometheuscommunity/postgres-exporter
|
||||
```
|
||||
|
||||
Test with:
|
||||
```bash
|
||||
curl "http://localhost:9187/metrics"
|
||||
```
|
||||
|
||||
Example Prometheus config:
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: postgres
|
||||
static_configs:
|
||||
- targets: ["127.0.0.1:9187"] # Replace IP with the hostname of the docker container if you're running the container in a separate network
|
||||
```
|
||||
|
||||
Now use the DATA_SOURCE_PASS_FILE with a mounted file containing the password to prevent having the password in an environment variable.
|
||||
|
||||
The container process runs with uid/gid 65534 (important for file permissions).
|
||||
|
||||
## Multi-Target Support (BETA)
|
||||
**This Feature is in beta and may require changes in future releases. Feedback is welcome.**
|
||||
|
||||
@ -30,6 +49,26 @@ To use the multi-target functionality, send an http request to the endpoint `/pr
|
||||
|
||||
To avoid putting sensitive information like username and password in the URL, preconfigured auth modules are supported via the [auth_modules](#auth_modules) section of the config file. auth_modules for DSNs can be used with the `/probe` endpoint by specifying the `?auth_module=foo` http parameter.
|
||||
|
||||
Example Prometheus config:
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'postgres'
|
||||
static_configs:
|
||||
- targets:
|
||||
- server1:5432
|
||||
- server2:5432
|
||||
metrics_path: /probe
|
||||
params:
|
||||
auth_module: [foo]
|
||||
relabel_configs:
|
||||
- source_labels: [__address__]
|
||||
target_label: __param_target
|
||||
- source_labels: [__param_target]
|
||||
target_label: instance
|
||||
- target_label: __address__
|
||||
replacement: 127.0.0.1:9116 # The postgres exporter's real hostname:port.
|
||||
```
|
||||
|
||||
## Configuration File
|
||||
|
||||
The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postgres_exporter.yml`.
|
||||
@ -61,7 +100,7 @@ auth_modules:
|
||||
To build the Docker image:
|
||||
|
||||
make promu
|
||||
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/amd64 -p linux/ppc64le
|
||||
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/arm64 -p linux/ppc64le
|
||||
make docker
|
||||
|
||||
This will build the docker image as `prometheuscommunity/postgres_exporter:${branch}`.
|
||||
@ -73,13 +112,22 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
|
||||
|
||||
|
||||
* `[no-]collector.database`
|
||||
Enable the database collector (default: enabled).
|
||||
Enable the `database` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.database_wraparound`
|
||||
Enable the `database_wraparound` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.locks`
|
||||
Enable the `locks` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.long_running_transactions`
|
||||
Enable the `long_running_transactions` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.postmaster`
|
||||
Enable the `postmaster` collector (default: enabled).
|
||||
Enable the `postmaster` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.process_idle`
|
||||
Enable the `process_idle` collector (default: enabled).
|
||||
Enable the `process_idle` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.replication`
|
||||
Enable the `replication` collector (default: enabled).
|
||||
@ -87,14 +135,17 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
|
||||
* `[no-]collector.replication_slot`
|
||||
Enable the `replication_slot` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.stat_activity_autovacuum`
|
||||
Enable the `stat_activity_autovacuum` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.stat_bgwriter`
|
||||
Enable the `stat_bgwriter` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.stat_database`
|
||||
Enable the `stat_database` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.statio_user_tables`
|
||||
Enable the `statio_user_tables` collector (default: enabled).
|
||||
* `[no-]collector.stat_progress_vacuum`
|
||||
Enable the `stat_progress_vacuum` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.stat_statements`
|
||||
Enable the `stat_statements` collector (default: disabled).
|
||||
@ -102,6 +153,21 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
|
||||
* `[no-]collector.stat_user_tables`
|
||||
Enable the `stat_user_tables` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.stat_wal_receiver`
|
||||
Enable the `stat_wal_receiver` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.statio_user_indexes`
|
||||
Enable the `statio_user_indexes` collector (default: disabled).
|
||||
|
||||
* `[no-]collector.statio_user_tables`
|
||||
Enable the `statio_user_tables` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.wal`
|
||||
Enable the `wal` collector (default: enabled).
|
||||
|
||||
* `[no-]collector.xlog_location`
|
||||
Enable the `xlog_location` collector (default: disabled).
|
||||
|
||||
* `config.file`
|
||||
Set the config file path. Default is `postgres_exporter.yml`
|
||||
|
||||
@ -164,7 +230,7 @@ The following environment variables configure the exporter:
|
||||
* `DATA_SOURCE_URI`
|
||||
an alternative to `DATA_SOURCE_NAME` which exclusively accepts the hostname
|
||||
without a username and password component. For example, `my_pg_hostname` or
|
||||
`my_pg_hostname?sslmode=disable`.
|
||||
`my_pg_hostname:5432/postgres?sslmode=disable`.
|
||||
|
||||
* `DATA_SOURCE_URI_FILE`
|
||||
The same as above but reads the URI from a file.
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -39,19 +38,19 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
|
||||
var err error
|
||||
dsnURI, err = url.Parse(dsn)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Unable to parse DSN as URI", "dsn", loggableDSN(dsn), "err", err)
|
||||
logger.Error("Unable to parse DSN as URI", "dsn", loggableDSN(dsn), "err", err)
|
||||
continue
|
||||
}
|
||||
} else if connstringRe.MatchString(dsn) {
|
||||
dsnConnstring = dsn
|
||||
} else {
|
||||
level.Error(logger).Log("msg", "Unable to parse DSN as either URI or connstring", "dsn", loggableDSN(dsn))
|
||||
logger.Error("Unable to parse DSN as either URI or connstring", "dsn", loggableDSN(dsn))
|
||||
continue
|
||||
}
|
||||
|
||||
server, err := e.servers.GetServer(dsn)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
|
||||
logger.Error("Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
|
||||
continue
|
||||
}
|
||||
dsns[dsn] = struct{}{}
|
||||
@ -61,7 +60,7 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
|
||||
|
||||
databaseNames, err := queryDatabases(server)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Error querying databases", "dsn", loggableDSN(dsn), "err", err)
|
||||
logger.Error("Error querying databases", "dsn", loggableDSN(dsn), "err", err)
|
||||
continue
|
||||
}
|
||||
for _, databaseName := range databaseNames {
|
||||
@ -109,7 +108,7 @@ func (e *Exporter) scrapeDSN(ch chan<- prometheus.Metric, dsn string) error {
|
||||
|
||||
// Check if map versions need to be updated
|
||||
if err := e.checkMapVersions(ch, server); err != nil {
|
||||
level.Warn(logger).Log("msg", "Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
|
||||
logger.Warn("Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
|
||||
}
|
||||
|
||||
return server.Scrape(ch, e.disableSettingsMetrics)
|
||||
|
@ -20,14 +20,13 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus-community/postgres_exporter/collector"
|
||||
"github.com/prometheus-community/postgres_exporter/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/common/promlog"
|
||||
"github.com/prometheus/common/promlog/flag"
|
||||
"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"
|
||||
@ -50,7 +49,7 @@ var (
|
||||
excludeDatabases = kingpin.Flag("exclude-databases", "A list of databases to remove when autoDiscoverDatabases is enabled (DEPRECATED)").Default("").Envar("PG_EXPORTER_EXCLUDE_DATABASES").String()
|
||||
includeDatabases = kingpin.Flag("include-databases", "A list of databases to include when autoDiscoverDatabases is enabled (DEPRECATED)").Default("").Envar("PG_EXPORTER_INCLUDE_DATABASES").String()
|
||||
metricPrefix = kingpin.Flag("metric-prefix", "A metric prefix can be used to have non-default (not \"pg\") prefixes for each of the metrics").Default("pg").Envar("PG_EXPORTER_METRIC_PREFIX").String()
|
||||
logger = log.NewNopLogger()
|
||||
logger = promslog.NewNopLogger()
|
||||
)
|
||||
|
||||
// Metric name parts.
|
||||
@ -70,11 +69,11 @@ const (
|
||||
|
||||
func main() {
|
||||
kingpin.Version(version.Print(exporterName))
|
||||
promlogConfig := &promlog.Config{}
|
||||
flag.AddFlags(kingpin.CommandLine, promlogConfig)
|
||||
promslogConfig := &promslog.Config{}
|
||||
flag.AddFlags(kingpin.CommandLine, promslogConfig)
|
||||
kingpin.HelpFlag.Short('h')
|
||||
kingpin.Parse()
|
||||
logger = promlog.New(promlogConfig)
|
||||
logger = promslog.New(promslogConfig)
|
||||
|
||||
if *onlyDumpMaps {
|
||||
dumpMaps()
|
||||
@ -83,28 +82,28 @@ func main() {
|
||||
|
||||
if err := c.ReloadConfig(*configFile, logger); err != nil {
|
||||
// This is not fatal, but it means that auth must be provided for every dsn.
|
||||
level.Warn(logger).Log("msg", "Error loading config", "err", err)
|
||||
logger.Warn("Error loading config", "err", err)
|
||||
}
|
||||
|
||||
dsns, err := getDataSources()
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
|
||||
logger.Error("Failed reading data sources", "err", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
excludedDatabases := strings.Split(*excludeDatabases, ",")
|
||||
logger.Log("msg", "Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
|
||||
logger.Info("Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
|
||||
|
||||
if *queriesPath != "" {
|
||||
level.Warn(logger).Log("msg", "The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
|
||||
logger.Warn("The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
|
||||
}
|
||||
|
||||
if *autoDiscoverDatabases || *excludeDatabases != "" || *includeDatabases != "" {
|
||||
level.Warn(logger).Log("msg", "Scraping additional databases via auto discovery is DEPRECATED")
|
||||
logger.Warn("Scraping additional databases via auto discovery is DEPRECATED")
|
||||
}
|
||||
|
||||
if *constantLabelsList != "" {
|
||||
level.Warn(logger).Log("msg", "Constant labels on all metrics is DEPRECATED")
|
||||
logger.Warn("Constant labels on all metrics is DEPRECATED")
|
||||
}
|
||||
|
||||
opts := []ExporterOpt{
|
||||
@ -122,7 +121,7 @@ func main() {
|
||||
exporter.servers.Close()
|
||||
}()
|
||||
|
||||
prometheus.MustRegister(version.NewCollector(exporterName))
|
||||
prometheus.MustRegister(versioncollector.NewCollector(exporterName))
|
||||
|
||||
prometheus.MustRegister(exporter)
|
||||
|
||||
@ -139,7 +138,7 @@ func main() {
|
||||
[]string{},
|
||||
)
|
||||
if err != nil {
|
||||
level.Warn(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error())
|
||||
logger.Warn("Failed to create PostgresCollector", "err", err.Error())
|
||||
} else {
|
||||
prometheus.MustRegister(pe)
|
||||
}
|
||||
@ -160,7 +159,7 @@ func main() {
|
||||
}
|
||||
landingPage, err := web.NewLandingPage(landingConfig)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("err", err)
|
||||
logger.Error("error creating landing page", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
http.Handle("/", landingPage)
|
||||
@ -170,7 +169,7 @@ func main() {
|
||||
|
||||
srv := &http.Server{}
|
||||
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {
|
||||
level.Error(logger).Log("msg", "Error running HTTP server", "err", err)
|
||||
logger.Error("Error running HTTP server", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/lib/pq"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
@ -190,10 +189,10 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
|
||||
scrapeStart := time.Now()
|
||||
|
||||
for namespace, mapping := range server.metricMap {
|
||||
level.Debug(logger).Log("msg", "Querying namespace", "namespace", namespace)
|
||||
logger.Debug("Querying namespace", "namespace", namespace)
|
||||
|
||||
if mapping.master && !server.master {
|
||||
level.Debug(logger).Log("msg", "Query skipped...")
|
||||
logger.Debug("Query skipped...")
|
||||
continue
|
||||
}
|
||||
|
||||
@ -202,7 +201,7 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
|
||||
serVersion, _ := semver.Parse(server.lastMapVersion.String())
|
||||
runServerRange, _ := semver.ParseRange(server.runonserver)
|
||||
if !runServerRange(serVersion) {
|
||||
level.Debug(logger).Log("msg", "Query skipped for this database version", "version", server.lastMapVersion.String(), "target_version", server.runonserver)
|
||||
logger.Debug("Query skipped for this database version", "version", server.lastMapVersion.String(), "target_version", server.runonserver)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -233,12 +232,12 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
|
||||
// Serious error - a namespace disappeared
|
||||
if err != nil {
|
||||
namespaceErrors[namespace] = err
|
||||
level.Info(logger).Log("err", err)
|
||||
logger.Info("error finding namespace", "err", err)
|
||||
}
|
||||
// Non-serious errors - likely version or parsing problems.
|
||||
if len(nonFatalErrors) > 0 {
|
||||
for _, err := range nonFatalErrors {
|
||||
level.Info(logger).Log("err", err)
|
||||
logger.Info("error querying namespace", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -32,7 +31,7 @@ var (
|
||||
|
||||
// Query the pg_settings view containing runtime variables
|
||||
func querySettings(ch chan<- prometheus.Metric, server *Server) error {
|
||||
level.Debug(logger).Log("msg", "Querying pg_setting view", "server", server)
|
||||
logger.Debug("Querying pg_setting view", "server", server)
|
||||
|
||||
// pg_settings docs: https://www.postgresql.org/docs/current/static/view-pg-settings.html
|
||||
//
|
||||
@ -68,7 +67,7 @@ type pgSetting struct {
|
||||
func (s *pgSetting) metric(labels prometheus.Labels) prometheus.Metric {
|
||||
var (
|
||||
err error
|
||||
name = strings.Replace(s.name, ".", "_", -1)
|
||||
name = strings.ReplaceAll(strings.ReplaceAll(s.name, ".", "_"), "-", "_")
|
||||
unit = s.unit // nolint: ineffassign
|
||||
shortDesc = fmt.Sprintf("Server Parameter: %s", s.name)
|
||||
subsystem = "settings"
|
||||
@ -129,10 +128,10 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) {
|
||||
return
|
||||
case "ms", "s", "min", "h", "d":
|
||||
unit = "seconds"
|
||||
case "B", "kB", "MB", "GB", "TB", "4kB", "8kB", "16kB", "32kB", "64kB", "16MB", "32MB", "64MB":
|
||||
case "B", "kB", "MB", "GB", "TB", "1kB", "2kB", "4kB", "8kB", "16kB", "32kB", "64kB", "16MB", "32MB", "64MB":
|
||||
unit = "bytes"
|
||||
default:
|
||||
err = fmt.Errorf("Unknown unit for runtime variable: %q", s.unit)
|
||||
err = fmt.Errorf("unknown unit for runtime variable: %q", s.unit)
|
||||
return
|
||||
}
|
||||
|
||||
@ -158,6 +157,10 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) {
|
||||
val *= math.Pow(2, 30)
|
||||
case "TB":
|
||||
val *= math.Pow(2, 40)
|
||||
case "1kB":
|
||||
val *= math.Pow(2, 10)
|
||||
case "2kB":
|
||||
val *= math.Pow(2, 11)
|
||||
case "4kB":
|
||||
val *= math.Pow(2, 12)
|
||||
case "8kB":
|
||||
|
@ -40,7 +40,7 @@ var fixtures = []fixture{
|
||||
unit: "seconds",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_seconds_fixture_metric_seconds", help: "Server Parameter: seconds_fixture_metric [Units converted to seconds.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_seconds_fixture_metric_seconds", help: "Server Parameter: seconds_fixture_metric [Units converted to seconds.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 5,
|
||||
},
|
||||
{
|
||||
@ -56,7 +56,7 @@ var fixtures = []fixture{
|
||||
unit: "seconds",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_milliseconds_fixture_metric_seconds", help: "Server Parameter: milliseconds_fixture_metric [Units converted to seconds.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_milliseconds_fixture_metric_seconds", help: "Server Parameter: milliseconds_fixture_metric [Units converted to seconds.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 5,
|
||||
},
|
||||
{
|
||||
@ -72,7 +72,7 @@ var fixtures = []fixture{
|
||||
unit: "bytes",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_eight_kb_fixture_metric_bytes", help: "Server Parameter: eight_kb_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_eight_kb_fixture_metric_bytes", help: "Server Parameter: eight_kb_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 139264,
|
||||
},
|
||||
{
|
||||
@ -88,7 +88,7 @@ var fixtures = []fixture{
|
||||
unit: "bytes",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_16_kb_real_fixture_metric_bytes", help: "Server Parameter: 16_kb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_16_kb_real_fixture_metric_bytes", help: "Server Parameter: 16_kb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 49152,
|
||||
},
|
||||
{
|
||||
@ -104,7 +104,7 @@ var fixtures = []fixture{
|
||||
unit: "bytes",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_16_mb_real_fixture_metric_bytes", help: "Server Parameter: 16_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_16_mb_real_fixture_metric_bytes", help: "Server Parameter: 16_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 5.0331648e+07,
|
||||
},
|
||||
{
|
||||
@ -120,7 +120,7 @@ var fixtures = []fixture{
|
||||
unit: "bytes",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_32_mb_real_fixture_metric_bytes", help: "Server Parameter: 32_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_32_mb_real_fixture_metric_bytes", help: "Server Parameter: 32_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 1.00663296e+08,
|
||||
},
|
||||
{
|
||||
@ -136,7 +136,7 @@ var fixtures = []fixture{
|
||||
unit: "bytes",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_64_mb_real_fixture_metric_bytes", help: "Server Parameter: 64_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_64_mb_real_fixture_metric_bytes", help: "Server Parameter: 64_mb_real_fixture_metric [Units converted to bytes.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: 2.01326592e+08,
|
||||
},
|
||||
{
|
||||
@ -152,7 +152,7 @@ var fixtures = []fixture{
|
||||
unit: "",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_bool_on_fixture_metric", help: "Server Parameter: bool_on_fixture_metric", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_bool_on_fixture_metric", help: "Server Parameter: bool_on_fixture_metric", constLabels: {}, variableLabels: {}}`,
|
||||
v: 1,
|
||||
},
|
||||
{
|
||||
@ -168,7 +168,7 @@ var fixtures = []fixture{
|
||||
unit: "",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_bool_off_fixture_metric", help: "Server Parameter: bool_off_fixture_metric", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_bool_off_fixture_metric", help: "Server Parameter: bool_off_fixture_metric", constLabels: {}, variableLabels: {}}`,
|
||||
v: 0,
|
||||
},
|
||||
{
|
||||
@ -184,7 +184,7 @@ var fixtures = []fixture{
|
||||
unit: "seconds",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_special_minus_one_value_seconds", help: "Server Parameter: special_minus_one_value [Units converted to seconds.]", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_special_minus_one_value_seconds", help: "Server Parameter: special_minus_one_value [Units converted to seconds.]", constLabels: {}, variableLabels: {}}`,
|
||||
v: -1,
|
||||
},
|
||||
{
|
||||
@ -200,7 +200,7 @@ var fixtures = []fixture{
|
||||
unit: "",
|
||||
err: "",
|
||||
},
|
||||
d: `Desc{fqName: "pg_settings_rds_rds_superuser_reserved_connections", help: "Server Parameter: rds.rds_superuser_reserved_connections", constLabels: {}, variableLabels: []}`,
|
||||
d: `Desc{fqName: "pg_settings_rds_rds_superuser_reserved_connections", help: "Server Parameter: rds.rds_superuser_reserved_connections", constLabels: {}, variableLabels: {}}`,
|
||||
v: 2,
|
||||
},
|
||||
{
|
||||
@ -214,7 +214,7 @@ var fixtures = []fixture{
|
||||
n: normalised{
|
||||
val: 10,
|
||||
unit: "",
|
||||
err: `Unknown unit for runtime variable: "nonexistent"`,
|
||||
err: `unknown unit for runtime variable: "nonexistent"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -240,7 +240,7 @@ func (s *PgSettingSuite) TestNormaliseUnit(c *C) {
|
||||
func (s *PgSettingSuite) TestMetric(c *C) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if r.(error).Error() != `Unknown unit for runtime variable: "nonexistent"` {
|
||||
if r.(error).Error() != `unknown unit for runtime variable: "nonexistent"` {
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -176,15 +175,6 @@ var builtinMetricMaps = map[string]intermediateMetricMap{
|
||||
true,
|
||||
0,
|
||||
},
|
||||
"pg_locks": {
|
||||
map[string]ColumnMapping{
|
||||
"datname": {LABEL, "Name of this database", nil, nil},
|
||||
"mode": {LABEL, "Type of Lock", nil, nil},
|
||||
"count": {GAUGE, "Number of locks", nil, nil},
|
||||
},
|
||||
true,
|
||||
0,
|
||||
},
|
||||
"pg_stat_replication": {
|
||||
map[string]ColumnMapping{
|
||||
"procpid": {DISCARD, "Process ID of a WAL sender process", nil, semver.MustParseRange("<9.2.0")},
|
||||
@ -261,6 +251,9 @@ var builtinMetricMaps = map[string]intermediateMetricMap{
|
||||
"state": {LABEL, "connection state", nil, semver.MustParseRange(">=9.2.0")},
|
||||
"usename": {LABEL, "connection usename", nil, nil},
|
||||
"application_name": {LABEL, "connection application_name", nil, nil},
|
||||
"backend_type": {LABEL, "connection backend_type", nil, nil},
|
||||
"wait_event_type": {LABEL, "connection wait_event_type", nil, nil},
|
||||
"wait_event": {LABEL, "connection wait_event", nil, nil},
|
||||
"count": {GAUGE, "number of connections in this state", nil, nil},
|
||||
"max_tx_duration": {GAUGE, "max duration in seconds any active transaction has been running", nil, nil},
|
||||
},
|
||||
@ -293,7 +286,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
|
||||
if !columnMapping.supportedVersions(pgVersion) {
|
||||
// It's very useful to be able to see what columns are being
|
||||
// rejected.
|
||||
level.Debug(logger).Log("msg", "Column is being forced to discard due to version incompatibility", "column", columnName)
|
||||
logger.Debug("Column is being forced to discard due to version incompatibility", "column", columnName)
|
||||
thisMap[columnName] = MetricMap{
|
||||
discard: true,
|
||||
conversion: func(_ interface{}) (float64, bool) {
|
||||
@ -380,7 +373,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
|
||||
case string:
|
||||
durationString = t
|
||||
default:
|
||||
level.Error(logger).Log("msg", "Duration conversion metric was not a string")
|
||||
logger.Error("Duration conversion metric was not a string")
|
||||
return math.NaN(), false
|
||||
}
|
||||
|
||||
@ -390,7 +383,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
|
||||
|
||||
d, err := time.ParseDuration(durationString)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Failed converting result to metric", "column", columnName, "in", in, "err", err)
|
||||
logger.Error("Failed converting result to metric", "column", columnName, "in", in, "err", err)
|
||||
return math.NaN(), false
|
||||
}
|
||||
return float64(d / time.Millisecond), true
|
||||
@ -500,7 +493,7 @@ func parseConstLabels(s string) prometheus.Labels {
|
||||
for _, p := range parts {
|
||||
keyValue := strings.Split(strings.TrimSpace(p), "=")
|
||||
if len(keyValue) != 2 {
|
||||
level.Error(logger).Log(`Wrong constant labels format, should be "key=value"`, "input", p)
|
||||
logger.Error(`Wrong constant labels format, should be "key=value"`, "input", p)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
@ -591,7 +584,7 @@ func newDesc(subsystem, name, help string, labels prometheus.Labels) *prometheus
|
||||
}
|
||||
|
||||
func checkPostgresVersion(db *sql.DB, server string) (semver.Version, string, error) {
|
||||
level.Debug(logger).Log("msg", "Querying PostgreSQL version", "server", server)
|
||||
logger.Debug("Querying PostgreSQL version", "server", server)
|
||||
versionRow := db.QueryRow("SELECT version();")
|
||||
var versionString string
|
||||
err := versionRow.Scan(&versionString)
|
||||
@ -614,12 +607,12 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
|
||||
}
|
||||
|
||||
if !e.disableDefaultMetrics && semanticVersion.LT(lowestSupportedVersion) {
|
||||
level.Warn(logger).Log("msg", "PostgreSQL version is lower than our lowest supported version", "server", server, "version", semanticVersion, "lowest_supported_version", lowestSupportedVersion)
|
||||
logger.Warn("PostgreSQL version is lower than our lowest supported version", "server", server, "version", semanticVersion, "lowest_supported_version", lowestSupportedVersion)
|
||||
}
|
||||
|
||||
// Check if semantic version changed and recalculate maps if needed.
|
||||
if semanticVersion.NE(server.lastMapVersion) || server.metricMap == nil {
|
||||
level.Info(logger).Log("msg", "Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
|
||||
logger.Info("Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
|
||||
server.mappingMtx.Lock()
|
||||
|
||||
// Get Default Metrics only for master database
|
||||
@ -640,13 +633,13 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
|
||||
// Calculate the hashsum of the useQueries
|
||||
userQueriesData, err := os.ReadFile(e.userQueriesPath)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "Failed to reload user queries", "path", e.userQueriesPath, "err", err)
|
||||
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
|
||||
e.userQueriesError.WithLabelValues(e.userQueriesPath, "").Set(1)
|
||||
} else {
|
||||
hashsumStr := fmt.Sprintf("%x", sha256.Sum256(userQueriesData))
|
||||
|
||||
if err := addQueries(userQueriesData, semanticVersion, server); err != nil {
|
||||
level.Error(logger).Log("msg", "Failed to reload user queries", "path", e.userQueriesPath, "err", err)
|
||||
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
|
||||
e.userQueriesError.WithLabelValues(e.userQueriesPath, hashsumStr).Set(1)
|
||||
} else {
|
||||
// Mark user queries as successfully loaded
|
||||
@ -688,7 +681,7 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
|
||||
if err := e.scrapeDSN(ch, dsn); err != nil {
|
||||
errorsCount++
|
||||
|
||||
level.Error(logger).Log("err", err)
|
||||
logger.Error("error scraping dsn", "err", err, "dsn", loggableDSN(dsn))
|
||||
|
||||
if _, ok := err.(*ErrorConnectToServer); ok {
|
||||
connectionErrorsCount++
|
||||
|
@ -15,17 +15,16 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus-community/postgres_exporter/collector"
|
||||
"github.com/prometheus-community/postgres_exporter/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc {
|
||||
func handleProbe(logger *slog.Logger, excludeDatabases []string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
conf := c.GetConfig()
|
||||
@ -38,7 +37,7 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
|
||||
var authModule config.AuthModule
|
||||
authModuleName := params.Get("auth_module")
|
||||
if authModuleName == "" {
|
||||
level.Info(logger).Log("msg", "no auth_module specified, using default")
|
||||
logger.Info("no auth_module specified, using default")
|
||||
} else {
|
||||
var ok bool
|
||||
authModule, ok = conf.AuthModules[authModuleName]
|
||||
@ -54,14 +53,14 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
|
||||
|
||||
dsn, err := authModule.ConfigureTarget(target)
|
||||
if err != nil {
|
||||
level.Error(logger).Log("msg", "failed to configure target", "err", err)
|
||||
logger.Error("failed to configure target", "err", err)
|
||||
http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(@sysadmind): Timeout
|
||||
|
||||
tl := log.With(logger, "target", target)
|
||||
tl := logger.With("target", target)
|
||||
|
||||
registry := prometheus.NewRegistry()
|
||||
|
||||
@ -85,6 +84,7 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
|
||||
// Run the probe
|
||||
pc, err := collector.NewProbeCollector(tl, excludeDatabases, registry, dsn)
|
||||
if err != nil {
|
||||
logger.Error("Error creating probe collector", "err", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/go-kit/log/level"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@ -46,39 +45,14 @@ type OverrideQuery struct {
|
||||
// Overriding queries for namespaces above.
|
||||
// TODO: validate this is a closed set in tests, and there are no overlaps
|
||||
var queryOverrides = map[string][]OverrideQuery{
|
||||
"pg_locks": {
|
||||
{
|
||||
semver.MustParseRange(">0.0.0"),
|
||||
`SELECT pg_database.datname,tmp.mode,COALESCE(count,0) as count
|
||||
FROM
|
||||
(
|
||||
VALUES ('accesssharelock'),
|
||||
('rowsharelock'),
|
||||
('rowexclusivelock'),
|
||||
('shareupdateexclusivelock'),
|
||||
('sharelock'),
|
||||
('sharerowexclusivelock'),
|
||||
('exclusivelock'),
|
||||
('accessexclusivelock'),
|
||||
('sireadlock')
|
||||
) AS tmp(mode) CROSS JOIN pg_database
|
||||
LEFT JOIN
|
||||
(SELECT database, lower(mode) AS mode,count(*) AS count
|
||||
FROM pg_locks WHERE database IS NOT NULL
|
||||
GROUP BY database, lower(mode)
|
||||
) AS tmp2
|
||||
ON tmp.mode=tmp2.mode and pg_database.oid = tmp2.database ORDER BY 1`,
|
||||
},
|
||||
},
|
||||
|
||||
"pg_stat_replication": {
|
||||
{
|
||||
semver.MustParseRange(">=10.0.0"),
|
||||
`
|
||||
SELECT *,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_current_wal_lsn() end) AS pg_current_wal_lsn,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_wal_lsn_diff(pg_current_wal_lsn(), pg_lsn('0/0'))::float end) AS pg_current_wal_lsn_bytes,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)::float end) AS pg_wal_lsn_diff
|
||||
(case pg_is_in_recovery() when 't' then pg_last_wal_receive_lsn() else pg_current_wal_lsn() end) AS pg_current_wal_lsn,
|
||||
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_lsn('0/0'))::float else pg_wal_lsn_diff(pg_current_wal_lsn(), pg_lsn('0/0'))::float end) AS pg_current_wal_lsn_bytes,
|
||||
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), replay_lsn)::float else pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)::float end) AS pg_wal_lsn_diff
|
||||
FROM pg_stat_replication
|
||||
`,
|
||||
},
|
||||
@ -86,8 +60,8 @@ var queryOverrides = map[string][]OverrideQuery{
|
||||
semver.MustParseRange(">=9.2.0 <10.0.0"),
|
||||
`
|
||||
SELECT *,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_current_xlog_location() end) AS pg_current_xlog_location,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
|
||||
(case pg_is_in_recovery() when 't' then pg_last_xlog_receive_location() else pg_current_xlog_location() end) AS pg_current_xlog_location,
|
||||
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), replay_location)::float else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
|
||||
FROM pg_stat_replication
|
||||
`,
|
||||
},
|
||||
@ -95,7 +69,7 @@ var queryOverrides = map[string][]OverrideQuery{
|
||||
semver.MustParseRange("<9.2.0"),
|
||||
`
|
||||
SELECT *,
|
||||
(case pg_is_in_recovery() when 't' then null else pg_current_xlog_location() end) AS pg_current_xlog_location
|
||||
(case pg_is_in_recovery() when 't' then pg_last_xlog_receive_location() else pg_current_xlog_location() end) AS pg_current_xlog_location
|
||||
FROM pg_stat_replication
|
||||
`,
|
||||
},
|
||||
@ -105,14 +79,16 @@ var queryOverrides = map[string][]OverrideQuery{
|
||||
{
|
||||
semver.MustParseRange(">=9.4.0 <10.0.0"),
|
||||
`
|
||||
SELECT slot_name, database, active, pg_xlog_location_diff(pg_current_xlog_location(), restart_lsn)
|
||||
SELECT slot_name, database, active,
|
||||
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), restart_lsn) else pg_xlog_location_diff(pg_current_xlog_location(), restart_lsn) end) as pg_xlog_location_diff
|
||||
FROM pg_replication_slots
|
||||
`,
|
||||
},
|
||||
{
|
||||
semver.MustParseRange(">=10.0.0"),
|
||||
`
|
||||
SELECT slot_name, database, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
|
||||
SELECT slot_name, database, active,
|
||||
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), restart_lsn) else pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) end) as pg_wal_lsn_diff
|
||||
FROM pg_replication_slots
|
||||
`,
|
||||
},
|
||||
@ -139,6 +115,9 @@ var queryOverrides = map[string][]OverrideQuery{
|
||||
tmp.state,
|
||||
tmp2.usename,
|
||||
tmp2.application_name,
|
||||
tmp2.backend_type,
|
||||
tmp2.wait_event_type,
|
||||
tmp2.wait_event,
|
||||
COALESCE(count,0) as count,
|
||||
COALESCE(max_tx_duration,0) as max_tx_duration
|
||||
FROM
|
||||
@ -157,9 +136,13 @@ var queryOverrides = map[string][]OverrideQuery{
|
||||
state,
|
||||
usename,
|
||||
application_name,
|
||||
backend_type,
|
||||
wait_event_type,
|
||||
wait_event,
|
||||
count(*) AS count,
|
||||
MAX(EXTRACT(EPOCH FROM now() - xact_start))::float AS max_tx_duration
|
||||
FROM pg_stat_activity GROUP BY datname,state,usename,application_name) AS tmp2
|
||||
FROM pg_stat_activity
|
||||
GROUP BY datname,state,usename,application_name,backend_type,wait_event_type,wait_event) AS tmp2
|
||||
ON tmp.state = tmp2.state AND pg_database.datname = tmp2.datname
|
||||
`,
|
||||
},
|
||||
@ -195,7 +178,7 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][]
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
level.Warn(logger).Log("msg", "No query matched override, disabling metric space", "name", name)
|
||||
logger.Warn("No query matched override, disabling metric space", "name", name)
|
||||
resultMap[name] = ""
|
||||
}
|
||||
}
|
||||
@ -216,7 +199,7 @@ func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[str
|
||||
newQueryOverrides := make(map[string]string)
|
||||
|
||||
for metric, specs := range userQueries {
|
||||
level.Debug(logger).Log("msg", "New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
|
||||
logger.Debug("New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
|
||||
newQueryOverrides[metric] = specs.Query
|
||||
metricMap, ok := metricMaps[metric]
|
||||
if !ok {
|
||||
@ -268,9 +251,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
|
||||
for k, v := range partialExporterMap {
|
||||
_, found := server.metricMap[k]
|
||||
if found {
|
||||
level.Debug(logger).Log("msg", "Overriding metric from user YAML file", "metric", k)
|
||||
logger.Debug("Overriding metric from user YAML file", "metric", k)
|
||||
} else {
|
||||
level.Debug(logger).Log("msg", "Adding new metric from user YAML file", "metric", k)
|
||||
logger.Debug("Adding new metric from user YAML file", "metric", k)
|
||||
}
|
||||
server.metricMap[k] = v
|
||||
}
|
||||
@ -279,9 +262,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
|
||||
for k, v := range newQueryOverrides {
|
||||
_, found := server.queryOverrides[k]
|
||||
if found {
|
||||
level.Debug(logger).Log("msg", "Overriding query override from user YAML file", "query_override", k)
|
||||
logger.Debug("Overriding query override from user YAML file", "query_override", k)
|
||||
} else {
|
||||
level.Debug(logger).Log("msg", "Adding new query override from user YAML file", "query_override", k)
|
||||
logger.Debug("Adding new query override from user YAML file", "query_override", k)
|
||||
}
|
||||
server.queryOverrides[k] = v
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -71,7 +70,7 @@ func NewServer(dsn string, opts ...ServerOpt) (*Server, error) {
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
level.Info(logger).Log("msg", "Established new database connection", "fingerprint", fingerprint)
|
||||
logger.Info("Established new database connection", "fingerprint", fingerprint)
|
||||
|
||||
s := &Server{
|
||||
db: db,
|
||||
@ -98,7 +97,7 @@ func (s *Server) Close() error {
|
||||
func (s *Server) Ping() error {
|
||||
if err := s.db.Ping(); err != nil {
|
||||
if cerr := s.Close(); cerr != nil {
|
||||
level.Error(logger).Log("msg", "Error while closing non-pinging DB connection", "server", s, "err", cerr)
|
||||
logger.Error("Error while closing non-pinging DB connection", "server", s, "err", cerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -184,7 +183,7 @@ func (s *Servers) Close() {
|
||||
defer s.m.Unlock()
|
||||
for _, server := range s.servers {
|
||||
if err := server.Close(); err != nil {
|
||||
level.Error(logger).Log("msg", "Failed to close connection", "server", server, "err", err)
|
||||
logger.Error("Failed to close connection", "server", server, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
@ -82,14 +81,14 @@ func dbToFloat64(t interface{}) (float64, bool) {
|
||||
strV := string(v)
|
||||
result, err := strconv.ParseFloat(strV, 64)
|
||||
if err != nil {
|
||||
level.Info(logger).Log("msg", "Could not parse []byte", "err", err)
|
||||
logger.Info("Could not parse []byte", "err", err)
|
||||
return math.NaN(), false
|
||||
}
|
||||
return result, true
|
||||
case string:
|
||||
result, err := strconv.ParseFloat(v, 64)
|
||||
if err != nil {
|
||||
level.Info(logger).Log("msg", "Could not parse string", "err", err)
|
||||
logger.Info("Could not parse string", "err", err)
|
||||
return math.NaN(), false
|
||||
}
|
||||
return result, true
|
||||
@ -122,14 +121,14 @@ func dbToUint64(t interface{}) (uint64, bool) {
|
||||
strV := string(v)
|
||||
result, err := strconv.ParseUint(strV, 10, 64)
|
||||
if err != nil {
|
||||
level.Info(logger).Log("msg", "Could not parse []byte", "err", err)
|
||||
logger.Info("Could not parse []byte", "err", err)
|
||||
return 0, false
|
||||
}
|
||||
return result, true
|
||||
case string:
|
||||
result, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
level.Info(logger).Log("msg", "Could not parse string", "err", err)
|
||||
logger.Info("Could not parse string", "err", err)
|
||||
return 0, false
|
||||
}
|
||||
return result, true
|
||||
@ -160,7 +159,7 @@ func dbToString(t interface{}) (string, bool) {
|
||||
// Try and convert to string
|
||||
return string(v), true
|
||||
case string:
|
||||
return v, true
|
||||
return strings.ToValidUTF8(v, "<22>"), true
|
||||
case bool:
|
||||
if v {
|
||||
return "true", true
|
||||
|
@ -17,12 +17,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kingpin/v2"
|
||||
"github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -62,7 +61,7 @@ type Collector interface {
|
||||
}
|
||||
|
||||
type collectorConfig struct {
|
||||
logger log.Logger
|
||||
logger *slog.Logger
|
||||
excludeDatabases []string
|
||||
}
|
||||
|
||||
@ -89,7 +88,7 @@ func registerCollector(name string, isDefaultEnabled bool, createFunc func(colle
|
||||
// PostgresCollector implements the prometheus.Collector interface.
|
||||
type PostgresCollector struct {
|
||||
Collectors map[string]Collector
|
||||
logger log.Logger
|
||||
logger *slog.Logger
|
||||
|
||||
instance *instance
|
||||
}
|
||||
@ -97,7 +96,7 @@ type PostgresCollector struct {
|
||||
type Option func(*PostgresCollector) error
|
||||
|
||||
// NewPostgresCollector creates a new PostgresCollector.
|
||||
func NewPostgresCollector(logger log.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
|
||||
func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
|
||||
p := &PostgresCollector{
|
||||
logger: logger,
|
||||
}
|
||||
@ -131,7 +130,7 @@ func NewPostgresCollector(logger log.Logger, excludeDatabases []string, dsn stri
|
||||
collectors[key] = collector
|
||||
} else {
|
||||
collector, err := factories[key](collectorConfig{
|
||||
logger: log.With(logger, "collector", key),
|
||||
logger: logger.With("collector", key),
|
||||
excludeDatabases: excludeDatabases,
|
||||
})
|
||||
if err != nil {
|
||||
@ -166,18 +165,30 @@ func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
// Collect implements the prometheus.Collector interface.
|
||||
func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
ctx := context.TODO()
|
||||
|
||||
// copy the instance so that concurrent scrapes have independent instances
|
||||
inst := p.instance.copy()
|
||||
|
||||
// Set up the database connection for the collector.
|
||||
err := inst.setup()
|
||||
if err != nil {
|
||||
p.logger.Error("Error opening connection to database", "err", err)
|
||||
return
|
||||
}
|
||||
defer inst.Close()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(p.Collectors))
|
||||
for name, c := range p.Collectors {
|
||||
go func(name string, c Collector) {
|
||||
execute(ctx, name, c, p.instance, ch, p.logger)
|
||||
execute(ctx, name, c, inst, ch, p.logger)
|
||||
wg.Done()
|
||||
}(name, c)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func execute(ctx context.Context, name string, c Collector, instance *instance, ch chan<- prometheus.Metric, logger log.Logger) {
|
||||
func execute(ctx context.Context, name string, c Collector, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) {
|
||||
begin := time.Now()
|
||||
err := c.Update(ctx, instance, ch)
|
||||
duration := time.Since(begin)
|
||||
@ -185,13 +196,13 @@ func execute(ctx context.Context, name string, c Collector, instance *instance,
|
||||
|
||||
if err != nil {
|
||||
if IsNoDataError(err) {
|
||||
level.Debug(logger).Log("msg", "collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
|
||||
logger.Debug("collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
|
||||
} else {
|
||||
level.Error(logger).Log("msg", "collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
|
||||
logger.Error("collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
|
||||
}
|
||||
success = 0
|
||||
} else {
|
||||
level.Debug(logger).Log("msg", "collector succeeded", "name", name, "duration_seconds", duration.Seconds())
|
||||
logger.Debug("collector succeeded", "name", name, "duration_seconds", duration.Seconds())
|
||||
success = 1
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name)
|
||||
|
@ -48,9 +48,15 @@ func readMetric(m prometheus.Metric) MetricResult {
|
||||
|
||||
func sanitizeQuery(q string) string {
|
||||
q = strings.Join(strings.Fields(q), " ")
|
||||
q = strings.Replace(q, "(", "\\(", -1)
|
||||
q = strings.Replace(q, ")", "\\)", -1)
|
||||
q = strings.Replace(q, "*", "\\*", -1)
|
||||
q = strings.Replace(q, "$", "\\$", -1)
|
||||
q = strings.ReplaceAll(q, "(", "\\(")
|
||||
q = strings.ReplaceAll(q, "?", "\\?")
|
||||
q = strings.ReplaceAll(q, ")", "\\)")
|
||||
q = strings.ReplaceAll(q, "[", "\\[")
|
||||
q = strings.ReplaceAll(q, "]", "\\]")
|
||||
q = strings.ReplaceAll(q, "{", "\\{")
|
||||
q = strings.ReplaceAll(q, "}", "\\}")
|
||||
q = strings.ReplaceAll(q, "*", "\\*")
|
||||
q = strings.ReplaceAll(q, "^", "\\^")
|
||||
q = strings.ReplaceAll(q, "$", "\\$")
|
||||
return q
|
||||
}
|
||||
|
@ -22,29 +22,50 @@ import (
|
||||
)
|
||||
|
||||
type instance struct {
|
||||
dsn string
|
||||
db *sql.DB
|
||||
version semver.Version
|
||||
}
|
||||
|
||||
func newInstance(dsn string) (*instance, error) {
|
||||
i := &instance{}
|
||||
i := &instance{
|
||||
dsn: dsn,
|
||||
}
|
||||
|
||||
// "Create" a database handle to verify the DSN provided is valid.
|
||||
// Open is not guaranteed to create a connection.
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Close()
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// copy returns a copy of the instance.
|
||||
func (i *instance) copy() *instance {
|
||||
return &instance{
|
||||
dsn: i.dsn,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *instance) setup() error {
|
||||
db, err := sql.Open("postgres", i.dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
i.db = db
|
||||
|
||||
version, err := queryVersion(db)
|
||||
version, err := queryVersion(i.db)
|
||||
if err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
return fmt.Errorf("error querying postgresql version: %w", err)
|
||||
} else {
|
||||
i.version = version
|
||||
}
|
||||
|
||||
i.version = version
|
||||
|
||||
return i, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *instance) getDB() *sql.DB {
|
||||
|
@ -16,8 +16,8 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGDatabaseCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
excludedDatabases []string
|
||||
}
|
||||
|
||||
@ -53,12 +53,21 @@ var (
|
||||
"Disk space used by the database",
|
||||
[]string{"datname"}, nil,
|
||||
)
|
||||
pgDatabaseConnectionLimitsDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
databaseSubsystem,
|
||||
"connection_limit",
|
||||
),
|
||||
"Connection limit set for the database",
|
||||
[]string{"datname"}, nil,
|
||||
)
|
||||
|
||||
pgDatabaseQuery = "SELECT pg_database.datname FROM pg_database;"
|
||||
pgDatabaseQuery = "SELECT pg_database.datname, pg_database.datconnlimit FROM pg_database;"
|
||||
pgDatabaseSizeQuery = "SELECT pg_database_size($1)"
|
||||
)
|
||||
|
||||
// Update implements Collector and exposes database size.
|
||||
// Update implements Collector and exposes database size and connection limits.
|
||||
// It is called by the Prometheus registry when collecting metrics.
|
||||
// The list of databases is retrieved from pg_database and filtered
|
||||
// by the excludeDatabase config parameter. The tradeoff here is that
|
||||
@ -81,21 +90,32 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
|
||||
|
||||
for rows.Next() {
|
||||
var datname sql.NullString
|
||||
if err := rows.Scan(&datname); err != nil {
|
||||
var connLimit sql.NullInt64
|
||||
if err := rows.Scan(&datname, &connLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !datname.Valid {
|
||||
continue
|
||||
}
|
||||
database := datname.String
|
||||
// Ignore excluded databases
|
||||
// Filtering is done here instead of in the query to avoid
|
||||
// a complicated NOT IN query with a variable number of parameters
|
||||
if sliceContains(c.excludedDatabases, datname.String) {
|
||||
if sliceContains(c.excludedDatabases, database) {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, datname.String)
|
||||
databases = append(databases, database)
|
||||
|
||||
connLimitMetric := 0.0
|
||||
if connLimit.Valid {
|
||||
connLimitMetric = float64(connLimit.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgDatabaseConnectionLimitsDesc,
|
||||
prometheus.GaugeValue, connLimitMetric, database,
|
||||
)
|
||||
}
|
||||
|
||||
// Query the size of the databases
|
||||
@ -114,11 +134,9 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
|
||||
pgDatabaseSizeDesc,
|
||||
prometheus.GaugeValue, sizeMetric, datname,
|
||||
)
|
||||
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func sliceContains(slice []string, s string) bool {
|
||||
|
@ -31,8 +31,8 @@ func TestPGDatabaseCollector(t *testing.T) {
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
|
||||
AddRow("postgres"))
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
|
||||
AddRow("postgres", 15))
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
|
||||
AddRow(1024))
|
||||
@ -47,6 +47,7 @@ func TestPGDatabaseCollector(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "postgres"}, value: 15, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"datname": "postgres"}, value: 1024, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
@ -71,8 +72,8 @@ func TestPGDatabaseCollectorNullMetric(t *testing.T) {
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
|
||||
AddRow("postgres"))
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
|
||||
AddRow("postgres", nil))
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
|
||||
AddRow(nil))
|
||||
@ -88,6 +89,7 @@ func TestPGDatabaseCollectorNullMetric(t *testing.T) {
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "postgres"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"datname": "postgres"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
for _, expect := range expected {
|
||||
|
114
collector/pg_database_wraparound.go
Normal file
114
collector/pg_database_wraparound.go
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const databaseWraparoundSubsystem = "database_wraparound"
|
||||
|
||||
func init() {
|
||||
registerCollector(databaseWraparoundSubsystem, defaultDisabled, NewPGDatabaseWraparoundCollector)
|
||||
}
|
||||
|
||||
type PGDatabaseWraparoundCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGDatabaseWraparoundCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGDatabaseWraparoundCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
databaseWraparoundAgeDatfrozenxid = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datfrozenxid_seconds"),
|
||||
"Age of the oldest transaction ID that has not been frozen.",
|
||||
[]string{"datname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
databaseWraparoundAgeDatminmxid = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datminmxid_seconds"),
|
||||
"Age of the oldest multi-transaction ID that has been replaced with a transaction ID.",
|
||||
[]string{"datname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
databaseWraparoundQuery = `
|
||||
SELECT
|
||||
datname,
|
||||
age(d.datfrozenxid) as age_datfrozenxid,
|
||||
mxid_age(d.datminmxid) as age_datminmxid
|
||||
FROM
|
||||
pg_catalog.pg_database d
|
||||
WHERE
|
||||
d.datallowconn
|
||||
`
|
||||
)
|
||||
|
||||
func (c *PGDatabaseWraparoundCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
databaseWraparoundQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var datname sql.NullString
|
||||
var ageDatfrozenxid, ageDatminmxid sql.NullFloat64
|
||||
|
||||
if err := rows.Scan(&datname, &ageDatfrozenxid, &ageDatminmxid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !datname.Valid {
|
||||
c.log.Debug("Skipping database with NULL name")
|
||||
continue
|
||||
}
|
||||
if !ageDatfrozenxid.Valid {
|
||||
c.log.Debug("Skipping stat emission with NULL age_datfrozenxid")
|
||||
continue
|
||||
}
|
||||
if !ageDatminmxid.Valid {
|
||||
c.log.Debug("Skipping stat emission with NULL age_datminmxid")
|
||||
continue
|
||||
}
|
||||
|
||||
ageDatfrozenxidMetric := ageDatfrozenxid.Float64
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
databaseWraparoundAgeDatfrozenxid,
|
||||
prometheus.GaugeValue,
|
||||
ageDatfrozenxidMetric, datname.String,
|
||||
)
|
||||
|
||||
ageDatminmxidMetric := ageDatminmxid.Float64
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
databaseWraparoundAgeDatminmxid,
|
||||
prometheus.GaugeValue,
|
||||
ageDatminmxidMetric, datname.String,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
64
collector/pg_database_wraparound_test.go
Normal file
64
collector/pg_database_wraparound_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGDatabaseWraparoundCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"datname",
|
||||
"age_datfrozenxid",
|
||||
"age_datminmxid",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("newreddit", 87126426, 0)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(databaseWraparoundQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGDatabaseWraparoundCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGDatabaseWraparoundCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "newreddit"}, value: 87126426, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"datname": "newreddit"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
129
collector/pg_locks.go
Normal file
129
collector/pg_locks.go
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const locksSubsystem = "locks"
|
||||
|
||||
func init() {
|
||||
registerCollector(locksSubsystem, defaultEnabled, NewPGLocksCollector)
|
||||
}
|
||||
|
||||
type PGLocksCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGLocksCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGLocksCollector{
|
||||
log: config.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
pgLocksDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
locksSubsystem,
|
||||
"count",
|
||||
),
|
||||
"Number of locks",
|
||||
[]string{"datname", "mode"}, nil,
|
||||
)
|
||||
|
||||
pgLocksQuery = `
|
||||
SELECT
|
||||
pg_database.datname as datname,
|
||||
tmp.mode as mode,
|
||||
COALESCE(count, 0) as count
|
||||
FROM
|
||||
(
|
||||
VALUES
|
||||
('accesssharelock'),
|
||||
('rowsharelock'),
|
||||
('rowexclusivelock'),
|
||||
('shareupdateexclusivelock'),
|
||||
('sharelock'),
|
||||
('sharerowexclusivelock'),
|
||||
('exclusivelock'),
|
||||
('accessexclusivelock'),
|
||||
('sireadlock')
|
||||
) AS tmp(mode)
|
||||
CROSS JOIN pg_database
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
database,
|
||||
lower(mode) AS mode,
|
||||
count(*) AS count
|
||||
FROM
|
||||
pg_locks
|
||||
WHERE
|
||||
database IS NOT NULL
|
||||
GROUP BY
|
||||
database,
|
||||
lower(mode)
|
||||
) AS tmp2 ON tmp.mode = tmp2.mode
|
||||
and pg_database.oid = tmp2.database
|
||||
ORDER BY
|
||||
1
|
||||
`
|
||||
)
|
||||
|
||||
// Update implements Collector and exposes database locks.
|
||||
// It is called by the Prometheus registry when collecting metrics.
|
||||
func (c PGLocksCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
// Query the list of databases
|
||||
rows, err := db.QueryContext(ctx,
|
||||
pgLocksQuery,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var datname, mode sql.NullString
|
||||
var count sql.NullInt64
|
||||
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&datname, &mode, &count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !datname.Valid || !mode.Valid {
|
||||
continue
|
||||
}
|
||||
|
||||
countMetric := 0.0
|
||||
if count.Valid {
|
||||
countMetric = float64(count.Int64)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgLocksDesc,
|
||||
prometheus.GaugeValue, countMetric,
|
||||
datname.String, mode.String,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
60
collector/pg_locks_test.go
Normal file
60
collector/pg_locks_test.go
Normal file
@ -0,0 +1,60 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGLocksCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
rows := sqlmock.NewRows([]string{"datname", "mode", "count"}).
|
||||
AddRow("test", "exclusivelock", 42)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgLocksQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGLocksCollector{}
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGLocksCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "test", "mode": "exclusivelock"}, value: 42, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
95
collector/pg_long_running_transactions.go
Normal file
95
collector/pg_long_running_transactions.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const longRunningTransactionsSubsystem = "long_running_transactions"
|
||||
|
||||
func init() {
|
||||
registerCollector(longRunningTransactionsSubsystem, defaultDisabled, NewPGLongRunningTransactionsCollector)
|
||||
}
|
||||
|
||||
type PGLongRunningTransactionsCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGLongRunningTransactionsCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGLongRunningTransactionsCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
longRunningTransactionsCount = prometheus.NewDesc(
|
||||
"pg_long_running_transactions",
|
||||
"Current number of long running transactions",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
longRunningTransactionsAgeInSeconds = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, longRunningTransactionsSubsystem, "oldest_timestamp_seconds"),
|
||||
"The current maximum transaction age in seconds",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
longRunningTransactionsQuery = `
|
||||
SELECT
|
||||
COUNT(*) as transactions,
|
||||
MAX(EXTRACT(EPOCH FROM clock_timestamp() - pg_stat_activity.xact_start)) AS oldest_timestamp_seconds
|
||||
FROM pg_catalog.pg_stat_activity
|
||||
WHERE state IS DISTINCT FROM 'idle'
|
||||
AND query NOT LIKE 'autovacuum:%'
|
||||
AND pg_stat_activity.xact_start IS NOT NULL;
|
||||
`
|
||||
)
|
||||
|
||||
func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
longRunningTransactionsQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var transactions, ageInSeconds float64
|
||||
|
||||
if err := rows.Scan(&transactions, &ageInSeconds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
longRunningTransactionsCount,
|
||||
prometheus.GaugeValue,
|
||||
transactions,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
longRunningTransactionsAgeInSeconds,
|
||||
prometheus.GaugeValue,
|
||||
ageInSeconds,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
63
collector/pg_long_running_transactions_test.go
Normal file
63
collector/pg_long_running_transactions_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGLongRunningTransactionsCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"transactions",
|
||||
"age_in_seconds",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(20, 1200)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(longRunningTransactionsQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGLongRunningTransactionsCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGLongRunningTransactionsCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, value: 20, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{}, value: 1200, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ import (
|
||||
const postmasterSubsystem = "postmaster"
|
||||
|
||||
func init() {
|
||||
registerCollector(postmasterSubsystem, defaultEnabled, NewPGPostmasterCollector)
|
||||
registerCollector(postmasterSubsystem, defaultDisabled, NewPGPostmasterCollector)
|
||||
}
|
||||
|
||||
type PGPostmasterCollector struct {
|
||||
@ -44,7 +44,7 @@ var (
|
||||
[]string{}, nil,
|
||||
)
|
||||
|
||||
pgPostmasterQuery = "SELECT pg_postmaster_start_time from pg_postmaster_start_time();"
|
||||
pgPostmasterQuery = "SELECT extract(epoch from pg_postmaster_start_time) from pg_postmaster_start_time();"
|
||||
)
|
||||
|
||||
func (c *PGPostmasterCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
|
@ -16,8 +16,9 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/lib/pq"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -27,7 +28,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGProcessIdleCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
const processIdleSubsystem = "process_idle"
|
||||
@ -39,25 +40,27 @@ func NewPGProcessIdleCollector(config collectorConfig) (Collector, error) {
|
||||
var pgProcessIdleSeconds = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, processIdleSubsystem, "seconds"),
|
||||
"Idle time of server processes",
|
||||
[]string{"application_name"},
|
||||
[]string{"state", "application_name"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch chan<- prometheus.Metric) error {
|
||||
db := inst.getDB()
|
||||
func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx,
|
||||
`WITH
|
||||
metrics AS (
|
||||
SELECT
|
||||
state,
|
||||
application_name,
|
||||
SUM(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - state_change))::bigint)::float AS process_idle_seconds_sum,
|
||||
COUNT(*) AS process_idle_seconds_count
|
||||
FROM pg_stat_activity
|
||||
WHERE state = 'idle'
|
||||
GROUP BY application_name
|
||||
WHERE state ~ '^idle'
|
||||
GROUP BY state, application_name
|
||||
),
|
||||
buckets AS (
|
||||
SELECT
|
||||
state,
|
||||
application_name,
|
||||
le,
|
||||
SUM(
|
||||
@ -69,35 +72,42 @@ func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch cha
|
||||
FROM
|
||||
pg_stat_activity,
|
||||
UNNEST(ARRAY[1, 2, 5, 15, 30, 60, 90, 120, 300]) AS le
|
||||
GROUP BY application_name, le
|
||||
ORDER BY application_name, le
|
||||
GROUP BY state, application_name, le
|
||||
ORDER BY state, application_name, le
|
||||
)
|
||||
SELECT
|
||||
state,
|
||||
application_name,
|
||||
process_idle_seconds_sum as seconds_sum,
|
||||
process_idle_seconds_count as seconds_count,
|
||||
ARRAY_AGG(le) AS seconds,
|
||||
ARRAY_AGG(bucket) AS seconds_bucket
|
||||
FROM metrics JOIN buckets USING (application_name)
|
||||
GROUP BY 1, 2, 3;`)
|
||||
FROM metrics JOIN buckets USING (state, application_name)
|
||||
GROUP BY 1, 2, 3, 4;`)
|
||||
|
||||
var state sql.NullString
|
||||
var applicationName sql.NullString
|
||||
var secondsSum sql.NullInt64
|
||||
var secondsSum sql.NullFloat64
|
||||
var secondsCount sql.NullInt64
|
||||
var seconds []int64
|
||||
var secondsBucket []uint64
|
||||
var seconds []float64
|
||||
var secondsBucket []int64
|
||||
|
||||
err := row.Scan(&applicationName, &secondsSum, &secondsCount, &seconds, &secondsBucket)
|
||||
err := row.Scan(&state, &applicationName, &secondsSum, &secondsCount, pq.Array(&seconds), pq.Array(&secondsBucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buckets = make(map[float64]uint64, len(seconds))
|
||||
for i, second := range seconds {
|
||||
if i >= len(secondsBucket) {
|
||||
break
|
||||
}
|
||||
buckets[float64(second)] = secondsBucket[i]
|
||||
buckets[second] = uint64(secondsBucket[i])
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
stateLabel := "unknown"
|
||||
if state.Valid {
|
||||
stateLabel = state.String
|
||||
}
|
||||
|
||||
applicationNameLabel := "unknown"
|
||||
@ -111,12 +121,12 @@ func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch cha
|
||||
}
|
||||
secondsSumMetric := 0.0
|
||||
if secondsSum.Valid {
|
||||
secondsSumMetric = float64(secondsSum.Int64)
|
||||
secondsSumMetric = secondsSum.Float64
|
||||
}
|
||||
ch <- prometheus.MustNewConstHistogram(
|
||||
pgProcessIdleSeconds,
|
||||
secondsCountMetric, secondsSumMetric, buckets,
|
||||
applicationNameLabel,
|
||||
stateLabel, applicationNameLabel,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
@ -30,7 +29,7 @@ type PGReplicationCollector struct {
|
||||
}
|
||||
|
||||
func NewPGReplicationCollector(collectorConfig) (Collector, error) {
|
||||
return &PGPostmasterCollector{}, nil
|
||||
return &PGReplicationCollector{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
@ -52,26 +51,39 @@ var (
|
||||
"Indicates if the server is a replica",
|
||||
[]string{}, nil,
|
||||
)
|
||||
pgReplicationLastReplay = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
replicationSubsystem,
|
||||
"last_replay_seconds",
|
||||
),
|
||||
"Age of last replay in seconds",
|
||||
[]string{}, nil,
|
||||
)
|
||||
|
||||
pgReplicationQuery = `SELECT
|
||||
CASE
|
||||
WHEN NOT pg_is_in_recovery() THEN 0
|
||||
WHEN pg_last_wal_receive_lsn () = pg_last_wal_replay_lsn () THEN 0
|
||||
ELSE GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())))
|
||||
END AS lag,
|
||||
CASE
|
||||
WHEN pg_is_in_recovery() THEN 1
|
||||
ELSE 0
|
||||
END as is_replica`
|
||||
END as is_replica,
|
||||
GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) as last_replay`
|
||||
)
|
||||
|
||||
func (c *PGReplicationCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error {
|
||||
func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx,
|
||||
pgReplicationQuery,
|
||||
)
|
||||
|
||||
var lag float64
|
||||
var isReplica int64
|
||||
err := row.Scan(&lag, &isReplica)
|
||||
var replayAge float64
|
||||
err := row.Scan(&lag, &isReplica, &replayAge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -83,5 +95,9 @@ func (c *PGReplicationCollector) Update(ctx context.Context, db *sql.DB, ch chan
|
||||
pgReplicationIsReplica,
|
||||
prometheus.GaugeValue, float64(isReplica),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationLastReplay,
|
||||
prometheus.GaugeValue, replayAge,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
@ -16,8 +16,9 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -28,7 +29,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGReplicationSlotCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGReplicationSlotCollector(config collectorConfig) (Collector, error) {
|
||||
@ -43,7 +44,7 @@ var (
|
||||
"slot_current_wal_lsn",
|
||||
),
|
||||
"current wal lsn value",
|
||||
[]string{"slot_name"}, nil,
|
||||
[]string{"slot_name", "slot_type"}, nil,
|
||||
)
|
||||
pgReplicationSlotCurrentFlushDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
@ -52,7 +53,7 @@ var (
|
||||
"slot_confirmed_flush_lsn",
|
||||
),
|
||||
"last lsn confirmed flushed to the replication slot",
|
||||
[]string{"slot_name"}, nil,
|
||||
[]string{"slot_name", "slot_type"}, nil,
|
||||
)
|
||||
pgReplicationSlotIsActiveDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
@ -61,22 +62,62 @@ var (
|
||||
"slot_is_active",
|
||||
),
|
||||
"whether the replication slot is active or not",
|
||||
[]string{"slot_name"}, nil,
|
||||
[]string{"slot_name", "slot_type"}, nil,
|
||||
)
|
||||
pgReplicationSlotSafeWal = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
replicationSlotSubsystem,
|
||||
"safe_wal_size_bytes",
|
||||
),
|
||||
"number of bytes that can be written to WAL such that this slot is not in danger of getting in state lost",
|
||||
[]string{"slot_name", "slot_type"}, nil,
|
||||
)
|
||||
pgReplicationSlotWalStatus = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
replicationSlotSubsystem,
|
||||
"wal_status",
|
||||
),
|
||||
"availability of WAL files claimed by this slot",
|
||||
[]string{"slot_name", "slot_type", "wal_status"}, nil,
|
||||
)
|
||||
|
||||
pgReplicationSlotQuery = `SELECT
|
||||
slot_name,
|
||||
pg_current_wal_lsn() - '0/0' AS current_wal_lsn,
|
||||
coalesce(confirmed_flush_lsn, '0/0') - '0/0',
|
||||
slot_type,
|
||||
CASE WHEN pg_is_in_recovery() THEN
|
||||
pg_last_wal_receive_lsn() - '0/0'
|
||||
ELSE
|
||||
pg_current_wal_lsn() - '0/0'
|
||||
END AS current_wal_lsn,
|
||||
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
|
||||
active
|
||||
FROM
|
||||
pg_replication_slots;`
|
||||
FROM pg_replication_slots;`
|
||||
pgReplicationSlotNewQuery = `SELECT
|
||||
slot_name,
|
||||
slot_type,
|
||||
CASE WHEN pg_is_in_recovery() THEN
|
||||
pg_last_wal_receive_lsn() - '0/0'
|
||||
ELSE
|
||||
pg_current_wal_lsn() - '0/0'
|
||||
END AS current_wal_lsn,
|
||||
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
|
||||
active,
|
||||
safe_wal_size,
|
||||
wal_status
|
||||
FROM pg_replication_slots;`
|
||||
)
|
||||
|
||||
func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
query := pgReplicationSlotQuery
|
||||
abovePG13 := instance.version.GTE(semver.MustParse("13.0.0"))
|
||||
if abovePG13 {
|
||||
query = pgReplicationSlotNewQuery
|
||||
}
|
||||
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
pgReplicationSlotQuery)
|
||||
query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -84,10 +125,28 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
|
||||
|
||||
for rows.Next() {
|
||||
var slotName sql.NullString
|
||||
var slotType sql.NullString
|
||||
var walLSN sql.NullFloat64
|
||||
var flushLSN sql.NullFloat64
|
||||
var isActive sql.NullBool
|
||||
if err := rows.Scan(&slotName, &walLSN, &flushLSN, &isActive); err != nil {
|
||||
var safeWalSize sql.NullInt64
|
||||
var walStatus sql.NullString
|
||||
|
||||
r := []any{
|
||||
&slotName,
|
||||
&slotType,
|
||||
&walLSN,
|
||||
&flushLSN,
|
||||
&isActive,
|
||||
}
|
||||
|
||||
if abovePG13 {
|
||||
r = append(r, &safeWalSize)
|
||||
r = append(r, &walStatus)
|
||||
}
|
||||
|
||||
err := rows.Scan(r...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -99,6 +158,10 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
|
||||
if slotName.Valid {
|
||||
slotNameLabel = slotName.String
|
||||
}
|
||||
slotTypeLabel := "unknown"
|
||||
if slotType.Valid {
|
||||
slotTypeLabel = slotType.String
|
||||
}
|
||||
|
||||
var walLSNMetric float64
|
||||
if walLSN.Valid {
|
||||
@ -106,7 +169,7 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationSlotCurrentWalDesc,
|
||||
prometheus.GaugeValue, walLSNMetric, slotNameLabel,
|
||||
prometheus.GaugeValue, walLSNMetric, slotNameLabel, slotTypeLabel,
|
||||
)
|
||||
if isActive.Valid && isActive.Bool {
|
||||
var flushLSNMetric float64
|
||||
@ -115,16 +178,27 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationSlotCurrentFlushDesc,
|
||||
prometheus.GaugeValue, flushLSNMetric, slotNameLabel,
|
||||
prometheus.GaugeValue, flushLSNMetric, slotNameLabel, slotTypeLabel,
|
||||
)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationSlotIsActiveDesc,
|
||||
prometheus.GaugeValue, isActiveValue, slotNameLabel,
|
||||
prometheus.GaugeValue, isActiveValue, slotNameLabel, slotTypeLabel,
|
||||
)
|
||||
|
||||
if safeWalSize.Valid {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationSlotSafeWal,
|
||||
prometheus.GaugeValue, float64(safeWalSize.Int64), slotNameLabel, slotTypeLabel,
|
||||
)
|
||||
}
|
||||
|
||||
if walStatus.Valid {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgReplicationSlotWalStatus,
|
||||
prometheus.GaugeValue, 1, slotNameLabel, slotTypeLabel, walStatus.String,
|
||||
)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return rows.Err()
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
@ -29,12 +30,12 @@ func TestPgReplicationSlotCollectorActive(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
|
||||
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("test_slot", 5, 3, true)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
|
||||
AddRow("test_slot", "physical", 5, 3, true, 323906992, "reserved")
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -47,9 +48,11 @@ func TestPgReplicationSlotCollectorActive(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 5, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 3, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 5, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 3, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 323906992, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical", "wal_status": "reserved"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
@ -70,12 +73,12 @@ func TestPgReplicationSlotCollectorInActive(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
|
||||
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("test_slot", 6, 12, false)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
|
||||
AddRow("test_slot", "physical", 6, 12, false, -4000, "extended")
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -88,8 +91,10 @@ func TestPgReplicationSlotCollectorInActive(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 6, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 6, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: -4000, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical", "wal_status": "extended"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
@ -111,12 +116,12 @@ func TestPgReplicationSlotCollectorActiveNil(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
|
||||
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("test_slot", 6, 12, nil)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
|
||||
AddRow("test_slot", "physical", 6, 12, nil, nil, "lost")
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -129,8 +134,9 @@ func TestPgReplicationSlotCollectorActiveNil(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 6, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 6, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "test_slot", "slot_type": "physical", "wal_status": "lost"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
@ -151,12 +157,12 @@ func TestPgReplicationSlotCollectorTestNilValues(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
|
||||
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(nil, nil, nil, true)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
|
||||
AddRow(nil, nil, nil, nil, true, nil, nil)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -169,9 +175,9 @@ func TestPgReplicationSlotCollectorTestNilValues(t *testing.T) {
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"slot_name": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "unknown"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "unknown", "slot_type": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "unknown", "slot_type": "unknown"}, value: 0, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"slot_name": "unknown", "slot_type": "unknown"}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
|
@ -29,9 +29,11 @@ func TestPgReplicationCollector(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
columns := []string{"lag", "is_replica"}
|
||||
inst := &instance{db: db}
|
||||
|
||||
columns := []string{"lag", "is_replica", "last_replay"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(1000, 1)
|
||||
AddRow(1000, 1, 3)
|
||||
mock.ExpectQuery(sanitizeQuery(pgReplicationQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
@ -39,7 +41,7 @@ func TestPgReplicationCollector(t *testing.T) {
|
||||
defer close(ch)
|
||||
c := PGReplicationCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), db, ch); err != nil {
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGReplicationCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
@ -47,6 +49,7 @@ func TestPgReplicationCollector(t *testing.T) {
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, value: 1000, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{}, value: 1, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{}, value: 3, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
|
91
collector/pg_roles.go
Normal file
91
collector/pg_roles.go
Normal file
@ -0,0 +1,91 @@
|
||||
// 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 language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const rolesSubsystem = "roles"
|
||||
|
||||
func init() {
|
||||
registerCollector(rolesSubsystem, defaultEnabled, NewPGRolesCollector)
|
||||
}
|
||||
|
||||
type PGRolesCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGRolesCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGRolesCollector{
|
||||
log: config.logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
pgRolesConnectionLimitsDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
rolesSubsystem,
|
||||
"connection_limit",
|
||||
),
|
||||
"Connection limit set for the role",
|
||||
[]string{"rolname"}, nil,
|
||||
)
|
||||
|
||||
pgRolesConnectionLimitsQuery = "SELECT pg_roles.rolname, pg_roles.rolconnlimit FROM pg_roles"
|
||||
)
|
||||
|
||||
// Update implements Collector and exposes roles connection limits.
|
||||
// It is called by the Prometheus registry when collecting metrics.
|
||||
func (c PGRolesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
// Query the list of databases
|
||||
rows, err := db.QueryContext(ctx,
|
||||
pgRolesConnectionLimitsQuery,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var rolname sql.NullString
|
||||
var connLimit sql.NullInt64
|
||||
if err := rows.Scan(&rolname, &connLimit); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !rolname.Valid {
|
||||
continue
|
||||
}
|
||||
rolnameLabel := rolname.String
|
||||
|
||||
if !connLimit.Valid {
|
||||
continue
|
||||
}
|
||||
connLimitMetric := float64(connLimit.Int64)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgRolesConnectionLimitsDesc,
|
||||
prometheus.GaugeValue, connLimitMetric, rolnameLabel,
|
||||
)
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
58
collector/pg_roles_test.go
Normal file
58
collector/pg_roles_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGRolesCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgRolesConnectionLimitsQuery)).WillReturnRows(sqlmock.NewRows([]string{"rolname", "rolconnlimit"}).
|
||||
AddRow("postgres", 15))
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGRolesCollector{}
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGRolesCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"rolname": "postgres"}, value: 15, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
84
collector/pg_stat_activity_autovacuum.go
Normal file
84
collector/pg_stat_activity_autovacuum.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const statActivityAutovacuumSubsystem = "stat_activity_autovacuum"
|
||||
|
||||
func init() {
|
||||
registerCollector(statActivityAutovacuumSubsystem, defaultDisabled, NewPGStatActivityAutovacuumCollector)
|
||||
}
|
||||
|
||||
type PGStatActivityAutovacuumCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatActivityAutovacuumCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatActivityAutovacuumCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
statActivityAutovacuumAgeInSeconds = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statActivityAutovacuumSubsystem, "timestamp_seconds"),
|
||||
"Start timestamp of the vacuum process in seconds",
|
||||
[]string{"relname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statActivityAutovacuumQuery = `
|
||||
SELECT
|
||||
SPLIT_PART(query, '.', 2) AS relname,
|
||||
EXTRACT(EPOCH FROM xact_start) AS timestamp_seconds
|
||||
FROM
|
||||
pg_catalog.pg_stat_activity
|
||||
WHERE
|
||||
query LIKE 'autovacuum:%'
|
||||
`
|
||||
)
|
||||
|
||||
func (PGStatActivityAutovacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
statActivityAutovacuumQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var relname string
|
||||
var ageInSeconds float64
|
||||
|
||||
if err := rows.Scan(&relname, &ageInSeconds); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statActivityAutovacuumAgeInSeconds,
|
||||
prometheus.GaugeValue,
|
||||
ageInSeconds, relname,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
62
collector/pg_stat_activity_autovacuum_test.go
Normal file
62
collector/pg_stat_activity_autovacuum_test.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGStatActivityAutovacuumCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"relname",
|
||||
"timestamp_seconds",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("test", 3600)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statActivityAutovacuumQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatActivityAutovacuumCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatActivityAutovacuumCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"relname": "test"}, value: 3600, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -101,7 +102,7 @@ var (
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statBGWriterQuery = `SELECT
|
||||
statBGWriterQueryBefore17 = `SELECT
|
||||
checkpoints_timed
|
||||
,checkpoints_req
|
||||
,checkpoint_write_time
|
||||
@ -114,121 +115,177 @@ var (
|
||||
,buffers_alloc
|
||||
,stats_reset
|
||||
FROM pg_stat_bgwriter;`
|
||||
|
||||
statBGWriterQueryAfter17 = `SELECT
|
||||
buffers_clean
|
||||
,maxwritten_clean
|
||||
,buffers_alloc
|
||||
,stats_reset
|
||||
FROM pg_stat_bgwriter;`
|
||||
)
|
||||
|
||||
func (PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx,
|
||||
statBGWriterQuery)
|
||||
if instance.version.GE(semver.MustParse("17.0.0")) {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx, statBGWriterQueryAfter17)
|
||||
|
||||
var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64
|
||||
var cpwt, cpst sql.NullFloat64
|
||||
var sr sql.NullTime
|
||||
var bc, mwc, ba sql.NullInt64
|
||||
var sr sql.NullTime
|
||||
|
||||
err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := row.Scan(&bc, &mwc, &ba, &sr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cptMetric := 0.0
|
||||
if cpt.Valid {
|
||||
cptMetric = float64(cpt.Int64)
|
||||
bcMetric := 0.0
|
||||
if bc.Valid {
|
||||
bcMetric = float64(bc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
bcMetric,
|
||||
)
|
||||
mwcMetric := 0.0
|
||||
if mwc.Valid {
|
||||
mwcMetric = float64(mwc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterMaxwrittenCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
mwcMetric,
|
||||
)
|
||||
baMetric := 0.0
|
||||
if ba.Valid {
|
||||
baMetric = float64(ba.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersAllocDesc,
|
||||
prometheus.CounterValue,
|
||||
baMetric,
|
||||
)
|
||||
srMetric := 0.0
|
||||
if sr.Valid {
|
||||
srMetric = float64(sr.Time.Unix())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterStatsResetDesc,
|
||||
prometheus.CounterValue,
|
||||
srMetric,
|
||||
)
|
||||
} else {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx, statBGWriterQueryBefore17)
|
||||
|
||||
var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64
|
||||
var cpwt, cpst sql.NullFloat64
|
||||
var sr sql.NullTime
|
||||
|
||||
err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cptMetric := 0.0
|
||||
if cpt.Valid {
|
||||
cptMetric = float64(cpt.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsTimedDesc,
|
||||
prometheus.CounterValue,
|
||||
cptMetric,
|
||||
)
|
||||
cprMetric := 0.0
|
||||
if cpr.Valid {
|
||||
cprMetric = float64(cpr.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsReqDesc,
|
||||
prometheus.CounterValue,
|
||||
cprMetric,
|
||||
)
|
||||
cpwtMetric := 0.0
|
||||
if cpwt.Valid {
|
||||
cpwtMetric = float64(cpwt.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsReqTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
cpwtMetric,
|
||||
)
|
||||
cpstMetric := 0.0
|
||||
if cpst.Valid {
|
||||
cpstMetric = float64(cpst.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsSyncTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
cpstMetric,
|
||||
)
|
||||
bcpMetric := 0.0
|
||||
if bcp.Valid {
|
||||
bcpMetric = float64(bcp.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersCheckpointDesc,
|
||||
prometheus.CounterValue,
|
||||
bcpMetric,
|
||||
)
|
||||
bcMetric := 0.0
|
||||
if bc.Valid {
|
||||
bcMetric = float64(bc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
bcMetric,
|
||||
)
|
||||
mwcMetric := 0.0
|
||||
if mwc.Valid {
|
||||
mwcMetric = float64(mwc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterMaxwrittenCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
mwcMetric,
|
||||
)
|
||||
bbMetric := 0.0
|
||||
if bb.Valid {
|
||||
bbMetric = float64(bb.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersBackendDesc,
|
||||
prometheus.CounterValue,
|
||||
bbMetric,
|
||||
)
|
||||
bbfMetric := 0.0
|
||||
if bbf.Valid {
|
||||
bbfMetric = float64(bbf.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersBackendFsyncDesc,
|
||||
prometheus.CounterValue,
|
||||
bbfMetric,
|
||||
)
|
||||
baMetric := 0.0
|
||||
if ba.Valid {
|
||||
baMetric = float64(ba.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersAllocDesc,
|
||||
prometheus.CounterValue,
|
||||
baMetric,
|
||||
)
|
||||
srMetric := 0.0
|
||||
if sr.Valid {
|
||||
srMetric = float64(sr.Time.Unix())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterStatsResetDesc,
|
||||
prometheus.CounterValue,
|
||||
srMetric,
|
||||
)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsTimedDesc,
|
||||
prometheus.CounterValue,
|
||||
cptMetric,
|
||||
)
|
||||
cprMetric := 0.0
|
||||
if cpr.Valid {
|
||||
cprMetric = float64(cpr.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsReqDesc,
|
||||
prometheus.CounterValue,
|
||||
cprMetric,
|
||||
)
|
||||
cpwtMetric := 0.0
|
||||
if cpwt.Valid {
|
||||
cpwtMetric = float64(cpwt.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsReqTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
cpwtMetric,
|
||||
)
|
||||
cpstMetric := 0.0
|
||||
if cpst.Valid {
|
||||
cpstMetric = float64(cpst.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterCheckpointsSyncTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
cpstMetric,
|
||||
)
|
||||
bcpMetric := 0.0
|
||||
if bcp.Valid {
|
||||
bcpMetric = float64(bcp.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersCheckpointDesc,
|
||||
prometheus.CounterValue,
|
||||
bcpMetric,
|
||||
)
|
||||
bcMetric := 0.0
|
||||
if bc.Valid {
|
||||
bcMetric = float64(bc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
bcMetric,
|
||||
)
|
||||
mwcMetric := 0.0
|
||||
if mwc.Valid {
|
||||
mwcMetric = float64(mwc.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterMaxwrittenCleanDesc,
|
||||
prometheus.CounterValue,
|
||||
mwcMetric,
|
||||
)
|
||||
bbMetric := 0.0
|
||||
if bb.Valid {
|
||||
bbMetric = float64(bb.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersBackendDesc,
|
||||
prometheus.CounterValue,
|
||||
bbMetric,
|
||||
)
|
||||
bbfMetric := 0.0
|
||||
if bbf.Valid {
|
||||
bbfMetric = float64(bbf.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersBackendFsyncDesc,
|
||||
prometheus.CounterValue,
|
||||
bbfMetric,
|
||||
)
|
||||
baMetric := 0.0
|
||||
if ba.Valid {
|
||||
baMetric = float64(ba.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterBuffersAllocDesc,
|
||||
prometheus.CounterValue,
|
||||
baMetric,
|
||||
)
|
||||
srMetric := 0.0
|
||||
if sr.Valid {
|
||||
srMetric = float64(sr.Time.Unix())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statBGWriterStatsResetDesc,
|
||||
prometheus.CounterValue,
|
||||
srMetric,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -51,8 +51,8 @@ func TestPGStatBGWriterCollector(t *testing.T) {
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(354, 4945, 289097744, 1242257, 3275602074, 89320867, 450139, 2034563757, 0, 2725688749, srT)
|
||||
mock.ExpectQuery(sanitizeQuery(statBGWriterQuery)).WillReturnRows(rows)
|
||||
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, 0, int64(2725688749), srT)
|
||||
mock.ExpectQuery(sanitizeQuery(statBGWriterQueryBefore17)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -113,7 +113,7 @@ func TestPGStatBGWriterCollectorNullValues(t *testing.T) {
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
mock.ExpectQuery(sanitizeQuery(statBGWriterQuery)).WillReturnRows(rows)
|
||||
mock.ExpectQuery(sanitizeQuery(statBGWriterQueryBefore17)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
|
231
collector/pg_stat_checkpointer.go
Normal file
231
collector/pg_stat_checkpointer.go
Normal file
@ -0,0 +1,231 @@
|
||||
// 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 language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const statCheckpointerSubsystem = "stat_checkpointer"
|
||||
|
||||
func init() {
|
||||
// WARNING:
|
||||
// Disabled by default because this set of metrics is only available from Postgres 17
|
||||
registerCollector(statCheckpointerSubsystem, defaultDisabled, NewPGStatCheckpointerCollector)
|
||||
}
|
||||
|
||||
type PGStatCheckpointerCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatCheckpointerCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatCheckpointerCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
statCheckpointerNumTimedDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_timed_total"),
|
||||
"Number of scheduled checkpoints due to timeout",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerNumRequestedDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_requested_total"),
|
||||
"Number of requested checkpoints that have been performed",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerRestartpointsTimedDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_timed_total"),
|
||||
"Number of scheduled restartpoints due to timeout or after a failed attempt to perform it",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerRestartpointsReqDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_req_total"),
|
||||
"Number of requested restartpoints",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerRestartpointsDoneDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_done_total"),
|
||||
"Number of restartpoints that have been performed",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerWriteTimeDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "write_time_total"),
|
||||
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are written to disk, in milliseconds",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerSyncTimeDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "sync_time_total"),
|
||||
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are synchronized to disk, in milliseconds",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerBuffersWrittenDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "buffers_written_total"),
|
||||
"Number of buffers written during checkpoints and restartpoints",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statCheckpointerStatsResetDesc = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "stats_reset_total"),
|
||||
"Time at which these statistics were last reset",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statCheckpointerQuery = `SELECT
|
||||
num_timed
|
||||
,num_requested
|
||||
,restartpoints_timed
|
||||
,restartpoints_req
|
||||
,restartpoints_done
|
||||
,write_time
|
||||
,sync_time
|
||||
,buffers_written
|
||||
,stats_reset
|
||||
FROM pg_stat_checkpointer;`
|
||||
)
|
||||
|
||||
func (c PGStatCheckpointerCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
|
||||
before17 := instance.version.LT(semver.MustParse("17.0.0"))
|
||||
if before17 {
|
||||
c.log.Warn("pg_stat_checkpointer collector is not available on PostgreSQL < 17.0.0, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
row := db.QueryRowContext(ctx, statCheckpointerQuery)
|
||||
|
||||
// num_timed = nt = bigint
|
||||
// num_requested = nr = bigint
|
||||
// restartpoints_timed = rpt = bigint
|
||||
// restartpoints_req = rpr = bigint
|
||||
// restartpoints_done = rpd = bigint
|
||||
// write_time = wt = double precision
|
||||
// sync_time = st = double precision
|
||||
// buffers_written = bw = bigint
|
||||
// stats_reset = sr = timestamp
|
||||
|
||||
var nt, nr, rpt, rpr, rpd, bw sql.NullInt64
|
||||
var wt, st sql.NullFloat64
|
||||
var sr sql.NullTime
|
||||
|
||||
err := row.Scan(&nt, &nr, &rpt, &rpr, &rpd, &wt, &st, &bw, &sr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ntMetric := 0.0
|
||||
if nt.Valid {
|
||||
ntMetric = float64(nt.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerNumTimedDesc,
|
||||
prometheus.CounterValue,
|
||||
ntMetric,
|
||||
)
|
||||
|
||||
nrMetric := 0.0
|
||||
if nr.Valid {
|
||||
nrMetric = float64(nr.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerNumRequestedDesc,
|
||||
prometheus.CounterValue,
|
||||
nrMetric,
|
||||
)
|
||||
|
||||
rptMetric := 0.0
|
||||
if rpt.Valid {
|
||||
rptMetric = float64(rpt.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerRestartpointsTimedDesc,
|
||||
prometheus.CounterValue,
|
||||
rptMetric,
|
||||
)
|
||||
|
||||
rprMetric := 0.0
|
||||
if rpr.Valid {
|
||||
rprMetric = float64(rpr.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerRestartpointsReqDesc,
|
||||
prometheus.CounterValue,
|
||||
rprMetric,
|
||||
)
|
||||
|
||||
rpdMetric := 0.0
|
||||
if rpd.Valid {
|
||||
rpdMetric = float64(rpd.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerRestartpointsDoneDesc,
|
||||
prometheus.CounterValue,
|
||||
rpdMetric,
|
||||
)
|
||||
|
||||
wtMetric := 0.0
|
||||
if wt.Valid {
|
||||
wtMetric = float64(wt.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerWriteTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
wtMetric,
|
||||
)
|
||||
|
||||
stMetric := 0.0
|
||||
if st.Valid {
|
||||
stMetric = float64(st.Float64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerSyncTimeDesc,
|
||||
prometheus.CounterValue,
|
||||
stMetric,
|
||||
)
|
||||
|
||||
bwMetric := 0.0
|
||||
if bw.Valid {
|
||||
bwMetric = float64(bw.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerBuffersWrittenDesc,
|
||||
prometheus.CounterValue,
|
||||
bwMetric,
|
||||
)
|
||||
|
||||
srMetric := 0.0
|
||||
if sr.Valid {
|
||||
srMetric = float64(sr.Time.Unix())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statCheckpointerStatsResetDesc,
|
||||
prometheus.CounterValue,
|
||||
srMetric,
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
144
collector/pg_stat_checkpointer_test.go
Normal file
144
collector/pg_stat_checkpointer_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
// 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 language governing permissions and
|
||||
// limitations under the License.
|
||||
package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGStatCheckpointerCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"num_timed",
|
||||
"num_requested",
|
||||
"restartpoints_timed",
|
||||
"restartpoints_req",
|
||||
"restartpoints_done",
|
||||
"write_time",
|
||||
"sync_time",
|
||||
"buffers_written",
|
||||
"stats_reset"}
|
||||
|
||||
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing time: %s", err)
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, srT)
|
||||
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatCheckpointerCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 354},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 4945},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 289097744},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1242257},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 3275602074},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 89320867},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 450139},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 2034563757},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1685059842},
|
||||
}
|
||||
|
||||
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 TestPGStatCheckpointerCollectorNullValues(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"num_timed",
|
||||
"num_requested",
|
||||
"restartpoints_timed",
|
||||
"restartpoints_req",
|
||||
"restartpoints_done",
|
||||
"write_time",
|
||||
"sync_time",
|
||||
"buffers_written",
|
||||
"stats_reset"}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatCheckpointerCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
@ -16,7 +16,11 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -26,10 +30,12 @@ func init() {
|
||||
registerCollector(statDatabaseSubsystem, defaultEnabled, NewPGStatDatabaseCollector)
|
||||
}
|
||||
|
||||
type PGStatDatabaseCollector struct{}
|
||||
type PGStatDatabaseCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatDatabaseCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatDatabaseCollector{}, nil
|
||||
return &PGStatDatabaseCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
@ -202,36 +208,53 @@ var (
|
||||
[]string{"datid", "datname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statDatabaseQuery = `
|
||||
SELECT
|
||||
datid
|
||||
,datname
|
||||
,numbackends
|
||||
,xact_commit
|
||||
,xact_rollback
|
||||
,blks_read
|
||||
,blks_hit
|
||||
,tup_returned
|
||||
,tup_fetched
|
||||
,tup_inserted
|
||||
,tup_updated
|
||||
,tup_deleted
|
||||
,conflicts
|
||||
,temp_files
|
||||
,temp_bytes
|
||||
,deadlocks
|
||||
,blk_read_time
|
||||
,blk_write_time
|
||||
,stats_reset
|
||||
FROM pg_stat_database;
|
||||
`
|
||||
statDatabaseActiveTime = prometheus.NewDesc(prometheus.BuildFQName(
|
||||
namespace,
|
||||
statDatabaseSubsystem,
|
||||
"active_time_seconds_total",
|
||||
),
|
||||
"Time spent executing SQL statements in this database, in seconds",
|
||||
[]string{"datid", "datname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
)
|
||||
|
||||
func (PGStatDatabaseCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
func statDatabaseQuery(columns []string) string {
|
||||
return fmt.Sprintf("SELECT %s FROM pg_stat_database;", strings.Join(columns, ","))
|
||||
}
|
||||
|
||||
func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
"datname",
|
||||
"numbackends",
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
"blks_read",
|
||||
"blks_hit",
|
||||
"tup_returned",
|
||||
"tup_fetched",
|
||||
"tup_inserted",
|
||||
"tup_updated",
|
||||
"tup_deleted",
|
||||
"conflicts",
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
}
|
||||
|
||||
activeTimeAvail := instance.version.GTE(semver.MustParse("14.0.0"))
|
||||
if activeTimeAvail {
|
||||
columns = append(columns, "active_time")
|
||||
}
|
||||
|
||||
rows, err := db.QueryContext(ctx,
|
||||
statDatabaseQuery,
|
||||
statDatabaseQuery(columns),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -240,10 +263,10 @@ func (PGStatDatabaseCollector) Update(ctx context.Context, instance *instance, c
|
||||
|
||||
for rows.Next() {
|
||||
var datid, datname sql.NullString
|
||||
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime sql.NullFloat64
|
||||
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime, activeTime sql.NullFloat64
|
||||
var statsReset sql.NullTime
|
||||
|
||||
err := rows.Scan(
|
||||
r := []any{
|
||||
&datid,
|
||||
&datname,
|
||||
&numBackends,
|
||||
@ -263,222 +286,231 @@ func (PGStatDatabaseCollector) Update(ctx context.Context, instance *instance, c
|
||||
&blkReadTime,
|
||||
&blkWriteTime,
|
||||
&statsReset,
|
||||
)
|
||||
}
|
||||
|
||||
if activeTimeAvail {
|
||||
r = append(r, &activeTime)
|
||||
}
|
||||
|
||||
err := rows.Scan(r...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
datidLabel := "unknown"
|
||||
if datid.Valid {
|
||||
datidLabel = datid.String
|
||||
}
|
||||
datnameLabel := "unknown"
|
||||
if datname.Valid {
|
||||
datnameLabel = datname.String
|
||||
}
|
||||
|
||||
numBackendsMetric := 0.0
|
||||
if numBackends.Valid {
|
||||
numBackendsMetric = numBackends.Float64
|
||||
if !datid.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no datid")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseNumbackends,
|
||||
prometheus.GaugeValue,
|
||||
numBackendsMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
xactCommitMetric := 0.0
|
||||
if xactCommit.Valid {
|
||||
xactCommitMetric = xactCommit.Float64
|
||||
if !datname.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no datname")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseXactCommit,
|
||||
prometheus.CounterValue,
|
||||
xactCommitMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
xactRollbackMetric := 0.0
|
||||
if xactRollback.Valid {
|
||||
xactRollbackMetric = xactRollback.Float64
|
||||
if !numBackends.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no numbackends")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseXactRollback,
|
||||
prometheus.CounterValue,
|
||||
xactRollbackMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
blksReadMetric := 0.0
|
||||
if blksRead.Valid {
|
||||
blksReadMetric = blksRead.Float64
|
||||
if !xactCommit.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no xact_commit")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlksRead,
|
||||
prometheus.CounterValue,
|
||||
blksReadMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
blksHitMetric := 0.0
|
||||
if blksHit.Valid {
|
||||
blksHitMetric = blksHit.Float64
|
||||
if !xactRollback.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no xact_rollback")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlksHit,
|
||||
prometheus.CounterValue,
|
||||
blksHitMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tupReturnedMetric := 0.0
|
||||
if tupReturned.Valid {
|
||||
tupReturnedMetric = tupReturned.Float64
|
||||
if !blksRead.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no blks_read")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupReturned,
|
||||
prometheus.CounterValue,
|
||||
tupReturnedMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tupFetchedMetric := 0.0
|
||||
if tupFetched.Valid {
|
||||
tupFetchedMetric = tupFetched.Float64
|
||||
if !blksHit.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no blks_hit")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupFetched,
|
||||
prometheus.CounterValue,
|
||||
tupFetchedMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tupInsertedMetric := 0.0
|
||||
if tupInserted.Valid {
|
||||
tupInsertedMetric = tupInserted.Float64
|
||||
if !tupReturned.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no tup_returned")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupInserted,
|
||||
prometheus.CounterValue,
|
||||
tupInsertedMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tupUpdatedMetric := 0.0
|
||||
if tupUpdated.Valid {
|
||||
tupUpdatedMetric = tupUpdated.Float64
|
||||
if !tupFetched.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no tup_fetched")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupUpdated,
|
||||
prometheus.CounterValue,
|
||||
tupUpdatedMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tupDeletedMetric := 0.0
|
||||
if tupDeleted.Valid {
|
||||
tupDeletedMetric = tupDeleted.Float64
|
||||
if !tupInserted.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no tup_inserted")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupDeleted,
|
||||
prometheus.CounterValue,
|
||||
tupDeletedMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
conflictsMetric := 0.0
|
||||
if conflicts.Valid {
|
||||
conflictsMetric = conflicts.Float64
|
||||
if !tupUpdated.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no tup_updated")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseConflicts,
|
||||
prometheus.CounterValue,
|
||||
conflictsMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tempFilesMetric := 0.0
|
||||
if tempFiles.Valid {
|
||||
tempFilesMetric = tempFiles.Float64
|
||||
if !tupDeleted.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no tup_deleted")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTempFiles,
|
||||
prometheus.CounterValue,
|
||||
tempFilesMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
tempBytesMetric := 0.0
|
||||
if tempBytes.Valid {
|
||||
tempBytesMetric = tempBytes.Float64
|
||||
if !conflicts.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no conflicts")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTempBytes,
|
||||
prometheus.CounterValue,
|
||||
tempBytesMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
deadlocksMetric := 0.0
|
||||
if deadlocks.Valid {
|
||||
deadlocksMetric = deadlocks.Float64
|
||||
if !tempFiles.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no temp_files")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseDeadlocks,
|
||||
prometheus.CounterValue,
|
||||
deadlocksMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
blkReadTimeMetric := 0.0
|
||||
if blkReadTime.Valid {
|
||||
blkReadTimeMetric = blkReadTime.Float64
|
||||
if !tempBytes.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no temp_bytes")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlkReadTime,
|
||||
prometheus.CounterValue,
|
||||
blkReadTimeMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
blkWriteTimeMetric := 0.0
|
||||
if blkWriteTime.Valid {
|
||||
blkWriteTimeMetric = blkWriteTime.Float64
|
||||
if !deadlocks.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no deadlocks")
|
||||
continue
|
||||
}
|
||||
if !blkReadTime.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no blk_read_time")
|
||||
continue
|
||||
}
|
||||
if !blkWriteTime.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no blk_write_time")
|
||||
continue
|
||||
}
|
||||
if activeTimeAvail && !activeTime.Valid {
|
||||
c.log.Debug("Skipping collecting metric because it has no active_time")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlkWriteTime,
|
||||
prometheus.CounterValue,
|
||||
blkWriteTimeMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
)
|
||||
|
||||
statsResetMetric := 0.0
|
||||
if !statsReset.Valid {
|
||||
c.log.Debug("No metric for stats_reset, will collect 0 instead")
|
||||
}
|
||||
if statsReset.Valid {
|
||||
statsResetMetric = float64(statsReset.Time.Unix())
|
||||
}
|
||||
|
||||
labels := []string{datid.String, datname.String}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseNumbackends,
|
||||
prometheus.GaugeValue,
|
||||
numBackends.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseXactCommit,
|
||||
prometheus.CounterValue,
|
||||
xactCommit.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseXactRollback,
|
||||
prometheus.CounterValue,
|
||||
xactRollback.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlksRead,
|
||||
prometheus.CounterValue,
|
||||
blksRead.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlksHit,
|
||||
prometheus.CounterValue,
|
||||
blksHit.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupReturned,
|
||||
prometheus.CounterValue,
|
||||
tupReturned.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupFetched,
|
||||
prometheus.CounterValue,
|
||||
tupFetched.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupInserted,
|
||||
prometheus.CounterValue,
|
||||
tupInserted.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupUpdated,
|
||||
prometheus.CounterValue,
|
||||
tupUpdated.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTupDeleted,
|
||||
prometheus.CounterValue,
|
||||
tupDeleted.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseConflicts,
|
||||
prometheus.CounterValue,
|
||||
conflicts.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTempFiles,
|
||||
prometheus.CounterValue,
|
||||
tempFiles.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseTempBytes,
|
||||
prometheus.CounterValue,
|
||||
tempBytes.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseDeadlocks,
|
||||
prometheus.CounterValue,
|
||||
deadlocks.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlkReadTime,
|
||||
prometheus.CounterValue,
|
||||
blkReadTime.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseBlkWriteTime,
|
||||
prometheus.CounterValue,
|
||||
blkWriteTime.Float64,
|
||||
labels...,
|
||||
)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseStatsReset,
|
||||
prometheus.CounterValue,
|
||||
statsResetMetric,
|
||||
datidLabel,
|
||||
datnameLabel,
|
||||
labels...,
|
||||
)
|
||||
|
||||
if activeTimeAvail {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statDatabaseActiveTime,
|
||||
prometheus.CounterValue,
|
||||
activeTime.Float64/1000.0,
|
||||
labels...,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -18,8 +18,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/prometheus/common/promslog"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
@ -30,7 +32,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
@ -52,6 +54,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
"active_time",
|
||||
}
|
||||
|
||||
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
|
||||
@ -67,26 +70,30 @@ func TestPGStatDatabaseCollector(t *testing.T) {
|
||||
4945,
|
||||
289097744,
|
||||
1242257,
|
||||
3275602074,
|
||||
int64(3275602074),
|
||||
89320867,
|
||||
450139,
|
||||
2034563757,
|
||||
0,
|
||||
2725688749,
|
||||
int64(2725688749),
|
||||
23,
|
||||
52,
|
||||
74,
|
||||
925,
|
||||
16,
|
||||
823,
|
||||
srT)
|
||||
srT,
|
||||
33,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatDatabaseCollector{}
|
||||
c := PGStatDatabaseCollector{
|
||||
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
|
||||
}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
|
||||
@ -111,6 +118,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.033},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
@ -131,7 +139,11 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing time: %s", err)
|
||||
}
|
||||
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
@ -153,109 +165,32 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
"active_time",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatDatabaseCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
}
|
||||
|
||||
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 TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
"datname",
|
||||
"numbackends",
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
"blks_read",
|
||||
"blks_hit",
|
||||
"tup_returned",
|
||||
"tup_fetched",
|
||||
"tup_inserted",
|
||||
"tup_updated",
|
||||
"tup_deleted",
|
||||
"conflicts",
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
}
|
||||
|
||||
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing time: %s", err)
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
"postgres",
|
||||
354,
|
||||
4945,
|
||||
289097744,
|
||||
1242257,
|
||||
int64(3275602074),
|
||||
89320867,
|
||||
450139,
|
||||
2034563757,
|
||||
0,
|
||||
int64(2725688749),
|
||||
23,
|
||||
52,
|
||||
74,
|
||||
925,
|
||||
16,
|
||||
823,
|
||||
srT,
|
||||
32,
|
||||
).
|
||||
AddRow(
|
||||
"pid",
|
||||
"postgres",
|
||||
@ -263,47 +198,29 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
|
||||
4945,
|
||||
289097744,
|
||||
1242257,
|
||||
3275602074,
|
||||
int64(3275602074),
|
||||
89320867,
|
||||
450139,
|
||||
2034563757,
|
||||
0,
|
||||
2725688749,
|
||||
int64(2725688749),
|
||||
23,
|
||||
52,
|
||||
74,
|
||||
925,
|
||||
16,
|
||||
823,
|
||||
srT).
|
||||
AddRow(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
srT,
|
||||
32,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatDatabaseCollector{}
|
||||
c := PGStatDatabaseCollector{
|
||||
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
|
||||
}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
|
||||
@ -328,23 +245,277 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.032},
|
||||
}
|
||||
|
||||
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 TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
"datname",
|
||||
"numbackends",
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
"blks_read",
|
||||
"blks_hit",
|
||||
"tup_returned",
|
||||
"tup_fetched",
|
||||
"tup_inserted",
|
||||
"tup_updated",
|
||||
"tup_deleted",
|
||||
"conflicts",
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
"active_time",
|
||||
}
|
||||
|
||||
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing time: %s", err)
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
"pid",
|
||||
"postgres",
|
||||
354,
|
||||
4945,
|
||||
289097744,
|
||||
1242257,
|
||||
int64(3275602074),
|
||||
89320867,
|
||||
450139,
|
||||
2034563757,
|
||||
0,
|
||||
int64(2725688749),
|
||||
23,
|
||||
52,
|
||||
74,
|
||||
925,
|
||||
16,
|
||||
823,
|
||||
srT,
|
||||
14,
|
||||
).
|
||||
AddRow(
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
).
|
||||
AddRow(
|
||||
"pid",
|
||||
"postgres",
|
||||
355,
|
||||
4946,
|
||||
289097745,
|
||||
1242258,
|
||||
int64(3275602075),
|
||||
89320868,
|
||||
450140,
|
||||
2034563758,
|
||||
1,
|
||||
int64(2725688750),
|
||||
24,
|
||||
53,
|
||||
75,
|
||||
926,
|
||||
17,
|
||||
824,
|
||||
srT,
|
||||
15,
|
||||
)
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatDatabaseCollector{
|
||||
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
|
||||
}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.014},
|
||||
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 355},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4946},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097745},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242258},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602075},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320868},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450140},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563758},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688750},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 24},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 53},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 75},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 926},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 17},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 824},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.015},
|
||||
}
|
||||
|
||||
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 TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
|
||||
|
||||
columns := []string{
|
||||
"datid",
|
||||
"datname",
|
||||
"numbackends",
|
||||
"xact_commit",
|
||||
"xact_rollback",
|
||||
"blks_read",
|
||||
"blks_hit",
|
||||
"tup_returned",
|
||||
"tup_fetched",
|
||||
"tup_inserted",
|
||||
"tup_updated",
|
||||
"tup_deleted",
|
||||
"conflicts",
|
||||
"temp_files",
|
||||
"temp_bytes",
|
||||
"deadlocks",
|
||||
"blk_read_time",
|
||||
"blk_write_time",
|
||||
"stats_reset",
|
||||
"active_time",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
"pid",
|
||||
"postgres",
|
||||
354,
|
||||
4945,
|
||||
289097744,
|
||||
1242257,
|
||||
int64(3275602074),
|
||||
89320867,
|
||||
450139,
|
||||
2034563757,
|
||||
0,
|
||||
int64(2725688749),
|
||||
23,
|
||||
52,
|
||||
74,
|
||||
925,
|
||||
16,
|
||||
823,
|
||||
nil,
|
||||
7,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatDatabaseCollector{
|
||||
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
|
||||
}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
|
||||
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.007},
|
||||
}
|
||||
|
||||
convey.Convey("Metrics comparison", t, func() {
|
||||
|
222
collector/pg_stat_progress_vacuum.go
Normal file
222
collector/pg_stat_progress_vacuum.go
Normal file
@ -0,0 +1,222 @@
|
||||
// Copyright 2025 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const progressVacuumSubsystem = "stat_progress_vacuum"
|
||||
|
||||
func init() {
|
||||
registerCollector(progressVacuumSubsystem, defaultEnabled, NewPGStatProgressVacuumCollector)
|
||||
}
|
||||
|
||||
type PGStatProgressVacuumCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatProgressVacuumCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatProgressVacuumCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var vacuumPhases = []string{
|
||||
"initializing",
|
||||
"scanning heap",
|
||||
"vacuuming indexes",
|
||||
"vacuuming heap",
|
||||
"cleaning up indexes",
|
||||
"truncating heap",
|
||||
"performing final cleanup",
|
||||
}
|
||||
|
||||
var (
|
||||
statProgressVacuumPhase = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "phase"),
|
||||
"Current vacuum phase (1 = active, 0 = inactive). Label 'phase' is human-readable.",
|
||||
[]string{"datname", "relname", "phase"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumHeapBlksTotal = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks"),
|
||||
"Total number of heap blocks in the table being vacuumed.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumHeapBlksScanned = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks_scanned"),
|
||||
"Number of heap blocks scanned so far.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumHeapBlksVacuumed = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks_vacuumed"),
|
||||
"Number of heap blocks vacuumed so far.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumIndexVacuumCount = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "index_vacuums"),
|
||||
"Number of completed index vacuum cycles.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumMaxDeadTuples = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "max_dead_tuples"),
|
||||
"Maximum number of dead tuples that can be stored before cleanup is performed.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
statProgressVacuumNumDeadTuples = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "num_dead_tuples"),
|
||||
"Current number of dead tuples found so far.",
|
||||
[]string{"datname", "relname"},
|
||||
nil,
|
||||
)
|
||||
|
||||
// This is the view definition of pg_stat_progress_vacuum, albeit without the conversion
|
||||
// of "phase" to a human-readable string. We will prefer the numeric representation.
|
||||
statProgressVacuumQuery = `SELECT
|
||||
d.datname,
|
||||
s.relid::regclass::text AS relname,
|
||||
s.param1 AS phase,
|
||||
s.param2 AS heap_blks_total,
|
||||
s.param3 AS heap_blks_scanned,
|
||||
s.param4 AS heap_blks_vacuumed,
|
||||
s.param5 AS index_vacuum_count,
|
||||
s.param6 AS max_dead_tuples,
|
||||
s.param7 AS num_dead_tuples
|
||||
FROM
|
||||
pg_stat_get_progress_info('VACUUM'::text)
|
||||
s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
|
||||
LEFT JOIN
|
||||
pg_database d ON s.datid = d.oid`
|
||||
)
|
||||
|
||||
func (c *PGStatProgressVacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
statProgressVacuumQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
datname sql.NullString
|
||||
relname sql.NullString
|
||||
phase sql.NullInt64
|
||||
heapBlksTotal sql.NullInt64
|
||||
heapBlksScanned sql.NullInt64
|
||||
heapBlksVacuumed sql.NullInt64
|
||||
indexVacuumCount sql.NullInt64
|
||||
maxDeadTuples sql.NullInt64
|
||||
numDeadTuples sql.NullInt64
|
||||
)
|
||||
|
||||
if err := rows.Scan(
|
||||
&datname,
|
||||
&relname,
|
||||
&phase,
|
||||
&heapBlksTotal,
|
||||
&heapBlksScanned,
|
||||
&heapBlksVacuumed,
|
||||
&indexVacuumCount,
|
||||
&maxDeadTuples,
|
||||
&numDeadTuples,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datnameLabel := "unknown"
|
||||
if datname.Valid {
|
||||
datnameLabel = datname.String
|
||||
}
|
||||
relnameLabel := "unknown"
|
||||
if relname.Valid {
|
||||
relnameLabel = relname.String
|
||||
}
|
||||
|
||||
labels := []string{datnameLabel, relnameLabel}
|
||||
|
||||
var phaseMetric *float64
|
||||
if phase.Valid {
|
||||
v := float64(phase.Int64)
|
||||
phaseMetric = &v
|
||||
}
|
||||
|
||||
for i, label := range vacuumPhases {
|
||||
v := 0.0
|
||||
// Only the current phase should be 1.0.
|
||||
if phaseMetric != nil && float64(i) == *phaseMetric {
|
||||
v = 1.0
|
||||
}
|
||||
labelsCopy := append(labels, label)
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumPhase, prometheus.GaugeValue, v, labelsCopy...)
|
||||
}
|
||||
|
||||
heapTotal := 0.0
|
||||
if heapBlksTotal.Valid {
|
||||
heapTotal = float64(heapBlksTotal.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksTotal, prometheus.GaugeValue, heapTotal, labels...)
|
||||
|
||||
heapScanned := 0.0
|
||||
if heapBlksScanned.Valid {
|
||||
heapScanned = float64(heapBlksScanned.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksScanned, prometheus.GaugeValue, heapScanned, labels...)
|
||||
|
||||
heapVacuumed := 0.0
|
||||
if heapBlksVacuumed.Valid {
|
||||
heapVacuumed = float64(heapBlksVacuumed.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksVacuumed, prometheus.GaugeValue, heapVacuumed, labels...)
|
||||
|
||||
indexCount := 0.0
|
||||
if indexVacuumCount.Valid {
|
||||
indexCount = float64(indexVacuumCount.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumIndexVacuumCount, prometheus.GaugeValue, indexCount, labels...)
|
||||
|
||||
maxDead := 0.0
|
||||
if maxDeadTuples.Valid {
|
||||
maxDead = float64(maxDeadTuples.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumMaxDeadTuples, prometheus.GaugeValue, maxDead, labels...)
|
||||
|
||||
numDead := 0.0
|
||||
if numDeadTuples.Valid {
|
||||
numDead = float64(numDeadTuples.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(statProgressVacuumNumDeadTuples, prometheus.GaugeValue, numDead, labels...)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
135
collector/pg_stat_progress_vacuum_test.go
Normal file
135
collector/pg_stat_progress_vacuum_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright 2025 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGStatProgressVacuumCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
columns := []string{
|
||||
"datname", "relname", "phase", "heap_blks_total", "heap_blks_scanned",
|
||||
"heap_blks_vacuumed", "index_vacuum_count", "max_dead_tuples", "num_dead_tuples",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).AddRow(
|
||||
"postgres", "a_table", 3, 3000, 400, 200, 2, 500, 123)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statProgressVacuumQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatProgressVacuumCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatProgressVacuumCollector.Update; %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "initializing"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "scanning heap"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "vacuuming indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "vacuuming heap"}, metricType: dto.MetricType_GAUGE, value: 1},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "cleaning up indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "truncating heap"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "performing final cleanup"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 3000},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 400},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 200},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 2},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 500},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 123},
|
||||
}
|
||||
|
||||
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: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGStatProgressVacuumCollectorNullValues(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
columns := []string{
|
||||
"datname", "relname", "phase", "heap_blks_total", "heap_blks_scanned",
|
||||
"heap_blks_vacuumed", "index_vacuum_count", "max_dead_tuples", "num_dead_tuples",
|
||||
}
|
||||
|
||||
rows := sqlmock.NewRows(columns).AddRow(
|
||||
"postgres", nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statProgressVacuumQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatProgressVacuumCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatProgressVacuumCollector.Update; %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "initializing"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "scanning heap"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "vacuuming indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "vacuuming heap"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "cleaning up indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "truncating heap"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "performing final cleanup"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
|
||||
}
|
||||
|
||||
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: %+v", err)
|
||||
}
|
||||
}
|
@ -16,8 +16,9 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -31,7 +32,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGStatStatementsCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatStatementsCollector(config collectorConfig) (Collector, error) {
|
||||
@ -90,12 +91,63 @@ var (
|
||||
)
|
||||
ORDER BY seconds_total DESC
|
||||
LIMIT 100;`
|
||||
|
||||
pgStatStatementsNewQuery = `SELECT
|
||||
pg_get_userbyid(userid) as user,
|
||||
pg_database.datname,
|
||||
pg_stat_statements.queryid,
|
||||
pg_stat_statements.calls as calls_total,
|
||||
pg_stat_statements.total_exec_time / 1000.0 as seconds_total,
|
||||
pg_stat_statements.rows as rows_total,
|
||||
pg_stat_statements.blk_read_time / 1000.0 as block_read_seconds_total,
|
||||
pg_stat_statements.blk_write_time / 1000.0 as block_write_seconds_total
|
||||
FROM pg_stat_statements
|
||||
JOIN pg_database
|
||||
ON pg_database.oid = pg_stat_statements.dbid
|
||||
WHERE
|
||||
total_exec_time > (
|
||||
SELECT percentile_cont(0.1)
|
||||
WITHIN GROUP (ORDER BY total_exec_time)
|
||||
FROM pg_stat_statements
|
||||
)
|
||||
ORDER BY seconds_total DESC
|
||||
LIMIT 100;`
|
||||
|
||||
pgStatStatementsQuery_PG17 = `SELECT
|
||||
pg_get_userbyid(userid) as user,
|
||||
pg_database.datname,
|
||||
pg_stat_statements.queryid,
|
||||
pg_stat_statements.calls as calls_total,
|
||||
pg_stat_statements.total_exec_time / 1000.0 as seconds_total,
|
||||
pg_stat_statements.rows as rows_total,
|
||||
pg_stat_statements.shared_blk_read_time / 1000.0 as block_read_seconds_total,
|
||||
pg_stat_statements.shared_blk_write_time / 1000.0 as block_write_seconds_total
|
||||
FROM pg_stat_statements
|
||||
JOIN pg_database
|
||||
ON pg_database.oid = pg_stat_statements.dbid
|
||||
WHERE
|
||||
total_exec_time > (
|
||||
SELECT percentile_cont(0.1)
|
||||
WITHIN GROUP (ORDER BY total_exec_time)
|
||||
FROM pg_stat_statements
|
||||
)
|
||||
ORDER BY seconds_total DESC
|
||||
LIMIT 100;`
|
||||
)
|
||||
|
||||
func (PGStatStatementsCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
var query string
|
||||
switch {
|
||||
case instance.version.GE(semver.MustParse("17.0.0")):
|
||||
query = pgStatStatementsQuery_PG17
|
||||
case instance.version.GE(semver.MustParse("13.0.0")):
|
||||
query = pgStatStatementsNewQuery
|
||||
default:
|
||||
query = pgStatStatementsQuery
|
||||
}
|
||||
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
pgStatStatementsQuery)
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
@ -29,7 +30,7 @@ func TestPGStateStatementsCollector(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("12.0.0")}
|
||||
|
||||
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
@ -72,12 +73,12 @@ func TestPGStateStatementsCollectorNull(t *testing.T) {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(nil, nil, nil, nil, nil, nil, nil, nil)
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatStatementsQuery)).WillReturnRows(rows)
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatStatementsNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -107,3 +108,89 @@ func TestPGStateStatementsCollectorNull(t *testing.T) {
|
||||
t.Errorf("there were unfulfilled exceptions: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPGStateStatementsCollectorNewPG(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
|
||||
|
||||
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("postgres", "postgres", 1500, 5, 0.4, 100, 0.1, 0.2)
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatStatementsNewQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatStatementsCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatStatementsCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 5},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.4},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 100},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.1},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.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 TestPGStateStatementsCollector_PG17(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
|
||||
|
||||
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("postgres", "postgres", 1500, 5, 0.4, 100, 0.1, 0.2)
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatStatementsQuery_PG17)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatStatementsCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatStatementsCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 5},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.4},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 100},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.1},
|
||||
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.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)
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGStatUserTablesCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatUserTablesCollector(config collectorConfig) (Collector, error) {
|
||||
@ -150,6 +150,12 @@ var (
|
||||
[]string{"datname", "schemaname", "relname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statUserTablesTotalSize = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, userTableSubsystem, "size_bytes"),
|
||||
"Total disk space used by this table, in bytes, including all indexes and TOAST data",
|
||||
[]string{"datname", "schemaname", "relname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statUserTablesQuery = `SELECT
|
||||
current_database() datname,
|
||||
@ -173,7 +179,8 @@ var (
|
||||
vacuum_count,
|
||||
autovacuum_count,
|
||||
analyze_count,
|
||||
autoanalyze_count
|
||||
autoanalyze_count,
|
||||
pg_total_relation_size(relid) as total_size
|
||||
FROM
|
||||
pg_stat_user_tables`
|
||||
)
|
||||
@ -191,10 +198,10 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
|
||||
for rows.Next() {
|
||||
var datname, schemaname, relname sql.NullString
|
||||
var seqScan, seqTupRead, idxScan, idxTupFetch, nTupIns, nTupUpd, nTupDel, nTupHotUpd, nLiveTup, nDeadTup,
|
||||
nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount sql.NullInt64
|
||||
nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount, totalSize sql.NullInt64
|
||||
var lastVacuum, lastAutovacuum, lastAnalyze, lastAutoanalyze sql.NullTime
|
||||
|
||||
if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount); err != nil {
|
||||
if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount, &totalSize); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -419,6 +426,17 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
|
||||
autoanalyzeCountMetric,
|
||||
datnameLabel, schemanameLabel, relnameLabel,
|
||||
)
|
||||
|
||||
totalSizeMetric := 0.0
|
||||
if totalSize.Valid {
|
||||
totalSizeMetric = float64(totalSize.Int64)
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statUserTablesTotalSize,
|
||||
prometheus.GaugeValue,
|
||||
totalSizeMetric,
|
||||
datnameLabel, schemanameLabel, relnameLabel,
|
||||
)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
|
@ -71,7 +71,8 @@ func TestPGStatUserTablesCollector(t *testing.T) {
|
||||
"vacuum_count",
|
||||
"autovacuum_count",
|
||||
"analyze_count",
|
||||
"autoanalyze_count"}
|
||||
"autoanalyze_count",
|
||||
"total_size"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("postgres",
|
||||
"public",
|
||||
@ -94,7 +95,8 @@ func TestPGStatUserTablesCollector(t *testing.T) {
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14)
|
||||
14,
|
||||
15)
|
||||
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
@ -170,7 +172,8 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
|
||||
"vacuum_count",
|
||||
"autovacuum_count",
|
||||
"analyze_count",
|
||||
"autoanalyze_count"}
|
||||
"autoanalyze_count",
|
||||
"total_size"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("postgres",
|
||||
nil,
|
||||
@ -193,6 +196,7 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil)
|
||||
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
|
||||
ch := make(chan prometheus.Metric)
|
||||
|
270
collector/pg_stat_walreceiver.go
Normal file
270
collector/pg_stat_walreceiver.go
Normal file
@ -0,0 +1,270 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCollector(statWalReceiverSubsystem, defaultDisabled, NewPGStatWalReceiverCollector)
|
||||
}
|
||||
|
||||
type PGStatWalReceiverCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
const statWalReceiverSubsystem = "stat_wal_receiver"
|
||||
|
||||
func NewPGStatWalReceiverCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatWalReceiverCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
labelCats = []string{"upstream_host", "slot_name", "status"}
|
||||
statWalReceiverReceiveStartLsn = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "receive_start_lsn"),
|
||||
"First write-ahead log location used when WAL receiver is started represented as a decimal",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverReceiveStartTli = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "receive_start_tli"),
|
||||
"First timeline number used when WAL receiver is started",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverFlushedLSN = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "flushed_lsn"),
|
||||
"Last write-ahead log location already received and flushed to disk, the initial value of this field being the first log location used when WAL receiver is started represented as a decimal",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverReceivedTli = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "received_tli"),
|
||||
"Timeline number of last write-ahead log location received and flushed to disk",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverLastMsgSendTime = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "last_msg_send_time"),
|
||||
"Send time of last message received from origin WAL sender",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverLastMsgReceiptTime = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "last_msg_receipt_time"),
|
||||
"Send time of last message received from origin WAL sender",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverLatestEndLsn = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "latest_end_lsn"),
|
||||
"Last write-ahead log location reported to origin WAL sender as integer",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverLatestEndTime = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "latest_end_time"),
|
||||
"Time of last write-ahead log location reported to origin WAL sender",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statWalReceiverUpstreamNode = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "upstream_node"),
|
||||
"Node ID of the upstream node",
|
||||
labelCats,
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
pgStatWalColumnQuery = `
|
||||
SELECT
|
||||
column_name
|
||||
FROM information_schema.columns
|
||||
WHERE
|
||||
table_name = 'pg_stat_wal_receiver' and
|
||||
column_name = 'flushed_lsn'
|
||||
`
|
||||
|
||||
pgStatWalReceiverQueryTemplate = `
|
||||
SELECT
|
||||
trim(both '''' from substring(conninfo from 'host=([^ ]*)')) as upstream_host,
|
||||
slot_name,
|
||||
status,
|
||||
(receive_start_lsn- '0/0') %% (2^52)::bigint as receive_start_lsn,
|
||||
%s
|
||||
receive_start_tli,
|
||||
received_tli,
|
||||
extract(epoch from last_msg_send_time) as last_msg_send_time,
|
||||
extract(epoch from last_msg_receipt_time) as last_msg_receipt_time,
|
||||
(latest_end_lsn - '0/0') %% (2^52)::bigint as latest_end_lsn,
|
||||
extract(epoch from latest_end_time) as latest_end_time,
|
||||
substring(slot_name from 'repmgr_slot_([0-9]*)') as upstream_node
|
||||
FROM pg_catalog.pg_stat_wal_receiver
|
||||
`
|
||||
)
|
||||
|
||||
func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
hasFlushedLSNRows, err := db.QueryContext(ctx, pgStatWalColumnQuery)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasFlushedLSN := hasFlushedLSNRows.Next()
|
||||
var query string
|
||||
if hasFlushedLSN {
|
||||
query = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "(flushed_lsn - '0/0') % (2^52)::bigint as flushed_lsn,\n")
|
||||
} else {
|
||||
query = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "")
|
||||
}
|
||||
|
||||
hasFlushedLSNRows.Close()
|
||||
|
||||
rows, err := db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var upstreamHost, slotName, status sql.NullString
|
||||
var receiveStartLsn, receiveStartTli, flushedLsn, receivedTli, latestEndLsn, upstreamNode sql.NullInt64
|
||||
var lastMsgSendTime, lastMsgReceiptTime, latestEndTime sql.NullFloat64
|
||||
|
||||
if hasFlushedLSN {
|
||||
if err := rows.Scan(&upstreamHost, &slotName, &status, &receiveStartLsn, &receiveStartTli, &flushedLsn, &receivedTli, &lastMsgSendTime, &lastMsgReceiptTime, &latestEndLsn, &latestEndTime, &upstreamNode); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := rows.Scan(&upstreamHost, &slotName, &status, &receiveStartLsn, &receiveStartTli, &receivedTli, &lastMsgSendTime, &lastMsgReceiptTime, &latestEndLsn, &latestEndTime, &upstreamNode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !upstreamHost.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because upstream host is null")
|
||||
continue
|
||||
}
|
||||
|
||||
if !slotName.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because slotname host is null")
|
||||
continue
|
||||
}
|
||||
|
||||
if !status.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because status is null")
|
||||
continue
|
||||
}
|
||||
labels := []string{upstreamHost.String, slotName.String, status.String}
|
||||
|
||||
if !receiveStartLsn.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because receive_start_lsn is null")
|
||||
continue
|
||||
}
|
||||
if !receiveStartTli.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because receive_start_tli is null")
|
||||
continue
|
||||
}
|
||||
if hasFlushedLSN && !flushedLsn.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because flushed_lsn is null")
|
||||
continue
|
||||
}
|
||||
if !receivedTli.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because received_tli is null")
|
||||
continue
|
||||
}
|
||||
if !lastMsgSendTime.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because last_msg_send_time is null")
|
||||
continue
|
||||
}
|
||||
if !lastMsgReceiptTime.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because last_msg_receipt_time is null")
|
||||
continue
|
||||
}
|
||||
if !latestEndLsn.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because latest_end_lsn is null")
|
||||
continue
|
||||
}
|
||||
if !latestEndTime.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats because latest_end_time is null")
|
||||
continue
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverReceiveStartLsn,
|
||||
prometheus.CounterValue,
|
||||
float64(receiveStartLsn.Int64),
|
||||
labels...)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverReceiveStartTli,
|
||||
prometheus.GaugeValue,
|
||||
float64(receiveStartTli.Int64),
|
||||
labels...)
|
||||
|
||||
if hasFlushedLSN {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverFlushedLSN,
|
||||
prometheus.CounterValue,
|
||||
float64(flushedLsn.Int64),
|
||||
labels...)
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverReceivedTli,
|
||||
prometheus.GaugeValue,
|
||||
float64(receivedTli.Int64),
|
||||
labels...)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverLastMsgSendTime,
|
||||
prometheus.CounterValue,
|
||||
float64(lastMsgSendTime.Float64),
|
||||
labels...)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverLastMsgReceiptTime,
|
||||
prometheus.CounterValue,
|
||||
float64(lastMsgReceiptTime.Float64),
|
||||
labels...)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverLatestEndLsn,
|
||||
prometheus.CounterValue,
|
||||
float64(latestEndLsn.Int64),
|
||||
labels...)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverLatestEndTime,
|
||||
prometheus.CounterValue,
|
||||
latestEndTime.Float64,
|
||||
labels...)
|
||||
|
||||
if !upstreamNode.Valid {
|
||||
c.log.Debug("Skipping wal receiver stats upstream_node because it is null")
|
||||
} else {
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statWalReceiverUpstreamNode,
|
||||
prometheus.GaugeValue,
|
||||
float64(upstreamNode.Int64),
|
||||
labels...)
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
186
collector/pg_stat_walreceiver_test.go
Normal file
186
collector/pg_stat_walreceiver_test.go
Normal file
@ -0,0 +1,186 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var queryWithFlushedLSN = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "(flushed_lsn - '0/0') % (2^52)::bigint as flushed_lsn,\n")
|
||||
var queryWithNoFlushedLSN = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "")
|
||||
|
||||
func TestPGStatWalReceiverCollectorWithFlushedLSN(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
infoSchemaColumns := []string{
|
||||
"column_name",
|
||||
}
|
||||
|
||||
infoSchemaRows := sqlmock.NewRows(infoSchemaColumns).
|
||||
AddRow(
|
||||
"flushed_lsn",
|
||||
)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatWalColumnQuery)).WillReturnRows(infoSchemaRows)
|
||||
|
||||
columns := []string{
|
||||
"upstream_host",
|
||||
"slot_name",
|
||||
"status",
|
||||
"receive_start_lsn",
|
||||
"receive_start_tli",
|
||||
"flushed_lsn",
|
||||
"received_tli",
|
||||
"last_msg_send_time",
|
||||
"last_msg_receipt_time",
|
||||
"latest_end_lsn",
|
||||
"latest_end_time",
|
||||
"upstream_node",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
"foo",
|
||||
"bar",
|
||||
"stopping",
|
||||
int64(1200668684563608),
|
||||
1687321285,
|
||||
int64(1200668684563609),
|
||||
1687321280,
|
||||
1687321275,
|
||||
1687321276,
|
||||
int64(1200668684563610),
|
||||
1687321277,
|
||||
5,
|
||||
)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(queryWithFlushedLSN)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatWalReceiverCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PgStatWalReceiverCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563608, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321285, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563609, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321280, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321275, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321276, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563610, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321277, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 5, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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 TestPGStatWalReceiverCollectorWithNoFlushedLSN(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
infoSchemaColumns := []string{
|
||||
"column_name",
|
||||
}
|
||||
|
||||
infoSchemaRows := sqlmock.NewRows(infoSchemaColumns)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(pgStatWalColumnQuery)).WillReturnRows(infoSchemaRows)
|
||||
|
||||
columns := []string{
|
||||
"upstream_host",
|
||||
"slot_name",
|
||||
"status",
|
||||
"receive_start_lsn",
|
||||
"receive_start_tli",
|
||||
"received_tli",
|
||||
"last_msg_send_time",
|
||||
"last_msg_receipt_time",
|
||||
"latest_end_lsn",
|
||||
"latest_end_time",
|
||||
"upstream_node",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(
|
||||
"foo",
|
||||
"bar",
|
||||
"starting",
|
||||
int64(1200668684563608),
|
||||
1687321285,
|
||||
1687321280,
|
||||
1687321275,
|
||||
1687321276,
|
||||
int64(1200668684563610),
|
||||
1687321277,
|
||||
5,
|
||||
)
|
||||
mock.ExpectQuery(sanitizeQuery(queryWithNoFlushedLSN)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatWalReceiverCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PgStatWalReceiverCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1200668684563608, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321285, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321280, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321275, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321276, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1200668684563610, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321277, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 5, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
118
collector/pg_statio_user_indexes.go
Normal file
118
collector/pg_statio_user_indexes.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerCollector(statioUserIndexesSubsystem, defaultDisabled, NewPGStatioUserIndexesCollector)
|
||||
}
|
||||
|
||||
type PGStatioUserIndexesCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
const statioUserIndexesSubsystem = "statio_user_indexes"
|
||||
|
||||
func NewPGStatioUserIndexesCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGStatioUserIndexesCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
statioUserIndexesIdxBlksRead = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statioUserIndexesSubsystem, "idx_blks_read_total"),
|
||||
"Number of disk blocks read from this index",
|
||||
[]string{"schemaname", "relname", "indexrelname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
statioUserIndexesIdxBlksHit = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, statioUserIndexesSubsystem, "idx_blks_hit_total"),
|
||||
"Number of buffer hits in this index",
|
||||
[]string{"schemaname", "relname", "indexrelname"},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
statioUserIndexesQuery = `
|
||||
SELECT
|
||||
schemaname,
|
||||
relname,
|
||||
indexrelname,
|
||||
idx_blks_read,
|
||||
idx_blks_hit
|
||||
FROM pg_statio_user_indexes
|
||||
`
|
||||
)
|
||||
|
||||
func (c *PGStatioUserIndexesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
rows, err := db.QueryContext(ctx,
|
||||
statioUserIndexesQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var schemaname, relname, indexrelname sql.NullString
|
||||
var idxBlksRead, idxBlksHit sql.NullFloat64
|
||||
|
||||
if err := rows.Scan(&schemaname, &relname, &indexrelname, &idxBlksRead, &idxBlksHit); err != nil {
|
||||
return err
|
||||
}
|
||||
schemanameLabel := "unknown"
|
||||
if schemaname.Valid {
|
||||
schemanameLabel = schemaname.String
|
||||
}
|
||||
relnameLabel := "unknown"
|
||||
if relname.Valid {
|
||||
relnameLabel = relname.String
|
||||
}
|
||||
indexrelnameLabel := "unknown"
|
||||
if indexrelname.Valid {
|
||||
indexrelnameLabel = indexrelname.String
|
||||
}
|
||||
labels := []string{schemanameLabel, relnameLabel, indexrelnameLabel}
|
||||
|
||||
idxBlksReadMetric := 0.0
|
||||
if idxBlksRead.Valid {
|
||||
idxBlksReadMetric = idxBlksRead.Float64
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statioUserIndexesIdxBlksRead,
|
||||
prometheus.CounterValue,
|
||||
idxBlksReadMetric,
|
||||
labels...,
|
||||
)
|
||||
|
||||
idxBlksHitMetric := 0.0
|
||||
if idxBlksHit.Valid {
|
||||
idxBlksHitMetric = idxBlksHit.Float64
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
statioUserIndexesIdxBlksHit,
|
||||
prometheus.CounterValue,
|
||||
idxBlksHitMetric,
|
||||
labels...,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
109
collector/pg_statio_user_indexes_test.go
Normal file
109
collector/pg_statio_user_indexes_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPgStatioUserIndexesCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"schemaname",
|
||||
"relname",
|
||||
"indexrelname",
|
||||
"idx_blks_read",
|
||||
"idx_blks_hit",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow("public", "pgtest_accounts", "pgtest_accounts_pkey", 8, 9)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statioUserIndexesQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatioUserIndexesCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatioUserIndexesCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"schemaname": "public", "relname": "pgtest_accounts", "indexrelname": "pgtest_accounts_pkey"}, value: 8, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"schemaname": "public", "relname": "pgtest_accounts", "indexrelname": "pgtest_accounts_pkey"}, value: 9, metricType: dto.MetricType_COUNTER},
|
||||
}
|
||||
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 TestPgStatioUserIndexesCollectorNull(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"schemaname",
|
||||
"relname",
|
||||
"indexrelname",
|
||||
"idx_blks_read",
|
||||
"idx_blks_hit",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(nil, nil, nil, nil, nil)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(statioUserIndexesQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGStatioUserIndexesCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGStatioUserIndexesCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{"schemaname": "unknown", "relname": "unknown", "indexrelname": "unknown"}, value: 0, metricType: dto.MetricType_COUNTER},
|
||||
{labels: labelMap{"schemaname": "unknown", "relname": "unknown", "indexrelname": "unknown"}, value: 0, metricType: dto.MetricType_COUNTER},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -16,8 +16,8 @@ package collector
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@ -28,7 +28,7 @@ func init() {
|
||||
}
|
||||
|
||||
type PGStatIOUserTablesCollector struct {
|
||||
log log.Logger
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGStatIOUserTablesCollector(config collectorConfig) (Collector, error) {
|
||||
@ -218,8 +218,5 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
|
||||
datnameLabel, schemanameLabel, relnameLabel,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return rows.Err()
|
||||
}
|
||||
|
84
collector/pg_wal.go
Normal file
84
collector/pg_wal.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const walSubsystem = "wal"
|
||||
|
||||
func init() {
|
||||
registerCollector(walSubsystem, defaultEnabled, NewPGWALCollector)
|
||||
}
|
||||
|
||||
type PGWALCollector struct {
|
||||
}
|
||||
|
||||
func NewPGWALCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGWALCollector{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
pgWALSegments = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
walSubsystem,
|
||||
"segments",
|
||||
),
|
||||
"Number of WAL segments",
|
||||
[]string{}, nil,
|
||||
)
|
||||
pgWALSize = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(
|
||||
namespace,
|
||||
walSubsystem,
|
||||
"size_bytes",
|
||||
),
|
||||
"Total size of WAL segments",
|
||||
[]string{}, nil,
|
||||
)
|
||||
|
||||
pgWALQuery = `
|
||||
SELECT
|
||||
COUNT(*) AS segments,
|
||||
SUM(size) AS size
|
||||
FROM pg_ls_waldir()
|
||||
WHERE name ~ '^[0-9A-F]{24}$'`
|
||||
)
|
||||
|
||||
func (c PGWALCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
row := db.QueryRowContext(ctx,
|
||||
pgWALQuery,
|
||||
)
|
||||
|
||||
var segments uint64
|
||||
var size uint64
|
||||
err := row.Scan(&segments, &size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgWALSegments,
|
||||
prometheus.GaugeValue, float64(segments),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
pgWALSize,
|
||||
prometheus.GaugeValue, float64(size),
|
||||
)
|
||||
return nil
|
||||
}
|
63
collector/pg_wal_test.go
Normal file
63
collector/pg_wal_test.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPgWALCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
inst := &instance{db: db}
|
||||
|
||||
columns := []string{"segments", "size"}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(47, 788529152)
|
||||
mock.ExpectQuery(sanitizeQuery(pgWALQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGWALCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGWALCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, value: 47, metricType: dto.MetricType_GAUGE},
|
||||
{labels: labelMap{}, value: 788529152, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
90
collector/pg_xlog_location.go
Normal file
90
collector/pg_xlog_location.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const xlogLocationSubsystem = "xlog_location"
|
||||
|
||||
func init() {
|
||||
registerCollector(xlogLocationSubsystem, defaultDisabled, NewPGXlogLocationCollector)
|
||||
}
|
||||
|
||||
type PGXlogLocationCollector struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewPGXlogLocationCollector(config collectorConfig) (Collector, error) {
|
||||
return &PGXlogLocationCollector{log: config.logger}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
xlogLocationBytes = prometheus.NewDesc(
|
||||
prometheus.BuildFQName(namespace, xlogLocationSubsystem, "bytes"),
|
||||
"Postgres LSN (log sequence number) being generated on primary or replayed on replica (truncated to low 52 bits)",
|
||||
[]string{},
|
||||
prometheus.Labels{},
|
||||
)
|
||||
|
||||
xlogLocationQuery = `
|
||||
SELECT CASE
|
||||
WHEN pg_is_in_recovery() THEN (pg_last_xlog_replay_location() - '0/0') % (2^52)::bigint
|
||||
ELSE (pg_current_xlog_location() - '0/0') % (2^52)::bigint
|
||||
END AS bytes
|
||||
`
|
||||
)
|
||||
|
||||
func (c PGXlogLocationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
|
||||
db := instance.getDB()
|
||||
|
||||
// xlog was renmaed to WAL in PostgreSQL 10
|
||||
// https://wiki.postgresql.org/wiki/New_in_postgres_10#Renaming_of_.22xlog.22_to_.22wal.22_Globally_.28and_location.2Flsn.29
|
||||
after10 := instance.version.Compare(semver.MustParse("10.0.0"))
|
||||
if after10 >= 0 {
|
||||
c.log.Warn("xlog_location collector is not available on PostgreSQL >= 10.0.0, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := db.QueryContext(ctx,
|
||||
xlogLocationQuery)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var bytes float64
|
||||
|
||||
if err := rows.Scan(&bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
xlogLocationBytes,
|
||||
prometheus.GaugeValue,
|
||||
bytes,
|
||||
)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
61
collector/pg_xlog_location_test.go
Normal file
61
collector/pg_xlog_location_test.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright 2023 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 collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestPGXlogLocationCollector(t *testing.T) {
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening a stub db connection: %s", err)
|
||||
}
|
||||
defer db.Close()
|
||||
inst := &instance{db: db}
|
||||
columns := []string{
|
||||
"bytes",
|
||||
}
|
||||
rows := sqlmock.NewRows(columns).
|
||||
AddRow(53401)
|
||||
|
||||
mock.ExpectQuery(sanitizeQuery(xlogLocationQuery)).WillReturnRows(rows)
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
c := PGXlogLocationCollector{}
|
||||
|
||||
if err := c.Update(context.Background(), inst, ch); err != nil {
|
||||
t.Errorf("Error calling PGXlogLocationCollector.Update: %s", err)
|
||||
}
|
||||
}()
|
||||
expected := []MetricResult{
|
||||
{labels: labelMap{}, value: 53401, metricType: dto.MetricType_GAUGE},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
@ -15,9 +15,9 @@ package collector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus-community/postgres_exporter/config"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
@ -25,11 +25,11 @@ import (
|
||||
type ProbeCollector struct {
|
||||
registry *prometheus.Registry
|
||||
collectors map[string]Collector
|
||||
logger log.Logger
|
||||
logger *slog.Logger
|
||||
instance *instance
|
||||
}
|
||||
|
||||
func NewProbeCollector(logger log.Logger, excludeDatabases []string, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) {
|
||||
func NewProbeCollector(logger *slog.Logger, excludeDatabases []string, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) {
|
||||
collectors := make(map[string]Collector)
|
||||
initiatedCollectorsMtx.Lock()
|
||||
defer initiatedCollectorsMtx.Unlock()
|
||||
@ -46,7 +46,7 @@ func NewProbeCollector(logger log.Logger, excludeDatabases []string, registry *p
|
||||
} else {
|
||||
collector, err := factories[key](
|
||||
collectorConfig{
|
||||
logger: log.With(logger, "collector", key),
|
||||
logger: logger.With("collector", key),
|
||||
excludeDatabases: excludeDatabases,
|
||||
})
|
||||
if err != nil {
|
||||
@ -74,6 +74,14 @@ func (pc *ProbeCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
}
|
||||
|
||||
func (pc *ProbeCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
// Set up the database connection for the collector.
|
||||
err := pc.instance.setup()
|
||||
if err != nil {
|
||||
pc.logger.Error("Error opening connection to database", "err", err)
|
||||
return
|
||||
}
|
||||
defer pc.instance.Close()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(pc.collectors))
|
||||
for name, c := range pc.collectors {
|
||||
|
@ -15,10 +15,10 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/go-kit/log"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -65,7 +65,7 @@ func (ch *Handler) GetConfig() *Config {
|
||||
return ch.Config
|
||||
}
|
||||
|
||||
func (ch *Handler) ReloadConfig(f string, logger log.Logger) error {
|
||||
func (ch *Handler) ReloadConfig(f string, logger *slog.Logger) error {
|
||||
config := &Config{}
|
||||
var err error
|
||||
defer func() {
|
||||
@ -79,14 +79,14 @@ func (ch *Handler) ReloadConfig(f string, logger log.Logger) error {
|
||||
|
||||
yamlReader, err := os.Open(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error opening config file %q: %s", f, err)
|
||||
return fmt.Errorf("error opening config file %q: %s", f, err)
|
||||
}
|
||||
defer yamlReader.Close()
|
||||
decoder := yaml.NewDecoder(yamlReader)
|
||||
decoder.KnownFields(true)
|
||||
|
||||
if err = decoder.Decode(config); err != nil {
|
||||
return fmt.Errorf("Error parsing config file %q: %s", f, err)
|
||||
return fmt.Errorf("error parsing config file %q: %s", f, err)
|
||||
}
|
||||
|
||||
ch.Lock()
|
||||
|
@ -24,7 +24,7 @@ func TestLoadConfig(t *testing.T) {
|
||||
|
||||
err := ch.ReloadConfig("testdata/config-good.yaml", nil)
|
||||
if err != nil {
|
||||
t.Errorf("Error loading config: %s", err)
|
||||
t.Errorf("error loading config: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,11 +39,11 @@ func TestLoadBadConfigs(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
input: "testdata/config-bad-auth-module.yaml",
|
||||
want: "Error parsing config file \"testdata/config-bad-auth-module.yaml\": yaml: unmarshal errors:\n line 3: field pretendauth not found in type config.AuthModule",
|
||||
want: "error parsing config file \"testdata/config-bad-auth-module.yaml\": yaml: unmarshal errors:\n line 3: field pretendauth not found in type config.AuthModule",
|
||||
},
|
||||
{
|
||||
input: "testdata/config-bad-extra-field.yaml",
|
||||
want: "Error parsing config file \"testdata/config-bad-extra-field.yaml\": yaml: unmarshal errors:\n line 8: field doesNotExist not found in type config.AuthModule",
|
||||
want: "error parsing config file \"testdata/config-bad-extra-field.yaml\": yaml: unmarshal errors:\n line 8: field doesNotExist not found in type config.AuthModule",
|
||||
},
|
||||
}
|
||||
|
||||
|
47
go.mod
47
go.mod
@ -1,18 +1,19 @@
|
||||
module github.com/prometheus-community/postgres_exporter
|
||||
|
||||
go 1.19
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/alecthomas/kingpin/v2 v2.4.0
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/go-kit/log v0.2.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/prometheus/client_golang v1.15.1
|
||||
github.com/prometheus/client_model v0.4.0
|
||||
github.com/prometheus/common v0.44.0
|
||||
github.com/prometheus/exporter-toolkit v0.10.0
|
||||
github.com/smartystreets/goconvey v1.8.0
|
||||
github.com/prometheus/client_golang v1.21.1
|
||||
github.com/prometheus/client_model v0.6.1
|
||||
github.com/prometheus/common v0.63.0
|
||||
github.com/prometheus/exporter-toolkit v0.14.0
|
||||
github.com/smartystreets/goconvey v1.8.1
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@ -21,27 +22,27 @@ require (
|
||||
require (
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // 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/klauspost/compress v1.17.11 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // 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.9.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/smartystreets/assertions v1.13.1 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/oauth2 v0.25.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
110
go.sum
110
go.sum
@ -1,38 +1,33 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
|
||||
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
|
||||
github.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-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
|
||||
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
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/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.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@ -40,63 +35,58 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8=
|
||||
github.com/prometheus/exporter-toolkit v0.10.0/go.mod h1:+sVFzuvV5JDyw+Ih6p3zFxZNVnKQa3x5qPmDSiPu4ZY=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
|
||||
github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
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/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
|
||||
github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY=
|
||||
github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
|
||||
github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
|
||||
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/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
|
||||
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
|
Loading…
Reference in New Issue
Block a user