diff --git a/.promu.yml b/.promu.yml index dbad0ba95..72664252a 100644 --- a/.promu.yml +++ b/.promu.yml @@ -2,7 +2,7 @@ go: # This must match .circle/config.yml. version: 1.21 repository: - path: github.com/prometheus-community/postgres_exporter + path: github.com/percona/postgres_exporter build: binaries: - name: postgres_exporter diff --git a/README.md b/README.md index de712d68b..e2bcca34e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build Status](https://circleci.com/gh/prometheus-community/postgres_exporter.svg?style=svg)](https://circleci.com/gh/prometheus-community/postgres_exporter) [![Coverage Status](https://coveralls.io/repos/github/prometheus-community/postgres_exporter/badge.svg?branch=master)](https://coveralls.io/github/prometheus-community/postgres_exporter?branch=master) -[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus-community/postgres_exporter)](https://goreportcard.com/report/github.com/prometheus-community/postgres_exporter) +[![Go Report Card](https://goreportcard.com/badge/github.com/percona/postgres_exporter)](https://goreportcard.com/report/github.com/percona/postgres_exporter) [![Docker Pulls](https://img.shields.io/docker/pulls/prometheuscommunity/postgres-exporter.svg)](https://hub.docker.com/r/prometheuscommunity/postgres-exporter/tags) # PostgreSQL Server Exporter @@ -73,7 +73,7 @@ auth_modules: ## Building and running - git clone https://github.com/prometheus-community/postgres_exporter.git + git clone https://github.com/percona/postgres_exporter.git cd postgres_exporter make build ./postgres_exporter diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go index 07f0e542a..00a9de222 100644 --- a/cmd/postgres_exporter/main.go +++ b/cmd/postgres_exporter/main.go @@ -23,7 +23,7 @@ import ( "github.com/alecthomas/kingpin/v2" "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/prometheus-community/postgres_exporter/config" + "github.com/percona/postgres_exporter/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" vc "github.com/prometheus/client_golang/prometheus/collectors/version" diff --git a/cmd/postgres_exporter/percona_exporter.go b/cmd/postgres_exporter/percona_exporter.go index bf3749d24..fe18657fc 100644 --- a/cmd/postgres_exporter/percona_exporter.go +++ b/cmd/postgres_exporter/percona_exporter.go @@ -16,7 +16,7 @@ import ( "github.com/blang/semver/v4" "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/prometheus-community/postgres_exporter/collector" + "github.com/percona/postgres_exporter/collector" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/semaphore" diff --git a/cmd/postgres_exporter/postgres_exporter.go b/cmd/postgres_exporter/postgres_exporter.go index 3d890a16f..4666d9f83 100644 --- a/cmd/postgres_exporter/postgres_exporter.go +++ b/cmd/postgres_exporter/postgres_exporter.go @@ -81,7 +81,7 @@ type Mapping map[string]MappingOptions // Regex used to get the "short-version" from the postgres version field. var versionRegex = regexp.MustCompile(`^\w+ ((\d+)(\.\d+)?(\.\d+)?)`) -var lowestSupportedVersion = semver.MustParse("9.1.0") +var lowestSupportedVersion = semver.MustParse("10.0.0") // Parses the version of postgres into the short version string we can use to // match behaviors. diff --git a/cmd/postgres_exporter/postgres_exporter_test.go b/cmd/postgres_exporter/postgres_exporter_test.go index 5b2879d94..e9115563d 100644 --- a/cmd/postgres_exporter/postgres_exporter_test.go +++ b/cmd/postgres_exporter/postgres_exporter_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/blang/semver/v4" + "github.com/percona/postgres_exporter/distribution" "github.com/prometheus/client_golang/prometheus" . "gopkg.in/check.v1" ) @@ -409,7 +410,7 @@ func (s *FunctionalSuite) TestBooleanConversionToValueAndString(c *C) { func (s *FunctionalSuite) TestParseUserQueries(c *C) { userQueriesData, err := os.ReadFile("./tests/user_queries_ok.yaml") if err == nil { - metricMaps, newQueryOverrides, err := parseUserQueries(userQueriesData) + metricMaps, newQueryOverrides, err := parseUserQueries(userQueriesData, distribution.Standard) c.Assert(err, Equals, nil) c.Assert(metricMaps, NotNil) c.Assert(newQueryOverrides, NotNil) diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go index 654b7d8bf..5e0ce8f80 100644 --- a/cmd/postgres_exporter/probe.go +++ b/cmd/postgres_exporter/probe.go @@ -19,8 +19,8 @@ import ( "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/percona/postgres_exporter/collector" + "github.com/percona/postgres_exporter/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "golang.org/x/sync/semaphore" diff --git a/cmd/postgres_exporter/queries.go b/cmd/postgres_exporter/queries.go index 3596b2905..d8f16dd42 100644 --- a/cmd/postgres_exporter/queries.go +++ b/cmd/postgres_exporter/queries.go @@ -21,12 +21,20 @@ import ( "github.com/blang/semver/v4" "github.com/go-kit/log/level" "gopkg.in/yaml.v2" + + "github.com/percona/postgres_exporter/distribution" +) + +const ( + // '!' is a reserved character indicating that the query is not supported on Aurora and should be skipped for Aurora instances. + notSupportedByAurora = "!" ) -// UserQuery represents a user defined query +// UserQuery represents a user defined query, including support for Aurora, if needed type UserQuery struct { - Query string `yaml:"query"` - Metrics []Mapping `yaml:"metrics"` + Query string `yaml:"query"` // Standard query + QueryAurora string `yaml:"query_aurora"` // Aurora specific query + Metrics []Mapping `yaml:"metrics"` // Metrics to be collected Master bool `yaml:"master"` // Querying only for master database CacheSeconds uint64 `yaml:"cache_seconds"` // Number of seconds to cache the namespace result metrics for. RunOnServer string `yaml:"runonserver"` // Querying to run on which server version @@ -197,7 +205,7 @@ func makeQueryOverrideMap(pgVersion semver.Version, queryOverrides map[string][] return resultMap } -func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[string]string, error) { +func parseUserQueries(content []byte, dist string) (map[string]intermediateMetricMap, map[string]string, error) { var userQueries UserQueries err := yaml.Unmarshal(content, &userQueries) @@ -211,7 +219,25 @@ func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[str for metric, specs := range userQueries { level.Debug(logger).Log("msg", "New user metric namespace from YAML metric", "metric", metric, "cache_seconds", specs.CacheSeconds) - newQueryOverrides[metric] = specs.Query + + // Query selection logic: + // For Aurora: use query_aurora if defined and not empty, otherwise use query if defined and not empty. + // If query_aurora is set to '!', skip this query for Aurora (not supported). + // For standard (non-Aurora): always use query. + switch dist { + case distribution.Aurora: + if specs.QueryAurora != "" { + if specs.QueryAurora == notSupportedByAurora { + continue + } + newQueryOverrides[metric] = specs.QueryAurora + } else { + newQueryOverrides[metric] = specs.Query + } + default: + newQueryOverrides[metric] = specs.Query + } + metricMap, ok := metricMaps[metric] if !ok { // Namespace for metric not found - add it. @@ -251,7 +277,7 @@ func parseUserQueries(content []byte) (map[string]intermediateMetricMap, map[str // TODO: test code for all cu. // TODO: the YAML this supports is "non-standard" - we should move away from it. func addQueries(content []byte, pgVersion semver.Version, server *Server) error { - metricMaps, newQueryOverrides, err := parseUserQueries(content) + metricMaps, newQueryOverrides, err := parseUserQueries(content, server.distribution) if err != nil { return err } diff --git a/cmd/postgres_exporter/queries_test.go b/cmd/postgres_exporter/queries_test.go new file mode 100644 index 000000000..c1e7fd716 --- /dev/null +++ b/cmd/postgres_exporter/queries_test.go @@ -0,0 +1,104 @@ +// Copyright (C) 2023 Percona LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseUserQueries_DistributionSelection(t *testing.T) { + cases := []struct { + name string + yamlInput string + distribution string + wantQuery string + }{ + { + name: "Standard uses query", + yamlInput: ` +pg_replication: + query: "standard" + query_aurora: "aurora" +`, + distribution: "standard", + wantQuery: "standard", + }, + { + name: "Aurora uses query_aurora", + yamlInput: ` +pg_replication: + query: "standard" + query_aurora: "aurora" +`, + distribution: "aurora", + wantQuery: "aurora", + }, + { + name: "Aurora falls back to query", + yamlInput: ` +pg_replication: + query: "standard" +`, + distribution: "aurora", + wantQuery: "standard", + }, + { + name: "Aurora skips if neither", + yamlInput: ` +pg_replication: +`, + distribution: "aurora", + wantQuery: "", + }, + { + name: "Standard query only", + yamlInput: ` +pg_replication: + query: "standard" +`, + distribution: "standard", + wantQuery: "standard", + }, + { + name: "Aurora query only", + yamlInput: ` +pg_replication: + query_aurora: "aurora" +`, + distribution: "aurora", + wantQuery: "aurora", + }, + { + name: "Not supported by Aurora", + yamlInput: ` +pg_replication: + query: "standard" + query_aurora: "!" +`, + distribution: "aurora", + wantQuery: "", + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, metricsQueries, err := parseUserQueries([]byte(tc.yamlInput), tc.distribution) + require.NoError(t, err) + require.Equal(t, tc.wantQuery, metricsQueries["pg_replication"]) + }) + } +} diff --git a/cmd/postgres_exporter/server.go b/cmd/postgres_exporter/server.go index fd590a8b8..cc66c1404 100644 --- a/cmd/postgres_exporter/server.go +++ b/cmd/postgres_exporter/server.go @@ -21,16 +21,18 @@ import ( "github.com/blang/semver/v4" "github.com/go-kit/log/level" + "github.com/percona/postgres_exporter/distribution" "github.com/prometheus/client_golang/prometheus" ) // Server describes a connection to Postgres. // Also it contains metrics map and query overrides. type Server struct { - db *sql.DB - labels prometheus.Labels - master bool - runonserver string + db *sql.DB + distribution string + labels prometheus.Labels + master bool + runonserver string // Last version used to calculate metric map. If mismatch on scrape, // then maps are recalculated. @@ -82,6 +84,7 @@ func NewServer(dsn string, opts ...ServerOpt) (*Server, error) { }, metricCache: make(map[string]cachedMetrics), } + s.distribution = distribution.Get(dsn, db) for _, opt := range opts { opt(s) diff --git a/collector/pg_replication_slot.go b/collector/pg_replication_slot.go index 5d1be02d1..bb2dd75bb 100644 --- a/collector/pg_replication_slot.go +++ b/collector/pg_replication_slot.go @@ -18,6 +18,7 @@ import ( "database/sql" "github.com/go-kit/log" + "github.com/percona/postgres_exporter/distribution" "github.com/prometheus/client_golang/prometheus" ) @@ -80,6 +81,11 @@ var ( func (PGReplicationSlotCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() + // Skip Aurora instances (not supported pg_last_wal_receive_lsn) + if distribution.IsAurora(instance.dsn, db) { + return nil + } + rows, err := db.QueryContext(ctx, pgReplicationSlotQuery) if err != nil { diff --git a/collector/pg_stat_walreceiver.go b/collector/pg_stat_walreceiver.go index db533ab55..a30eacea8 100644 --- a/collector/pg_stat_walreceiver.go +++ b/collector/pg_stat_walreceiver.go @@ -19,6 +19,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/percona/postgres_exporter/distribution" "github.com/prometheus/client_golang/prometheus" ) @@ -122,6 +123,11 @@ receive_start_tli, func (c *PGStatWalReceiverCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { db := instance.getDB() + // Skip Aurora instances (not supported pg_stat_get_wal_receiver) + if distribution.IsAurora(instance.dsn, db) { + return nil + } + hasFlushedLSNRows, err := db.QueryContext(ctx, pgStatWalColumnQuery) if err != nil { return err diff --git a/collector/pg_xlog_location.go b/collector/pg_xlog_location.go deleted file mode 100644 index 237204f7d..000000000 --- a/collector/pg_xlog_location.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package collector - -import ( - "context" - - "github.com/blang/semver/v4" - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/prometheus/client_golang/prometheus" -) - -const xlogLocationSubsystem = "xlog_location" - -func init() { - registerCollector(xlogLocationSubsystem, defaultDisabled, NewPGXlogLocationCollector) -} - -type PGXlogLocationCollector struct { - log log.Logger -} - -func NewPGXlogLocationCollector(config collectorConfig) (Collector, error) { - return &PGXlogLocationCollector{log: config.logger}, nil -} - -var ( - xlogLocationBytes = prometheus.NewDesc( - prometheus.BuildFQName(namespace, xlogLocationSubsystem, "bytes"), - "Postgres LSN (log sequence number) being generated on primary or replayed on replica (truncated to low 52 bits)", - []string{}, - prometheus.Labels{}, - ) - - xlogLocationQuery = ` - SELECT CASE - WHEN pg_is_in_recovery() THEN (pg_last_xlog_replay_location() - '0/0') % (2^52)::bigint - ELSE (pg_current_xlog_location() - '0/0') % (2^52)::bigint - END AS bytes - ` -) - -func (c PGXlogLocationCollector) Update(ctx context.Context, instance *instance, ch chan<- prometheus.Metric) error { - db := instance.getDB() - - // xlog was renmaed to WAL in PostgreSQL 10 - // https://wiki.postgresql.org/wiki/New_in_postgres_10#Renaming_of_.22xlog.22_to_.22wal.22_Globally_.28and_location.2Flsn.29 - after10 := instance.version.Compare(semver.MustParse("10.0.0")) - if after10 >= 0 { - level.Warn(c.log).Log("msg", "xlog_location collector is not available on PostgreSQL >= 10.0.0, skipping") - return nil - } - - rows, err := db.QueryContext(ctx, - xlogLocationQuery) - - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var bytes float64 - - if err := rows.Scan(&bytes); err != nil { - return err - } - - ch <- prometheus.MustNewConstMetric( - xlogLocationBytes, - prometheus.GaugeValue, - bytes, - ) - } - if err := rows.Err(); err != nil { - return err - } - return nil -} diff --git a/collector/pg_xlog_location_test.go b/collector/pg_xlog_location_test.go deleted file mode 100644 index 561a7df94..000000000 --- a/collector/pg_xlog_location_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023 The Prometheus Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package collector - -import ( - "context" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/prometheus/client_golang/prometheus" - dto "github.com/prometheus/client_model/go" - "github.com/smartystreets/goconvey/convey" -) - -func TestPGXlogLocationCollector(t *testing.T) { - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("Error opening a stub db connection: %s", err) - } - defer db.Close() - inst := &instance{db: db} - columns := []string{ - "bytes", - } - rows := sqlmock.NewRows(columns). - AddRow(53401) - - mock.ExpectQuery(sanitizeQuery(xlogLocationQuery)).WillReturnRows(rows) - - ch := make(chan prometheus.Metric) - go func() { - defer close(ch) - c := PGXlogLocationCollector{} - - if err := c.Update(context.Background(), inst, ch); err != nil { - t.Errorf("Error calling PGXlogLocationCollector.Update: %s", err) - } - }() - expected := []MetricResult{ - {labels: labelMap{}, value: 53401, metricType: dto.MetricType_GAUGE}, - } - convey.Convey("Metrics comparison", t, func() { - for _, expect := range expected { - m := readMetric(<-ch) - convey.So(expect, convey.ShouldResemble, m) - } - }) - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled exceptions: %s", err) - } -} diff --git a/collector/probe.go b/collector/probe.go index 5ac46aab4..0b4a6d217 100644 --- a/collector/probe.go +++ b/collector/probe.go @@ -19,7 +19,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" - "github.com/prometheus-community/postgres_exporter/config" + "github.com/percona/postgres_exporter/config" "github.com/prometheus/client_golang/prometheus" "golang.org/x/sync/semaphore" ) diff --git a/distribution/distribution.go b/distribution/distribution.go new file mode 100644 index 000000000..bcbca5a5f --- /dev/null +++ b/distribution/distribution.go @@ -0,0 +1,53 @@ +// Copyright (C) 2023 Percona LLC +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package distribution + +import ( + "database/sql" + "sync" +) + +const ( + Standard = "standard" + Aurora = "aurora" +) + +// cache stores detection results keyed by DSN. If a DSN is not found in +// the cache, we query the database to determine if it is an Aurora instance. +var cache sync.Map // map[string]string + +func Get(dsn string, db *sql.DB) string { + if v, ok := cache.Load(dsn); ok { + if s, ok := v.(string); ok { + return s + } + } + + // Detect Aurora by checking if aurora_version function exists. + row := db.QueryRow("SELECT to_regproc('aurora_version') IS NOT NULL;") + var detected bool + if err := row.Scan(&detected); err == nil { + cache.Store(dsn, Aurora) + return Aurora + } + + cache.Store(dsn, Standard) + return Standard +} + +func IsAurora(dsn string, db *sql.DB) bool { + return Get(dsn, db) == Aurora +} diff --git a/go.mod b/go.mod index 67e405f3e..ddbfe303e 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/prometheus-community/postgres_exporter +module github.com/percona/postgres_exporter go 1.25.7 diff --git a/percona_tests/custom-queries/medium-resolution/queries.yaml b/percona_tests/custom-queries/medium-resolution/queries.yaml index b28a4f7a2..a4667da25 100644 --- a/percona_tests/custom-queries/medium-resolution/queries.yaml +++ b/percona_tests/custom-queries/medium-resolution/queries.yaml @@ -1,6 +1,9 @@ #### Queries are commented due to PMM-8859 pg_replication: - query: "SELECT CASE WHEN NOT pg_is_in_recovery() THEN 0 ELSE GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) END AS lag" +# Standard query +- query: "SELECT CASE WHEN NOT pg_is_in_recovery() THEN 0 ELSE GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) END AS lag" +# Aurora query +- query_aurora: "SELECT CASE WHEN pg_is_in_recovery() THEN (SELECT COALESCE(MAX(replica_lag_in_msec), 0) / 1000.0 FROM aurora_replica_status()) ELSE 0 END AS lag" master: true metrics: - lag: diff --git a/queries-hr.yml b/queries-hr.yml index eeb0bf033..f6726e636 100644 --- a/queries-hr.yml +++ b/queries-hr.yml @@ -31,6 +31,7 @@ pg_custom_replication_slots: pg_custom_stat_wal_receiver: master: true + # standard query query: | SELECT status, @@ -43,6 +44,8 @@ pg_custom_stat_wal_receiver: EXTRACT(EPOCH FROM last_msg_send_time) AS last_msg_send_time_seconds, EXTRACT(EPOCH FROM last_msg_receipt_time) AS last_msg_receipt_time_seconds FROM pg_stat_wal_receiver; + # skipped for Aurora by query_aurora == '!' + query_aurora: "!" metrics: - status: usage: "LABEL" diff --git a/queries-mr.yaml b/queries-mr.yaml index 700e74b65..ac74dd7ea 100644 --- a/queries-mr.yaml +++ b/queries-mr.yaml @@ -1,6 +1,9 @@ #### Queries are commented due to PMM-8859 pg_replication: + # Standard query query: "SELECT CASE WHEN NOT pg_is_in_recovery() THEN 0 ELSE GREATEST (0, EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))) END AS lag" + # Aurora query + query_aurora: "SELECT CASE WHEN pg_is_in_recovery() THEN (SELECT COALESCE(MAX(replica_lag_in_msec), 0) / 1000.0 FROM aurora_replica_status()) ELSE 0 END AS lag" master: true metrics: - lag: