Compare commits

...

59 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
55 changed files with 1862 additions and 525 deletions

View File

@ -8,7 +8,7 @@ executors:
# This must match .promu.yml.
golang:
docker:
- image: cimg/go:1.21
- image: cimg/go:1.24
jobs:
test:
@ -23,7 +23,7 @@ jobs:
integration:
docker:
- image: cimg/go:1.20
- image: cimg/go:1.24
- image: << parameters.postgres_image >>
environment:
POSTGRES_DB: circle_test
@ -63,6 +63,7 @@ workflows:
- 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

@ -12,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@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- name: install Go
uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0
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@3a919529898de77ec3da873e3063ca4b10e7f5cc # v3.7.0
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
with:
version: v1.54.2
args: --verbose
version: v2.0.2

View File

@ -1,23 +1,36 @@
---
version: "2"
linters:
enable:
- misspell
- 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
revive:
- 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:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
- name: unused-parameter
severity: warning
disabled: true
- linters:
- errcheck
path: _test.go
paths:
- third_party$
- builtin$
- examples$
formatters:
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@ -1,6 +1,6 @@
go:
# This must match .circle/config.yml.
version: 1.21
version: 1.24
repository:
path: github.com/prometheus-community/postgres_exporter
build:

View File

@ -1,5 +1,7 @@
---
extends: default
ignore: |
**/node_modules
rules:
braces:

View File

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

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.15.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.54.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: `11`, `12`, `13`, `14`, `15`, `16`
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.**
@ -125,6 +144,9 @@ This will build the docker image as `prometheuscommunity/postgres_exporter:${bra
* `[no-]collector.stat_database`
Enable the `stat_database` collector (default: enabled).
* `[no-]collector.stat_progress_vacuum`
Enable the `stat_progress_vacuum` collector (default: enabled).
* `[no-]collector.stat_statements`
Enable the `stat_statements` collector (default: disabled).
@ -208,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.15.0
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"
@ -132,7 +131,7 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) {
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
}

View File

@ -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"
)
@ -252,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},
},
@ -284,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) {
@ -371,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
}
@ -381,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
@ -491,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])
@ -582,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)
@ -605,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
@ -631,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
@ -679,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,7 +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 {
level.Error(logger).Log("msg", "Error creating probe collector", "err", err)
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"
)
@ -51,9 +50,9 @@ var queryOverrides = map[string][]OverrideQuery{
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
`,
},
@ -61,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
`,
},
@ -70,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
`,
},
@ -80,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
`,
},
@ -114,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
@ -132,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
`,
},
@ -170,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] = ""
}
}
@ -191,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 {
@ -243,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
}
@ -254,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 {
@ -173,7 +172,7 @@ func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
// Set up the database connection for the collector.
err := inst.setup()
if err != nil {
level.Error(p.logger).Log("msg", "Error opening connection to database", "err", err)
p.logger.Error("Error opening connection to database", "err", err)
return
}
defer inst.Close()
@ -189,7 +188,7 @@ func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) {
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)
@ -197,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,15 +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.Replace(q, "]", "\\]", -1)
q = strings.Replace(q, "{", "\\{", -1)
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

@ -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,6 +134,7 @@ func (c PGDatabaseCollector) Update(ctx context.Context, instance *instance, ch
pgDatabaseSizeDesc,
prometheus.GaugeValue, sizeMetric, datname,
)
}
return rows.Err()
}

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

