Compare commits

..

6 Commits

Author SHA1 Message Date
Joe Adams
8c3604b85e
Bump version file
Signed-off-by: Joe Adams <github@joeadams.io>
2023-07-21 14:21:09 -04:00
Joe Adams
803e131ee4 Update changelog for release 0.13.2 ()
Signed-off-by: Joe Adams <github@joeadams.io>
2023-07-21 14:20:47 -04:00
Ben Kochie
bad8b233d8 Cleanup collectors ()
Fix up `replication` and `process_idle` Update input params to match
the rest of the collectors.

Signed-off-by: SuperQ <superq@gmail.com>
2023-07-21 14:18:57 -04:00
Tom Hughes
392d8ca16a Unpack postgres arrays for process idle times correctly ()
Signed-off-by: Ben Kochie <superq@gmail.com>
2023-07-21 14:10:59 -04:00
Tom Hughes
0850b195a0 Fix replication collector
Signed-off-by: Tom Hughes <tom@compton.nu>
2023-07-21 14:08:26 -04:00
Felix Yuan
dd87ad094a Bug Fix: Fix lingering type issues ()
* Fix postmaster type issue
* Disable postmaster collector by default

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
2023-07-21 14:07:24 -04:00
66 changed files with 721 additions and 4069 deletions

View File

@ -8,7 +8,7 @@ executors:
# This must match .promu.yml.
golang:
docker:
- image: cimg/go:1.24
- image: cimg/go:1.20
jobs:
test:
@ -16,14 +16,13 @@ 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.24
- image: cimg/go:1.20
- image: << parameters.postgres_image >>
environment:
POSTGRES_DB: circle_test
@ -57,13 +56,12 @@ workflows:
matrix:
parameters:
postgres_image:
- circleci/postgres:10
- circleci/postgres:11
- circleci/postgres:12
- circleci/postgres:13
- cimg/postgres:14.9
- cimg/postgres:15.4
- cimg/postgres:16.0
- cimg/postgres:17.0
- cimg/postgres:14.1
- cimg/postgres:15.1
- prometheus/build:
name: build
parallelism: 3

View File

@ -1,57 +0,0 @@
---
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: ''

View File

@ -1,5 +1,3 @@
---
# This action is synced from https://github.com/prometheus/prometheus
name: golangci-lint
on:
push:
@ -12,28 +10,21 @@ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
uses: actions/checkout@v3
- name: install Go
uses: actions/setup-go@v3
with:
go-version: 1.24.x
go-version: 1.20.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@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
uses: golangci/golangci-lint-action@v3.4.0
with:
args: --verbose
version: v2.0.2
version: v1.51.2

View File

@ -1,36 +1,16 @@
version: "2"
---
linters:
enable:
- 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$
- 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

View File

@ -1,12 +1,13 @@
go:
# This must match .circle/config.yml.
version: 1.24
version: 1.20
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}}

View File

