Compare commits

...

111 Commits

Author SHA1 Message Date
dependabot[bot]
f8b7139174
Bump github.com/prometheus/common from 0.62.0 to 0.63.0 ()
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.62.0 to 0.63.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md)
- [Commits](https://github.com/prometheus/common/compare/v0.62.0...v0.63.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-version: 0.63.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 18:18:17 +02:00
dependabot[bot]
43576acc76
Bump github.com/prometheus/client_golang from 1.21.0 to 1.21.1 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.21.0 to 1.21.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 18:17:55 +02:00
Ben Kochie
8d5ec4b3ea
Update Go ()
* Update Go to 1.24.
* Update golangci-lint to v2.
* Fixup linting issues.

Signed-off-by: SuperQ <superq@gmail.com>
2025-04-03 17:23:40 +02:00
Ian Bibby
9e86f1ee38
Adds pg_stat_progress_vacuum collector ()
Signed-off-by: Ian Bibby <ian.bibby@reddit.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
2025-04-03 16:45:29 +02:00
PrometheusBot
fca2ad84cd
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-03-30 14:49:06 -04:00
PrometheusBot
2ce65c324c
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-03-23 16:54:21 +01:00
dependabot[bot]
b0e61bf263
Bump golang.org/x/net from 0.33.0 to 0.36.0 ()
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.33.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-16 22:34:30 +01:00
PrometheusBot
602302ffe2
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-03-08 15:00:54 +01:00
dependabot[bot]
457b6fa8cd
Bump github.com/prometheus/client_golang from 1.20.5 to 1.21.0 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.5 to 1.21.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.5...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 18:26:29 +01:00
Ben Kochie
1e574cf4fd
Release v0.17.1 ()
* [BUGFIX] Fix: Handle incoming labels with invalid UTF-8 

Signed-off-by: SuperQ <superq@gmail.com>
2025-02-26 08:41:27 -05:00
vancwo
2869087f3c
Fix: Handle incoming labels with invalid UTF-8 ()
It's possible that incoming labels will contain invalid UTF-8 characters. This results in a panic. This fix sanitizes the label's string to ensure only valid UTF-8 characters are included, by replacing invalid characters with � (REPLACEMENT CHARACTER)

Signed-off-by: Cooper Worobetz <cooper@worobetz.ca>
2025-02-26 14:21:39 +01:00
Joe Adams
51006aba2f
Prep for v0.17 ()
Signed-off-by: Joe Adams <github@joeadams.io>
2025-02-21 17:38:46 -05:00
Nicolas Rodriguez
8bb1a41abf
Skip pg_stat_checkpointer collector if pg<17 ()
* fix: skip collector if pg<17

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>

* fix: better condition

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>

* fix: fix PGStatCheckpointerCollector tests

Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>

---------

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>
Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>
Co-authored-by: Michael Todorovic <michael.todorovic@outlook.com>
2025-02-19 21:49:11 -05:00
Joe Adams
4c170ed564
Fix missing dsn sanitization for logging ()
This log line was not sanitized previously which could result in logging sensitive information. I have scanned the rest of the files and I don't see anywhere else that DSN is used in a log line without this filter.

Resolves 

Signed-off-by: Joe Adams <github@joeadams.io>
2025-02-15 16:35:04 +01:00
dependabot[bot]
99e1b5118c
Bump github.com/prometheus/exporter-toolkit from 0.13.2 to 0.14.0 ()
Bumps [github.com/prometheus/exporter-toolkit](https://github.com/prometheus/exporter-toolkit) from 0.13.2 to 0.14.0.
- [Release notes](https://github.com/prometheus/exporter-toolkit/releases)
- [Changelog](https://github.com/prometheus/exporter-toolkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/exporter-toolkit/compare/v0.13.2...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/exporter-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-15 10:01:47 -05:00
Conrad Hoffmann
c3885e840a
Export last replay age in replication collector ()
The exported replication lag does not handle all failure modes, and can
report 0 for replicas that are out of sync and incapable of recovery.

A proper replacement for that metric would require a different approach
(see e.g. ), but for a lot of folks, simply exporting the age of
the last replay can provide a pretty strong signal for something being
amiss.

I think this solution might be preferable to , though the lag
metric needs to be fixed or abandoned eventually.

Signed-off-by: Conrad Hoffmann <ch@bitfehler.net>
2025-02-15 09:15:44 -05:00
Felipe Galindo Sanchez
2ee2a8fa7c
feat: add wait/backend to pg_stat_activity ()
Signed-off-by: Felipe Galindo Sanchez <felipe.galindo.sanchez@intel.com>
2025-02-15 09:08:24 -05:00
Michael Todorovic
9e42fc0145
fix: handle pg_replication_slots on pg<13 ()
* fix: handle pg_replication_slots on pg<13

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>

* fix: tests

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>

---------

Signed-off-by: Michael Todorovic <michael.todorovic@outlook.com>
2025-02-15 09:00:48 -05:00
Nevermind
072864d179
pg_stat_statements PG17 ()
Signed-off-by: Nevermind <79126473+NevermindZ4@users.noreply.github.com>
2025-02-15 08:54:12 -05:00
PrometheusBot
d85a7710bf
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-02-14 09:36:03 +01:00
dependabot[bot]
3acc4793fc
Bump github.com/prometheus/common from 0.61.0 to 0.62.0 ()
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.61.0 to 0.62.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md)
- [Commits](https://github.com/prometheus/common/compare/v0.61.0...v0.62.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 03:35:38 +01:00
Khiem Doan
7d4c278221
Add Postgres 17 for CI test ()
Signed-off-by: Khiem Doan <doankhiem.crazy@gmail.com>
2025-01-12 21:24:15 -05:00
PrometheusBot
9de4f19d43
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-01-07 21:20:19 +01:00
PrometheusBot
ecb5ec5dff
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2025-01-07 09:28:42 +01:00
Nicolas Rodriguez
bea2609519
Checkpoint related columns in PG 17 have been moved from pg_stat_bgwriter to pg_stat_checkpointer ()
* Checkpoint related columns in PG 17 have been moved from pg_stat_bgwriter to pg_stat_checkpointer

Fix https://github.com/prometheus-community/postgres_exporter/issues/1060

See: https://www.dbi-services.com/blog/postgresql-17-new-catalog-view-pg_stat_checkpointer/
Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>

* Add support for pg_stat_checkpointer

See: https://www.dbi-services.com/blog/postgresql-17-new-catalog-view-pg_stat_checkpointer/
Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>

* Run integration tests with Postgres 17

Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>

* Update date in file header

Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>

---------

Signed-off-by: Nicolas Rodriguez <nico@nicoladmin.fr>
2025-01-01 16:03:43 -05:00
dependabot[bot]
5145620988
Bump github.com/prometheus/exporter-toolkit from 0.13.1 to 0.13.2 ()
Bumps [github.com/prometheus/exporter-toolkit](https://github.com/prometheus/exporter-toolkit) from 0.13.1 to 0.13.2.
- [Release notes](https://github.com/prometheus/exporter-toolkit/releases)
- [Changelog](https://github.com/prometheus/exporter-toolkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/exporter-toolkit/compare/v0.13.1...v0.13.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/exporter-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-01 18:36:32 +01:00
aagarwalla-fx
5bb1702321
Fix to replace dashes with underscore in the metric names ()
* Fix to replace dashes with underscore in the metric names

Signed-off-by: aagarwalla-fx <arpit.agarwalla@falconx.io>

* Code style fix

Signed-off-by: aagarwalla-fx <arpit.agarwalla@falconx.io>

---------

Signed-off-by: aagarwalla-fx <arpit.agarwalla@falconx.io>
2024-12-22 16:14:19 -05:00
Jyothi Kiran Thammana
6f36adfadf
Update pg_long_running_transactions.go ()
To extract time in seconds for pg_long_running_transactions_oldest_timestamp_seconds query which currently return epoch time.

Signed-off-by: Jyothi Kiran Thammana <147131742+jyothikirant-sayukth@users.noreply.github.com>
2024-12-22 15:09:35 -05:00
Joe Adams
a324fe37bc
Fix version header in changelog ()
Signed-off-by: Joe Adams <github@joeadams.io>
2024-11-10 16:05:34 -05:00
Joe Adams
4abdfa5bfd
Update changelog and version for a v0.16.0 release ()
Signed-off-by: Joe Adams <github@joeadams.io>
2024-11-10 15:55:46 -05:00
PrometheusBot
0045c4f93e
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-11-08 19:51:27 +01:00
dependabot[bot]
340a104d25
Bump github.com/prometheus/exporter-toolkit from 0.13.0 to 0.13.1 ()
Bumps [github.com/prometheus/exporter-toolkit](https://github.com/prometheus/exporter-toolkit) from 0.13.0 to 0.13.1.
- [Release notes](https://github.com/prometheus/exporter-toolkit/releases)
- [Changelog](https://github.com/prometheus/exporter-toolkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/exporter-toolkit/compare/v0.13.0...v0.13.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/exporter-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 18:48:19 +01:00
PrometheusBot
c52405ab48
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-11-06 18:47:57 +01:00
Conrad Hoffmann
552ff92f8b
Make walreceiver collector useful w/o repmgr ()
In a streaming replication setup that was created without replication
manager (`repmgr`), the `stat_wal_receiver` collector does not return
any metrics, because one value it wants to export is not present.

This is rather overly opinionated. The missing metric is comparatively
uninteresting and does not justify discarding all the others. And
replication setups created without `repmgr` are not exactly rare.

This commit makes the one relevant metric optional and simply skips it
if the respective value cannot be determined.

Signed-off-by: Conrad Hoffmann <ch@bitfehler.net>
2024-11-06 18:47:30 +01:00
dependabot[bot]
f9c74570ed
Bump github.com/prometheus/common from 0.60.0 to 0.60.1 ()
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.60.0 to 0.60.1.
- [Release notes](https://github.com/prometheus/common/releases)
- [Changelog](https://github.com/prometheus/common/blob/main/RELEASE.md)
- [Commits](https://github.com/prometheus/common/compare/v0.60.0...v0.60.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 21:28:26 +01:00
dependabot[bot]
071ebb6244
Bump github.com/prometheus/client_golang from 1.20.4 to 1.20.5 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.20.4 to 1.20.5.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.20.4...v1.20.5)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-26 22:28:29 +02:00
TJ Hoplock
e8540767e4
chore!: adopt log/slog, drop go-kit/log ()
* ci: update go to version 1.23

Signed-off-by: TJ Hoplock <t.hoplock@gmail.com>

* build(deps): bump prometheus/{client_golang,common,exporter-toolkit}

Signed-off-by: TJ Hoplock <t.hoplock@gmail.com>

* chore!: adopt log/slog, drop go-kit/log

The bulk of this change set was automated by the following script which
is being used to aid in converting the various exporters/projects to use
slog:

https://gist.github.com/tjhop/49f96fb7ebbe55b12deee0b0312d8434

Signed-off-by: TJ Hoplock <t.hoplock@gmail.com>

---------

Signed-off-by: TJ Hoplock <t.hoplock@gmail.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
2024-10-26 21:44:17 +02:00
PrometheusBot
3743987494
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-10-26 16:37:52 +02:00
PrometheusBot
3be4edccd4
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2024-10-17 17:47:21 +02:00
Steffen Zieger
98f75c7e7e
stop logging errors on replicas, fixes ()
Signed-off-by: Steffen Zieger <github@saz.sh>
2024-09-05 09:28:31 -04:00
fhackenberger
3c5ef40e2b
Update README.md ()
Better example for the quick start with prometheus config and avoiding deprecated env variables.

Signed-off-by: fhackenberger <florian@hackenberger.at>
2024-07-06 12:36:52 -04:00
Marc W
49f66e1bfb
fix: Only query active_time on pg>=14 ()
Signed-off-by: MarcWort <113890636+MarcWort@users.noreply.github.com>
2024-06-25 09:15:21 -04:00
Marc W
a4ac0e6747
feat: Add safe_wal_size and wal_status to replication_slot ()
* feat: Add safe_wal_size to replication_slot

Signed-off-by: MarcWort <113890636+MarcWort@users.noreply.github.com>

* feat: Add wal_status to replication_slot

Signed-off-by: MarcWort <113890636+MarcWort@users.noreply.github.com>

---------

Signed-off-by: MarcWort <113890636+MarcWort@users.noreply.github.com>
2024-05-11 14:59:55 +02:00
dependabot[bot]
cc0fd2eda5
Bump golang.org/x/net from 0.20.0 to 0.23.0 ()
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.20.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-21 15:39:52 -04:00
dependabot[bot]
ddd51368a1
Bump github.com/prometheus/exporter-toolkit from 0.10.0 to 0.11.0 ()
Bumps [github.com/prometheus/exporter-toolkit](https://github.com/prometheus/exporter-toolkit) from 0.10.0 to 0.11.0.
- [Release notes](https://github.com/prometheus/exporter-toolkit/releases)
- [Changelog](https://github.com/prometheus/exporter-toolkit/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/exporter-toolkit/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/exporter-toolkit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 12:25:56 +01:00
dependabot[bot]
5ffc58cd28
Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 ()
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-13 22:19:36 -04:00
dependabot[bot]
b126e621db
Bump github.com/prometheus/client_model from 0.5.0 to 0.6.0 ()
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.5.0 to 0.6.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.5.0...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 10:36:40 -05:00
dependabot[bot]
89087f1744
Bump github.com/prometheus/client_golang from 1.18.0 to 1.19.0 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.18.0 to 1.19.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/v1.19.0/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.18.0...v1.19.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-06 09:30:47 -05:00
dependabot[bot]
838f09c97f
Bump github.com/DATA-DOG/go-sqlmock from 1.5.0 to 1.5.2 ()
Bumps [github.com/DATA-DOG/go-sqlmock](https://github.com/DATA-DOG/go-sqlmock) from 1.5.0 to 1.5.2.
- [Release notes](https://github.com/DATA-DOG/go-sqlmock/releases)
- [Commits](https://github.com/DATA-DOG/go-sqlmock/compare/v1.5.0...v1.5.2)

---
updated-dependencies:
- dependency-name: github.com/DATA-DOG/go-sqlmock
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-21 22:03:36 -05:00
Jocelyn Thode
8f39f5b114
Add connection limits metrics for pg_roles and pg_database ()
* Add database connection limits metrics

Signed-off-by: Jocelyn Thode <jocelyn@thode.email>

* Add roles connection limits metrics

Signed-off-by: Jocelyn Thode <jocelyn@thode.email>

* Fix copyright year

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Jocelyn Thode <jocelynthode@users.noreply.github.com>

* Fix spacing in pgDatabaseQuery

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Jocelyn Thode <jocelynthode@users.noreply.github.com>

* Fix case on pgRolesConnectionLimitsQuery

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Jocelyn Thode <jocelynthode@users.noreply.github.com>

* Do not add roleMetrics when row is not valid

Signed-off-by: Jocelyn Thode <jocelyn@thode.email>

---------

Signed-off-by: Jocelyn Thode <jocelyn@thode.email>
Signed-off-by: Jocelyn Thode <jocelynthode@users.noreply.github.com>
Co-authored-by: Joe Adams <github@joeadams.io>
2024-02-21 21:10:17 -05:00
Keegan Carruthers-Smith
f98834a678
use Info level for excluded databases log message ()
This is the only log message which didn't specify a level in the
postgres_exporter. I am unsure if this log message should be info or
debug, but leaning towards the more important since previously it would
just always log.

The way I validated this was the only non-leveled logger was via grep.
Both of these only returned this callsite previously:

  git grep 'logger\.Log'
  git grep '\.Log(' | grep -v level

Signed-off-by: Keegan Carruthers-Smith <keegan.csmith@gmail.com>
2024-02-14 13:38:27 -05:00
dependabot[bot]
9cfa132115
Bump github.com/prometheus/client_golang from 1.17.0 to 1.18.0 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 18:12:56 -05:00
dependabot[bot]
825cc8af13
Bump golang.org/x/crypto from 0.14.0 to 0.17.0 ()
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.14.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.14.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-21 14:10:42 -05:00
Jiri Sveceny
f5b613aba7
pg_stat_database: added support for active_time counter ()
* feat(pg_stat_database): active time metric

---------

Signed-off-by: Jiri Sveceny <jiri.sveceny@icloud.com>
2023-11-28 15:12:07 +01:00
dependabot[bot]
5ceae7f414
Bump github.com/prometheus/client_model ()
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.4.1-0.20230718164431-9a2bf3000d16 to 0.5.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/commits/v0.5.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_model
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-24 09:42:42 +01:00
dependabot[bot]
34f5443ca0
Bump github.com/prometheus/common from 0.44.0 to 0.45.0 ()
Bumps [github.com/prometheus/common](https://github.com/prometheus/common) from 0.44.0 to 0.45.0.
- [Release notes](https://github.com/prometheus/common/releases)
- [Commits](https://github.com/prometheus/common/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/common
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-23 09:45:22 +01:00
Alex Simenduev
ae1375b28e
pg_replication_slot: add slot type label ()
Signed-off-by: Alex Simenduev <shamil.si@gmail.com>
2023-11-23 09:44:58 +01:00
PrometheusBot
f0ea0163bb
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-11-23 09:42:58 +01:00
PrometheusBot
94b0651246
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-11-14 22:03:52 -05:00
Ben Kochie
68c176b883
Release v0.15.0 ()
* [ENHANCEMENT] Add 1kB and 2kB units 
* [BUGFIX] Add error log when probe collector creation fails 
* [BUGFIX] Fix test build failures on 32-bit arch 
* [BUGFIX] Adjust collector to use separate connection per scrape 

Signed-off-by: SuperQ <superq@gmail.com>
2023-10-27 16:25:39 +02:00
dependabot[bot]
e2892a7976
Bump golang.org/x/net from 0.10.0 to 0.17.0 ()
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.10.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.10.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 13:57:17 +02:00
Joe Adams
2a5692c028
Adjust collector to use separate connection per scrape ()
Fixes 

Signed-off-by: Joe Adams <github@joeadams.io>
2023-10-10 07:07:37 -04:00
PrometheusBot
f0f051cb9a
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-10-08 08:02:26 -04:00
dependabot[bot]
69fc35b0ec
Bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 ()
* Bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0

Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update tests for latest client_golang.

Signed-off-by: SuperQ <superq@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: SuperQ <superq@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SuperQ <superq@gmail.com>
2023-10-02 16:00:41 +02:00
Ben Kochie
5e24d43e3e
Add 32-bit testing to CI ()
Run Go tests with 32-bit to validate value overflow.

Signed-off-by: SuperQ <superq@gmail.com>
2023-09-21 15:27:00 +02:00
Daniel Swarbrick
51415a0e5b
Fix test build failures on 32-bit arch again ()
Another case of untyped integer overflows on 32-bit arch.

Signed-off-by: Daniel Swarbrick <daniel.swarbrick@gmail.com>
2023-09-21 14:58:46 +02:00
Joe Adams
30d7d25a7e
Add error log when probe collector creation fails ()
Signed-off-by: Joe Adams <github@joeadams.io>
2023-09-21 07:13:14 -04:00
Eric Tyrrell
e3eaa91c0b
Adds 1kB and 2kB units ()
Signed-off-by: Eric tyrrell <eric.tyrrell18+github@gmail.com>
2023-09-19 21:40:29 -04:00
Joe Adams
c06e57db4e
Add changelog for v0.14 ()
* Add changelog for v0.14

- Add changelog entries since v0.13.2
- Update README with new options
- Bump version file

Signed-off-by: Joe Adams <github@joeadams.io>

* Add changelog entry for 

Signed-off-by: Joe Adams <github@joeadams.io>

---------

Signed-off-by: Joe Adams <github@joeadams.io>
2023-09-19 21:27:45 -04:00
PrometheusBot
add5b86cff
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-09-17 11:04:14 +02:00
Joe Adams
4e521d460e
Fix bugs mentioned in ()
* Fix bugs mentioned in 

These collectors are disabled by default, so unless enabled, they are not tested regularly.

Signed-off-by: Joe Adams <github@joeadams.io>

---------

Signed-off-by: Joe Adams <github@joeadams.io>
2023-09-13 09:19:21 -04:00
David Cook
31ef4ed5a2
stat_user_tables: Add total size metric ()
Signed-off-by: David Cook <dcook@divviup.org>
2023-09-12 09:07:36 -04:00
Vladimir Luksha
0b6d9860ab
fix pg_replication_lag_seconds ()
Signed-off-by: Vladimir Luksha <waldemarluksha@gmail.com>
Co-authored-by: Vladimir Luksha <luksha@limcore.io>
2023-09-08 16:20:19 -04:00
David Cook
dbc7b0b229
Fix cross-compilation command in README.md ()
Signed-off-by: David Cook <dcook@divviup.org>
2023-09-08 16:08:06 -04:00
Christian Albrecht
68ea167866
Fix a connection leak ()
The leak was introduced in PR#882

Signed-off-by: Christian Albrecht <cal@albix.de>
Co-authored-by: Christian Albrecht <christian.albrecht@akquinet.de>
2023-09-05 22:07:37 -04:00
PrometheusBot
a181fba674
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-09-03 08:49:01 +02:00
Felix Yuan
5890879126
Gitlab Collector: Long running transactions collector and test ()
* Long running transactions collector and test

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
2023-08-25 11:20:10 +02:00
Mathis Raguin
ce4ee0507f
Update README to reflect changes made in ()
Signed-off-by: Mathis Raguin <mathis.raguin@gitguardian.com>
2023-08-24 09:58:41 +02:00
Felix Yuan
ce74daee92
Gitlab Collector: User Index io stats collector and test ()
* User Index io stats collector and test

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
2023-08-24 09:55:26 +02:00
Felix Yuan
2402783205
Bugfix: Make statsreset nullable ()
* Stats_reset as null seems to actually be legitimate for new databases,
so don't fail for it

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
2023-08-24 09:51:26 +02:00
Joe Adams
b74852a535
Delay database connection until scrape ()
This no longer returns an error when creating a collector.instance when the database cannot be reached for the version query. This will resolve the entire postgresCollector not being registered for metrics collection when a database is not available. If the version query fails, the scrape will fail.

Resolves 

Signed-off-by: Joe Adams <github@joeadams.io>
2023-08-23 17:33:47 -04:00
Ben Kochie
04bb60ce31
Add a multi-target example config ()
Add an example Prometheus scrape config, similar to the
blackbox_exporter's example config.

Fixes: https://github.com/prometheus-community/postgres_exporter/issues/888

Signed-off-by: SuperQ <superq@gmail.com>
2023-08-15 13:49:05 +02:00
Ben Kochie
716ac23f20
Fixup new pg_stats_statements query ()
Fix all renames of `total_time` to `total_exec_time`.

Fixes: https://github.com/prometheus-community/postgres_exporter/issues/502

Signed-off-by: SuperQ <superq@gmail.com>
2023-07-25 22:36:51 +02:00
Ben Kochie
f9277b04b7
Handle new pg_stat_statements column names ()
Update pg_stat_statements collector to handle the new column names in
PostgreSQL 13.

Fixes: https://github.com/prometheus-community/postgres_exporter/issues/502

Signed-off-by: SuperQ <superq@gmail.com>
2023-07-25 16:20:37 +02:00
Felix Yuan
74800f483a
Gitlab collector: Xlog location collector and test ()
* Xlog location collector and test

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Add more escapes

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Change to Gauge

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
2023-07-21 14:42:43 -04:00
Felix Yuan
2d7e152751
Gitlab Collector: Wal Receiver Collector and Test ()
* Wal Receiver Collector and Test

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Add more escapes

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Corrections to wal_receiver

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Continue on null labels

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Skip nulls and log a message

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Redundant breaks

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Fix up walreceiver

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Remove extra label

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Update collector/pg_stat_walreceiver.go

Co-authored-by: Ben Kochie <superq@gmail.com>
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Clean up the extra assignments

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Update collector/pg_stat_walreceiver.go

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
Co-authored-by: Joe Adams <github@joeadams.io>
2023-07-21 14:42:08 -04:00
Felix Yuan
dc3e813f43
Gitlab Collector: Autovacuum collector and test ()
* Autovacuum collector and test

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Update collector/pg_stat_activity_autovacuum.go

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Update collector/pg_stat_activity_autovacuum.go

Co-authored-by: Joe Adams <github@joeadams.io>
Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Use timestamp seconds

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* query formating

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* SQL format

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

* Loosen autovacuum query

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
Co-authored-by: Joe Adams <github@joeadams.io>
2023-07-21 14:41:25 -04:00
Joe Adams
24a45f2fe3
Update changelog for release 0.13.2 ()
Signed-off-by: Joe Adams <github@joeadams.io>
2023-07-21 14:20:19 -04:00
Joe Adams
c3eec6263b
Merge pull request from Sticksman/bugfix/add-logger-stat-database
Add a logger to stat_database collector
2023-07-20 09:27:12 -04:00
Felix Yuan
12c12cf368 Add a logger to stat_database collector to get better handle on error
(also clean up some metric validity checks)

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
2023-07-19 14:26:41 -07:00
Felix Yuan
4aa8cd4996
Gitlab collector: Database wraparound collector and test ()
* Database wraparound collector and test

---------

Signed-off-by: Felix Yuan <felix.yuan@reddit.com>
Co-authored-by: Joe Adams <github@joeadams.io>
2023-07-14 22:42:12 +02:00
Joe Adams
4ac5481917
Merge pull request from tomhughes/idle-state
Include all idle processes in the process idle metrics
2023-07-06 19:58:39 -04:00
Joe Adams
9a9a4294c4
Merge pull request from prometheus-community/repo_sync
Synchronize common files from prometheus/prometheus
2023-07-06 14:19:11 -04:00
prombot
c514fcad2d Update common Prometheus files
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-07-06 17:49:55 +00:00
PrometheusBot
d7766801fd
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
2023-07-06 14:13:24 +02:00
Ben Kochie
5f917ccdd9
Improve linting ()
* Disable unused-parameter check due to false positives on Collect()
  calls.
* Enable misspell.
* Simplify error returns.

Signed-off-by: SuperQ <superq@gmail.com>
2023-07-06 13:08:45 +02:00
Tom Hughes
a8b86cf7da Include all idle processes in the process idle metrics
Signed-off-by: Tom Hughes <tom@compton.nu>
2023-07-06 08:48:59 +01:00
Tom Hughes
6b56e2f057
Unpack postgres arrays for process idle times correctly ()
Signed-off-by: Ben Kochie <superq@gmail.com>
2023-07-06 09:33:54 +02:00
dependabot[bot]
401711b2e3
Bump github.com/smartystreets/goconvey from 1.8.0 to 1.8.1 ()
Bumps [github.com/smartystreets/goconvey](https://github.com/smartystreets/goconvey) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/smartystreets/goconvey/releases)
- [Commits](https://github.com/smartystreets/goconvey/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/smartystreets/goconvey
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-06 08:42:23 +02:00
Daniel Swarbrick
2477aba363
Fix untyped integer overflows on 32-bit archs ()
go-sqlmock's Rows.AddRow() takes values which have a type alias of
"any", and appear to default to untyped ints if not explicitly cast.
When large values are passed which would overflow int32, tests fail.

Signed-off-by: Daniel Swarbrick <daniel.swarbrick@gmail.com>
2023-07-05 15:10:47 +02:00
dependabot[bot]
a6012e0b54
Bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 ()
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.15.1...v1.16.0)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-05 15:10:03 +02:00
Joe Adams
cf67a472d0
Merge pull request from tomhughes/wal
Add a collector to gather metrics on WAL size
2023-07-05 08:53:04 -04:00
Tom Hughes
2ca1798188 Add a collector to gather metrics on WAL size
Signed-off-by: Tom Hughes <tom@compton.nu>
2023-07-05 11:51:57 +01:00
Tom Hughes
099d3ddb6f Add some more escapes to the query sanitizer
Signed-off-by: Tom Hughes <tom@compton.nu>
2023-07-04 19:08:35 +01:00
Joe Adams
d01184f28d
Merge pull request from tomhughes/replication
Fix replication collector
2023-07-03 13:00:20 -04:00
Tom Hughes
d920553227 Fix replication collector
Signed-off-by: Tom Hughes <tom@compton.nu>
2023-07-03 17:51:50 +01:00
PrometheusBot
dcf498e709
Update common Prometheus files ()
Signed-off-by: prombot <prometheus-team@googlegroups.com>
2023-06-27 20:18:40 +02:00
Felix Yuan
e6ce2ecba9
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-06-27 20:18:02 +02:00
Ben Kochie
030a2a9bc7
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-06-27 16:40:12 +02:00
Ben Kochie
1a4e8993f6
Migrate pg_locks to collector package ()
Migrate the `pg_locks_count` query from `main` to the `collector`
package.

Signed-off-by: SuperQ <superq@gmail.com>
2023-06-27 15:59:30 +02:00
Vadim Voitenko
6a1bb59efb
Fixed replication pgReplicationSlotQuery - now it's working correctly for replica and primary ()
Signed-off-by: Vadim Voitenko <vadim.voitenko@exness.com>
Co-authored-by: Vadim Voitenko <vadim.voitenko@exness.com>
2023-06-27 15:47:33 +02:00
67 changed files with 4208 additions and 877 deletions

View File

@ -8,7 +8,7 @@ executors:
# This must match .promu.yml.
golang:
docker:
- image: cimg/go:1.20
- image: cimg/go:1.24
jobs:
test:
@ -16,13 +16,14 @@ jobs:
steps:
- prometheus/setup_environment
- run: GOHOSTARCH=386 GOARCH=386 make test
- run: make
- prometheus/store_artifact:
file: postgres_exporter
integration:
docker:
- image: cimg/go:1.20
- image: cimg/go:1.24
- image: << parameters.postgres_image >>
environment:
POSTGRES_DB: circle_test
@ -56,12 +57,13 @@ workflows:
matrix:
parameters:
postgres_image:
- circleci/postgres:10
- circleci/postgres:11
- circleci/postgres:12
- circleci/postgres:13
- cimg/postgres:14.1
- cimg/postgres:15.1
- cimg/postgres:14.9
- cimg/postgres:15.4
- cimg/postgres:16.0
- cimg/postgres:17.0
- prometheus/build:
name: build
parallelism: 3

View File

@ -0,0 +1,57 @@
---
name: Push README to Docker Hub
on:
push:
paths:
- "README.md"
- "README-containers.md"
- ".github/workflows/container_description.yml"
branches: [ main, master ]
permissions:
contents: read
jobs:
PushDockerHubReadme:
runs-on: ubuntu-latest
name: Push README to Docker Hub
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set docker hub repo name
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
- name: Push README to Dockerhub
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
env:
DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }}
DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }}
with:
destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
provider: dockerhub
short_description: ${{ env.DOCKER_REPO_NAME }}
# Empty string results in README-containers.md being pushed if it
# exists. Otherwise, README.md is pushed.
readme_file: ''
PushQuayIoReadme:
runs-on: ubuntu-latest
name: Push README to quay.io
if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks.
steps:
- name: git checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set quay.io org name
run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV
- name: Set quay.io repo name
run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV
- name: Push README to quay.io
uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1
env:
DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }}
with:
destination_container_repo: ${{ env.DOCKER_REPO_NAME }}
provider: quay
# Empty string results in README-containers.md being pushed if it
# exists. Otherwise, README.md is pushed.
readme_file: ''

View File

@ -1,3 +1,5 @@
---
# This action is synced from https://github.com/prometheus/prometheus
name: golangci-lint
on:
push:
@ -10,21 +12,28 @@ on:
- ".golangci.yml"
pull_request:
permissions: # added using https://github.com/step-security/secure-repo
contents: read
jobs:
golangci:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: install Go
uses: actions/setup-go@v3
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: 1.20.x
go-version: 1.24.x
- name: Install snmp_exporter/generator dependencies
run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
if: github.repository == 'prometheus/snmp_exporter'
- name: Lint
uses: golangci/golangci-lint-action@v3.4.0
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
with:
version: v1.51.2
args: --verbose
version: v2.0.2

View File

@ -1,16 +1,36 @@
---
version: "2"
linters:
enable:
- revive
issues:
exclude-rules:
- path: _test.go
linters:
- errcheck
linters-settings:
errcheck:
exclude-functions:
# Never check for logger errors.
- (github.com/go-kit/log.Logger).Log
- misspell
- revive
settings:
errcheck:
exclude-functions:
- (github.com/go-kit/log.Logger).Log
revive:
rules:
- name: unused-parameter
severity: warning
disabled: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
path: _test.go
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -1,13 +1,12 @@
go:
# This must match .circle/config.yml.
version: 1.20
version: 1.24
repository:
path: github.com/prometheus-community/postgres_exporter
build:
binaries:
- name: postgres_exporter
path: ./cmd/postgres_exporter
flags: -a -tags 'netgo static_build'
ldflags: |
-X github.com/prometheus/common/version.Version={{.Version}}
-X github.com/prometheus/common/version.Revision={{.Revision}}

View File

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

View File

@ -1,3 +1,75 @@
## 0.17.1 / 2025-02-26
* [BUGFIX] Fix: Handle incoming labels with invalid UTF-8 #1131
## 0.17.0 / 2025-02-16
## What's Changed
* [ENHANCEMENT] Add Postgres 17 for CI test by @khiemdoan in https://github.com/prometheus-community/postgres_exporter/pull/1105
* [ENHANCEMENT] Add wait/backend to pg_stat_activity by @fgalind1 in https://github.com/prometheus-community/postgres_exporter/pull/1106
* [ENHANCEMENT] Export last replay age in replication collector by @bitfehler in https://github.com/prometheus-community/postgres_exporter/pull/1085
* [BUGFIX] Fix pg_long_running_transactions time by @jyothikirant-sayukth in https://github.com/prometheus-community/postgres_exporter/pull/1092
* [BUGFIX] Fix to replace dashes with underscore in the metric names by @aagarwalla-fx in https://github.com/prometheus-community/postgres_exporter/pull/1103
* [BIGFIX] Checkpoint related columns in PG 17 have been moved from pg_stat_bgwriter to pg_stat_checkpointer by @n-rodriguez in https://github.com/prometheus-community/postgres_exporter/pull/1072
* [BUGFIX] Fix pg_stat_statements for PG17 by @NevermindZ4 in https://github.com/prometheus-community/postgres_exporter/pull/1114
* [BUGFIX] Handle pg_replication_slots on pg<13 by @michael-todorovic in https://github.com/prometheus-community/postgres_exporter/pull/1098
* [BUGFIX] Fix missing dsn sanitization for logging by @sysadmind in https://github.com/prometheus-community/postgres_exporter/pull/1104
## New Contributors
* @jyothikirant-sayukth made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1092
* @aagarwalla-fx made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1103
* @NevermindZ4 made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1114
* @michael-todorovic made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1098
* @fgalind1 made their first contribution in https://github.com/prometheus-community/postgres_exporter/pull/1106
**Full Changelog**: https://github.com/prometheus-community/postgres_exporter/compare/v0.16.0...v0.17.0
## 0.16.0 / 2024-11-10
BREAKING CHANGES:
The logging system has been replaced with log/slog from the stdlib. This change is being made across the prometheus ecosystem. The logging output has changed, but the messages and levels remain the same. The `ts` label for the timestamp has bewen replaced with `time`, the accuracy is less, and the timezone is not forced to UTC. The `caller` field has been replaced by the `source` field, which now includes the full path to the source file. The `level` field now exposes the log level in capital letters.
* [CHANGE] Replace logging system #1073
* [ENHANCEMENT] Add save_wal_size and wal_status to replication_slot collector #1027
* [ENHANCEMENT] Add roles collector and connection limit metrics to database collector #997
* [ENHANCEMENT] Excluded databases log messgae is now info level #1003
* [ENHANCEMENT] Add active_time to stat_database collector #961
* [ENHANCEMENT] Add slot_type label to replication_slot collector #960
* [BUGFIX] Fix walreceiver collectore when no repmgr #1086
* [BUGFIX] Remove logging errors on replicas #1048
* [BUGFIX] Fix active_time query on postgres>=14 #1045
## 0.15.0 / 2023-10-27
* [ENHANCEMENT] Add 1kB and 2kB units #915
* [BUGFIX] Add error log when probe collector creation fails #918
* [BUGFIX] Fix test build failures on 32-bit arch #919
* [BUGFIX] Adjust collector to use separate connection per scrape #936
## 0.14.0 / 2023-09-11
* [CHANGE] Add `state` label to pg_process_idle_seconds #862
* [CHANGE] Change database connections to one per scrape #882 #902
* [ENHANCEMENT] Add wal collector #858
* [ENHANCEMENT] Add database_wraparound collector #834
* [ENHANCEMENT] Add stat_activity_autovacuum collector #840
* [ENHANCEMENT] Add stat_wal_receiver collector #844
* [ENHANCEMENT] Add xlog_location collector #849
* [ENHANCEMENT] Add statio_user_indexes collector #845
* [ENHANCEMENT] Add long_running_transactions collector #836
* [ENHANCEMENT] Add pg_stat_user_tables_size_bytes metric #904
* [BUGFIX] Fix tests on 32-bit systems #857
* [BUGFIX] Fix pg_stat_statements metrics on Postgres 13+ #874 #876
* [BUGFIX] Fix pg_stat_database metrics for NULL stats_reset #877
* [BUGFIX] Fix pg_replication_lag_seconds on Postgres 10+ when master is idle #895
## 0.13.2 / 2023-07-21
* [BUGFIX] Fix type issues on pg_postmaster metrics #828
* [BUGFIX] Fix pg_replication collector instantiation #854
* [BUGFIX] Fix pg_process_idle metrics #855
## 0.13.1 / 2023-06-27
* [BUGFIX] Make collectors not fail on null values #823

View File

@ -49,23 +49,23 @@ endif
GOTEST := $(GO) test
GOTEST_DIR :=
ifneq ($(CIRCLE_JOB),)
ifneq ($(shell command -v gotestsum > /dev/null),)
ifneq ($(shell command -v gotestsum 2> /dev/null),)
GOTEST_DIR := test-results
GOTEST := gotestsum --junitfile $(GOTEST_DIR)/unit-tests.xml --
endif
endif
PROMU_VERSION ?= 0.14.0
PROMU_VERSION ?= 0.17.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v1.51.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64.
GOLANGCI_LINT_VERSION ?= v2.0.2
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386))
ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386 arm64))
# If we're in CI and there is an Actions file, that means the linter
# is being run in Actions, so we don't need to run it here.
ifneq (,$(SKIP_GOLANGCI_LINT))
@ -169,16 +169,20 @@ common-vet:
common-lint: $(GOLANGCI_LINT)
ifdef GOLANGCI_LINT
@echo ">> running golangci-lint"
# 'go list' needs to be executed before staticcheck to prepopulate the modules cache.
# Otherwise staticcheck might fail randomly for some reason not yet explained.
$(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null
$(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs)
endif
.PHONY: common-lint-fix
common-lint-fix: $(GOLANGCI_LINT)
ifdef GOLANGCI_LINT
@echo ">> running golangci-lint fix"
$(GOLANGCI_LINT) run --fix $(GOLANGCI_LINT_OPTS) $(pkgs)
endif
.PHONY: common-yamllint
common-yamllint:
@echo ">> running yamllint on all YAML files in the repository"
ifeq (, $(shell command -v yamllint > /dev/null))
ifeq (, $(shell command -v yamllint 2> /dev/null))
@echo "yamllint not installed so skipping"
else
yamllint .
@ -204,6 +208,10 @@ common-tarball: promu
@echo ">> building release tarball"
$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR)
.PHONY: common-docker-repo-name
common-docker-repo-name:
@echo "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)"
.PHONY: common-docker $(BUILD_DOCKER_ARCHS)
common-docker: $(BUILD_DOCKER_ARCHS)
$(BUILD_DOCKER_ARCHS): common-docker-%:
@ -267,3 +275,9 @@ $(1)_precheck:
exit 1; \
fi
endef
govulncheck: install-govulncheck
govulncheck ./...
install-govulncheck:
command -v govulncheck > /dev/null || go install golang.org/x/vuln/cmd/govulncheck@latest

View File

@ -7,7 +7,7 @@
Prometheus exporter for PostgreSQL server metrics.
CI Tested PostgreSQL versions: `10`, `11`, `12`, `13`, `14`, `15`
CI Tested PostgreSQL versions: `11`, `12`, `13`, `14`, `15`, `16`, `17`.
## Quick Start
This package is available for Docker:
@ -17,10 +17,29 @@ docker run --net=host -it --rm -e POSTGRES_PASSWORD=password postgres
# Connect to it
docker run \
--net=host \
-e DATA_SOURCE_NAME="postgresql://postgres:password@localhost:5432/postgres?sslmode=disable" \
-e DATA_SOURCE_URI="localhost:5432/postgres?sslmode=disable" \
-e DATA_SOURCE_USER=postgres \
-e DATA_SOURCE_PASS=password \
quay.io/prometheuscommunity/postgres-exporter
```
Test with:
```bash
curl "http://localhost:9187/metrics"
```
Example Prometheus config:
```yaml
scrape_configs:
- job_name: postgres
static_configs:
- targets: ["127.0.0.1:9187"] # Replace IP with the hostname of the docker container if you're running the container in a separate network
```
Now use the DATA_SOURCE_PASS_FILE with a mounted file containing the password to prevent having the password in an environment variable.
The container process runs with uid/gid 65534 (important for file permissions).
## Multi-Target Support (BETA)
**This Feature is in beta and may require changes in future releases. Feedback is welcome.**
@ -30,6 +49,26 @@ To use the multi-target functionality, send an http request to the endpoint `/pr
To avoid putting sensitive information like username and password in the URL, preconfigured auth modules are supported via the [auth_modules](#auth_modules) section of the config file. auth_modules for DSNs can be used with the `/probe` endpoint by specifying the `?auth_module=foo` http parameter.
Example Prometheus config:
```yaml
scrape_configs:
- job_name: 'postgres'
static_configs:
- targets:
- server1:5432
- server2:5432
metrics_path: /probe
params:
auth_module: [foo]
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 127.0.0.1:9116 # The postgres exporter's real hostname:port.
```
## Configuration File
The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postgres_exporter.yml`.
@ -61,7 +100,7 @@ auth_modules:
To build the Docker image:
make promu
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/amd64 -p linux/ppc64le
promu crossbuild -p linux/amd64 -p linux/armv7 -p linux/arm64 -p linux/ppc64le
make docker
This will build the docker image as `prometheuscommunity/postgres_exporter:${branch}`.
@ -73,13 +112,22 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.database`
Enable the database collector (default: enabled).
Enable the `database` collector (default: enabled).
* `[no-]collector.database_wraparound`
Enable the `database_wraparound` collector (default: disabled).
* `[no-]collector.locks`
Enable the `locks` collector (default: enabled).
* `[no-]collector.long_running_transactions`
Enable the `long_running_transactions` collector (default: disabled).
* `[no-]collector.postmaster`
Enable the `postmaster` collector (default: enabled).
Enable the `postmaster` collector (default: disabled).
* `[no-]collector.process_idle`
Enable the `process_idle` collector (default: enabled).
Enable the `process_idle` collector (default: disabled).
* `[no-]collector.replication`
Enable the `replication` collector (default: enabled).
@ -87,14 +135,17 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.replication_slot`
Enable the `replication_slot` collector (default: enabled).
* `[no-]collector.stat_activity_autovacuum`
Enable the `stat_activity_autovacuum` collector (default: disabled).
* `[no-]collector.stat_bgwriter`
Enable the `stat_bgwriter` collector (default: enabled).
* `[no-]collector.stat_database`
Enable the `stat_database` collector (default: enabled).
* `[no-]collector.statio_user_tables`
Enable the `statio_user_tables` collector (default: enabled).
* `[no-]collector.stat_progress_vacuum`
Enable the `stat_progress_vacuum` collector (default: enabled).
* `[no-]collector.stat_statements`
Enable the `stat_statements` collector (default: disabled).
@ -102,6 +153,21 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.stat_user_tables`
Enable the `stat_user_tables` collector (default: enabled).
* `[no-]collector.stat_wal_receiver`
Enable the `stat_wal_receiver` collector (default: disabled).
* `[no-]collector.statio_user_indexes`
Enable the `statio_user_indexes` collector (default: disabled).
* `[no-]collector.statio_user_tables`
Enable the `statio_user_tables` collector (default: enabled).
* `[no-]collector.wal`
Enable the `wal` collector (default: enabled).
* `[no-]collector.xlog_location`
Enable the `xlog_location` collector (default: disabled).
* `config.file`
Set the config file path. Default is `postgres_exporter.yml`
@ -164,7 +230,7 @@ The following environment variables configure the exporter:
* `DATA_SOURCE_URI`
an alternative to `DATA_SOURCE_NAME` which exclusively accepts the hostname
without a username and password component. For example, `my_pg_hostname` or
`my_pg_hostname?sslmode=disable`.
`my_pg_hostname:5432/postgres?sslmode=disable`.
* `DATA_SOURCE_URI_FILE`
The same as above but reads the URI from a file.

View File

@ -1 +1 @@
0.13.1
0.17.1

View File

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

View File

@ -20,14 +20,13 @@ import (
"strings"
"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus-community/postgres_exporter/collector"
"github.com/prometheus-community/postgres_exporter/config"
"github.com/prometheus/client_golang/prometheus"
versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/common/promlog"
"github.com/prometheus/common/promlog/flag"
"github.com/prometheus/common/promslog"
"github.com/prometheus/common/promslog/flag"
"github.com/prometheus/common/version"
"github.com/prometheus/exporter-toolkit/web"
"github.com/prometheus/exporter-toolkit/web/kingpinflag"
@ -50,7 +49,7 @@ var (
excludeDatabases = kingpin.Flag("exclude-databases", "A list of databases to remove when autoDiscoverDatabases is enabled (DEPRECATED)").Default("").Envar("PG_EXPORTER_EXCLUDE_DATABASES").String()
includeDatabases = kingpin.Flag("include-databases", "A list of databases to include when autoDiscoverDatabases is enabled (DEPRECATED)").Default("").Envar("PG_EXPORTER_INCLUDE_DATABASES").String()
metricPrefix = kingpin.Flag("metric-prefix", "A metric prefix can be used to have non-default (not \"pg\") prefixes for each of the metrics").Default("pg").Envar("PG_EXPORTER_METRIC_PREFIX").String()
logger = log.NewNopLogger()
logger = promslog.NewNopLogger()
)
// Metric name parts.
@ -70,11 +69,11 @@ const (
func main() {
kingpin.Version(version.Print(exporterName))
promlogConfig := &promlog.Config{}
flag.AddFlags(kingpin.CommandLine, promlogConfig)
promslogConfig := &promslog.Config{}
flag.AddFlags(kingpin.CommandLine, promslogConfig)
kingpin.HelpFlag.Short('h')
kingpin.Parse()
logger = promlog.New(promlogConfig)
logger = promslog.New(promslogConfig)
if *onlyDumpMaps {
dumpMaps()
@ -83,28 +82,28 @@ func main() {
if err := c.ReloadConfig(*configFile, logger); err != nil {
// This is not fatal, but it means that auth must be provided for every dsn.
level.Warn(logger).Log("msg", "Error loading config", "err", err)
logger.Warn("Error loading config", "err", err)
}
dsns, err := getDataSources()
if err != nil {
level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error())
logger.Error("Failed reading data sources", "err", err.Error())
os.Exit(1)
}
excludedDatabases := strings.Split(*excludeDatabases, ",")
logger.Log("msg", "Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
logger.Info("Excluded databases", "databases", fmt.Sprintf("%v", excludedDatabases))
if *queriesPath != "" {
level.Warn(logger).Log("msg", "The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
logger.Warn("The extended queries.yaml config is DEPRECATED", "file", *queriesPath)
}
if *autoDiscoverDatabases || *excludeDatabases != "" || *includeDatabases != "" {
level.Warn(logger).Log("msg", "Scraping additional databases via auto discovery is DEPRECATED")
logger.Warn("Scraping additional databases via auto discovery is DEPRECATED")
}
if *constantLabelsList != "" {
level.Warn(logger).Log("msg", "Constant labels on all metrics is DEPRECATED")
logger.Warn("Constant labels on all metrics is DEPRECATED")
}
opts := []ExporterOpt{
@ -122,7 +121,7 @@ func main() {
exporter.servers.Close()
}()
prometheus.MustRegister(version.NewCollector(exporterName))
prometheus.MustRegister(versioncollector.NewCollector(exporterName))
prometheus.MustRegister(exporter)
@ -139,7 +138,7 @@ func main() {
[]string{},
)
if err != nil {
level.Warn(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error())
logger.Warn("Failed to create PostgresCollector", "err", err.Error())
} else {
prometheus.MustRegister(pe)
}
@ -160,7 +159,7 @@ func main() {
}
landingPage, err := web.NewLandingPage(landingConfig)
if err != nil {
level.Error(logger).Log("err", err)
logger.Error("error creating landing page", "err", err)
os.Exit(1)
}
http.Handle("/", landingPage)
@ -170,7 +169,7 @@ func main() {
srv := &http.Server{}
if err := web.ListenAndServe(srv, webConfig, logger); err != nil {
level.Error(logger).Log("msg", "Error running HTTP server", "err", err)
logger.Error("Error running HTTP server", "err", err)
os.Exit(1)
}
}

View File

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

View File

@ -19,7 +19,6 @@ import (
"strconv"
"strings"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -32,7 +31,7 @@ var (
// Query the pg_settings view containing runtime variables
func querySettings(ch chan<- prometheus.Metric, server *Server) error {
level.Debug(logger).Log("msg", "Querying pg_setting view", "server", server)
logger.Debug("Querying pg_setting view", "server", server)
// pg_settings docs: https://www.postgresql.org/docs/current/static/view-pg-settings.html
//
@ -68,7 +67,7 @@ type pgSetting struct {
func (s *pgSetting) metric(labels prometheus.Labels) prometheus.Metric {
var (
err error
name = strings.Replace(s.name, ".", "_", -1)
name = strings.ReplaceAll(strings.ReplaceAll(s.name, ".", "_"), "-", "_")
unit = s.unit // nolint: ineffassign
shortDesc = fmt.Sprintf("Server Parameter: %s", s.name)
subsystem = "settings"
@ -129,10 +128,10 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) {
return
case "ms", "s", "min", "h", "d":
unit = "seconds"
case "B", "kB", "MB", "GB", "TB", "4kB", "8kB", "16kB", "32kB", "64kB", "16MB", "32MB", "64MB":
case "B", "kB", "MB", "GB", "TB", "1kB", "2kB", "4kB", "8kB", "16kB", "32kB", "64kB", "16MB", "32MB", "64MB":
unit = "bytes"
default:
err = fmt.Errorf("Unknown unit for runtime variable: %q", s.unit)
err = fmt.Errorf("unknown unit for runtime variable: %q", s.unit)
return
}
@ -158,6 +157,10 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) {
val *= math.Pow(2, 30)
case "TB":
val *= math.Pow(2, 40)
case "1kB":
val *= math.Pow(2, 10)
case "2kB":
val *= math.Pow(2, 11)
case "4kB":
val *= math.Pow(2, 12)
case "8kB":

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,7 +25,6 @@ import (
"time"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -176,15 +175,6 @@ var builtinMetricMaps = map[string]intermediateMetricMap{
true,
0,
},
"pg_locks": {
map[string]ColumnMapping{
"datname": {LABEL, "Name of this database", nil, nil},
"mode": {LABEL, "Type of Lock", nil, nil},
"count": {GAUGE, "Number of locks", nil, nil},
},
true,
0,
},
"pg_stat_replication": {
map[string]ColumnMapping{
"procpid": {DISCARD, "Process ID of a WAL sender process", nil, semver.MustParseRange("<9.2.0")},
@ -261,6 +251,9 @@ var builtinMetricMaps = map[string]intermediateMetricMap{
"state": {LABEL, "connection state", nil, semver.MustParseRange(">=9.2.0")},
"usename": {LABEL, "connection usename", nil, nil},
"application_name": {LABEL, "connection application_name", nil, nil},
"backend_type": {LABEL, "connection backend_type", nil, nil},
"wait_event_type": {LABEL, "connection wait_event_type", nil, nil},
"wait_event": {LABEL, "connection wait_event", nil, nil},
"count": {GAUGE, "number of connections in this state", nil, nil},
"max_tx_duration": {GAUGE, "max duration in seconds any active transaction has been running", nil, nil},
},
@ -293,7 +286,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
if !columnMapping.supportedVersions(pgVersion) {
// It's very useful to be able to see what columns are being
// rejected.
level.Debug(logger).Log("msg", "Column is being forced to discard due to version incompatibility", "column", columnName)
logger.Debug("Column is being forced to discard due to version incompatibility", "column", columnName)
thisMap[columnName] = MetricMap{
discard: true,
conversion: func(_ interface{}) (float64, bool) {
@ -380,7 +373,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
case string:
durationString = t
default:
level.Error(logger).Log("msg", "Duration conversion metric was not a string")
logger.Error("Duration conversion metric was not a string")
return math.NaN(), false
}
@ -390,7 +383,7 @@ func makeDescMap(pgVersion semver.Version, serverLabels prometheus.Labels, metri
d, err := time.ParseDuration(durationString)
if err != nil {
level.Error(logger).Log("msg", "Failed converting result to metric", "column", columnName, "in", in, "err", err)
logger.Error("Failed converting result to metric", "column", columnName, "in", in, "err", err)
return math.NaN(), false
}
return float64(d / time.Millisecond), true
@ -500,7 +493,7 @@ func parseConstLabels(s string) prometheus.Labels {
for _, p := range parts {
keyValue := strings.Split(strings.TrimSpace(p), "=")
if len(keyValue) != 2 {
level.Error(logger).Log(`Wrong constant labels format, should be "key=value"`, "input", p)
logger.Error(`Wrong constant labels format, should be "key=value"`, "input", p)
continue
}
key := strings.TrimSpace(keyValue[0])
@ -591,7 +584,7 @@ func newDesc(subsystem, name, help string, labels prometheus.Labels) *prometheus
}
func checkPostgresVersion(db *sql.DB, server string) (semver.Version, string, error) {
level.Debug(logger).Log("msg", "Querying PostgreSQL version", "server", server)
logger.Debug("Querying PostgreSQL version", "server", server)
versionRow := db.QueryRow("SELECT version();")
var versionString string
err := versionRow.Scan(&versionString)
@ -614,12 +607,12 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
}
if !e.disableDefaultMetrics && semanticVersion.LT(lowestSupportedVersion) {
level.Warn(logger).Log("msg", "PostgreSQL version is lower than our lowest supported version", "server", server, "version", semanticVersion, "lowest_supported_version", lowestSupportedVersion)
logger.Warn("PostgreSQL version is lower than our lowest supported version", "server", server, "version", semanticVersion, "lowest_supported_version", lowestSupportedVersion)
}
// Check if semantic version changed and recalculate maps if needed.
if semanticVersion.NE(server.lastMapVersion) || server.metricMap == nil {
level.Info(logger).Log("msg", "Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
logger.Info("Semantic version changed", "server", server, "from", server.lastMapVersion, "to", semanticVersion)
server.mappingMtx.Lock()
// Get Default Metrics only for master database
@ -640,13 +633,13 @@ func (e *Exporter) checkMapVersions(ch chan<- prometheus.Metric, server *Server)
// Calculate the hashsum of the useQueries
userQueriesData, err := os.ReadFile(e.userQueriesPath)
if err != nil {
level.Error(logger).Log("msg", "Failed to reload user queries", "path", e.userQueriesPath, "err", err)
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
e.userQueriesError.WithLabelValues(e.userQueriesPath, "").Set(1)
} else {
hashsumStr := fmt.Sprintf("%x", sha256.Sum256(userQueriesData))
if err := addQueries(userQueriesData, semanticVersion, server); err != nil {
level.Error(logger).Log("msg", "Failed to reload user queries", "path", e.userQueriesPath, "err", err)
logger.Error("Failed to reload user queries", "path", e.userQueriesPath, "err", err)
e.userQueriesError.WithLabelValues(e.userQueriesPath, hashsumStr).Set(1)
} else {
// Mark user queries as successfully loaded
@ -688,7 +681,7 @@ func (e *Exporter) scrape(ch chan<- prometheus.Metric) {
if err := e.scrapeDSN(ch, dsn); err != nil {
errorsCount++
level.Error(logger).Log("err", err)
logger.Error("error scraping dsn", "err", err, "dsn", loggableDSN(dsn))
if _, ok := err.(*ErrorConnectToServer); ok {
connectionErrorsCount++

View File

@ -15,17 +15,16 @@ package main
import (
"fmt"
"log/slog"
"net/http"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus-community/postgres_exporter/collector"
"github.com/prometheus-community/postgres_exporter/config"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc {
func handleProbe(logger *slog.Logger, excludeDatabases []string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
conf := c.GetConfig()
@ -38,7 +37,7 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
var authModule config.AuthModule
authModuleName := params.Get("auth_module")
if authModuleName == "" {
level.Info(logger).Log("msg", "no auth_module specified, using default")
logger.Info("no auth_module specified, using default")
} else {
var ok bool
authModule, ok = conf.AuthModules[authModuleName]
@ -54,14 +53,14 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
dsn, err := authModule.ConfigureTarget(target)
if err != nil {
level.Error(logger).Log("msg", "failed to configure target", "err", err)
logger.Error("failed to configure target", "err", err)
http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest)
return
}
// TODO(@sysadmind): Timeout
tl := log.With(logger, "target", target)
tl := logger.With("target", target)
registry := prometheus.NewRegistry()
@ -85,6 +84,7 @@ func handleProbe(logger log.Logger, excludeDatabases []string) http.HandlerFunc
// Run the probe
pc, err := collector.NewProbeCollector(tl, excludeDatabases, registry, dsn)
if err != nil {
logger.Error("Error creating probe collector", "err", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@ -18,7 +18,6 @@ import (
"fmt"
"github.com/blang/semver/v4"
"github.com/go-kit/log/level"
"gopkg.in/yaml.v2"
)
@ -46,39 +45,14 @@ type OverrideQuery struct {
// Overriding queries for namespaces above.
// TODO: validate this is a closed set in tests, and there are no overlaps
var queryOverrides = map[string][]OverrideQuery{
"pg_locks": {
{
semver.MustParseRange(">0.0.0"),
`SELECT pg_database.datname,tmp.mode,COALESCE(count,0) as count
FROM
(
VALUES ('accesssharelock'),
('rowsharelock'),
('rowexclusivelock'),
('shareupdateexclusivelock'),
('sharelock'),
('sharerowexclusivelock'),
('exclusivelock'),
('accessexclusivelock'),
('sireadlock')
) AS tmp(mode) CROSS JOIN pg_database
LEFT JOIN
(SELECT database, lower(mode) AS mode,count(*) AS count
FROM pg_locks WHERE database IS NOT NULL
GROUP BY database, lower(mode)
) AS tmp2
ON tmp.mode=tmp2.mode and pg_database.oid = tmp2.database ORDER BY 1`,
},
},
"pg_stat_replication": {
{
semver.MustParseRange(">=10.0.0"),
`
SELECT *,
(case pg_is_in_recovery() when 't' then null else pg_current_wal_lsn() end) AS pg_current_wal_lsn,
(case pg_is_in_recovery() when 't' then null else pg_wal_lsn_diff(pg_current_wal_lsn(), pg_lsn('0/0'))::float end) AS pg_current_wal_lsn_bytes,
(case pg_is_in_recovery() when 't' then null else pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)::float end) AS pg_wal_lsn_diff
(case pg_is_in_recovery() when 't' then pg_last_wal_receive_lsn() else pg_current_wal_lsn() end) AS pg_current_wal_lsn,
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_lsn('0/0'))::float else pg_wal_lsn_diff(pg_current_wal_lsn(), pg_lsn('0/0'))::float end) AS pg_current_wal_lsn_bytes,
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), replay_lsn)::float else pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)::float end) AS pg_wal_lsn_diff
FROM pg_stat_replication
`,
},
@ -86,8 +60,8 @@ var queryOverrides = map[string][]OverrideQuery{
semver.MustParseRange(">=9.2.0 <10.0.0"),
`
SELECT *,
(case pg_is_in_recovery() when 't' then null else pg_current_xlog_location() end) AS pg_current_xlog_location,
(case pg_is_in_recovery() when 't' then null else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
(case pg_is_in_recovery() when 't' then pg_last_xlog_receive_location() else pg_current_xlog_location() end) AS pg_current_xlog_location,
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), replay_location)::float else pg_xlog_location_diff(pg_current_xlog_location(), replay_location)::float end) AS pg_xlog_location_diff
FROM pg_stat_replication
`,
},
@ -95,7 +69,7 @@ var queryOverrides = map[string][]OverrideQuery{
semver.MustParseRange("<9.2.0"),
`
SELECT *,
(case pg_is_in_recovery() when 't' then null else pg_current_xlog_location() end) AS pg_current_xlog_location
(case pg_is_in_recovery() when 't' then pg_last_xlog_receive_location() else pg_current_xlog_location() end) AS pg_current_xlog_location
FROM pg_stat_replication
`,
},
@ -105,14 +79,16 @@ var queryOverrides = map[string][]OverrideQuery{
{
semver.MustParseRange(">=9.4.0 <10.0.0"),
`
SELECT slot_name, database, active, pg_xlog_location_diff(pg_current_xlog_location(), restart_lsn)
SELECT slot_name, database, active,
(case pg_is_in_recovery() when 't' then pg_xlog_location_diff(pg_last_xlog_receive_location(), restart_lsn) else pg_xlog_location_diff(pg_current_xlog_location(), restart_lsn) end) as pg_xlog_location_diff
FROM pg_replication_slots
`,
},
{
semver.MustParseRange(">=10.0.0"),
`
SELECT slot_name, database, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
SELECT slot_name, database, active,
(case pg_is_in_recovery() when 't' then pg_wal_lsn_diff(pg_last_wal_receive_lsn(), restart_lsn) else pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) end) as pg_wal_lsn_diff
FROM pg_replication_slots
`,
},
@ -139,6 +115,9 @@ var queryOverrides = map[string][]OverrideQuery{
tmp.state,
tmp2.usename,
tmp2.application_name,
tmp2.backend_type,
tmp2.wait_event_type,
tmp2.wait_event,
COALESCE(count,0) as count,
COALESCE(max_tx_duration,0) as max_tx_duration
FROM
@ -157,9 +136,13 @@ var queryOverrides = map[string][]OverrideQuery{
state,
usename,
application_name,
backend_type,
wait_event_type,
wait_event,
count(*) AS count,
MAX(EXTRACT(EPOCH FROM now() - xact_start))::float AS max_tx_duration
FROM pg_stat_activity GROUP BY datname,state,usename,application_name) AS tmp2
FROM pg_stat_activity
GROUP BY datname,state,usename,application_name,backend_type,wait_event_type,wait_event) AS tmp2
ON tmp.state = tmp2.state AND pg_database.datname = tmp2.datname
`,
},
@ -195,7 +178,7 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][]
}
}
if !matched {
level.Warn(logger).Log("msg", "No query matched override, disabling metric space", "name", name)
logger.Warn("No query matched override, disabling metric space", "name", name)
resultMap[name] = ""
}
}
@ -216,7 +199,7 @@ func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[str
newQueryOverrides := make(map[string]string)
for metric, specs := range userQueries {
level.Debug(logger).Log("msg", "New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
logger.Debug("New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds)
newQueryOverrides[metric] = specs.Query
metricMap, ok := metricMaps[metric]
if !ok {
@ -268,9 +251,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
for k, v := range partialExporterMap {
_, found := server.metricMap[k]
if found {
level.Debug(logger).Log("msg", "Overriding metric from user YAML file", "metric", k)
logger.Debug("Overriding metric from user YAML file", "metric", k)
} else {
level.Debug(logger).Log("msg", "Adding new metric from user YAML file", "metric", k)
logger.Debug("Adding new metric from user YAML file", "metric", k)
}
server.metricMap[k] = v
}
@ -279,9 +262,9 @@ func addQueries(content []byte, pgVersion semver.Version, server *Server) error
for k, v := range newQueryOverrides {
_, found := server.queryOverrides[k]
if found {
level.Debug(logger).Log("msg", "Overriding query override from user YAML file", "query_override", k)
logger.Debug("Overriding query override from user YAML file", "query_override", k)
} else {
level.Debug(logger).Log("msg", "Adding new query override from user YAML file", "query_override", k)
logger.Debug("Adding new query override from user YAML file", "query_override", k)
}
server.queryOverrides[k] = v
}

View File

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

View File

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

View File

@ -17,12 +17,11 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -62,7 +61,7 @@ type Collector interface {
}
type collectorConfig struct {
logger log.Logger
logger *slog.Logger
excludeDatabases []string
}
@ -89,7 +88,7 @@ func registerCollector(name string, isDefaultEnabled bool, createFunc func(colle
// PostgresCollector implements the prometheus.Collector interface.
type PostgresCollector struct {
Collectors map[string]Collector
logger log.Logger
logger *slog.Logger
instance *instance
}
@ -97,7 +96,7 @@ type PostgresCollector struct {
type Option func(*PostgresCollector) error
// NewPostgresCollector creates a new PostgresCollector.
func NewPostgresCollector(logger log.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
func NewPostgresCollector(logger *slog.Logger, excludeDatabases []string, dsn string, filters []string, options ...Option) (*PostgresCollector, error) {
p := &PostgresCollector{
logger: logger,
}
@ -131,7 +130,7 @@ func NewPostgresCollector(logger log.Logger, excludeDatabases []string, dsn stri
collectors[key] = collector
} else {
collector, err := factories[key](collectorConfig{
logger: log.With(logger, "collector", key),
logger: logger.With("collector", key),
excludeDatabases: excludeDatabases,
})
if err != nil {
@ -166,18 +165,30 @@ func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) {
// Collect implements the prometheus.Collector interface.
func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
ctx := context.TODO()
// copy the instance so that concurrent scrapes have independent instances
inst := p.instance.copy()
// Set up the database connection for the collector.
err := inst.setup()
if err != nil {
p.logger.Error("Error opening connection to database", "err", err)
return
}
defer inst.Close()
wg := sync.WaitGroup{}
wg.Add(len(p.Collectors))
for name, c := range p.Collectors {
go func(name string, c Collector) {
execute(ctx, name, c, p.instance, ch, p.logger)
execute(ctx, name, c, inst, ch, p.logger)
wg.Done()
}(name, c)
}
wg.Wait()
}
func execute(ctx context.Context, name string, c Collector, instance *instance, ch chan<- prometheus.Metric, logger log.Logger) {
func execute(ctx context.Context, name string, c Collector, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) {
begin := time.Now()
err := c.Update(ctx, instance, ch)
duration := time.Since(begin)
@ -185,13 +196,13 @@ func execute(ctx context.Context, name string, c Collector, instance *instance,
if err != nil {
if IsNoDataError(err) {
level.Debug(logger).Log("msg", "collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
logger.Debug("collector returned no data", "name", name, "duration_seconds", duration.Seconds(), "err", err)
} else {
level.Error(logger).Log("msg", "collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
logger.Error("collector failed", "name", name, "duration_seconds", duration.Seconds(), "err", err)
}
success = 0
} else {
level.Debug(logger).Log("msg", "collector succeeded", "name", name, "duration_seconds", duration.Seconds())
logger.Debug("collector succeeded", "name", name, "duration_seconds", duration.Seconds())
success = 1
}
ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, duration.Seconds(), name)

View File

@ -48,9 +48,15 @@ func readMetric(m prometheus.Metric) MetricResult {
func sanitizeQuery(q string) string {
q = strings.Join(strings.Fields(q), " ")
q = strings.Replace(q, "(", "\\(", -1)
q = strings.Replace(q, ")", "\\)", -1)
q = strings.Replace(q, "*", "\\*", -1)
q = strings.Replace(q, "$", "\\$", -1)
q = strings.ReplaceAll(q, "(", "\\(")
q = strings.ReplaceAll(q, "?", "\\?")
q = strings.ReplaceAll(q, ")", "\\)")
q = strings.ReplaceAll(q, "[", "\\[")
q = strings.ReplaceAll(q, "]", "\\]")
q = strings.ReplaceAll(q, "{", "\\{")
q = strings.ReplaceAll(q, "}", "\\}")
q = strings.ReplaceAll(q, "*", "\\*")
q = strings.ReplaceAll(q, "^", "\\^")
q = strings.ReplaceAll(q, "$", "\\$")
return q
}

View File

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

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 log.Logger
log *slog.Logger
excludedDatabases []string
}
@ -53,12 +53,21 @@ var (
"Disk space used by the database",
[]string{"datname"}, nil,
)
pgDatabaseConnectionLimitsDesc = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
databaseSubsystem,
"connection_limit",
),
"Connection limit set for the database",
[]string{"datname"}, nil,
)
pgDatabaseQuery = "SELECT pg_database.datname FROM pg_database;"
pgDatabaseQuery = "SELECT pg_database.datname, pg_database.datconnlimit FROM pg_database;"
pgDatabaseSizeQuery = "SELECT pg_database_size($1)"
)
// Update implements Collector and exposes database size.
// Update implements Collector and exposes database size and connection limits.
// It is called by the Prometheus registry when collecting metrics.
// The list of databases is retrieved from pg_database and filtered
// by the excludeDatabase config parameter. The tradeoff here is that
@ -81,21 +90,32 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
for rows.Next() {
var datname sql.NullString
if err := rows.Scan(&datname); err != nil {
var connLimit sql.NullInt64
if err := rows.Scan(&datname, &connLimit); err != nil {
return err
}
if !datname.Valid {
continue
}
database := datname.String
// Ignore excluded databases
// Filtering is done here instead of in the query to avoid
// a complicated NOT IN query with a variable number of parameters
if sliceContains(c.excludedDatabases, datname.String) {
if sliceContains(c.excludedDatabases, database) {
continue
}
databases = append(databases, datname.String)
databases = append(databases, database)
connLimitMetric := 0.0
if connLimit.Valid {
connLimitMetric = float64(connLimit.Int64)
}
ch <- prometheus.MustNewConstMetric(
pgDatabaseConnectionLimitsDesc,
prometheus.GaugeValue, connLimitMetric, database,
)
}
// Query the size of the databases
@ -114,11 +134,9 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
pgDatabaseSizeDesc,
prometheus.GaugeValue, sizeMetric, datname,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
return rows.Err()
}
func sliceContains(slice []string, s string) bool {

View File

@ -31,8 +31,8 @@ func TestPGDatabaseCollector(t *testing.T) {
inst := &instance{db: db}
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
AddRow("postgres"))
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
AddRow("postgres", 15))
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
AddRow(1024))
@ -47,6 +47,7 @@ func TestPGDatabaseCollector(t *testing.T) {
}()
expected := []MetricResult{
{labels: labelMap{"datname": "postgres"}, value: 15, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"datname": "postgres"}, value: 1024, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
@ -71,8 +72,8 @@ func TestPGDatabaseCollectorNullMetric(t *testing.T) {
inst := &instance{db: db}
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname"}).
AddRow("postgres"))
mock.ExpectQuery(sanitizeQuery(pgDatabaseQuery)).WillReturnRows(sqlmock.NewRows([]string{"datname", "datconnlimit"}).
AddRow("postgres", nil))
mock.ExpectQuery(sanitizeQuery(pgDatabaseSizeQuery)).WithArgs("postgres").WillReturnRows(sqlmock.NewRows([]string{"pg_database_size"}).
AddRow(nil))
@ -88,6 +89,7 @@ func TestPGDatabaseCollectorNullMetric(t *testing.T) {
expected := []MetricResult{
{labels: labelMap{"datname": "postgres"}, value: 0, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"datname": "postgres"}, value: 0, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {

View File

@ -0,0 +1,114 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const databaseWraparoundSubsystem = "database_wraparound"
func init() {
registerCollector(databaseWraparoundSubsystem, defaultDisabled, NewPGDatabaseWraparoundCollector)
}
type PGDatabaseWraparoundCollector struct {
log *slog.Logger
}
func NewPGDatabaseWraparoundCollector(config collectorConfig) (Collector, error) {
return &PGDatabaseWraparoundCollector{log: config.logger}, nil
}
var (
databaseWraparoundAgeDatfrozenxid = prometheus.NewDesc(
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datfrozenxid_seconds"),
"Age of the oldest transaction ID that has not been frozen.",
[]string{"datname"},
prometheus.Labels{},
)
databaseWraparoundAgeDatminmxid = prometheus.NewDesc(
prometheus.BuildFQName(namespace, databaseWraparoundSubsystem, "age_datminmxid_seconds"),
"Age of the oldest multi-transaction ID that has been replaced with a transaction ID.",
[]string{"datname"},
prometheus.Labels{},
)
databaseWraparoundQuery = `
SELECT
datname,
age(d.datfrozenxid) as age_datfrozenxid,
mxid_age(d.datminmxid) as age_datminmxid
FROM
pg_catalog.pg_database d
WHERE
d.datallowconn
`
)
func (c *PGDatabaseWraparoundCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
databaseWraparoundQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var datname sql.NullString
var ageDatfrozenxid, ageDatminmxid sql.NullFloat64
if err := rows.Scan(&datname, &ageDatfrozenxid, &ageDatminmxid); err != nil {
return err
}
if !datname.Valid {
c.log.Debug("Skipping database with NULL name")
continue
}
if !ageDatfrozenxid.Valid {
c.log.Debug("Skipping stat emission with NULL age_datfrozenxid")
continue
}
if !ageDatminmxid.Valid {
c.log.Debug("Skipping stat emission with NULL age_datminmxid")
continue
}
ageDatfrozenxidMetric := ageDatfrozenxid.Float64
ch <- prometheus.MustNewConstMetric(
databaseWraparoundAgeDatfrozenxid,
prometheus.GaugeValue,
ageDatfrozenxidMetric, datname.String,
)
ageDatminmxidMetric := ageDatminmxid.Float64
ch <- prometheus.MustNewConstMetric(
databaseWraparoundAgeDatminmxid,
prometheus.GaugeValue,
ageDatminmxidMetric, datname.String,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,64 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGDatabaseWraparoundCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"datname",
"age_datfrozenxid",
"age_datminmxid",
}
rows := sqlmock.NewRows(columns).
AddRow("newreddit", 87126426, 0)
mock.ExpectQuery(sanitizeQuery(databaseWraparoundQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGDatabaseWraparoundCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGDatabaseWraparoundCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datname": "newreddit"}, value: 87126426, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"datname": "newreddit"}, value: 0, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

129
collector/pg_locks.go Normal file
View File

@ -0,0 +1,129 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const locksSubsystem = "locks"
func init() {
registerCollector(locksSubsystem, defaultEnabled, NewPGLocksCollector)
}
type PGLocksCollector struct {
log *slog.Logger
}
func NewPGLocksCollector(config collectorConfig) (Collector, error) {
return &PGLocksCollector{
log: config.logger,
}, nil
}
var (
pgLocksDesc = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
locksSubsystem,
"count",
),
"Number of locks",
[]string{"datname", "mode"}, nil,
)
pgLocksQuery = `
SELECT
pg_database.datname as datname,
tmp.mode as mode,
COALESCE(count, 0) as count
FROM
(
VALUES
('accesssharelock'),
('rowsharelock'),
('rowexclusivelock'),
('shareupdateexclusivelock'),
('sharelock'),
('sharerowexclusivelock'),
('exclusivelock'),
('accessexclusivelock'),
('sireadlock')
) AS tmp(mode)
CROSS JOIN pg_database
LEFT JOIN (
SELECT
database,
lower(mode) AS mode,
count(*) AS count
FROM
pg_locks
WHERE
database IS NOT NULL
GROUP BY
database,
lower(mode)
) AS tmp2 ON tmp.mode = tmp2.mode
and pg_database.oid = tmp2.database
ORDER BY
1
`
)
// Update implements Collector and exposes database locks.
// It is called by the Prometheus registry when collecting metrics.
func (c PGLocksCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
// Query the list of databases
rows, err := db.QueryContext(ctx,
pgLocksQuery,
)
if err != nil {
return err
}
defer rows.Close()
var datname, mode sql.NullString
var count sql.NullInt64
for rows.Next() {
if err := rows.Scan(&datname, &mode, &count); err != nil {
return err
}
if !datname.Valid || !mode.Valid {
continue
}
countMetric := 0.0
if count.Valid {
countMetric = float64(count.Int64)
}
ch <- prometheus.MustNewConstMetric(
pgLocksDesc,
prometheus.GaugeValue, countMetric,
datname.String, mode.String,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,60 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGLocksCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
rows := sqlmock.NewRows([]string{"datname", "mode", "count"}).
AddRow("test", "exclusivelock", 42)
mock.ExpectQuery(sanitizeQuery(pgLocksQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGLocksCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGLocksCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datname": "test", "mode": "exclusivelock"}, value: 42, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -0,0 +1,95 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const longRunningTransactionsSubsystem = "long_running_transactions"
func init() {
registerCollector(longRunningTransactionsSubsystem, defaultDisabled, NewPGLongRunningTransactionsCollector)
}
type PGLongRunningTransactionsCollector struct {
log *slog.Logger
}
func NewPGLongRunningTransactionsCollector(config collectorConfig) (Collector, error) {
return &PGLongRunningTransactionsCollector{log: config.logger}, nil
}
var (
longRunningTransactionsCount = prometheus.NewDesc(
"pg_long_running_transactions",
"Current number of long running transactions",
[]string{},
prometheus.Labels{},
)
longRunningTransactionsAgeInSeconds = prometheus.NewDesc(
prometheus.BuildFQName(namespace, longRunningTransactionsSubsystem, "oldest_timestamp_seconds"),
"The current maximum transaction age in seconds",
[]string{},
prometheus.Labels{},
)
longRunningTransactionsQuery = `
SELECT
COUNT(*) as transactions,
MAX(EXTRACT(EPOCH FROM clock_timestamp() - pg_stat_activity.xact_start)) AS oldest_timestamp_seconds
FROM pg_catalog.pg_stat_activity
WHERE state IS DISTINCT FROM 'idle'
AND query NOT LIKE 'autovacuum:%'
AND pg_stat_activity.xact_start IS NOT NULL;
`
)
func (PGLongRunningTransactionsCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
longRunningTransactionsQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var transactions, ageInSeconds float64
if err := rows.Scan(&transactions, &ageInSeconds); err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(
longRunningTransactionsCount,
prometheus.GaugeValue,
transactions,
)
ch <- prometheus.MustNewConstMetric(
longRunningTransactionsAgeInSeconds,
prometheus.GaugeValue,
ageInSeconds,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,63 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGLongRunningTransactionsCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"transactions",
"age_in_seconds",
}
rows := sqlmock.NewRows(columns).
AddRow(20, 1200)
mock.ExpectQuery(sanitizeQuery(longRunningTransactionsQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGLongRunningTransactionsCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGLongRunningTransactionsCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{}, value: 20, metricType: dto.MetricType_GAUGE},
{labels: labelMap{}, value: 1200, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -23,7 +23,7 @@ import (
const postmasterSubsystem = "postmaster"
func init() {
registerCollector(postmasterSubsystem, defaultEnabled, NewPGPostmasterCollector)
registerCollector(postmasterSubsystem, defaultDisabled, NewPGPostmasterCollector)
}
type PGPostmasterCollector struct {
@ -44,7 +44,7 @@ var (
[]string{}, nil,
)
pgPostmasterQuery = "SELECT pg_postmaster_start_time from pg_postmaster_start_time();"
pgPostmasterQuery = "SELECT extract(epoch from pg_postmaster_start_time) from pg_postmaster_start_time();"
)
func (c *PGPostmasterCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {

View File

@ -16,8 +16,9 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
)
@ -27,7 +28,7 @@ func init() {
}
type PGProcessIdleCollector struct {
log log.Logger
log *slog.Logger
}
const processIdleSubsystem = "process_idle"
@ -39,25 +40,27 @@ func NewPGProcessIdleCollector(config collectorConfig) (Collector, error) {
var pgProcessIdleSeconds = prometheus.NewDesc(
prometheus.BuildFQName(namespace, processIdleSubsystem, "seconds"),
"Idle time of server processes",
[]string{"application_name"},
[]string{"state", "application_name"},
prometheus.Labels{},
)
func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch chan<- prometheus.Metric) error {
db := inst.getDB()
func (PGProcessIdleCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
row := db.QueryRowContext(ctx,
`WITH
metrics AS (
SELECT
state,
application_name,
SUM(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - state_change))::bigint)::float AS process_idle_seconds_sum,
COUNT(*) AS process_idle_seconds_count
FROM pg_stat_activity
WHERE state = 'idle'
GROUP BY application_name
WHERE state ~ '^idle'
GROUP BY state, application_name
),
buckets AS (
SELECT
state,
application_name,
le,
SUM(
@ -69,35 +72,42 @@ func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch cha
FROM
pg_stat_activity,
UNNEST(ARRAY[1, 2, 5, 15, 30, 60, 90, 120, 300]) AS le
GROUP BY application_name, le
ORDER BY application_name, le
GROUP BY state, application_name, le
ORDER BY state, application_name, le
)
SELECT
state,
application_name,
process_idle_seconds_sum as seconds_sum,
process_idle_seconds_count as seconds_count,
ARRAY_AGG(le) AS seconds,
ARRAY_AGG(bucket) AS seconds_bucket
FROM metrics JOIN buckets USING (application_name)
GROUP BY 1, 2, 3;`)
FROM metrics JOIN buckets USING (state, application_name)
GROUP BY 1, 2, 3, 4;`)
var state sql.NullString
var applicationName sql.NullString
var secondsSum sql.NullInt64
var secondsSum sql.NullFloat64
var secondsCount sql.NullInt64
var seconds []int64
var secondsBucket []uint64
var seconds []float64
var secondsBucket []int64
err := row.Scan(&applicationName, &secondsSum, &secondsCount, &seconds, &secondsBucket)
err := row.Scan(&state, &applicationName, &secondsSum, &secondsCount, pq.Array(&seconds), pq.Array(&secondsBucket))
if err != nil {
return err
}
var buckets = make(map[float64]uint64, len(seconds))
for i, second := range seconds {
if i >= len(secondsBucket) {
break
}
buckets[float64(second)] = secondsBucket[i]
buckets[second] = uint64(secondsBucket[i])
}
if err != nil {
return err
stateLabel := "unknown"
if state.Valid {
stateLabel = state.String
}
applicationNameLabel := "unknown"
@ -111,12 +121,12 @@ func (PGProcessIdleCollector) Update(ctx context.Context, inst *instance, ch cha
}
secondsSumMetric := 0.0
if secondsSum.Valid {
secondsSumMetric = float64(secondsSum.Int64)
secondsSumMetric = secondsSum.Float64
}
ch <- prometheus.MustNewConstHistogram(
pgProcessIdleSeconds,
secondsCountMetric, secondsSumMetric, buckets,
applicationNameLabel,
stateLabel, applicationNameLabel,
)
return nil
}

View File

@ -15,7 +15,6 @@ package collector
import (
"context"
"database/sql"
"github.com/prometheus/client_golang/prometheus"
)
@ -30,7 +29,7 @@ type PGReplicationCollector struct {
}
func NewPGReplicationCollector(collectorConfig) (Collector, error) {
return &PGPostmasterCollector{}, nil
return &PGReplicationCollector{}, nil
}
var (
@ -52,26 +51,39 @@ var (
"Indicates if the server is a replica",
[]string{}, nil,
)
pgReplicationLastReplay = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
replicationSubsystem,
"last_replay_seconds",
),
"Age of last replay in seconds",
[]string{}, nil,
)
pgReplicationQuery = `SELECT
CASE
WHEN NOT pg_is_in_recovery() THEN 0
WHEN pg_last_wal_receive_lsn () = pg_last_wal_replay_lsn () THEN 0
ELSE GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())))
END AS lag,
CASE
WHEN pg_is_in_recovery() THEN 1
ELSE 0
END as is_replica`
END as is_replica,
GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) as last_replay`
)
func (c *PGReplicationCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error {
func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
row := db.QueryRowContext(ctx,
pgReplicationQuery,
)
var lag float64
var isReplica int64
err := row.Scan(&lag, &isReplica)
var replayAge float64
err := row.Scan(&lag, &isReplica, &replayAge)
if err != nil {
return err
}
@ -83,5 +95,9 @@ func (c *PGReplicationCollector) Update(ctx context.Context, db *sql.DB, ch chan
pgReplicationIsReplica,
prometheus.GaugeValue, float64(isReplica),
)
ch <- prometheus.MustNewConstMetric(
pgReplicationLastReplay,
prometheus.GaugeValue, replayAge,
)
return nil
}

View File

@ -16,8 +16,9 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -28,7 +29,7 @@ func init() {
}
type PGReplicationSlotCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGReplicationSlotCollector(config collectorConfig) (Collector, error) {
@ -43,7 +44,7 @@ var (
"slot_current_wal_lsn",
),
"current wal lsn value",
[]string{"slot_name"}, nil,
[]string{"slot_name", "slot_type"}, nil,
)
pgReplicationSlotCurrentFlushDesc = prometheus.NewDesc(
prometheus.BuildFQName(
@ -52,7 +53,7 @@ var (
"slot_confirmed_flush_lsn",
),
"last lsn confirmed flushed to the replication slot",
[]string{"slot_name"}, nil,
[]string{"slot_name", "slot_type"}, nil,
)
pgReplicationSlotIsActiveDesc = prometheus.NewDesc(
prometheus.BuildFQName(
@ -61,22 +62,62 @@ var (
"slot_is_active",
),
"whether the replication slot is active or not",
[]string{"slot_name"}, nil,
[]string{"slot_name", "slot_type"}, nil,
)
pgReplicationSlotSafeWal = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
replicationSlotSubsystem,
"safe_wal_size_bytes",
),
"number of bytes that can be written to WAL such that this slot is not in danger of getting in state lost",
[]string{"slot_name", "slot_type"}, nil,
)
pgReplicationSlotWalStatus = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
replicationSlotSubsystem,
"wal_status",
),
"availability of WAL files claimed by this slot",
[]string{"slot_name", "slot_type", "wal_status"}, nil,
)
pgReplicationSlotQuery = `SELECT
slot_name,
pg_current_wal_lsn() - '0/0' AS current_wal_lsn,
coalesce(confirmed_flush_lsn, '0/0') - '0/0',
slot_type,
CASE WHEN pg_is_in_recovery() THEN
pg_last_wal_receive_lsn() - '0/0'
ELSE
pg_current_wal_lsn() - '0/0'
END AS current_wal_lsn,
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
active
FROM
pg_replication_slots;`
FROM pg_replication_slots;`
pgReplicationSlotNewQuery = `SELECT
slot_name,
slot_type,
CASE WHEN pg_is_in_recovery() THEN
pg_last_wal_receive_lsn() - '0/0'
ELSE
pg_current_wal_lsn() - '0/0'
END AS current_wal_lsn,
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
active,
safe_wal_size,
wal_status
FROM pg_replication_slots;`
)
func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
query := pgReplicationSlotQuery
abovePG13 := instance.version.GTE(semver.MustParse("13.0.0"))
if abovePG13 {
query = pgReplicationSlotNewQuery
}
db := instance.getDB()
rows, err := db.QueryContext(ctx,
pgReplicationSlotQuery)
query)
if err != nil {
return err
}
@ -84,10 +125,28 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
for rows.Next() {
var slotName sql.NullString
var slotType sql.NullString
var walLSN sql.NullFloat64
var flushLSN sql.NullFloat64
var isActive sql.NullBool
if err := rows.Scan(&slotName, &walLSN, &flushLSN, &isActive); err != nil {
var safeWalSize sql.NullInt64
var walStatus sql.NullString
r := []any{
&slotName,
&slotType,
&walLSN,
&flushLSN,
&isActive,
}
if abovePG13 {
r = append(r, &safeWalSize)
r = append(r, &walStatus)
}
err := rows.Scan(r...)
if err != nil {
return err
}
@ -99,6 +158,10 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
if slotName.Valid {
slotNameLabel = slotName.String
}
slotTypeLabel := "unknown"
if slotType.Valid {
slotTypeLabel = slotType.String
}
var walLSNMetric float64
if walLSN.Valid {
@ -106,7 +169,7 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotCurrentWalDesc,
prometheus.GaugeValue, walLSNMetric, slotNameLabel,
prometheus.GaugeValue, walLSNMetric, slotNameLabel, slotTypeLabel,
)
if isActive.Valid && isActive.Bool {
var flushLSNMetric float64
@ -115,16 +178,27 @@ func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotCurrentFlushDesc,
prometheus.GaugeValue, flushLSNMetric, slotNameLabel,
prometheus.GaugeValue, flushLSNMetric, slotNameLabel, slotTypeLabel,
)
}
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotIsActiveDesc,
prometheus.GaugeValue, isActiveValue, slotNameLabel,
prometheus.GaugeValue, isActiveValue, slotNameLabel, slotTypeLabel,
)
if safeWalSize.Valid {
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotSafeWal,
prometheus.GaugeValue, float64(safeWalSize.Int64), slotNameLabel, slotTypeLabel,
)
}
if walStatus.Valid {
ch <- prometheus.MustNewConstMetric(
pgReplicationSlotWalStatus,
prometheus.GaugeValue, 1, slotNameLabel, slotTypeLabel, walStatus.String,
)
}
}
if err := rows.Err(); err != nil {
return err
}
return nil
return rows.Err()
}

View File

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

View File

@ -29,9 +29,11 @@ func TestPgReplicationCollector(t *testing.T) {
}
defer db.Close()
columns := []string{"lag", "is_replica"}
inst := &instance{db: db}
columns := []string{"lag", "is_replica", "last_replay"}
rows := sqlmock.NewRows(columns).
AddRow(1000, 1)
AddRow(1000, 1, 3)
mock.ExpectQuery(sanitizeQuery(pgReplicationQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
@ -39,7 +41,7 @@ func TestPgReplicationCollector(t *testing.T) {
defer close(ch)
c := PGReplicationCollector{}
if err := c.Update(context.Background(), db, ch); err != nil {
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGReplicationCollector.Update: %s", err)
}
}()
@ -47,6 +49,7 @@ func TestPgReplicationCollector(t *testing.T) {
expected := []MetricResult{
{labels: labelMap{}, value: 1000, metricType: dto.MetricType_GAUGE},
{labels: labelMap{}, value: 1, metricType: dto.MetricType_GAUGE},
{labels: labelMap{}, value: 3, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {

91
collector/pg_roles.go Normal file
View File

@ -0,0 +1,91 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const rolesSubsystem = "roles"
func init() {
registerCollector(rolesSubsystem, defaultEnabled, NewPGRolesCollector)
}
type PGRolesCollector struct {
log *slog.Logger
}
func NewPGRolesCollector(config collectorConfig) (Collector, error) {
return &PGRolesCollector{
log: config.logger,
}, nil
}
var (
pgRolesConnectionLimitsDesc = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
rolesSubsystem,
"connection_limit",
),
"Connection limit set for the role",
[]string{"rolname"}, nil,
)
pgRolesConnectionLimitsQuery = "SELECT pg_roles.rolname, pg_roles.rolconnlimit FROM pg_roles"
)
// Update implements Collector and exposes roles connection limits.
// It is called by the Prometheus registry when collecting metrics.
func (c PGRolesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
// Query the list of databases
rows, err := db.QueryContext(ctx,
pgRolesConnectionLimitsQuery,
)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var rolname sql.NullString
var connLimit sql.NullInt64
if err := rows.Scan(&rolname, &connLimit); err != nil {
return err
}
if !rolname.Valid {
continue
}
rolnameLabel := rolname.String
if !connLimit.Valid {
continue
}
connLimitMetric := float64(connLimit.Int64)
ch <- prometheus.MustNewConstMetric(
pgRolesConnectionLimitsDesc,
prometheus.GaugeValue, connLimitMetric, rolnameLabel,
)
}
return rows.Err()
}

View File

@ -0,0 +1,58 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGRolesCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
mock.ExpectQuery(sanitizeQuery(pgRolesConnectionLimitsQuery)).WillReturnRows(sqlmock.NewRows([]string{"rolname", "rolconnlimit"}).
AddRow("postgres", 15))
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGRolesCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGRolesCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"rolname": "postgres"}, value: 15, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -0,0 +1,84 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const statActivityAutovacuumSubsystem = "stat_activity_autovacuum"
func init() {
registerCollector(statActivityAutovacuumSubsystem, defaultDisabled, NewPGStatActivityAutovacuumCollector)
}
type PGStatActivityAutovacuumCollector struct {
log *slog.Logger
}
func NewPGStatActivityAutovacuumCollector(config collectorConfig) (Collector, error) {
return &PGStatActivityAutovacuumCollector{log: config.logger}, nil
}
var (
statActivityAutovacuumAgeInSeconds = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statActivityAutovacuumSubsystem, "timestamp_seconds"),
"Start timestamp of the vacuum process in seconds",
[]string{"relname"},
prometheus.Labels{},
)
statActivityAutovacuumQuery = `
SELECT
SPLIT_PART(query, '.', 2) AS relname,
EXTRACT(EPOCH FROM xact_start) AS timestamp_seconds
FROM
pg_catalog.pg_stat_activity
WHERE
query LIKE 'autovacuum:%'
`
)
func (PGStatActivityAutovacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
statActivityAutovacuumQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var relname string
var ageInSeconds float64
if err := rows.Scan(&relname, &ageInSeconds); err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(
statActivityAutovacuumAgeInSeconds,
prometheus.GaugeValue,
ageInSeconds, relname,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,62 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGStatActivityAutovacuumCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"relname",
"timestamp_seconds",
}
rows := sqlmock.NewRows(columns).
AddRow("test", 3600)
mock.ExpectQuery(sanitizeQuery(statActivityAutovacuumQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatActivityAutovacuumCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatActivityAutovacuumCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"relname": "test"}, value: 3600, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -17,6 +17,7 @@ import (
"context"
"database/sql"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -101,7 +102,7 @@ var (
prometheus.Labels{},
)
statBGWriterQuery = `SELECT
statBGWriterQueryBefore17 = `SELECT
checkpoints_timed
,checkpoints_req
,checkpoint_write_time
@ -114,121 +115,177 @@ var (
,buffers_alloc
,stats_reset
FROM pg_stat_bgwriter;`
statBGWriterQueryAfter17 = `SELECT
buffers_clean
,maxwritten_clean
,buffers_alloc
,stats_reset
FROM pg_stat_bgwriter;`
)
func (PGStatBGWriterCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
row := db.QueryRowContext(ctx,
statBGWriterQuery)
if instance.version.GE(semver.MustParse("17.0.0")) {
db := instance.getDB()
row := db.QueryRowContext(ctx, statBGWriterQueryAfter17)
var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64
var cpwt, cpst sql.NullFloat64
var sr sql.NullTime
var bc, mwc, ba sql.NullInt64
var sr sql.NullTime
err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
if err != nil {
return err
}
err := row.Scan(&bc, &mwc, &ba, &sr)
if err != nil {
return err
}
cptMetric := 0.0
if cpt.Valid {
cptMetric = float64(cpt.Int64)
bcMetric := 0.0
if bc.Valid {
bcMetric = float64(bc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCleanDesc,
prometheus.CounterValue,
bcMetric,
)
mwcMetric := 0.0
if mwc.Valid {
mwcMetric = float64(mwc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterMaxwrittenCleanDesc,
prometheus.CounterValue,
mwcMetric,
)
baMetric := 0.0
if ba.Valid {
baMetric = float64(ba.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersAllocDesc,
prometheus.CounterValue,
baMetric,
)
srMetric := 0.0
if sr.Valid {
srMetric = float64(sr.Time.Unix())
}
ch <- prometheus.MustNewConstMetric(
statBGWriterStatsResetDesc,
prometheus.CounterValue,
srMetric,
)
} else {
db := instance.getDB()
row := db.QueryRowContext(ctx, statBGWriterQueryBefore17)
var cpt, cpr, bcp, bc, mwc, bb, bbf, ba sql.NullInt64
var cpwt, cpst sql.NullFloat64
var sr sql.NullTime
err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr)
if err != nil {
return err
}
cptMetric := 0.0
if cpt.Valid {
cptMetric = float64(cpt.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsTimedDesc,
prometheus.CounterValue,
cptMetric,
)
cprMetric := 0.0
if cpr.Valid {
cprMetric = float64(cpr.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsReqDesc,
prometheus.CounterValue,
cprMetric,
)
cpwtMetric := 0.0
if cpwt.Valid {
cpwtMetric = float64(cpwt.Float64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsReqTimeDesc,
prometheus.CounterValue,
cpwtMetric,
)
cpstMetric := 0.0
if cpst.Valid {
cpstMetric = float64(cpst.Float64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsSyncTimeDesc,
prometheus.CounterValue,
cpstMetric,
)
bcpMetric := 0.0
if bcp.Valid {
bcpMetric = float64(bcp.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCheckpointDesc,
prometheus.CounterValue,
bcpMetric,
)
bcMetric := 0.0
if bc.Valid {
bcMetric = float64(bc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCleanDesc,
prometheus.CounterValue,
bcMetric,
)
mwcMetric := 0.0
if mwc.Valid {
mwcMetric = float64(mwc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterMaxwrittenCleanDesc,
prometheus.CounterValue,
mwcMetric,
)
bbMetric := 0.0
if bb.Valid {
bbMetric = float64(bb.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendDesc,
prometheus.CounterValue,
bbMetric,
)
bbfMetric := 0.0
if bbf.Valid {
bbfMetric = float64(bbf.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendFsyncDesc,
prometheus.CounterValue,
bbfMetric,
)
baMetric := 0.0
if ba.Valid {
baMetric = float64(ba.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersAllocDesc,
prometheus.CounterValue,
baMetric,
)
srMetric := 0.0
if sr.Valid {
srMetric = float64(sr.Time.Unix())
}
ch <- prometheus.MustNewConstMetric(
statBGWriterStatsResetDesc,
prometheus.CounterValue,
srMetric,
)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsTimedDesc,
prometheus.CounterValue,
cptMetric,
)
cprMetric := 0.0
if cpr.Valid {
cprMetric = float64(cpr.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsReqDesc,
prometheus.CounterValue,
cprMetric,
)
cpwtMetric := 0.0
if cpwt.Valid {
cpwtMetric = float64(cpwt.Float64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsReqTimeDesc,
prometheus.CounterValue,
cpwtMetric,
)
cpstMetric := 0.0
if cpst.Valid {
cpstMetric = float64(cpst.Float64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterCheckpointsSyncTimeDesc,
prometheus.CounterValue,
cpstMetric,
)
bcpMetric := 0.0
if bcp.Valid {
bcpMetric = float64(bcp.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCheckpointDesc,
prometheus.CounterValue,
bcpMetric,
)
bcMetric := 0.0
if bc.Valid {
bcMetric = float64(bc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersCleanDesc,
prometheus.CounterValue,
bcMetric,
)
mwcMetric := 0.0
if mwc.Valid {
mwcMetric = float64(mwc.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterMaxwrittenCleanDesc,
prometheus.CounterValue,
mwcMetric,
)
bbMetric := 0.0
if bb.Valid {
bbMetric = float64(bb.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendDesc,
prometheus.CounterValue,
bbMetric,
)
bbfMetric := 0.0
if bbf.Valid {
bbfMetric = float64(bbf.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersBackendFsyncDesc,
prometheus.CounterValue,
bbfMetric,
)
baMetric := 0.0
if ba.Valid {
baMetric = float64(ba.Int64)
}
ch <- prometheus.MustNewConstMetric(
statBGWriterBuffersAllocDesc,
prometheus.CounterValue,
baMetric,
)
srMetric := 0.0
if sr.Valid {
srMetric = float64(sr.Time.Unix())
}
ch <- prometheus.MustNewConstMetric(
statBGWriterStatsResetDesc,
prometheus.CounterValue,
srMetric,
)
return nil
}

View File

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

View File

@ -0,0 +1,231 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
const statCheckpointerSubsystem = "stat_checkpointer"
func init() {
// WARNING:
// Disabled by default because this set of metrics is only available from Postgres 17
registerCollector(statCheckpointerSubsystem, defaultDisabled, NewPGStatCheckpointerCollector)
}
type PGStatCheckpointerCollector struct {
log *slog.Logger
}
func NewPGStatCheckpointerCollector(config collectorConfig) (Collector, error) {
return &PGStatCheckpointerCollector{log: config.logger}, nil
}
var (
statCheckpointerNumTimedDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_timed_total"),
"Number of scheduled checkpoints due to timeout",
[]string{},
prometheus.Labels{},
)
statCheckpointerNumRequestedDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "num_requested_total"),
"Number of requested checkpoints that have been performed",
[]string{},
prometheus.Labels{},
)
statCheckpointerRestartpointsTimedDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_timed_total"),
"Number of scheduled restartpoints due to timeout or after a failed attempt to perform it",
[]string{},
prometheus.Labels{},
)
statCheckpointerRestartpointsReqDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_req_total"),
"Number of requested restartpoints",
[]string{},
prometheus.Labels{},
)
statCheckpointerRestartpointsDoneDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "restartpoints_done_total"),
"Number of restartpoints that have been performed",
[]string{},
prometheus.Labels{},
)
statCheckpointerWriteTimeDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "write_time_total"),
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are written to disk, in milliseconds",
[]string{},
prometheus.Labels{},
)
statCheckpointerSyncTimeDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "sync_time_total"),
"Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are synchronized to disk, in milliseconds",
[]string{},
prometheus.Labels{},
)
statCheckpointerBuffersWrittenDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "buffers_written_total"),
"Number of buffers written during checkpoints and restartpoints",
[]string{},
prometheus.Labels{},
)
statCheckpointerStatsResetDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statCheckpointerSubsystem, "stats_reset_total"),
"Time at which these statistics were last reset",
[]string{},
prometheus.Labels{},
)
statCheckpointerQuery = `SELECT
num_timed
,num_requested
,restartpoints_timed
,restartpoints_req
,restartpoints_done
,write_time
,sync_time
,buffers_written
,stats_reset
FROM pg_stat_checkpointer;`
)
func (c PGStatCheckpointerCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
before17 := instance.version.LT(semver.MustParse("17.0.0"))
if before17 {
c.log.Warn("pg_stat_checkpointer collector is not available on PostgreSQL < 17.0.0, skipping")
return nil
}
row := db.QueryRowContext(ctx, statCheckpointerQuery)
// num_timed = nt = bigint
// num_requested = nr = bigint
// restartpoints_timed = rpt = bigint
// restartpoints_req = rpr = bigint
// restartpoints_done = rpd = bigint
// write_time = wt = double precision
// sync_time = st = double precision
// buffers_written = bw = bigint
// stats_reset = sr = timestamp
var nt, nr, rpt, rpr, rpd, bw sql.NullInt64
var wt, st sql.NullFloat64
var sr sql.NullTime
err := row.Scan(&nt, &nr, &rpt, &rpr, &rpd, &wt, &st, &bw, &sr)
if err != nil {
return err
}
ntMetric := 0.0
if nt.Valid {
ntMetric = float64(nt.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerNumTimedDesc,
prometheus.CounterValue,
ntMetric,
)
nrMetric := 0.0
if nr.Valid {
nrMetric = float64(nr.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerNumRequestedDesc,
prometheus.CounterValue,
nrMetric,
)
rptMetric := 0.0
if rpt.Valid {
rptMetric = float64(rpt.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsTimedDesc,
prometheus.CounterValue,
rptMetric,
)
rprMetric := 0.0
if rpr.Valid {
rprMetric = float64(rpr.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsReqDesc,
prometheus.CounterValue,
rprMetric,
)
rpdMetric := 0.0
if rpd.Valid {
rpdMetric = float64(rpd.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerRestartpointsDoneDesc,
prometheus.CounterValue,
rpdMetric,
)
wtMetric := 0.0
if wt.Valid {
wtMetric = float64(wt.Float64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerWriteTimeDesc,
prometheus.CounterValue,
wtMetric,
)
stMetric := 0.0
if st.Valid {
stMetric = float64(st.Float64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerSyncTimeDesc,
prometheus.CounterValue,
stMetric,
)
bwMetric := 0.0
if bw.Valid {
bwMetric = float64(bw.Int64)
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerBuffersWrittenDesc,
prometheus.CounterValue,
bwMetric,
)
srMetric := 0.0
if sr.Valid {
srMetric = float64(sr.Time.Unix())
}
ch <- prometheus.MustNewConstMetric(
statCheckpointerStatsResetDesc,
prometheus.CounterValue,
srMetric,
)
return nil
}

View File

@ -0,0 +1,144 @@
// Copyright 2024 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGStatCheckpointerCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
columns := []string{
"num_timed",
"num_requested",
"restartpoints_timed",
"restartpoints_req",
"restartpoints_done",
"write_time",
"sync_time",
"buffers_written",
"stats_reset"}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
if err != nil {
t.Fatalf("Error parsing time: %s", err)
}
rows := sqlmock.NewRows(columns).
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, srT)
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatCheckpointerCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 354},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 4945},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 289097744},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1242257},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 3275602074},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 89320867},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 450139},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 2034563757},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 1685059842},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatCheckpointerCollectorNullValues(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
columns := []string{
"num_timed",
"num_requested",
"restartpoints_timed",
"restartpoints_req",
"restartpoints_done",
"write_time",
"sync_time",
"buffers_written",
"stats_reset"}
rows := sqlmock.NewRows(columns).
AddRow(nil, nil, nil, nil, nil, nil, nil, nil, nil)
mock.ExpectQuery(sanitizeQuery(statCheckpointerQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatCheckpointerCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatCheckpointerCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{}, metricType: dto.MetricType_COUNTER, value: 0},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

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

View File

@ -18,8 +18,10 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/promslog"
"github.com/smartystreets/goconvey/convey"
)
@ -30,7 +32,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
@ -52,6 +54,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
@ -67,26 +70,30 @@ func TestPGStatDatabaseCollector(t *testing.T) {
4945,
289097744,
1242257,
3275602074,
int64(3275602074),
89320867,
450139,
2034563757,
0,
2725688749,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT)
srT,
33,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{}
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
@ -111,6 +118,7 @@ func TestPGStatDatabaseCollector(t *testing.T) {
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.033},
}
convey.Convey("Metrics comparison", t, func() {
@ -131,7 +139,11 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
if err != nil {
t.Fatalf("Error parsing time: %s", err)
}
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
@ -153,109 +165,32 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
rows := sqlmock.NewRows(columns).
AddRow(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"datid",
"datname",
"numbackends",
"xact_commit",
"xact_rollback",
"blks_read",
"blks_hit",
"tup_returned",
"tup_fetched",
"tup_inserted",
"tup_updated",
"tup_deleted",
"conflicts",
"temp_files",
"temp_bytes",
"deadlocks",
"blk_read_time",
"blk_write_time",
"stats_reset",
}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
if err != nil {
t.Fatalf("Error parsing time: %s", err)
}
rows := sqlmock.NewRows(columns).
"postgres",
354,
4945,
289097744,
1242257,
int64(3275602074),
89320867,
450139,
2034563757,
0,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT,
32,
).
AddRow(
"pid",
"postgres",
@ -263,47 +198,29 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
4945,
289097744,
1242257,
3275602074,
int64(3275602074),
89320867,
450139,
2034563757,
0,
2725688749,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT).
AddRow(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
srT,
32,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{}
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
@ -328,23 +245,277 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "unknown", "datname": "unknown"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.032},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
"datname",
"numbackends",
"xact_commit",
"xact_rollback",
"blks_read",
"blks_hit",
"tup_returned",
"tup_fetched",
"tup_inserted",
"tup_updated",
"tup_deleted",
"conflicts",
"temp_files",
"temp_bytes",
"deadlocks",
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
if err != nil {
t.Fatalf("Error parsing time: %s", err)
}
rows := sqlmock.NewRows(columns).
AddRow(
"pid",
"postgres",
354,
4945,
289097744,
1242257,
int64(3275602074),
89320867,
450139,
2034563757,
0,
int64(2725688749),
23,
52,
74,
925,
16,
823,
srT,
14,
).
AddRow(
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
).
AddRow(
"pid",
"postgres",
355,
4946,
289097745,
1242258,
int64(3275602075),
89320868,
450140,
2034563758,
1,
int64(2725688750),
24,
53,
75,
926,
17,
824,
srT,
15,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.014},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 355},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4946},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097745},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242258},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602075},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320868},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450140},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563758},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688750},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 24},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 53},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 75},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 926},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 17},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 824},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.015},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
"datname",
"numbackends",
"xact_commit",
"xact_rollback",
"blks_read",
"blks_hit",
"tup_returned",
"tup_fetched",
"tup_inserted",
"tup_updated",
"tup_deleted",
"conflicts",
"temp_files",
"temp_bytes",
"deadlocks",
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
rows := sqlmock.NewRows(columns).
AddRow(
"pid",
"postgres",
354,
4945,
289097744,
1242257,
int64(3275602074),
89320867,
450139,
2034563757,
0,
int64(2725688749),
23,
52,
74,
925,
16,
823,
nil,
7,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatDatabaseCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 354},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4945},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097744},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1242257},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 3275602074},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 89320867},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 450139},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2034563757},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 2725688749},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 23},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 52},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 74},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 925},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.007},
}
convey.Convey("Metrics comparison", t, func() {

View File

@ -0,0 +1,222 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
const progressVacuumSubsystem = "stat_progress_vacuum"
func init() {
registerCollector(progressVacuumSubsystem, defaultEnabled, NewPGStatProgressVacuumCollector)
}
type PGStatProgressVacuumCollector struct {
log *slog.Logger
}
func NewPGStatProgressVacuumCollector(config collectorConfig) (Collector, error) {
return &PGStatProgressVacuumCollector{log: config.logger}, nil
}
var vacuumPhases = []string{
"initializing",
"scanning heap",
"vacuuming indexes",
"vacuuming heap",
"cleaning up indexes",
"truncating heap",
"performing final cleanup",
}
var (
statProgressVacuumPhase = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "phase"),
"Current vacuum phase (1 = active, 0 = inactive). Label 'phase' is human-readable.",
[]string{"datname", "relname", "phase"},
nil,
)
statProgressVacuumHeapBlksTotal = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks"),
"Total number of heap blocks in the table being vacuumed.",
[]string{"datname", "relname"},
nil,
)
statProgressVacuumHeapBlksScanned = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks_scanned"),
"Number of heap blocks scanned so far.",
[]string{"datname", "relname"},
nil,
)
statProgressVacuumHeapBlksVacuumed = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "heap_blks_vacuumed"),
"Number of heap blocks vacuumed so far.",
[]string{"datname", "relname"},
nil,
)
statProgressVacuumIndexVacuumCount = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "index_vacuums"),
"Number of completed index vacuum cycles.",
[]string{"datname", "relname"},
nil,
)
statProgressVacuumMaxDeadTuples = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "max_dead_tuples"),
"Maximum number of dead tuples that can be stored before cleanup is performed.",
[]string{"datname", "relname"},
nil,
)
statProgressVacuumNumDeadTuples = prometheus.NewDesc(
prometheus.BuildFQName(namespace, progressVacuumSubsystem, "num_dead_tuples"),
"Current number of dead tuples found so far.",
[]string{"datname", "relname"},
nil,
)
// This is the view definition of pg_stat_progress_vacuum, albeit without the conversion
// of "phase" to a human-readable string. We will prefer the numeric representation.
statProgressVacuumQuery = `SELECT
d.datname,
s.relid::regclass::text AS relname,
s.param1 AS phase,
s.param2 AS heap_blks_total,
s.param3 AS heap_blks_scanned,
s.param4 AS heap_blks_vacuumed,
s.param5 AS index_vacuum_count,
s.param6 AS max_dead_tuples,
s.param7 AS num_dead_tuples
FROM
pg_stat_get_progress_info('VACUUM'::text)
s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
LEFT JOIN
pg_database d ON s.datid = d.oid`
)
func (c *PGStatProgressVacuumCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
statProgressVacuumQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
datname sql.NullString
relname sql.NullString
phase sql.NullInt64
heapBlksTotal sql.NullInt64
heapBlksScanned sql.NullInt64
heapBlksVacuumed sql.NullInt64
indexVacuumCount sql.NullInt64
maxDeadTuples sql.NullInt64
numDeadTuples sql.NullInt64
)
if err := rows.Scan(
&datname,
&relname,
&phase,
&heapBlksTotal,
&heapBlksScanned,
&heapBlksVacuumed,
&indexVacuumCount,
&maxDeadTuples,
&numDeadTuples,
); err != nil {
return err
}
datnameLabel := "unknown"
if datname.Valid {
datnameLabel = datname.String
}
relnameLabel := "unknown"
if relname.Valid {
relnameLabel = relname.String
}
labels := []string{datnameLabel, relnameLabel}
var phaseMetric *float64
if phase.Valid {
v := float64(phase.Int64)
phaseMetric = &v
}
for i, label := range vacuumPhases {
v := 0.0
// Only the current phase should be 1.0.
if phaseMetric != nil && float64(i) == *phaseMetric {
v = 1.0
}
labelsCopy := append(labels, label)
ch <- prometheus.MustNewConstMetric(statProgressVacuumPhase, prometheus.GaugeValue, v, labelsCopy...)
}
heapTotal := 0.0
if heapBlksTotal.Valid {
heapTotal = float64(heapBlksTotal.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksTotal, prometheus.GaugeValue, heapTotal, labels...)
heapScanned := 0.0
if heapBlksScanned.Valid {
heapScanned = float64(heapBlksScanned.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksScanned, prometheus.GaugeValue, heapScanned, labels...)
heapVacuumed := 0.0
if heapBlksVacuumed.Valid {
heapVacuumed = float64(heapBlksVacuumed.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumHeapBlksVacuumed, prometheus.GaugeValue, heapVacuumed, labels...)
indexCount := 0.0
if indexVacuumCount.Valid {
indexCount = float64(indexVacuumCount.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumIndexVacuumCount, prometheus.GaugeValue, indexCount, labels...)
maxDead := 0.0
if maxDeadTuples.Valid {
maxDead = float64(maxDeadTuples.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumMaxDeadTuples, prometheus.GaugeValue, maxDead, labels...)
numDead := 0.0
if numDeadTuples.Valid {
numDead = float64(numDeadTuples.Int64)
}
ch <- prometheus.MustNewConstMetric(statProgressVacuumNumDeadTuples, prometheus.GaugeValue, numDead, labels...)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,135 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGStatProgressVacuumCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"datname", "relname", "phase", "heap_blks_total", "heap_blks_scanned",
"heap_blks_vacuumed", "index_vacuum_count", "max_dead_tuples", "num_dead_tuples",
}
rows := sqlmock.NewRows(columns).AddRow(
"postgres", "a_table", 3, 3000, 400, 200, 2, 500, 123)
mock.ExpectQuery(sanitizeQuery(statProgressVacuumQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatProgressVacuumCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatProgressVacuumCollector.Update; %+v", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "initializing"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "scanning heap"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "vacuuming indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "vacuuming heap"}, metricType: dto.MetricType_GAUGE, value: 1},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "cleaning up indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "truncating heap"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table", "phase": "performing final cleanup"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 3000},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 400},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 200},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 2},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 500},
{labels: labelMap{"datname": "postgres", "relname": "a_table"}, metricType: dto.MetricType_GAUGE, value: 123},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(m, convey.ShouldResemble, expect)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("There were unfulfilled exceptions: %+v", err)
}
}
func TestPGStatProgressVacuumCollectorNullValues(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"datname", "relname", "phase", "heap_blks_total", "heap_blks_scanned",
"heap_blks_vacuumed", "index_vacuum_count", "max_dead_tuples", "num_dead_tuples",
}
rows := sqlmock.NewRows(columns).AddRow(
"postgres", nil, nil, nil, nil, nil, nil, nil, nil)
mock.ExpectQuery(sanitizeQuery(statProgressVacuumQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatProgressVacuumCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatProgressVacuumCollector.Update; %+v", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "initializing"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "scanning heap"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "vacuuming indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "vacuuming heap"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "cleaning up indexes"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "truncating heap"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown", "phase": "performing final cleanup"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
{labels: labelMap{"datname": "postgres", "relname": "unknown"}, metricType: dto.MetricType_GAUGE, value: 0},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("There were unfulfilled exceptions: %+v", err)
}
}

View File

@ -16,8 +16,9 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -31,7 +32,7 @@ func init() {
}
type PGStatStatementsCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGStatStatementsCollector(config collectorConfig) (Collector, error) {
@ -90,12 +91,63 @@ var (
)
ORDER BY seconds_total DESC
LIMIT 100;`
pgStatStatementsNewQuery = `SELECT
pg_get_userbyid(userid) as user,
pg_database.datname,
pg_stat_statements.queryid,
pg_stat_statements.calls as calls_total,
pg_stat_statements.total_exec_time / 1000.0 as seconds_total,
pg_stat_statements.rows as rows_total,
pg_stat_statements.blk_read_time / 1000.0 as block_read_seconds_total,
pg_stat_statements.blk_write_time / 1000.0 as block_write_seconds_total
FROM pg_stat_statements
JOIN pg_database
ON pg_database.oid = pg_stat_statements.dbid
WHERE
total_exec_time > (
SELECT percentile_cont(0.1)
WITHIN GROUP (ORDER BY total_exec_time)
FROM pg_stat_statements
)
ORDER BY seconds_total DESC
LIMIT 100;`
pgStatStatementsQuery_PG17 = `SELECT
pg_get_userbyid(userid) as user,
pg_database.datname,
pg_stat_statements.queryid,
pg_stat_statements.calls as calls_total,
pg_stat_statements.total_exec_time / 1000.0 as seconds_total,
pg_stat_statements.rows as rows_total,
pg_stat_statements.shared_blk_read_time / 1000.0 as block_read_seconds_total,
pg_stat_statements.shared_blk_write_time / 1000.0 as block_write_seconds_total
FROM pg_stat_statements
JOIN pg_database
ON pg_database.oid = pg_stat_statements.dbid
WHERE
total_exec_time > (
SELECT percentile_cont(0.1)
WITHIN GROUP (ORDER BY total_exec_time)
FROM pg_stat_statements
)
ORDER BY seconds_total DESC
LIMIT 100;`
)
func (PGStatStatementsCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
var query string
switch {
case instance.version.GE(semver.MustParse("17.0.0")):
query = pgStatStatementsQuery_PG17
case instance.version.GE(semver.MustParse("13.0.0")):
query = pgStatStatementsNewQuery
default:
query = pgStatStatementsQuery
}
db := instance.getDB()
rows, err := db.QueryContext(ctx,
pgStatStatementsQuery)
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err

View File

@ -17,6 +17,7 @@ import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
@ -29,7 +30,7 @@ func TestPGStateStatementsCollector(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("12.0.0")}
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
rows := sqlmock.NewRows(columns).
@ -72,12 +73,12 @@ func TestPGStateStatementsCollectorNull(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
rows := sqlmock.NewRows(columns).
AddRow(nil, nil, nil, nil, nil, nil, nil, nil)
mock.ExpectQuery(sanitizeQuery(pgStatStatementsQuery)).WillReturnRows(rows)
mock.ExpectQuery(sanitizeQuery(pgStatStatementsNewQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -107,3 +108,89 @@ func TestPGStateStatementsCollectorNull(t *testing.T) {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStateStatementsCollectorNewPG(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("13.3.7")}
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
rows := sqlmock.NewRows(columns).
AddRow("postgres", "postgres", 1500, 5, 0.4, 100, 0.1, 0.2)
mock.ExpectQuery(sanitizeQuery(pgStatStatementsNewQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatStatementsCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatStatementsCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 5},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.4},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 100},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.1},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.2},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStateStatementsCollector_PG17(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db, version: semver.MustParse("17.0.0")}
columns := []string{"user", "datname", "queryid", "calls_total", "seconds_total", "rows_total", "block_read_seconds_total", "block_write_seconds_total"}
rows := sqlmock.NewRows(columns).
AddRow("postgres", "postgres", 1500, 5, 0.4, 100, 0.1, 0.2)
mock.ExpectQuery(sanitizeQuery(pgStatStatementsQuery_PG17)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatStatementsCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatStatementsCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 5},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.4},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 100},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.1},
{labels: labelMap{"user": "postgres", "datname": "postgres", "queryid": "1500"}, metricType: dto.MetricType_COUNTER, value: 0.2},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

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 log.Logger
log *slog.Logger
}
func NewPGStatUserTablesCollector(config collectorConfig) (Collector, error) {
@ -150,6 +150,12 @@ var (
[]string{"datname", "schemaname", "relname"},
prometheus.Labels{},
)
statUserTablesTotalSize = prometheus.NewDesc(
prometheus.BuildFQName(namespace, userTableSubsystem, "size_bytes"),
"Total disk space used by this table, in bytes, including all indexes and TOAST data",
[]string{"datname", "schemaname", "relname"},
prometheus.Labels{},
)
statUserTablesQuery = `SELECT
current_database() datname,
@ -173,7 +179,8 @@ var (
vacuum_count,
autovacuum_count,
analyze_count,
autoanalyze_count
autoanalyze_count,
pg_total_relation_size(relid) as total_size
FROM
pg_stat_user_tables`
)
@ -191,10 +198,10 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
for rows.Next() {
var datname, schemaname, relname sql.NullString
var seqScan, seqTupRead, idxScan, idxTupFetch, nTupIns, nTupUpd, nTupDel, nTupHotUpd, nLiveTup, nDeadTup,
nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount sql.NullInt64
nModSinceAnalyze, vacuumCount, autovacuumCount, analyzeCount, autoanalyzeCount, totalSize sql.NullInt64
var lastVacuum, lastAutovacuum, lastAnalyze, lastAutoanalyze sql.NullTime
if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount); err != nil {
if err := rows.Scan(&datname, &schemaname, &relname, &seqScan, &seqTupRead, &idxScan, &idxTupFetch, &nTupIns, &nTupUpd, &nTupDel, &nTupHotUpd, &nLiveTup, &nDeadTup, &nModSinceAnalyze, &lastVacuum, &lastAutovacuum, &lastAnalyze, &lastAutoanalyze, &vacuumCount, &autovacuumCount, &analyzeCount, &autoanalyzeCount, &totalSize); err != nil {
return err
}
@ -419,6 +426,17 @@ func (c *PGStatUserTablesCollector) Update(ctx context.Context, instance *instan
autoanalyzeCountMetric,
datnameLabel, schemanameLabel, relnameLabel,
)
totalSizeMetric := 0.0
if totalSize.Valid {
totalSizeMetric = float64(totalSize.Int64)
}
ch <- prometheus.MustNewConstMetric(
statUserTablesTotalSize,
prometheus.GaugeValue,
totalSizeMetric,
datnameLabel, schemanameLabel, relnameLabel,
)
}
if err := rows.Err(); err != nil {

View File

@ -71,7 +71,8 @@ func TestPGStatUserTablesCollector(t *testing.T) {
"vacuum_count",
"autovacuum_count",
"analyze_count",
"autoanalyze_count"}
"autoanalyze_count",
"total_size"}
rows := sqlmock.NewRows(columns).
AddRow("postgres",
"public",
@ -94,7 +95,8 @@ func TestPGStatUserTablesCollector(t *testing.T) {
11,
12,
13,
14)
14,
15)
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
@ -170,7 +172,8 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
"vacuum_count",
"autovacuum_count",
"analyze_count",
"autoanalyze_count"}
"autoanalyze_count",
"total_size"}
rows := sqlmock.NewRows(columns).
AddRow("postgres",
nil,
@ -193,6 +196,7 @@ func TestPGStatUserTablesCollectorNullValues(t *testing.T) {
nil,
nil,
nil,
nil,
nil)
mock.ExpectQuery(sanitizeQuery(statUserTablesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)

View File

@ -0,0 +1,270 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"fmt"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
func init() {
registerCollector(statWalReceiverSubsystem, defaultDisabled, NewPGStatWalReceiverCollector)
}
type PGStatWalReceiverCollector struct {
log *slog.Logger
}
const statWalReceiverSubsystem = "stat_wal_receiver"
func NewPGStatWalReceiverCollector(config collectorConfig) (Collector, error) {
return &PGStatWalReceiverCollector{log: config.logger}, nil
}
var (
labelCats = []string{"upstream_host", "slot_name", "status"}
statWalReceiverReceiveStartLsn = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "receive_start_lsn"),
"First write-ahead log location used when WAL receiver is started represented as a decimal",
labelCats,
prometheus.Labels{},
)
statWalReceiverReceiveStartTli = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "receive_start_tli"),
"First timeline number used when WAL receiver is started",
labelCats,
prometheus.Labels{},
)
statWalReceiverFlushedLSN = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "flushed_lsn"),
"Last write-ahead log location already received and flushed to disk, the initial value of this field being the first log location used when WAL receiver is started represented as a decimal",
labelCats,
prometheus.Labels{},
)
statWalReceiverReceivedTli = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "received_tli"),
"Timeline number of last write-ahead log location received and flushed to disk",
labelCats,
prometheus.Labels{},
)
statWalReceiverLastMsgSendTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "last_msg_send_time"),
"Send time of last message received from origin WAL sender",
labelCats,
prometheus.Labels{},
)
statWalReceiverLastMsgReceiptTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "last_msg_receipt_time"),
"Send time of last message received from origin WAL sender",
labelCats,
prometheus.Labels{},
)
statWalReceiverLatestEndLsn = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "latest_end_lsn"),
"Last write-ahead log location reported to origin WAL sender as integer",
labelCats,
prometheus.Labels{},
)
statWalReceiverLatestEndTime = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "latest_end_time"),
"Time of last write-ahead log location reported to origin WAL sender",
labelCats,
prometheus.Labels{},
)
statWalReceiverUpstreamNode = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statWalReceiverSubsystem, "upstream_node"),
"Node ID of the upstream node",
labelCats,
prometheus.Labels{},
)
pgStatWalColumnQuery = `
SELECT
column_name
FROM information_schema.columns
WHERE
table_name = 'pg_stat_wal_receiver' and
column_name = 'flushed_lsn'
`
pgStatWalReceiverQueryTemplate = `
SELECT
trim(both '''' from substring(conninfo from 'host=([^ ]*)')) as upstream_host,
slot_name,
status,
(receive_start_lsn- '0/0') %% (2^52)::bigint as receive_start_lsn,
%s
receive_start_tli,
received_tli,
extract(epoch from last_msg_send_time) as last_msg_send_time,
extract(epoch from last_msg_receipt_time) as last_msg_receipt_time,
(latest_end_lsn - '0/0') %% (2^52)::bigint as latest_end_lsn,
extract(epoch from latest_end_time) as latest_end_time,
substring(slot_name from 'repmgr_slot_([0-9]*)') as upstream_node
FROM pg_catalog.pg_stat_wal_receiver
`
)
func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
hasFlushedLSNRows, err := db.QueryContext(ctx, pgStatWalColumnQuery)
if err != nil {
return err
}
hasFlushedLSN := hasFlushedLSNRows.Next()
var query string
if hasFlushedLSN {
query = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "(flushed_lsn - '0/0') % (2^52)::bigint as flushed_lsn,\n")
} else {
query = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "")
}
hasFlushedLSNRows.Close()
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var upstreamHost, slotName, status sql.NullString
var receiveStartLsn, receiveStartTli, flushedLsn, receivedTli, latestEndLsn, upstreamNode sql.NullInt64
var lastMsgSendTime, lastMsgReceiptTime, latestEndTime sql.NullFloat64
if hasFlushedLSN {
if err := rows.Scan(&upstreamHost, &slotName, &status, &receiveStartLsn, &receiveStartTli, &flushedLsn, &receivedTli, &lastMsgSendTime, &lastMsgReceiptTime, &latestEndLsn, &latestEndTime, &upstreamNode); err != nil {
return err
}
} else {
if err := rows.Scan(&upstreamHost, &slotName, &status, &receiveStartLsn, &receiveStartTli, &receivedTli, &lastMsgSendTime, &lastMsgReceiptTime, &latestEndLsn, &latestEndTime, &upstreamNode); err != nil {
return err
}
}
if !upstreamHost.Valid {
c.log.Debug("Skipping wal receiver stats because upstream host is null")
continue
}
if !slotName.Valid {
c.log.Debug("Skipping wal receiver stats because slotname host is null")
continue
}
if !status.Valid {
c.log.Debug("Skipping wal receiver stats because status is null")
continue
}
labels := []string{upstreamHost.String, slotName.String, status.String}
if !receiveStartLsn.Valid {
c.log.Debug("Skipping wal receiver stats because receive_start_lsn is null")
continue
}
if !receiveStartTli.Valid {
c.log.Debug("Skipping wal receiver stats because receive_start_tli is null")
continue
}
if hasFlushedLSN && !flushedLsn.Valid {
c.log.Debug("Skipping wal receiver stats because flushed_lsn is null")
continue
}
if !receivedTli.Valid {
c.log.Debug("Skipping wal receiver stats because received_tli is null")
continue
}
if !lastMsgSendTime.Valid {
c.log.Debug("Skipping wal receiver stats because last_msg_send_time is null")
continue
}
if !lastMsgReceiptTime.Valid {
c.log.Debug("Skipping wal receiver stats because last_msg_receipt_time is null")
continue
}
if !latestEndLsn.Valid {
c.log.Debug("Skipping wal receiver stats because latest_end_lsn is null")
continue
}
if !latestEndTime.Valid {
c.log.Debug("Skipping wal receiver stats because latest_end_time is null")
continue
}
ch <- prometheus.MustNewConstMetric(
statWalReceiverReceiveStartLsn,
prometheus.CounterValue,
float64(receiveStartLsn.Int64),
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverReceiveStartTli,
prometheus.GaugeValue,
float64(receiveStartTli.Int64),
labels...)
if hasFlushedLSN {
ch <- prometheus.MustNewConstMetric(
statWalReceiverFlushedLSN,
prometheus.CounterValue,
float64(flushedLsn.Int64),
labels...)
}
ch <- prometheus.MustNewConstMetric(
statWalReceiverReceivedTli,
prometheus.GaugeValue,
float64(receivedTli.Int64),
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverLastMsgSendTime,
prometheus.CounterValue,
float64(lastMsgSendTime.Float64),
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverLastMsgReceiptTime,
prometheus.CounterValue,
float64(lastMsgReceiptTime.Float64),
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverLatestEndLsn,
prometheus.CounterValue,
float64(latestEndLsn.Int64),
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverLatestEndTime,
prometheus.CounterValue,
latestEndTime.Float64,
labels...)
if !upstreamNode.Valid {
c.log.Debug("Skipping wal receiver stats upstream_node because it is null")
} else {
ch <- prometheus.MustNewConstMetric(
statWalReceiverUpstreamNode,
prometheus.GaugeValue,
float64(upstreamNode.Int64),
labels...)
}
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,186 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
var queryWithFlushedLSN = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "(flushed_lsn - '0/0') % (2^52)::bigint as flushed_lsn,\n")
var queryWithNoFlushedLSN = fmt.Sprintf(pgStatWalReceiverQueryTemplate, "")
func TestPGStatWalReceiverCollectorWithFlushedLSN(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
infoSchemaColumns := []string{
"column_name",
}
infoSchemaRows := sqlmock.NewRows(infoSchemaColumns).
AddRow(
"flushed_lsn",
)
mock.ExpectQuery(sanitizeQuery(pgStatWalColumnQuery)).WillReturnRows(infoSchemaRows)
columns := []string{
"upstream_host",
"slot_name",
"status",
"receive_start_lsn",
"receive_start_tli",
"flushed_lsn",
"received_tli",
"last_msg_send_time",
"last_msg_receipt_time",
"latest_end_lsn",
"latest_end_time",
"upstream_node",
}
rows := sqlmock.NewRows(columns).
AddRow(
"foo",
"bar",
"stopping",
int64(1200668684563608),
1687321285,
int64(1200668684563609),
1687321280,
1687321275,
1687321276,
int64(1200668684563610),
1687321277,
5,
)
mock.ExpectQuery(sanitizeQuery(queryWithFlushedLSN)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatWalReceiverCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PgStatWalReceiverCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563608, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321285, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563609, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321280, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321275, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321276, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1200668684563610, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 1687321277, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "stopping"}, value: 5, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPGStatWalReceiverCollectorWithNoFlushedLSN(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
infoSchemaColumns := []string{
"column_name",
}
infoSchemaRows := sqlmock.NewRows(infoSchemaColumns)
mock.ExpectQuery(sanitizeQuery(pgStatWalColumnQuery)).WillReturnRows(infoSchemaRows)
columns := []string{
"upstream_host",
"slot_name",
"status",
"receive_start_lsn",
"receive_start_tli",
"received_tli",
"last_msg_send_time",
"last_msg_receipt_time",
"latest_end_lsn",
"latest_end_time",
"upstream_node",
}
rows := sqlmock.NewRows(columns).
AddRow(
"foo",
"bar",
"starting",
int64(1200668684563608),
1687321285,
1687321280,
1687321275,
1687321276,
int64(1200668684563610),
1687321277,
5,
)
mock.ExpectQuery(sanitizeQuery(queryWithNoFlushedLSN)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatWalReceiverCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PgStatWalReceiverCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1200668684563608, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321285, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321280, metricType: dto.MetricType_GAUGE},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321275, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321276, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1200668684563610, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 1687321277, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"upstream_host": "foo", "slot_name": "bar", "status": "starting"}, value: 5, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -0,0 +1,118 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/prometheus/client_golang/prometheus"
)
func init() {
registerCollector(statioUserIndexesSubsystem, defaultDisabled, NewPGStatioUserIndexesCollector)
}
type PGStatioUserIndexesCollector struct {
log *slog.Logger
}
const statioUserIndexesSubsystem = "statio_user_indexes"
func NewPGStatioUserIndexesCollector(config collectorConfig) (Collector, error) {
return &PGStatioUserIndexesCollector{log: config.logger}, nil
}
var (
statioUserIndexesIdxBlksRead = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statioUserIndexesSubsystem, "idx_blks_read_total"),
"Number of disk blocks read from this index",
[]string{"schemaname", "relname", "indexrelname"},
prometheus.Labels{},
)
statioUserIndexesIdxBlksHit = prometheus.NewDesc(
prometheus.BuildFQName(namespace, statioUserIndexesSubsystem, "idx_blks_hit_total"),
"Number of buffer hits in this index",
[]string{"schemaname", "relname", "indexrelname"},
prometheus.Labels{},
)
statioUserIndexesQuery = `
SELECT
schemaname,
relname,
indexrelname,
idx_blks_read,
idx_blks_hit
FROM pg_statio_user_indexes
`
)
func (c *PGStatioUserIndexesCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
rows, err := db.QueryContext(ctx,
statioUserIndexesQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var schemaname, relname, indexrelname sql.NullString
var idxBlksRead, idxBlksHit sql.NullFloat64
if err := rows.Scan(&schemaname, &relname, &indexrelname, &idxBlksRead, &idxBlksHit); err != nil {
return err
}
schemanameLabel := "unknown"
if schemaname.Valid {
schemanameLabel = schemaname.String
}
relnameLabel := "unknown"
if relname.Valid {
relnameLabel = relname.String
}
indexrelnameLabel := "unknown"
if indexrelname.Valid {
indexrelnameLabel = indexrelname.String
}
labels := []string{schemanameLabel, relnameLabel, indexrelnameLabel}
idxBlksReadMetric := 0.0
if idxBlksRead.Valid {
idxBlksReadMetric = idxBlksRead.Float64
}
ch <- prometheus.MustNewConstMetric(
statioUserIndexesIdxBlksRead,
prometheus.CounterValue,
idxBlksReadMetric,
labels...,
)
idxBlksHitMetric := 0.0
if idxBlksHit.Valid {
idxBlksHitMetric = idxBlksHit.Float64
}
ch <- prometheus.MustNewConstMetric(
statioUserIndexesIdxBlksHit,
prometheus.CounterValue,
idxBlksHitMetric,
labels...,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,109 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPgStatioUserIndexesCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"schemaname",
"relname",
"indexrelname",
"idx_blks_read",
"idx_blks_hit",
}
rows := sqlmock.NewRows(columns).
AddRow("public", "pgtest_accounts", "pgtest_accounts_pkey", 8, 9)
mock.ExpectQuery(sanitizeQuery(statioUserIndexesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatioUserIndexesCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatioUserIndexesCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"schemaname": "public", "relname": "pgtest_accounts", "indexrelname": "pgtest_accounts_pkey"}, value: 8, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"schemaname": "public", "relname": "pgtest_accounts", "indexrelname": "pgtest_accounts_pkey"}, value: 9, metricType: dto.MetricType_COUNTER},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
func TestPgStatioUserIndexesCollectorNull(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"schemaname",
"relname",
"indexrelname",
"idx_blks_read",
"idx_blks_hit",
}
rows := sqlmock.NewRows(columns).
AddRow(nil, nil, nil, nil, nil)
mock.ExpectQuery(sanitizeQuery(statioUserIndexesQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatioUserIndexesCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGStatioUserIndexesCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{"schemaname": "unknown", "relname": "unknown", "indexrelname": "unknown"}, value: 0, metricType: dto.MetricType_COUNTER},
{labels: labelMap{"schemaname": "unknown", "relname": "unknown", "indexrelname": "unknown"}, value: 0, metricType: dto.MetricType_COUNTER},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

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

84
collector/pg_wal.go Normal file
View File

@ -0,0 +1,84 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"github.com/prometheus/client_golang/prometheus"
)
const walSubsystem = "wal"
func init() {
registerCollector(walSubsystem, defaultEnabled, NewPGWALCollector)
}
type PGWALCollector struct {
}
func NewPGWALCollector(config collectorConfig) (Collector, error) {
return &PGWALCollector{}, nil
}
var (
pgWALSegments = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
walSubsystem,
"segments",
),
"Number of WAL segments",
[]string{}, nil,
)
pgWALSize = prometheus.NewDesc(
prometheus.BuildFQName(
namespace,
walSubsystem,
"size_bytes",
),
"Total size of WAL segments",
[]string{}, nil,
)
pgWALQuery = `
SELECT
COUNT(*) AS segments,
SUM(size) AS size
FROM pg_ls_waldir()
WHERE name ~ '^[0-9A-F]{24}$'`
)
func (c PGWALCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
row := db.QueryRowContext(ctx,
pgWALQuery,
)
var segments uint64
var size uint64
err := row.Scan(&segments, &size)
if err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(
pgWALSegments,
prometheus.GaugeValue, float64(segments),
)
ch <- prometheus.MustNewConstMetric(
pgWALSize,
prometheus.GaugeValue, float64(size),
)
return nil
}

63
collector/pg_wal_test.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPgWALCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{"segments", "size"}
rows := sqlmock.NewRows(columns).
AddRow(47, 788529152)
mock.ExpectQuery(sanitizeQuery(pgWALQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGWALCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGWALCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{}, value: 47, metricType: dto.MetricType_GAUGE},
{labels: labelMap{}, value: 788529152, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

View File

@ -0,0 +1,90 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"log/slog"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
const xlogLocationSubsystem = "xlog_location"
func init() {
registerCollector(xlogLocationSubsystem, defaultDisabled, NewPGXlogLocationCollector)
}
type PGXlogLocationCollector struct {
log *slog.Logger
}
func NewPGXlogLocationCollector(config collectorConfig) (Collector, error) {
return &PGXlogLocationCollector{log: config.logger}, nil
}
var (
xlogLocationBytes = prometheus.NewDesc(
prometheus.BuildFQName(namespace, xlogLocationSubsystem, "bytes"),
"Postgres LSN (log sequence number) being generated on primary or replayed on replica (truncated to low 52 bits)",
[]string{},
prometheus.Labels{},
)
xlogLocationQuery = `
SELECT CASE
WHEN pg_is_in_recovery() THEN (pg_last_xlog_replay_location() - '0/0') % (2^52)::bigint
ELSE (pg_current_xlog_location() - '0/0') % (2^52)::bigint
END AS bytes
`
)
func (c PGXlogLocationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error {
db := instance.getDB()
// xlog was renmaed to WAL in PostgreSQL 10
// https://wiki.postgresql.org/wiki/New_in_postgres_10#Renaming_of_.22xlog.22_to_.22wal.22_Globally_.28and_location.2Flsn.29
after10 := instance.version.Compare(semver.MustParse("10.0.0"))
if after10 >= 0 {
c.log.Warn("xlog_location collector is not available on PostgreSQL >= 10.0.0, skipping")
return nil
}
rows, err := db.QueryContext(ctx,
xlogLocationQuery)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var bytes float64
if err := rows.Scan(&bytes); err != nil {
return err
}
ch <- prometheus.MustNewConstMetric(
xlogLocationBytes,
prometheus.GaugeValue,
bytes,
)
}
if err := rows.Err(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,61 @@
// Copyright 2023 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collector
import (
"context"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
)
func TestPGXlogLocationCollector(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("Error opening a stub db connection: %s", err)
}
defer db.Close()
inst := &instance{db: db}
columns := []string{
"bytes",
}
rows := sqlmock.NewRows(columns).
AddRow(53401)
mock.ExpectQuery(sanitizeQuery(xlogLocationQuery)).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGXlogLocationCollector{}
if err := c.Update(context.Background(), inst, ch); err != nil {
t.Errorf("Error calling PGXlogLocationCollector.Update: %s", err)
}
}()
expected := []MetricResult{
{labels: labelMap{}, value: 53401, metricType: dto.MetricType_GAUGE},
}
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expected {
m := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, m)
}
})
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}

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 log.Logger
logger *slog.Logger
instance *instance
}
func NewProbeCollector(logger log.Logger, excludeDatabases []string, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) {
func NewProbeCollector(logger *slog.Logger, excludeDatabases []string, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) {
collectors := make(map[string]Collector)
initiatedCollectorsMtx.Lock()
defer initiatedCollectorsMtx.Unlock()
@ -46,7 +46,7 @@ func NewProbeCollector(logger log.Logger, excludeDatabases []string, registry *p
} else {
collector, err := factories[key](
collectorConfig{
logger: log.With(logger, "collector", key),
logger: logger.With("collector", key),
excludeDatabases: excludeDatabases,
})
if err != nil {
@ -74,6 +74,14 @@ func (pc *ProbeCollector) Describe(ch chan<- *prometheus.Desc) {
}
func (pc *ProbeCollector) Collect(ch chan<- prometheus.Metric) {
// Set up the database connection for the collector.
err := pc.instance.setup()
if err != nil {
pc.logger.Error("Error opening connection to database", "err", err)
return
}
defer pc.instance.Close()
wg := sync.WaitGroup{}
wg.Add(len(pc.collectors))
for name, c := range pc.collectors {

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 log.Logger) error {
func (ch *Handler) ReloadConfig(f string, logger *slog.Logger) error {
config := &Config{}
var err error
defer func() {
@ -79,14 +79,14 @@ func (ch *Handler) ReloadConfig(f string, logger log.Logger) error {
yamlReader, err := os.Open(f)
if err != nil {
return fmt.Errorf("Error opening config file %q: %s", f, err)
return fmt.Errorf("error opening config file %q: %s", f, err)
}
defer yamlReader.Close()
decoder := yaml.NewDecoder(yamlReader)
decoder.KnownFields(true)
if err = decoder.Decode(config); err != nil {
return fmt.Errorf("Error parsing config file %q: %s", f, err)
return fmt.Errorf("error parsing config file %q: %s", f, err)
}
ch.Lock()

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,18 +1,19 @@
module github.com/prometheus-community/postgres_exporter
go 1.19
go 1.23.0
toolchain go1.24.1
require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/alecthomas/kingpin/v2 v2.3.2
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/alecthomas/kingpin/v2 v2.4.0
github.com/blang/semver/v4 v4.0.0
github.com/go-kit/log v0.2.1
github.com/lib/pq v1.10.9
github.com/prometheus/client_golang v1.15.1
github.com/prometheus/client_model v0.4.0
github.com/prometheus/common v0.44.0
github.com/prometheus/exporter-toolkit v0.10.0
github.com/smartystreets/goconvey v1.8.0
github.com/prometheus/client_golang v1.21.1
github.com/prometheus/client_model v0.6.1
github.com/prometheus/common v0.63.0
github.com/prometheus/exporter-toolkit v0.14.0
github.com/smartystreets/goconvey v1.8.1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@ -21,27 +22,27 @@ require (
require (
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/go-logfmt/logfmt v0.5.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/mdlayher/vsock v1.2.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/smartystreets/assertions v1.13.1 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)

110
go.sum
View File

@ -1,38 +1,33 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU=
github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -40,63 +35,58 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/mdlayher/vsock v1.2.1 h1:pC1mTJTvjo1r9n9fbm7S1j04rCgCzhCOS5DY0zqHlnQ=
github.com/mdlayher/vsock v1.2.1/go.mod h1:NRfCibel++DgeMD8z/hP+PPTjlNJsdPOmxcnENvE+SE=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/exporter-toolkit v0.10.0 h1:yOAzZTi4M22ZzVxD+fhy1URTuNRj/36uQJJ5S8IPza8=
github.com/prometheus/exporter-toolkit v0.10.0/go.mod h1:+sVFzuvV5JDyw+Ih6p3zFxZNVnKQa3x5qPmDSiPu4ZY=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/exporter-toolkit v0.14.0 h1:NMlswfibpcZZ+H0sZBiTjrA3/aBFHkNZqE+iCj5EmRg=
github.com/prometheus/exporter-toolkit v0.14.0/go.mod h1:Gu5LnVvt7Nr/oqTBUC23WILZepW0nffNo10XdhQcwWA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU=
github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY=
github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w=
github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=