@ -16,9 +16,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -29,7 +28,7 @@ func init() {
}
type PGDatabaseWraparoundCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGDatabaseWraparoundCollector(config collectorConfig) (Collector, error) {
@ -81,15 +80,15 @@ func (c *PGDatabaseWraparoundCollector) Update(ctx context.Context, instance *in
}
if !datname.Valid {
level.Debug(c.log).Log("msg", "Skipping database with NULL name")
c.log.Debug("Skipping database with NULL name")
continue
}
if !ageDatfrozenxid.Valid {
level.Debug(c.log).Log("msg", "Skipping stat emission with NULL age_datfrozenxid")
c.log.Debug("Skipping stat emission with NULL age_datfrozenxid")
continue
}
if !ageDatminmxid.Valid {
level.Debug(c.log).Log("msg", "Skipping stat emission with NULL age_datminmxid")
c.log.Debug("Skipping stat emission with NULL age_datminmxid")
continue
}

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 PGLocksCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGLocksCollector(config collectorConfig) (Collector, error) {

View File

@ -15,8 +15,8 @@ package collector
import (
"context"
"log/slog"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -27,7 +27,7 @@ func init() {
}
type PGLongRunningTransactionsCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGLongRunningTransactionsCollector(config collectorConfig) (Collector, error) {
@ -50,11 +50,13 @@ var (
)
longRunningTransactionsQuery = `
SELECT
COUNT(*) as transactions,
MAX(EXTRACT(EPOCH FROM clock_timestamp())) AS oldest_timestamp_seconds
FROM pg_catalog.pg_stat_activity
WHERE state is distinct from 'idle' AND query not like 'autovacuum:%'
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;
`
)

View File

@ -16,8 +16,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/lib/pq"
"github.com/prometheus/client_golang/prometheus"
)
@ -28,7 +28,7 @@ func init() {
}
type PGProcessIdleCollector struct {
log log.Logger
log *slog.Logger
}
const processIdleSubsystem = "process_idle"

View File

@ -51,6 +51,15 @@ 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
@ -61,7 +70,8 @@ var (
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, instance *instance, ch chan<- prometheus.Metric) error {
@ -72,7 +82,8 @@ func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance,
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
}
@ -84,5 +95,9 @@ func (c *PGReplicationCollector) Update(ctx context.Context, instance *instance,
pgReplicationIsReplica,
prometheus.GaugeValue, float64(isReplica),
)
ch <- prometheus.MustNewConstMetric(
pgReplicationLastReplay,
prometheus.GaugeValue, replayAge,
)
return nil
}

View File

@ -16,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,25 +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,
CASE WHEN pg_is_in_recovery() THEN
slot_type,
CASE WHEN pg_is_in_recovery() THEN
pg_last_wal_receive_lsn() - '0/0'
ELSE
pg_current_wal_lsn() - '0/0'
ELSE
pg_current_wal_lsn() - '0/0'
END AS current_wal_lsn,
COALESCE(confirmed_flush_lsn, '0/0') - '0/0',
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
active
FROM pg_replication_slots;`
pgReplicationSlotNewQuery = `SELECT
slot_name,
slot_type,
CASE WHEN pg_is_in_recovery() THEN
pg_last_wal_receive_lsn() - '0/0'
ELSE
pg_current_wal_lsn() - '0/0'
END AS current_wal_lsn,
COALESCE(confirmed_flush_lsn, '0/0') - '0/0' AS confirmed_flush_lsn,
active,
safe_wal_size,
wal_status
FROM pg_replication_slots;`
)
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
}
@ -87,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
}
@ -102,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 {
@ -109,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
@ -118,13 +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,
)
}
}
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

@ -31,9 +31,9 @@ func TestPgReplicationCollector(t *testing.T) {
inst := &instance{db: db}
columns := []string{"lag", "is_replica"}
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)
@ -49,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

@ -15,8 +15,8 @@ package collector
import (
"context"
"log/slog"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -27,7 +27,7 @@ func init() {
}
type PGStatActivityAutovacuumCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGStatActivityAutovacuumCollector(config collectorConfig) (Collector, error) {

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

@ -52,7 +52,7 @@ func TestPGStatBGWriterCollector(t *testing.T) {
rows := sqlmock.NewRows(columns).
AddRow(354, 4945, 289097744, 1242257, int64(3275602074), 89320867, 450139, 2034563757, 0, int64(2725688749), srT)
mock.ExpectQuery(sanitizeQuery(statBGWriterQuery)).WillReturnRows(rows)
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,9 +16,11 @@ package collector
import (
"context"
"database/sql"
"fmt"
"log/slog"
"strings"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)
@ -29,7 +31,7 @@ func init() {
}
type PGStatDatabaseCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGStatDatabaseCollector(config collectorConfig) (Collector, error) {
@ -206,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 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
@ -244,10 +263,10 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
for rows.Next() {
var datid, datname sql.NullString
var numBackends, xactCommit, xactRollback, blksRead, blksHit, tupReturned, tupFetched, tupInserted, tupUpdated, tupDeleted, conflicts, tempFiles, tempBytes, deadlocks, blkReadTime, blkWriteTime 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,
@ -267,87 +286,97 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
&blkReadTime,
&blkWriteTime,
&statsReset,
)
}
if activeTimeAvail {
r = append(r, &activeTime)
}
err := rows.Scan(r...)
if err != nil {
return err
}
if !datid.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no datid")
c.log.Debug("Skipping collecting metric because it has no datid")
continue
}
if !datname.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no datname")
c.log.Debug("Skipping collecting metric because it has no datname")
continue
}
if !numBackends.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no numbackends")
c.log.Debug("Skipping collecting metric because it has no numbackends")
continue
}
if !xactCommit.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no xact_commit")
c.log.Debug("Skipping collecting metric because it has no xact_commit")
continue
}
if !xactRollback.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no xact_rollback")
c.log.Debug("Skipping collecting metric because it has no xact_rollback")
continue
}
if !blksRead.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no blks_read")
c.log.Debug("Skipping collecting metric because it has no blks_read")
continue
}
if !blksHit.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no blks_hit")
c.log.Debug("Skipping collecting metric because it has no blks_hit")
continue
}
if !tupReturned.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no tup_returned")
c.log.Debug("Skipping collecting metric because it has no tup_returned")
continue
}
if !tupFetched.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no tup_fetched")
c.log.Debug("Skipping collecting metric because it has no tup_fetched")
continue
}
if !tupInserted.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no tup_inserted")
c.log.Debug("Skipping collecting metric because it has no tup_inserted")
continue
}
if !tupUpdated.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no tup_updated")
c.log.Debug("Skipping collecting metric because it has no tup_updated")
continue
}
if !tupDeleted.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no tup_deleted")
c.log.Debug("Skipping collecting metric because it has no tup_deleted")
continue
}
if !conflicts.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no conflicts")
c.log.Debug("Skipping collecting metric because it has no conflicts")
continue
}
if !tempFiles.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no temp_files")
c.log.Debug("Skipping collecting metric because it has no temp_files")
continue
}
if !tempBytes.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no temp_bytes")
c.log.Debug("Skipping collecting metric because it has no temp_bytes")
continue
}
if !deadlocks.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no deadlocks")
c.log.Debug("Skipping collecting metric because it has no deadlocks")
continue
}
if !blkReadTime.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no blk_read_time")
c.log.Debug("Skipping collecting metric because it has no blk_read_time")
continue
}
if !blkWriteTime.Valid {
level.Debug(c.log).Log("msg", "Skipping collecting metric because it has no blk_write_time")
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
}
statsResetMetric := 0.0
if !statsReset.Valid {
level.Debug(c.log).Log("msg", "No metric for stats_reset, will collect 0 instead")
c.log.Debug("No metric for stats_reset, will collect 0 instead")
}
if statsReset.Valid {
statsResetMetric = float64(statsReset.Time.Unix())
@ -473,6 +502,15 @@ func (c *PGStatDatabaseCollector) Update(ctx context.Context, instance *instance
statsResetMetric,
labels...,
)
if activeTimeAvail {
ch <- prometheus.MustNewConstMetric(
statDatabaseActiveTime,
prometheus.CounterValue,
activeTime.Float64/1000.0,
labels...,
)
}
}
return nil
}

View File

@ -18,9 +18,10 @@ import (
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/go-kit/log"
"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"
)
@ -31,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",
@ -53,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")
@ -80,15 +82,17 @@ func TestPGStatDatabaseCollector(t *testing.T) {
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{
log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"),
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
@ -114,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() {
@ -138,7 +143,7 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
if err != nil {
t.Fatalf("Error parsing time: %s", err)
}
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
@ -160,6 +165,7 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
rows := sqlmock.NewRows(columns).
@ -182,7 +188,9 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
925,
16,
823,
srT).
srT,
32,
).
AddRow(
"pid",
"postgres",
@ -202,14 +210,16 @@ func TestPGStatDatabaseCollectorNullValues(t *testing.T) {
925,
16,
823,
srT)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
srT,
32,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"),
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
@ -235,6 +245,7 @@ func TestPGStatDatabaseCollectorNullValues(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.032},
}
convey.Convey("Metrics comparison", t, func() {
@ -254,7 +265,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
@ -276,6 +287,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
srT, err := time.Parse("2006-01-02 15:04:05.00000-07", "2023-05-25 17:10:42.81132-07")
@ -303,7 +315,9 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
925,
16,
823,
srT).
srT,
14,
).
AddRow(
nil,
nil,
@ -324,6 +338,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
nil,
nil,
nil,
nil,
).
AddRow(
"pid",
@ -344,14 +359,16 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
926,
17,
824,
srT)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery)).WillReturnRows(rows)
srT,
15,
)
mock.ExpectQuery(sanitizeQuery(statDatabaseQuery(columns))).WillReturnRows(rows)
ch := make(chan prometheus.Metric)
go func() {
defer close(ch)
c := PGStatDatabaseCollector{
log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"),
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
@ -377,6 +394,8 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 16},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 823},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 1685059842},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 0.014},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_GAUGE, value: 355},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 4946},
{labels: labelMap{"datid": "pid", "datname": "postgres"}, metricType: dto.MetricType_COUNTER, value: 289097745},
@ -394,6 +413,7 @@ func TestPGStatDatabaseCollectorRowLeakTest(t *testing.T) {
{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() {
@ -414,7 +434,7 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) {
}
defer db.Close()
inst := &instance{db: db}
inst := &instance{db: db, version: semver.MustParse("14.0.0")}
columns := []string{
"datid",
@ -436,6 +456,7 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) {
"blk_read_time",
"blk_write_time",
"stats_reset",
"active_time",
}
rows := sqlmock.NewRows(columns).
@ -458,15 +479,17 @@ func TestPGStatDatabaseCollectorTestNilStatReset(t *testing.T) {
925,
16,
823,
nil)
nil,
7,
)
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{
log: log.With(log.NewNopLogger(), "collector", "pg_stat_database"),
log: promslog.NewNopLogger().With("collector", "pg_stat_database"),
}
if err := c.Update(context.Background(), inst, ch); err != nil {
@ -492,6 +515,7 @@ func TestPGStatDatabaseCollectorTestNilStatReset(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: 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,9 +16,9 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/blang/semver/v4"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -32,7 +32,7 @@ func init() {
}
type PGStatStatementsCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGStatStatementsCollector(config collectorConfig) (Collector, error) {
@ -112,12 +112,38 @@ var (
)
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 {
query := pgStatStatementsQuery
if instance.version.GE(semver.MustParse("13.0.0")) {
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()

View File

@ -151,3 +151,46 @@ func TestPGStateStatementsCollectorNewPG(t *testing.T) {
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) {

View File

@ -16,9 +16,8 @@ import (
"context"
"database/sql"
"fmt"
"log/slog"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -27,7 +26,7 @@ func init() {
}
type PGStatWalReceiverCollector struct {
log log.Logger
log *slog.Logger
}
const statWalReceiverSubsystem = "stat_wal_receiver"
@ -157,55 +156,51 @@ func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *insta
}
}
if !upstreamHost.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because upstream host is null")
c.log.Debug("Skipping wal receiver stats because upstream host is null")
continue
}
if !slotName.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because slotname host is null")
c.log.Debug("Skipping wal receiver stats because slotname host is null")
continue
}
if !status.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because status is null")
c.log.Debug("Skipping wal receiver stats because status is null")
continue
}
labels := []string{upstreamHost.String, slotName.String, status.String}
if !receiveStartLsn.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because receive_start_lsn is null")
c.log.Debug("Skipping wal receiver stats because receive_start_lsn is null")
continue
}
if !receiveStartTli.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because receive_start_tli is null")
c.log.Debug("Skipping wal receiver stats because receive_start_tli is null")
continue
}
if hasFlushedLSN && !flushedLsn.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because flushed_lsn is null")
c.log.Debug("Skipping wal receiver stats because flushed_lsn is null")
continue
}
if !receivedTli.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because received_tli is null")
c.log.Debug("Skipping wal receiver stats because received_tli is null")
continue
}
if !lastMsgSendTime.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because last_msg_send_time is null")
c.log.Debug("Skipping wal receiver stats because last_msg_send_time is null")
continue
}
if !lastMsgReceiptTime.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because last_msg_receipt_time is null")
c.log.Debug("Skipping wal receiver stats because last_msg_receipt_time is null")
continue
}
if !latestEndLsn.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because latest_end_lsn is null")
c.log.Debug("Skipping wal receiver stats because latest_end_lsn is null")
continue
}
if !latestEndTime.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because latest_end_time is null")
continue
}
if !upstreamNode.Valid {
level.Debug(c.log).Log("msg", "Skipping wal receiver stats because upstream_node is null")
c.log.Debug("Skipping wal receiver stats because latest_end_time is null")
continue
}
ch <- prometheus.MustNewConstMetric(
@ -258,11 +253,15 @@ func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *insta
latestEndTime.Float64,
labels...)
ch <- prometheus.MustNewConstMetric(
statWalReceiverUpstreamNode,
prometheus.GaugeValue,
float64(upstreamNode.Int64),
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

View File

@ -15,8 +15,8 @@ package collector
import (
"context"
"database/sql"
"log/slog"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)
@ -25,7 +25,7 @@ func init() {
}
type PGStatioUserIndexesCollector struct {
log log.Logger
log *slog.Logger
}
const statioUserIndexesSubsystem = "statio_user_indexes"

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) {

View File

@ -15,10 +15,9 @@ package collector
import (
"context"
"log/slog"
"github.com/blang/semver/v4"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/client_golang/prometheus"
)
@ -29,7 +28,7 @@ func init() {
}
type PGXlogLocationCollector struct {
log log.Logger
log *slog.Logger
}
func NewPGXlogLocationCollector(config collectorConfig) (Collector, error) {
@ -59,7 +58,7 @@ func (c PGXlogLocationCollector) Update(ctx context.Context, instance *instance,
// 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 {
level.Warn(c.log).Log("msg", "xlog_location collector is not available on PostgreSQL >= 10.0.0, skipping")
c.log.Warn("xlog_location collector is not available on PostgreSQL >= 10.0.0, skipping")
return nil
}

View File

@ -15,10 +15,9 @@ package collector
import (
"context"
"log/slog"
"sync"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus-community/postgres_exporter/config"
"github.com/prometheus/client_golang/prometheus"
)
@ -26,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()
@ -47,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 {
@ -78,7 +77,7 @@ func (pc *ProbeCollector) Collect(ch chan<- prometheus.Metric) {
// Set up the database connection for the collector.
err := pc.instance.setup()
if err != nil {
level.Error(pc.logger).Log("msg", "Error opening connection to database", "err", err)
pc.logger.Error("Error opening connection to database", "err", err)
return
}
defer pc.instance.Close()

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",
},
}

43
go.mod
View File

@ -1,17 +1,18 @@
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.17.0
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16
github.com/prometheus/common v0.44.0
github.com/prometheus/exporter-toolkit v0.10.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
@ -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.11.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.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
)

102
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,25 +35,31 @@ 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.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/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.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
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=
@ -68,35 +69,24 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS
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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.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=