@ -1,7 +1,5 @@
---
extends: default
ignore: |
**/node_modules
rules:
braces:
@ -22,4 +20,5 @@ rules:
config/testdata/section_key_dup.bad.yml
line-length: disable
truthy:
check-keys: false
ignore: |
.github/workflows/*.yml

View File

@ -1,69 +1,3 @@
## 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

View File

@ -49,23 +49,23 @@ endif
GOTEST := $(GO) test
GOTEST_DIR :=
ifneq ($(CIRCLE_JOB),)
ifneq ($(shell command -v gotestsum 2> /dev/null),)
ifneq ($(shell command -v gotestsum > /dev/null),)
GOTEST_DIR := test-results
GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --
endif
endif
PROMU_VERSION ?= 0.17.0
PROMU_VERSION ?= 0.14.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v2.0.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
GOLANGCI_LINT_VERSION ?= v1.51.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
# windows isn't included here because of the path separator being different.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
# 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,20 +169,16 @@ 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 2> /dev/null))
ifeq (, $(shell command -v yamllint > /dev/null))
@echo "yamllint not installed so skipping"
else
yamllint .
@ -208,10 +204,6 @@ 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-%:
@ -275,9 +267,3 @@ $(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

View File

@ -7,7 +7,7 @@
Prometheus exporter for PostgreSQL server metrics.
CI Tested PostgreSQL versions: `11`, `12`, `13`, `14`, `15`, `16`, `17`.
CI Tested PostgreSQL versions: `10`, `11`, `12`, `13`, `14`, `15`
## Quick Start
This package is available for Docker:
@ -17,29 +17,10 @@ docker run --net=host -it --rm -e POSTGRES_PASSWORD=password postgres
# Connect to it
docker run \
--net=host \
-e DATA_SOURCE_URI="localhost:5432/postgres?sslmode=disable" \
-e DATA_SOURCE_USER=postgres \
-e DATA_SOURCE_PASS=password \
-e DATA_SOURCE_NAME="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" \
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.**
@ -49,26 +30,6 @@ 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`.
@ -100,7 +61,7 @@ auth_modules:
To build the Docker image:
make promu
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/arm64 -p linux/ppc64le
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/amd64 -p linux/ppc64le
make docker
This will build the docker image as `prometheuscommunity/postgres_exporter:${branch}`.
@ -112,22 +73,13 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.database`
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).
Enable the database collector (default: enabled).
* `[no-]collector.postmaster`
Enable the `postmaster` collector (default: disabled).
Enable the `postmaster` collector (default: enabled).
* `[no-]collector.process_idle`
Enable the `process_idle` collector (default: disabled).
Enable the `process_idle` collector (default: enabled).
* `[no-]collector.replication`
Enable the `replication` collector (default: enabled).
@ -135,17 +87,14 @@ 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.stat_progress_vacuum`
Enable the `stat_progress_vacuum` collector (default: enabled).
* `[no-]collector.statio_user_tables`
Enable the `statio_user_tables` collector (default: enabled).
* `[no-]collector.stat_statements`
Enable the `stat_statements` collector (default: disabled).
@ -153,21 +102,6 @@ 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`
@ -230,7 +164,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:5432/postgres?sslmode=disable`.
`my_pg_hostname?sslmode=disable`.
* `DATA_SOURCE_URI_FILE`
The same as above but reads the URI from a file.

View File

@ -1 +1 @@
0.17.1
0.13.2

View File

@ -20,6 +20,7 @@ import (
"regexp"
"strings"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -38,19 +39,19 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
var err error
dsnURI, err = url.Parse(dsn)
if err != nil {
logger.Error("Unable to parse DSN as URI", "dsn", loggableDSN(dsn), "err", err)
level.Error(logger).Log("msg", "Unable to parse DSN as URI", "dsn", loggableDSN(dsn), "err", err)
continue
}
} else if connstringRe.MatchString(dsn) {
dsnConnstring = dsn
} else {
logger.Error("Unable to parse DSN as either URI or connstring", "dsn", loggableDSN(dsn))
level.Error(logger).Log("msg", "Unable to parse DSN as either URI or connstring", "dsn", loggableDSN(dsn))
continue
}
server, err := e.servers.GetServer(dsn)
if err != nil {
logger.Error("Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
level.Error(logger).Log("msg", "Error opening connection to database", "dsn", loggableDSN(dsn), "err", err)
continue
}
dsns[dsn] = struct{}{}
@ -60,7 +61,7 @@ func (e *Exporter) discoverDatabaseDSNs() []string {
databaseNames, err := queryDatabases(server)
if err != nil {
logger.Error("Error querying databases", "dsn", loggableDSN(dsn), "err", err)
level.Error(logger).Log("msg", "Error querying databases", "dsn", loggableDSN(dsn), "err", err)
continue
}
for _, databaseName := range databaseNames {
@ -108,7 +109,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 {
logger.Warn("Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
level.Warn(logger).Log("msg", "Proceeding with outdated query maps, as the Postgres version could not be determined", "err", err)
}
return server.Scrape(ch, e.disableSettingsMetrics)

View File

@ -20,13 +20,14 @@ 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/promslog"
"github.com/prometheus/common/promslog/flag"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web"
"github.com/prometheus/exporter-toolkit/web/kingpinflag"
@ -49,7 +50,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 = promslog.NewNopLogger()
logger = log.NewNopLogger()
)
// Metric name parts.
@ -69,11 +70,11 @@ const (
func main() {
kingpin.Version(version.Print(exporterName))
promslogConfig := &promslog.Config{}
flag.AddFlags(kingpin.CommandLine, promslogConfig)
promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig)
kingpin.HelpFlag.Short('h')
kingpin.Parse()
logger = promslog.New(promslogConfig)
logger = promlog.New(promlogConfig)
if *onlyDumpMaps {
dumpMaps()
@ -82,28 +83,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.
logger.Warn("Error loading config", "err", err)
level.Warn(logger).Log("msg", "Error loading config", "err", err)
}
dsns, err := getDataSources()
if err != nil {
logger.Error("Failed reading data sources", "err", err.Error())
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
os.Exit(1)
}
excludedDatabases := strings.Split(*excludeDatabases, ",")
logger.Info("Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
logger.Log("msg", "Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
if *queriesPath != "" {
logger.Warn("The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
level.Warn(logger).Log("msg", "The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
}
if *autoDiscoverDatabases || *excludeDatabases != "" || *includeDatabases != "" {
logger.Warn("Scraping additional databases via auto discovery is DEPRECATED")
level.Warn(logger).Log("msg", "Scraping additional databases via auto discovery is DEPRECATED")
}
if *constantLabelsList != "" {
logger.Warn("Constant labels on all metrics is DEPRECATED")
level.Warn(logger).Log("msg", "Constant labels on all metrics is DEPRECATED")
}
opts := []ExporterOpt{
@ -121,7 +122,7 @@ func main() {
exporter.servers.Close()
}()
prometheus.MustRegister(versioncollector.NewCollector(exporterName))
prometheus.MustRegister(version.NewCollector(exporterName))
prometheus.MustRegister(exporter)
@ -138,7 +139,7 @@ func main() {
[]string{},
)
if err != nil {
logger.Warn("Failed to create PostgresCollector", "err", err.Error())
level.Warn(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error())
} else {
prometheus.MustRegister(pe)
}
@ -159,7 +160,7 @@ func main() {
}
landingPage, err := web.NewLandingPage(landingConfig)
if err != nil {
logger.Error("error creating landing page", "err", err)
level.Error(logger).Log("err", err)
os.Exit(1)
}
http.Handle("/", landingPage)
@ -169,7 +170,7 @@ func main() {
srv := &http.Server{}
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {
logger.Error("Error running HTTP server", "err", err)
level.Error(logger).Log("msg", "Error running HTTP server", "err", err)
os.Exit(1)
}
}

View File

@ -20,6 +20,7 @@ import (
"time"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
)
@ -189,10 +190,10 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
scrapeStart := time.Now()
for namespace, mapping := range server.metricMap {
logger.Debug("Querying namespace", "namespace", namespace)
level.Debug(logger).Log("msg", "Querying namespace", "namespace", namespace)
if mapping.master && !server.master {
logger.Debug("Query skipped...")
level.Debug(logger).Log("msg", "Query skipped...")
continue
}
@ -201,7 +202,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) {
logger.Debug("Query skipped for this database version", "version", server.lastMapVersion.String(), "target_version", server.runonserver)
level.Debug(logger).Log("msg", "Query skipped for this database version", "version", server.lastMapVersion.String(), "target_version", server.runonserver)
continue
}
}
@ -232,12 +233,12 @@ func queryNamespaceMappings(ch chan<- prometheus.Metric, server *Server) map[str
// Serious error - a namespace disappeared
if err != nil {
namespaceErrors[namespace] = err
logger.Info("error finding namespace", "err", err)
level.Info(logger).Log("err", err)
}
// Non-serious errors - likely version or parsing problems.
if len(nonFatalErrors) > 0 {
for _, err := range nonFatalErrors {
logger.Info("error querying namespace", "err", err)
level.Info(logger).Log("err", err)
}
}

View File

@ -19,6 +19,7 @@ import (
"strconv"
"strings"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -31,7 +32,7 @@ var (
// Query the pg_settings view containing runtime variables
func querySettings(ch chan<- prometheus.Metric, server *Server) error {
logger.Debug("Querying pg_setting view", "server", server)
level.Debug(logger).Log("msg", "Querying pg_setting view", "server", server)
// pg_settings docs: https://www.postgresql.org/docs/current/static/view-pg-settings.html
//
@ -67,7 +68,7 @@ type pgSetting struct {
func (s *pgSetting) metric(labels prometheus.Labels) prometheus.Metric {
var (
err error
name = strings.ReplaceAll(strings.ReplaceAll(s.name, ".", "_"), "-", "_")
name = strings.Replace(s.name, ".", "_", -1)
unit = s.unit // nolint: ineffassign
shortDesc = fmt.Sprintf("Server Parameter: %s", s.name)
subsystem = "settings"
@ -128,10 +129,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", "1kB", "2kB", "4kB", "8kB", "16kB", "32kB", "64kB", "16MB", "32MB", "64MB":
case "B", "kB", "MB", "GB", "TB", "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
}
@ -157,10 +158,6 @@ 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":

View File

@ -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)
}
}

View File

@ -25,6 +25,7 @@ import (
"time"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -175,6 +176,15 @@ 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")},
@ -251,9 +261,6 @@ 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},
},
@ -286,7 +293,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.
logger.Debug("Column is being forced to discard due to version incompatibility", "column", columnName)
level.Debug(logger).Log("msg", "Column is being forced to discard due to version incompatibility", "column", columnName)
thisMap[columnName] = MetricMap{
discard: true,
conversion: func(_ interface{}) (float64, bool) {
@ -373,7 +380,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
case string:
durationString = t
default:
logger.Error("Duration conversion metric was not a string")
level.Error(logger).Log("msg", "Duration conversion metric was not a string")
return math.NaN(), false
}
@ -383,7 +390,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
d, err := time.ParseDuration(durationString)
if err != nil {
logger.Error("Failed converting result to metric", "column", columnName, "in", in, "err", err)
level.Error(logger).Log("msg", "Failed converting result to metric", "column", columnName, "in", in, "err", err)
return math.NaN(), false
}
return float64(d / time.Millisecond), true
@ -493,7 +500,7 @@ func parseConstLabels(s string) prometheus.Labels {
for _, p := range parts {
keyValue := strings.Split(strings.TrimSpace(p), "=")
if len(keyValue) != 2 {
logger.Error(`Wrong constant labels format, should be "key=value"`, "input", p)
level.Error(logger).Log(`Wrong constant labels format, should be "key=value"`, "input", p)
continue
}
key := strings.TrimSpace(keyValue[0])
@ -584,7 +591,7 @@ func newDesc(subsystem, name, help string, labels prometheus.Labels) *prometheus
}
func checkPostgresVersion(db *sql.DB, server string) (semver.Version, string, error) {
logger.Debug("Querying PostgreSQL version", "server", server)
level.Debug(logger).Log("msg", "Querying PostgreSQL version", "server", server)
versionRow := db.QueryRow("SELECT version();")
var versionString string
err := versionRow.Scan(&versionString)
@ -607,12 +614,12 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
}
if !e.disableDefaultMetrics && semanticVersion.LT(lowestSupportedVersion) {
logger.Warn("PostgreSQL version is lower than our lowest supported version", "server", server, "version", semanticVersion, "lowest_supported_version", lowestSupportedVersion)
level.Warn(logger).Log("msg", "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 {
logger.Info("Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
level.Info(logger).Log("msg", "Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
server.mappingMtx.Lock()
// Get Default Metrics only for master database
@ -633,13 +640,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 {
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
level.Error(logger).Log("msg", "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 {
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
level.Error(logger).Log("msg", "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
@ -681,7 +688,7 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
if err := e.scrapeDSN(ch, dsn); err != nil {
errorsCount++
logger.Error("error scraping dsn", "err", err, "dsn", loggableDSN(dsn))
level.Error(logger).Log("err", err)
if _, ok := err.(*ErrorConnectToServer); ok {
connectionErrorsCount++

View File

@ -15,16 +15,17 @@ 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 *slog.Logger, excludeDatabases []string) http.HandlerFunc {
func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
conf := c.GetConfig()
@ -37,7 +38,7 @@ func handleProbe(logger *slog.Logger, excludeDatabases []string) http.HandlerFun
var authModule config.AuthModule
authModuleName := params.Get("auth_module")
if authModuleName == "" {
logger.Info("no auth_module specified, using default")
level.Info(logger).Log("msg", "no auth_module specified, using default")
} else {
var ok bool
authModule, ok = conf.AuthModules[authModuleName]
@ -53,14 +54,14 @@ func handleProbe(logger *slog.Logger, excludeDatabases []string) http.HandlerFun
dsn, err := authModule.ConfigureTarget(target)
if err != nil {
logger.Error("failed to configure target", "err", err)
level.Error(logger).Log("msg", "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 := logger.With("target", target)
tl := log.With(logger, "target", target)
registry := prometheus.NewRegistry()
@ -84,7 +85,6 @@ func handleProbe(logger *slog.Logger, excludeDatabases []string) http.HandlerFun
// 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
}

View File

@ -18,6 +18,7 @@ import (
"fmt"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"gopkg.in/yaml.v2"
)
@ -45,14 +46,39 @@ 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 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
(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
FROM pg_stat_replication
`,
},
@ -60,8 +86,8 @@ var queryOverrides = map[string][]OverrideQuery{
semver.MustParseRange(">=9.2.0 <10.0.0"),
`
SELECT *,
(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
(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
FROM pg_stat_replication
`,
},
@ -69,7 +95,7 @@ var queryOverrides = map[string][]OverrideQuery{
semver.MustParseRange("<9.2.0"),
`
SELECT *,
(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 null else pg_current_xlog_location() end) AS pg_current_xlog_location
FROM pg_stat_replication
`,
},
@ -79,16 +105,14 @@ var queryOverrides = map[string][]OverrideQuery{
{
semver.MustParseRange(">=9.4.0 <10.0.0"),
`
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
SELECT slot_name, database, active, pg_xlog_location_diff(pg_current_xlog_location(), restart_lsn)
FROM pg_replication_slots
`,
},
{
semver.MustParseRange(">=10.0.0"),
`
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
SELECT slot_name, database, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
FROM pg_replication_slots
`,
},
@ -115,9 +139,6 @@ 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
@ -136,13 +157,9 @@ 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,backend_type,wait_event_type,wait_event) AS tmp2
FROM pg_stat_activity GROUP BY datname,state,usename,application_name) AS tmp2
ON tmp.state = tmp2.state AND pg_database.datname = tmp2.datname
`,
},
@ -178,7 +195,7 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][]
}
}
if !matched {
logger.Warn("No query matched override, disabling metric space", "name", name)
level.Warn(logger).Log("msg", "No query matched override, disabling metric space", "name", name)
resultMap[name] = ""
}
}
@ -199,7 +216,7 @@ func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[str
newQueryOverrides := make(map[string]string)
for metric, specs := range userQueries {
logger.Debug("New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
level.Debug(logger).Log("msg", "New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
newQueryOverrides[metric] = specs.Query
metricMap, ok := metricMaps[metric]
if !ok {
@ -251,9 +268,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
for k, v := range partialExporterMap {
_, found := server.metricMap[k]
if found {
logger.Debug("Overriding metric from user YAML file", "metric", k)
level.Debug(logger).Log("msg", "Overriding metric from user YAML file", "metric", k)
} else {
logger.Debug("Adding new metric from user YAML file", "metric", k)
level.Debug(logger).Log("msg", "Adding new metric from user YAML file", "metric", k)
}
server.metricMap[k] = v
}
@ -262,9 +279,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
for k, v := range newQueryOverrides {
_, found := server.queryOverrides[k]
if found {
logger.Debug("Overriding query override from user YAML file", "query_override", k)
level.Debug(logger).Log("msg", "Overriding query override from user YAML file", "query_override", k)
} else {
logger.Debug("Adding new query override from user YAML file", "query_override", k)
level.Debug(logger).Log("msg", "Adding new query override from user YAML file", "query_override", k)
}
server.queryOverrides[k] = v
}

View File

@ -20,6 +20,7 @@ import (
"time"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -70,7 +71,7 @@ func NewServer(dsn string, opts ...ServerOpt) (*Server, error) {
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
logger.Info("Established new database connection", "fingerprint", fingerprint)
level.Info(logger).Log("msg", "Established new database connection", "fingerprint", fingerprint)
s := &Server{
db: db,
@ -97,7 +98,7 @@ func (s *Server) Close() error {
func (s *Server) Ping() error {
if err := s.db.Ping(); err != nil {
if cerr := s.Close(); cerr != nil {
logger.Error("Error while closing non-pinging DB connection", "server", s, "err", cerr)
level.Error(logger).Log("msg", "Error while closing non-pinging DB connection", "server", s, "err", cerr)
}
return err
}
@ -183,7 +184,7 @@ func (s *Servers) Close() {
defer s.m.Unlock()
for _, server := range s.servers {
if err := server.Close(); err != nil {
logger.Error("Failed to close connection", "server", server, "err", err)
level.Error(logger).Log("msg", "Failed to close connection", "server", server, "err", err)
}
}
}

View File

@ -21,6 +21,7 @@ import (
"strings"
"time"
"github.com/go-kit/log/level"
"github.com/lib/pq"
)
@ -81,14 +82,14 @@ func dbToFloat64(t interface{}) (float64, bool) {
strV := string(v)
result, err := strconv.ParseFloat(strV, 64)
if err != nil {
logger.Info("Could not parse []byte", "err", err)
level.Info(logger).Log("msg", "Could not parse []byte", "err", err)
return math.NaN(), false
}
return result, true
case string:
result, err := strconv.ParseFloat(v, 64)
if err != nil {
logger.Info("Could not parse string", "err", err)
level.Info(logger).Log("msg", "Could not parse string", "err", err)
return math.NaN(), false
}
return result, true
@ -121,14 +122,14 @@ func dbToUint64(t interface{}) (uint64, bool) {
strV := string(v)
result, err := strconv.ParseUint(strV, 10, 64)
if err != nil {
logger.Info("Could not parse []byte", "err", err)
level.Info(logger).Log("msg", "Could not parse []byte", "err", err)
return 0, false
}
return result, true
case string:
result, err := strconv.ParseUint(v, 10, 64)
if err != nil {
logger.Info("Could not parse string", "err", err)
level.Info(logger).Log("msg", "Could not parse string", "err", err)
return 0, false
}
return result, true
@ -159,7 +160,7 @@ func dbToString(t interface{}) (string, bool) {
// Try and convert to string
return string(v), true
case string:
return strings.ToValidUTF8(v, "<22>"), true
return v, true
case bool:
if v {
return "true", true

View File

@ -17,11 +17,12 @@ 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"
)
@ -61,7 +62,7 @@ type Collector interface {
}
type collectorConfig struct {
logger *slog.Logger
logger log.Logger
excludeDatabases []string
}
@ -88,7 +89,7 @@ func registerCollector(name string, isDefaultEnabled bool, createFunc func(colle
// PostgresCollector implements the prometheus.Collector interface.
type PostgresCollector struct {
Collectors map[string]Collector
logger *slog.Logger
logger log.Logger
instance *instance
}
@ -96,7 +97,7 @@ type PostgresCollector struct {
type Option func(*PostgresCollector) error
// NewPostgresCollector creates a new PostgresCollector.
func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
func NewPostgresCollector(logger log.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
p := &PostgresCollector{
logger: logger,
}
@ -130,7 +131,7 @@ func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn st
collectors[key] = collector
} else {
collector, err := factories[key](collectorConfig{
logger: logger.With("collector", key),
logger: log.With(logger, "collector", key),
excludeDatabases: excludeDatabases,
})
if err != nil {
@ -165,30 +166,18 @@ 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, inst, ch, p.logger)
execute(ctx, name, c, p.instance, 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 *slog.Logger) {
func execute(ctx context.Context, name string, c Collector, instance *instance, ch chan<- prometheus.Metric, logger log.Logger) {
begin := time.Now()
err := c.Update(ctx, instance, ch)
duration := time.Since(begin)
@ -196,13 +185,13 @@ func execute(ctx context.Context, name string, c Collector, instance *instance,
if err != nil {
if IsNoDataError(err) {
logger.Debug("collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
level.Debug(logger).Log("msg", "collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
} else {
logger.Error("collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
level.Error(logger).Log("msg", "collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
}
success = 0
} else {
logger.Debug("collector succeeded", "name", name, "duration_seconds", duration.Seconds())
level.Debug(logger).Log("msg", "collector succeeded", "name", name, "duration_seconds", duration.Seconds())
success = 1
}
ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name)

View File

@ -48,15 +48,9 @@ func readMetric(m prometheus.Metric) MetricResult {
func sanitizeQuery(q string) string {
q = strings.Join(strings.Fields(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, "^", "\\^")
q = strings.ReplaceAll(q, "$", "\\$")
q = strings.Replace(q, "(", "\\(", -1)
q = strings.Replace(q, ")", "\\)", -1)
q = strings.Replace(q, "*", "\\*", -1)
q = strings.Replace(q, "$", "\\$", -1)
return q
}

View File

@ -22,50 +22,29 @@ import (
)
type instance struct {
dsn string
db *sql.DB
version semver.Version
}
func newInstance(dsn string) (*instance, error) {
i := &instance{
dsn: dsn,
}
// "Create" a database handle to verify the DSN provided is valid.
// Open is not guaranteed to create a connection.
i := &instance{}
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(i.db)
version, err := queryVersion(db)
if err != nil {
return fmt.Errorf("error querying postgresql version: %w", err)
} else {
i.version = version
db.Close()
return nil, err
}
return nil
i.version = version
return i, nil
}
func (i *instance) getDB() *sql.DB {

View File

@ -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 *slog.Logger
log log.Logger
excludedDatabases []string
}
@ -53,21 +53,12 @@ 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, pg_database.datconnlimit FROM pg_database;"
pgDatabaseQuery = "SELECT pg_database.datname FROM pg_database;"
pgDatabaseSizeQuery = "SELECT pg_database_size($1)"
)
// Update implements Collector and exposes database size and connection limits.
// Update implements Collector and exposes database size.
// 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
@ -90,32 +81,21 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
for rows.Next() {
var datname sql.NullString
var connLimit sql.NullInt64
if err := rows.Scan(&datname, &connLimit); err != nil {
if err := rows.Scan(&datname); 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, database) {
if sliceContains(c.excludedDatabases, datname.String) {
continue
}
databases = append(databases, database)
connLimitMetric := 0.0
if connLimit.Valid {
connLimitMetric = float64(connLimit.Int64)
}
ch <- prometheus.MustNewConstMetric(
pgDatabaseConnectionLimitsDesc,
prometheus.GaugeValue, connLimitMetric, database,
)
databases = append(databases, datname.String)
}
// Query the size of the databases
@ -134,9 +114,11 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
pgDatabaseSizeDesc,
prometheus.GaugeValue, sizeMetric, datname,
)
}
return rows.Err()
if err := rows.Err(); err != nil {
return err
}
return nil
}
func sliceContains(slice []string, s string) bool {

View File

@ -31,8 +31,8 @@ func TestPGDatabaseCollector(t *testing.T) {
inst := &instance{db: db}
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
AddRow("postgres", 15))
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
AddRow("postgres"))
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
AddRow(1024))
@ -47,7 +47,6 @@ 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() {
@ -72,8 +71,8 @@ func TestPGDatabaseCollectorNullMetric(t *testing.T) {
inst := &instance{db: db}
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
AddRow("postgres", nil))
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
AddRow("postgres"))
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
AddRow(nil))
@ -89,7 +88,6 @@ 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 {

View File

@ -1,114 +0,0 @@
// 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
}

View File

@ -1,64 +0,0 @@
// 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)
}
}

View File

@ -1,129 +0,0 @@
// 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
}

View File

@ -1,60 +0,0 @@
// 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)
}
}

View File

@ -1,95 +0,0 @@
// 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
}

View File

@ -1,63 +0,0 @@
// 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)
}
}

View File

@ -16,8 +16,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
)
@ -28,7 +28,7 @@ func init() {
}
type PGProcessIdleCollector struct {
log *slog.Logger
log log.Logger
}
const processIdleSubsystem = "process_idle"
@ -40,7 +40,7 @@ func NewPGProcessIdleCollector(config collectorConfig) (Collector, error) {
var pgProcessIdleSeconds = prometheus.NewDesc(
prometheus.BuildFQName(namespace, processIdleSubsystem, "seconds"),
"Idle time of server processes",
[]string{"state", "application_name"},
[]string{"application_name"},
prometheus.Labels{},
)
@ -50,17 +50,15 @@ func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch
`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 state, application_name
WHERE state = 'idle'
GROUP BY application_name
),
buckets AS (
SELECT
state,
application_name,
le,
SUM(
@ -72,27 +70,25 @@ func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch
FROM
pg_stat_activity,
UNNEST(ARRAY[1, 2, 5, 15, 30, 60, 90, 120, 300]) AS le
GROUP BY state, application_name, le
ORDER BY state, application_name, le
GROUP BY application_name, le
ORDER BY 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 (state, application_name)
GROUP BY 1, 2, 3, 4;`)
FROM metrics JOIN buckets USING (application_name)
GROUP BY 1, 2, 3;`)
var state sql.NullString
var applicationName sql.NullString
var secondsSum sql.NullFloat64
var secondsCount sql.NullInt64
var seconds []float64
var secondsBucket []int64
err := row.Scan(&state, &applicationName, &secondsSum, &secondsCount, pq.Array(&seconds), pq.Array(&secondsBucket))
err := row.Scan(&applicationName, &secondsSum, &secondsCount, pq.Array(&seconds), pq.Array(&secondsBucket))
if err != nil {
return err
}
@ -105,11 +101,6 @@ func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch
buckets[second] = uint64(secondsBucket[i])
}
stateLabel := "unknown"
if state.Valid {
stateLabel = state.String
}
applicationNameLabel := "unknown"
if applicationName.Valid {
applicationNameLabel = applicationName.String
@ -126,7 +117,7 @@ func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch
ch <- prometheus.MustNewConstHistogram(
pgProcessIdleSeconds,
secondsCountMetric, secondsSumMetric, buckets,
stateLabel, applicationNameLabel,
applicationNameLabel,
)
return nil
}

View File

@ -51,27 +51,16 @@ 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,
GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) as last_replay`
END as is_replica`
)
func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
@ -82,8 +71,7 @@ func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance,
var lag float64
var isReplica int64
var replayAge float64
err := row.Scan(&lag, &isReplica, &replayAge)
err := row.Scan(&lag, &isReplica)
if err != nil {
return err
}
@ -95,9 +83,5 @@ func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance,
pgReplicationIsReplica,
prometheus.GaugeValue, float64(isReplica),
)
ch <- prometheus.MustNewConstMetric(
pgReplicationLastReplay,
prometheus.GaugeValue, replayAge,
)
return nil
}

View File

@ -16,9 +16,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/blang/semver/v4"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -29,7 +28,7 @@ func init() {
}
type PGReplicationSlotCollector struct {
log *slog.Logger
log log.Logger
}
func NewPGReplicationSlotCollector(config collectorConfig) (Collector, error) {
@ -44,7 +43,7 @@ var (
"slot_current_wal_lsn",
),
"current wal lsn value",
[]string{"slot_name", "slot_type"}, nil,
[]string{"slot_name"}, nil,
)
pgReplicationSlotCurrentFlushDesc = prometheus.NewDesc(
prometheus.BuildFQName(
@ -53,7 +52,7 @@ var (
"slot_confirmed_flush_lsn",
),
"last lsn confirmed flushed to the replication slot",
[]string{"slot_name", "slot_type"}, nil,
[]string{"slot_name"}, nil,
)
pgReplicationSlotIsActiveDesc = prometheus.NewDesc(
prometheus.BuildFQName(
@ -62,62 +61,22 @@ var (
"slot_is_active",
),
"whether the replication slot is active or not",
[]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,
[]string{"slot_name"}, nil,
)
pgReplicationSlotQuery = `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,
pg_current_wal_lsn() - '0/0' AS current_wal_lsn,
coalesce(confirmed_flush_lsn, '0/0') - '0/0',
active
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;`
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,
query)
pgReplicationSlotQuery)
if err != nil {
return err
}
@ -125,28 +84,10 @@ 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
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 {
if err := rows.Scan(&slotName, &walLSN, &flushLSN, &isActive); err != nil {
return err
}
@ -158,10 +99,6 @@ 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 {
@ -169,7 +106,7 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotCurrentWalDesc,
prometheus.GaugeValue, walLSNMetric, slotNameLabel, slotTypeLabel,
prometheus.GaugeValue, walLSNMetric, slotNameLabel,
)
if isActive.Valid && isActive.Bool {
var flushLSNMetric float64
@ -178,27 +115,16 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotCurrentFlushDesc,
prometheus.GaugeValue, flushLSNMetric, slotNameLabel, slotTypeLabel,
prometheus.GaugeValue, flushLSNMetric, slotNameLabel,
)
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotIsActiveDesc,
prometheus.GaugeValue, isActiveValue, slotNameLabel, slotTypeLabel,
prometheus.GaugeValue, isActiveValue, slotNameLabel,
)
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,
)
}
}
return rows.Err()
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -17,7 +17,6 @@ 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"
@ -30,12 +29,12 @@ func TestPgReplicationSlotCollectorActive(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
inst := &instance{db: db}
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
rows := sqlmock.NewRows(columns).
AddRow("test_slot", "physical", 5, 3, true, 323906992, "reserved")
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
AddRow("test_slot", 5, 3, true)
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -48,11 +47,9 @@ func TestPgReplicationSlotCollectorActive(t *testing.T) {
}()
expected := []MetricResult{
{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},
{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},
}
convey.Convey("Metrics comparison", t, func() {
@ -73,12 +70,12 @@ func TestPgReplicationSlotCollectorInActive(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
inst := &instance{db: db}
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
rows := sqlmock.NewRows(columns).
AddRow("test_slot", "physical", 6, 12, false, -4000, "extended")
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
AddRow("test_slot", 6, 12, false)
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -91,10 +88,8 @@ func TestPgReplicationSlotCollectorInActive(t *testing.T) {
}()
expected := []MetricResult{
{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},
{labels: labelMap{"slot_name": "test_slot"}, value: 6, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"slot_name": "test_slot"}, value: 0, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
@ -116,12 +111,12 @@ func TestPgReplicationSlotCollectorActiveNil(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
inst := &instance{db: db}
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
rows := sqlmock.NewRows(columns).
AddRow("test_slot", "physical", 6, 12, nil, nil, "lost")
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
AddRow("test_slot", 6, 12, nil)
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -134,9 +129,8 @@ func TestPgReplicationSlotCollectorActiveNil(t *testing.T) {
}()
expected := []MetricResult{
{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},
{labels: labelMap{"slot_name": "test_slot"}, value: 6, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"slot_name": "test_slot"}, value: 0, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
@ -157,12 +151,12 @@ func TestPgReplicationSlotCollectorTestNilValues(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
inst := &instance{db: db}
columns := []string{"slot_name", "slot_type", "current_wal_lsn", "confirmed_flush_lsn", "active", "safe_wal_size", "wal_status"}
columns := []string{"slot_name", "current_wal_lsn", "confirmed_flush_lsn", "active"}
rows := sqlmock.NewRows(columns).
AddRow(nil, nil, nil, nil, true, nil, nil)
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotNewQuery)).WillReturnRows(rows)
AddRow(nil, nil, nil, true)
mock.ExpectQuery(sanitizeQuery(pgReplicationSlotQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -175,9 +169,9 @@ func TestPgReplicationSlotCollectorTestNilValues(t *testing.T) {
}()
expected := []MetricResult{
{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},
{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},
}
convey.Convey("Metrics comparison", t, func() {

View File

@ -31,9 +31,9 @@ func TestPgReplicationCollector(t *testing.T) {
inst := &instance{db: db}
columns := []string{"lag", "is_replica", "last_replay"}
columns := []string{"lag", "is_replica"}
rows := sqlmock.NewRows(columns).
AddRow(1000, 1, 3)
AddRow(1000, 1)
mock.ExpectQuery(sanitizeQuery(pgReplicationQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
@ -49,7 +49,6 @@ 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() {

View File

@ -1,91 +0,0 @@
// 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()
}

View File

@ -1,58 +0,0 @@
// 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)
}
}

View File

@ -1,84 +0,0 @@
// 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
}

View File

@ -1,62 +0,0 @@
// 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)
}
}

View File

@ -17,7 +17,6 @@ import (
"context"
"database/sql"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -102,7 +101,7 @@ var (
prometheus.Labels{},
)
statBGWriterQueryBefore17 = `SELECT
statBGWriterQuery = `SELECT
checkpoints_timed
,checkpoints_req
,checkpoint_write_time
@ -115,177 +114,121 @@ 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 {
if instance.version.GE(semver.MustParse("17.0.0")) {
db := instance.getDB()
row := db.QueryRowContext(ctx, statBGWriterQueryAfter17)
db := instance.getDB()
row := db.QueryRowContext(ctx,
statBGWriterQuery)
var bc, mwc, ba sql.NullInt64
var sr sql.NullTime
var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64
var cpwt, cpst sql.NullFloat64
var sr sql.NullTime
err := row.Scan(&bc, &mwc, &ba, &sr)
if err != nil {
return err
}
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,
)
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,
)
return nil
}

View File

@ -51,8 +51,8 @@ func TestPGStatBGWriterCollector(t *testing.T) {
}
rows := sqlmock.NewRows(columns).
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, 0, int64(2725688749), srT)
mock.ExpectQuery(sanitizeQuery(statBGWriterQueryBefore17)).WillReturnRows(rows)
AddRow(354, 4945, 289097744, 1242257, 3275602074, 89320867, 450139, 2034563757, 0, 2725688749, srT)
mock.ExpectQuery(sanitizeQuery(statBGWriterQuery)).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(statBGWriterQueryBefore17)).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(statBGWriterQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {

View File

@ -1,231 +0,0 @@
// 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
}

View File

@ -1,144 +0,0 @@
// 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)
}
}

View File

@ -16,11 +16,7 @@ package collector
import (
"context"
"database/sql"
"fmt"
"log/slog"
"strings"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -30,12 +26,10 @@ func init() {
registerCollector(statDatabaseSubsystem, defaultEnabled, NewPGStatDatabaseCollector)
}
type PGStatDatabaseCollector struct {
log *slog.Logger
}
type PGStatDatabaseCollector struct{}
func NewPGStatDatabaseCollector(config collectorConfig) (Collector, error) {
return &PGStatDatabaseCollector{log: config.logger}, nil
return &PGStatDatabaseCollector{}, nil
}
var (
@ -208,53 +202,36 @@ var (
[]string{"datid", "datname"},
prometheus.Labels{},
)
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{},
)
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;
`
)
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 {
func (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(columns),
statDatabaseQuery,
)
if err != nil {
return err
@ -263,10 +240,10 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
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, activeTime sql.NullFloat64
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime sql.NullFloat64
var statsReset sql.NullTime
r := []any{
err := rows.Scan(
&datid,
&datname,
&numBackends,
@ -286,231 +263,222 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
&blkReadTime,
&blkWriteTime,
&statsReset,
}
if activeTimeAvail {
r = append(r, &activeTime)
}
err := rows.Scan(r...)
)
if err != nil {
return err
}
if !datid.Valid {
c.log.Debug("Skipping collecting metric because it has no datid")
continue
datidLabel := "unknown"
if datid.Valid {
datidLabel = datid.String
}
if !datname.Valid {
c.log.Debug("Skipping collecting metric because it has no datname")
continue
}
if !numBackends.Valid {
c.log.Debug("Skipping collecting metric because it has no numbackends")
continue
}
if !xactCommit.Valid {
c.log.Debug("Skipping collecting metric because it has no xact_commit")
continue
}
if !xactRollback.Valid {
c.log.Debug("Skipping collecting metric because it has no xact_rollback")
continue
}
if !blksRead.Valid {
c.log.Debug("Skipping collecting metric because it has no blks_read")
continue
}
if !blksHit.Valid {
c.log.Debug("Skipping collecting metric because it has no blks_hit")
continue
}
if !tupReturned.Valid {
c.log.Debug("Skipping collecting metric because it has no tup_returned")
continue
}
if !tupFetched.Valid {
c.log.Debug("Skipping collecting metric because it has no tup_fetched")
continue
}
if !tupInserted.Valid {
c.log.Debug("Skipping collecting metric because it has no tup_inserted")
continue
}
if !tupUpdated.Valid {
c.log.Debug("Skipping collecting metric because it has no tup_updated")
continue
}
if !tupDeleted.Valid {
c.log.Debug("Skipping collecting metric because it has no tup_deleted")
continue
}
if !conflicts.Valid {
c.log.Debug("Skipping collecting metric because it has no conflicts")
continue
}
if !tempFiles.Valid {
c.log.Debug("Skipping collecting metric because it has no temp_files")
continue
}
if !tempBytes.Valid {
c.log.Debug("Skipping collecting metric because it has no temp_bytes")
continue
}
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
datnameLabel := "unknown"
if datname.Valid {
datnameLabel = datname.String
}
statsResetMetric := 0.0
if !statsReset.Valid {
c.log.Debug("No metric for stats_reset, will collect 0 instead")
numBackendsMetric := 0.0
if numBackends.Valid {
numBackendsMetric = numBackends.Float64
}
if statsReset.Valid {
statsResetMetric = float64(statsReset.Time.Unix())
}
labels := []string{datid.String, datname.String}
ch <- prometheus.MustNewConstMetric(
statDatabaseNumbackends,
prometheus.GaugeValue,
numBackends.Float64,
labels...,
numBackendsMetric,
datidLabel,
datnameLabel,
)
xactCommitMetric := 0.0
if xactCommit.Valid {
xactCommitMetric = xactCommit.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseXactCommit,
prometheus.CounterValue,
xactCommit.Float64,
labels...,
xactCommitMetric,
datidLabel,
datnameLabel,
)
xactRollbackMetric := 0.0
if xactRollback.Valid {
xactRollbackMetric = xactRollback.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseXactRollback,
prometheus.CounterValue,
xactRollback.Float64,
labels...,
xactRollbackMetric,
datidLabel,
datnameLabel,
)
blksReadMetric := 0.0
if blksRead.Valid {
blksReadMetric = blksRead.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseBlksRead,
prometheus.CounterValue,
blksRead.Float64,
labels...,
blksReadMetric,
datidLabel,
datnameLabel,
)
blksHitMetric := 0.0
if blksHit.Valid {
blksHitMetric = blksHit.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseBlksHit,
prometheus.CounterValue,
blksHit.Float64,
labels...,
blksHitMetric,
datidLabel,
datnameLabel,
)
tupReturnedMetric := 0.0
if tupReturned.Valid {
tupReturnedMetric = tupReturned.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTupReturned,
prometheus.CounterValue,
tupReturned.Float64,
labels...,
tupReturnedMetric,
datidLabel,
datnameLabel,
)
tupFetchedMetric := 0.0
if tupFetched.Valid {
tupFetchedMetric = tupFetched.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTupFetched,
prometheus.CounterValue,
tupFetched.Float64,
labels...,
tupFetchedMetric,
datidLabel,
datnameLabel,
)
tupInsertedMetric := 0.0
if tupInserted.Valid {
tupInsertedMetric = tupInserted.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTupInserted,
prometheus.CounterValue,
tupInserted.Float64,
labels...,
tupInsertedMetric,
datidLabel,
datnameLabel,
)
tupUpdatedMetric := 0.0
if tupUpdated.Valid {
tupUpdatedMetric = tupUpdated.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTupUpdated,
prometheus.CounterValue,
tupUpdated.Float64,
labels...,
tupUpdatedMetric,
datidLabel,
datnameLabel,
)
tupDeletedMetric := 0.0
if tupDeleted.Valid {
tupDeletedMetric = tupDeleted.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTupDeleted,
prometheus.CounterValue,
tupDeleted.Float64,
labels...,
tupDeletedMetric,
datidLabel,
datnameLabel,
)
conflictsMetric := 0.0
if conflicts.Valid {
conflictsMetric = conflicts.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseConflicts,
prometheus.CounterValue,
conflicts.Float64,
labels...,
conflictsMetric,
datidLabel,
datnameLabel,
)
tempFilesMetric := 0.0
if tempFiles.Valid {
tempFilesMetric = tempFiles.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTempFiles,
prometheus.CounterValue,
tempFiles.Float64,
labels...,
tempFilesMetric,
datidLabel,
datnameLabel,
)
tempBytesMetric := 0.0
if tempBytes.Valid {
tempBytesMetric = tempBytes.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseTempBytes,
prometheus.CounterValue,
tempBytes.Float64,
labels...,
tempBytesMetric,
datidLabel,
datnameLabel,
)
deadlocksMetric := 0.0
if deadlocks.Valid {
deadlocksMetric = deadlocks.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseDeadlocks,
prometheus.CounterValue,
deadlocks.Float64,
labels...,
deadlocksMetric,
datidLabel,
datnameLabel,
)
blkReadTimeMetric := 0.0
if blkReadTime.Valid {
blkReadTimeMetric = blkReadTime.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseBlkReadTime,
prometheus.CounterValue,
blkReadTime.Float64,
labels...,
blkReadTimeMetric,
datidLabel,
datnameLabel,
)
blkWriteTimeMetric := 0.0
if blkWriteTime.Valid {
blkWriteTimeMetric = blkWriteTime.Float64
}
ch <- prometheus.MustNewConstMetric(
statDatabaseBlkWriteTime,
prometheus.CounterValue,
blkWriteTime.Float64,
labels...,
blkWriteTimeMetric,
datidLabel,
datnameLabel,
)
statsResetMetric := 0.0
if statsReset.Valid {
statsResetMetric = float64(statsReset.Time.Unix())
}
ch <- prometheus.MustNewConstMetric(
statDatabaseStatsReset,
prometheus.CounterValue,
statsResetMetric,
labels...,
datidLabel,
datnameLabel,
)
if activeTimeAvail {
ch <- prometheus.MustNewConstMetric(
statDatabaseActiveTime,
prometheus.CounterValue,
activeTime.Float64/1000.0,
labels...,
)
}
}
return nil
}

View File

@ -18,10 +18,8 @@ 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"
)
@ -32,7 +30,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
inst := &instance{db: db}
columns := []string{
"datid",
@ -54,7 +52,6 @@ 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")
@ -70,30 +67,26 @@ func TestPGStatDatabaseCollector(t *testing.T) {
4945,
289097744,
1242257,
int64(3275602074),
3275602074,
89320867,
450139,
2034563757,
0,
int64(2725688749),
2725688749,
23,
52,
74,
925,
16,
823,
srT,
33,
)
srT)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
c := PGStatDatabaseCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
@ -118,7 +111,6 @@ 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() {
@ -139,11 +131,7 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
}
defer db.Close()
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")}
inst := &instance{db: db}
columns := []string{
"datid",
@ -165,62 +153,36 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
rows := sqlmock.NewRows(columns).
AddRow(
nil,
"postgres",
354,
4945,
289097744,
1242257,
int64(3275602074),
89320867,
450139,
2034563757,
0,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT,
32,
).
AddRow(
"pid",
"postgres",
354,
4945,
289097744,
1242257,
int64(3275602074),
89320867,
450139,
2034563757,
0,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT,
32,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
c := PGStatDatabaseCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
@ -228,24 +190,23 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
}()
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.032},
{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() {
@ -265,7 +226,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
inst := &instance{db: db}
columns := []string{
"datid",
@ -287,7 +248,6 @@ func TestPGStatDatabaseCollectorRowLeakTest(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")
@ -303,21 +263,19 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
4945,
289097744,
1242257,
int64(3275602074),
3275602074,
89320867,
450139,
2034563757,
0,
int64(2725688749),
2725688749,
23,
52,
74,
925,
16,
823,
srT,
14,
).
srT).
AddRow(
nil,
nil,
@ -338,38 +296,14 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
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)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
c := PGStatDatabaseCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
@ -394,128 +328,23 @@ 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": "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},
{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() {

View File

@ -1,222 +0,0 @@
// 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
}

View File

@ -1,135 +0,0 @@
// 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)
}
}

View File

@ -16,9 +16,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/blang/semver/v4"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -32,7 +31,7 @@ func init() {
}
type PGStatStatementsCollector struct {
log *slog.Logger
log log.Logger
}
func NewPGStatStatementsCollector(config collectorConfig) (Collector, error) {
@ -91,63 +90,12 @@ 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, query)
rows, err := db.QueryContext(ctx,
pgStatStatementsQuery)
if err != nil {
return err

View File

@ -17,7 +17,6 @@ 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"
@ -30,7 +29,7 @@ func TestPGStateStatementsCollector(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("12.0.0")}
inst := &instance{db: db}
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
rows := sqlmock.NewRows(columns).
@ -73,12 +72,12 @@ func TestPGStateStatementsCollectorNull(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
inst := &instance{db: db}
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(pgStatStatementsNewQuery)).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(pgStatStatementsQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -108,89 +107,3 @@ 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)
}
}

View File

@ -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 *slog.Logger
log log.Logger
}
func NewPGStatUserTablesCollector(config collectorConfig) (Collector, error) {
@ -150,18 +150,6 @@ var (
[]string{"datname", "schemaname", "relname"},
prometheus.Labels{},
)
statUserIndexSize = prometheus.NewDesc(
prometheus.BuildFQName(namespace, userTableSubsystem, "index_size_bytes"),
"Total disk space used by this index, in bytes",
[]string{"datname", "schemaname", "relname"},
prometheus.Labels{},
)
statUserTableSize = prometheus.NewDesc(
prometheus.BuildFQName(namespace, userTableSubsystem, "table_size_bytes"),
"Total disk space used by this table, in bytes",
[]string{"datname", "schemaname", "relname"},
prometheus.Labels{},
)
statUserTablesQuery = `SELECT
current_database() datname,
@ -185,9 +173,7 @@ var (
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count,
pg_indexes_size(relid) as indexes_size,
pg_table_size(relid) as table_size
autoanalyze_count
FROM
pg_stat_user_tables`
)
@ -205,10 +191,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, indexSize, tableSize sql.NullInt64
nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount 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, &indexSize, &tableSize); 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); err != nil {
return err
}
@ -433,28 +419,6 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
autoanalyzeCountMetric,
datnameLabel, schemanameLabel, relnameLabel,
)
indexSizeMetric := 0.0
if indexSize.Valid {
indexSizeMetric = float64(indexSize.Int64)
}
ch <- prometheus.MustNewConstMetric(
statUserIndexSize,
prometheus.GaugeValue,
indexSizeMetric,
datnameLabel, schemanameLabel, relnameLabel,
)
tableSizeMetric := 0.0
if tableSize.Valid {
tableSizeMetric = float64(tableSize.Int64)
}
ch <- prometheus.MustNewConstMetric(
statUserTableSize,
prometheus.GaugeValue,
tableSizeMetric,
datnameLabel, schemanameLabel, relnameLabel,
)
}
if err := rows.Err(); err != nil {

View File

@ -71,9 +71,7 @@ func TestPGStatUserTablesCollector(t *testing.T) {
"vacuum_count",
"autovacuum_count",
"analyze_count",
"autoanalyze_count",
"index_size",
"table_size"}
"autoanalyze_count"}
rows := sqlmock.NewRows(columns).
AddRow("postgres",
"public",
@ -96,9 +94,7 @@ func TestPGStatUserTablesCollector(t *testing.T) {
11,
12,
13,
14,
15,
16)
14)
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -130,8 +126,6 @@ func TestPGStatUserTablesCollector(t *testing.T) {
{labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 12},
{labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 13},
{labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_COUNTER, value: 14},
{labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 15},
{labels: labelMap{"datname": "postgres", "schemaname": "public", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 16},
}
convey.Convey("Metrics comparison", t, func() {
@ -176,9 +170,7 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
"vacuum_count",
"autovacuum_count",
"analyze_count",
"autoanalyze_count",
"index_size",
"table_size"}
"autoanalyze_count"}
rows := sqlmock.NewRows(columns).
AddRow("postgres",
nil,
@ -201,8 +193,6 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
nil,
nil,
nil,
nil,
nil,
nil)
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
@ -235,8 +225,6 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
{labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "schemaname": "unknown", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
}
convey.Convey("Metrics comparison", t, func() {

View File

@ -1,270 +0,0 @@
// 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
}

View File

@ -1,186 +0,0 @@
// 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)
}
}

View File

@ -1,118 +0,0 @@
// 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
}

View File

@ -1,109 +0,0 @@
// 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)
}
}

View File

@ -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 *slog.Logger
log log.Logger
}
func NewPGStatIOUserTablesCollector(config collectorConfig) (Collector, error) {
@ -218,5 +218,8 @@ func (PGStatIOUserTablesCollector) Update(ctx context.Context, instance *instanc
datnameLabel, schemanameLabel, relnameLabel,
)
}
return rows.Err()
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -1,84 +0,0 @@
// 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
}

View File

@ -1,63 +0,0 @@
// 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)
}
}

View File

@ -1,90 +0,0 @@
// 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
}

View File

@ -1,61 +0,0 @@
// 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)
}
}

View File

@ -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 *slog.Logger
logger log.Logger
instance *instance
}
func NewProbeCollector(logger *slog.Logger, excludeDatabases []string, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) {
func NewProbeCollector(logger log.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 *slog.Logger, excludeDatabases []string, registry
} else {
collector, err := factories[key](
collectorConfig{
logger: logger.With("collector", key),
logger: log.With(logger, "collector", key),
excludeDatabases: excludeDatabases,
})
if err != nil {
@ -74,14 +74,6 @@ 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 {

View File

@ -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 *slog.Logger) error {
func (ch *Handler) ReloadConfig(f string, logger log.Logger) error {
config := &Config{}
var err error
defer func() {
@ -79,14 +79,14 @@ func (ch *Handler) ReloadConfig(f string, logger *slog.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()

View File

@ -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
View File

@ -1,19 +1,18 @@
module github.com/prometheus-community/postgres_exporter
go 1.23.0
toolchain go1.24.1
go 1.19
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/alecthomas/kingpin/v2 v2.3.2
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.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
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
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@ -22,27 +21,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.3.0 // indirect
github.com/cespare/xxhash/v2 v2.2.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/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/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/smartystreets/assertions v1.13.1 // indirect
github.com/xhit/go-str2duration/v2 v2.1.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
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
)

110
go.sum
View File

@ -1,33 +1,38 @@
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/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/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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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/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/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=
@ -35,58 +40,63 @@ 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/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/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/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.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/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/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/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/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/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
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.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=
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=
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=