diff --git a/.circleci/config.yml b/.circleci/config.yml index db3cad4e5..ae168f04e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -63,16 +63,28 @@ workflows: - cimg/postgres:14.1 - prometheus/build: name: build + parallelism: 3 + promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386" filters: tags: - only: /.*/ + ignore: /^v.*/ + branches: + ignore: /^(main|master|release-.*|.*build-all.*)$/ + - prometheus/build: + name: build_all + parallelism: 12 + filters: + branches: + only: /^(main|master|release-.*|.*build-all.*)$/ + tags: + only: /^v.*/ - prometheus/publish_master: context: org-context docker_hub_organization: prometheuscommunity quay_io_organization: prometheuscommunity requires: - test - - build + - build_all filters: branches: only: master @@ -82,7 +94,7 @@ workflows: quay_io_organization: prometheuscommunity requires: - test - - build + - build_all filters: tags: only: /^v.*/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0572bf5dc..38d7e4d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,32 @@ ## master / unreleased -* [CHANGE] pg_stat_bgwriter counter metrics had the `_total` suffix added #556 +## 0.12.0-rc.0 / 2022-08-26 + +BREAKING CHANGES: + +This release changes support for multiple postgres servers to use the +multi-target exporter pattern. This makes it much easier to monitor multiple +PostgreSQL servers from a single exporter by passing the target via URL +params. See the Multi-Target Support section of the README. + +* [CHANGE] Add multi-target support #618 +* [BUGFIX] Add dsn type for handling datasources #678 + +## 0.11.1 / 2022-08-01 + +* [BUGFIX] Fix checkpoint_write_time value type #666 +* [BUGFIX] Fix checkpoint_sync_time value type #667 + +## 0.11.0 / 2022-07-28 + +NOTE: pg_stat_bgwriter counter metrics had the `_total` suffix added #556 + +* [CHANGE] refactor pg_stat_bgwriter metrics into standalone collector #556 +* [FEATURE] Add pg_database collector #613 * [ENHANCEMENT] Add pg_database_size_bytes metric #613 +* [BUGFIX] Avoid parsing error from bogus Azure Flexible Server custom GUC #587 +* [BUGFIX] Fix pg_stat_archiver error in 9.4 and earlier. #599 +* [BUGFIX] Sanitize setting values because of Aurora irregularity #620 ## 0.10.1 / 2022-01-14 diff --git a/Makefile.common b/Makefile.common index c263b733f..7642c4485 100644 --- a/Makefile.common +++ b/Makefile.common @@ -36,29 +36,6 @@ GO_VERSION ?= $(shell $(GO) version) GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION)) PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.') -GOVENDOR := -GO111MODULE := -ifeq (, $(PRE_GO_111)) - ifneq (,$(wildcard go.mod)) - # Enforce Go modules support just in case the directory is inside GOPATH (and for Travis CI). - GO111MODULE := on - - ifneq (,$(wildcard vendor)) - # Always use the local vendor/ directory to satisfy the dependencies. - GOOPTS := $(GOOPTS) -mod=vendor - endif - endif -else - ifneq (,$(wildcard go.mod)) - ifneq (,$(wildcard vendor)) -$(warning This repository requires Go >= 1.11 because of Go modules) -$(warning Some recipes may not work as expected as the current Go runtime is '$(GO_VERSION_NUMBER)') - endif - else - # This repository isn't using Go modules (yet). - GOVENDOR := $(FIRST_GOPATH)/bin/govendor - endif -endif PROMU := $(FIRST_GOPATH)/bin/promu pkgs = ./... @@ -81,16 +58,19 @@ endif PROMU_VERSION ?= 0.13.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.45.2 +GOLANGCI_LINT_VERSION ?= v1.49.0 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64. # windows isn't included here because of the path separator being different. ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin)) ifeq ($(GOHOSTARCH),$(filter $(GOHOSTARCH),amd64 i386)) # 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. - ifeq (,$(CIRCLE_JOB)) + ifneq (,$(SKIP_GOLANGCI_LINT)) + GOLANGCI_LINT := + else ifeq (,$(CIRCLE_JOB)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint else ifeq (,$(wildcard .github/workflows/golangci-lint.yml)) GOLANGCI_LINT := $(FIRST_GOPATH)/bin/golangci-lint @@ -150,11 +130,7 @@ common-check_license: .PHONY: common-deps common-deps: @echo ">> getting dependencies" -ifdef GO111MODULE - GO111MODULE=$(GO111MODULE) $(GO) mod download -else - $(GO) get $(GOOPTS) -t ./... -endif + $(GO) mod download .PHONY: update-go-deps update-go-deps: @@ -162,20 +138,17 @@ update-go-deps: @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ $(GO) get -d $$m; \ done - GO111MODULE=$(GO111MODULE) $(GO) mod tidy -ifneq (,$(wildcard vendor)) - GO111MODULE=$(GO111MODULE) $(GO) mod vendor -endif + $(GO) mod tidy .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" - GO111MODULE=$(GO111MODULE) $(GOTEST) -short $(GOOPTS) $(pkgs) + $(GOTEST) -short $(GOOPTS) $(pkgs) .PHONY: common-test common-test: $(GOTEST_DIR) @echo ">> running all tests" - GO111MODULE=$(GO111MODULE) $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) + $(GOTEST) $(test-flags) $(GOOPTS) $(pkgs) $(GOTEST_DIR): @mkdir -p $@ @@ -183,25 +156,21 @@ $(GOTEST_DIR): .PHONY: common-format common-format: @echo ">> formatting code" - GO111MODULE=$(GO111MODULE) $(GO) fmt $(pkgs) + $(GO) fmt $(pkgs) .PHONY: common-vet common-vet: @echo ">> vetting code" - GO111MODULE=$(GO111MODULE) $(GO) vet $(GOOPTS) $(pkgs) + $(GO) vet $(GOOPTS) $(pkgs) .PHONY: common-lint common-lint: $(GOLANGCI_LINT) ifdef GOLANGCI_LINT @echo ">> running golangci-lint" -ifdef GO111MODULE # 'go list' needs to be executed before staticcheck to prepopulate the modules cache. # Otherwise staticcheck might fail randomly for some reason not yet explained. - GO111MODULE=$(GO111MODULE) $(GO) list -e -compiled -test=true -export=false -deps=true -find=false -tags= -- ./... > /dev/null - GO111MODULE=$(GO111MODULE) $(GOLANGCI_LINT) run $(GOLANGCI_LINT_OPTS) $(pkgs) -else - $(GOLANGCI_LINT) run $(pkgs) -endif + $(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-yamllint @@ -218,28 +187,15 @@ endif common-staticcheck: lint .PHONY: common-unused -common-unused: $(GOVENDOR) -ifdef GOVENDOR - @echo ">> running check for unused packages" - @$(GOVENDOR) list +unused | grep . && exit 1 || echo 'No unused packages' -else -ifdef GO111MODULE +common-unused: @echo ">> running check for unused/missing packages in go.mod" - GO111MODULE=$(GO111MODULE) $(GO) mod tidy -ifeq (,$(wildcard vendor)) + $(GO) mod tidy @git diff --exit-code -- go.sum go.mod -else - @echo ">> running check for unused packages in vendor/" - GO111MODULE=$(GO111MODULE) $(GO) mod vendor - @git diff --exit-code -- go.sum go.mod vendor/ -endif -endif -endif .PHONY: common-build common-build: promu @echo ">> building binaries" - GO111MODULE=$(GO111MODULE) $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) + $(PROMU) build --prefix $(PREFIX) $(PROMU_BINARIES) .PHONY: common-tarball common-tarball: promu @@ -295,12 +251,6 @@ $(GOLANGCI_LINT): | sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION) endif -ifdef GOVENDOR -.PHONY: $(GOVENDOR) -$(GOVENDOR): - GOOS= GOARCH= $(GO) get -u github.com/kardianos/govendor -endif - .PHONY: precheck precheck:: diff --git a/README.md b/README.md index 4d23603a6..2fc0ad8bc 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,36 @@ docker run \ quay.io/prometheuscommunity/postgres-exporter ``` +## Multi-Target Support (BETA) +**This Feature is in beta and may require changes in future releases. Feedback is welcome.** + +This exporter supports the [multi-target pattern](https://prometheus.io/docs/guides/multi-target-exporter/). This allows running a single instance of this exporter for multiple postgres targets. Using the multi-target functionality of this exporter is **optional** and meant for cases where it is impossible to install the exporter as a sidecar, for example SaaS-managed services. + +To use the multi-target functionality, send an http request to the endpoint `/probe?target=foo:5432` where target is set to the DSN of the postgres instance to scrape metrics from. + +To avoid putting sensitive information like username and password in the URL, preconfigured auth modules are supported via the [auth_modules](#auth_modules) section of the config file. auth_modules for DSNs can be used with the `/probe` endpoint by specifying the `?auth_module=foo` http parameter. + +## Configuration File + +The configuration file controls the behavior of the exporter. It can be set using the `--config.file` command line flag and defaults to `postgres_exporter.yml`. + +### auth_modules +This section defines preset authentication and connection parameters for use in the [multi-target endpoint](#multi-target-support-beta). `auth_modules` is a map of modules with the key being the identifier which can be used in the `/probe` endpoint. +Currently only the `userpass` type is supported. + +Example: +```yaml +auth_modules: + foo1: # Set this to any name you want + type: userpass + userpass: + username: first + password: firstpass + options: + # options become key=value parameters of the DSN + sslmode: disable +``` + ## Building and running git clone https://github.com/prometheus-community/postgres_exporter.git diff --git a/VERSION b/VERSION index 571215736..68f8b7694 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.10.1 +0.12.0-rc.0 diff --git a/cmd/postgres_exporter/datasource.go b/cmd/postgres_exporter/datasource.go index 90d797b63..97f7ecd8b 100644 --- a/cmd/postgres_exporter/datasource.go +++ b/cmd/postgres_exporter/datasource.go @@ -162,6 +162,12 @@ func getDataSources() ([]string, error) { uri = os.Getenv("DATA_SOURCE_URI") } + // No datasources found. This allows us to support the multi-target pattern + // without an explicit datasource. + if uri == "" { + return []string{}, nil + } + dsn = "postgresql://" + ui + "@" + uri return []string{dsn}, nil diff --git a/cmd/postgres_exporter/main.go b/cmd/postgres_exporter/main.go index 35db723a2..8759f4372 100644 --- a/cmd/postgres_exporter/main.go +++ b/cmd/postgres_exporter/main.go @@ -20,6 +20,7 @@ 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/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/promlog" @@ -31,6 +32,11 @@ import ( ) var ( + c = config.ConfigHandler{ + Config: &config.Config{}, + } + + configFile = kingpin.Flag("config.file", "Postgres exporter configuration file.").Default("postgres_exporter.yml").String() listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9187").Envar("PG_EXPORTER_WEB_LISTEN_ADDRESS").String() webConfig = webflag.AddFlags(kingpin.CommandLine) metricPath = kingpin.Flag("web.telemetry-path", "Path under which to expose metrics.").Default("/metrics").Envar("PG_EXPORTER_WEB_TELEMETRY_PATH").String() @@ -85,14 +91,14 @@ func main() { return } - dsn, err := getDataSources() - if err != nil { - level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error()) - os.Exit(1) + if err := c.ReloadConfig(*configFile, logger); err != nil { + // This is not fatal, but it means that auth must be provided for every dsn. + level.Error(logger).Log("msg", "Error loading config", "err", err) } - if len(dsn) == 0 { - level.Error(logger).Log("msg", "Couldn't find environment variables describing the datasource to use") + dsns, err := getDataSources() + if err != nil { + level.Error(logger).Log("msg", "Failed reading data sources", "err", err.Error()) os.Exit(1) } @@ -106,7 +112,7 @@ func main() { IncludeDatabases(*includeDatabases), } - exporter := NewExporter(dsn, opts...) + exporter := NewExporter(dsns, opts...) defer func() { exporter.servers.Close() }() @@ -115,6 +121,12 @@ func main() { prometheus.MustRegister(exporter) + // TODO(@sysadmind): Remove this with multi-target support. We are removing multiple DSN support + dsn := "" + if len(dsns) > 0 { + dsn = dsns[0] + } + pe, err := collector.NewPostgresCollector( logger, dsn, @@ -122,9 +134,9 @@ func main() { ) if err != nil { level.Error(logger).Log("msg", "Failed to create PostgresCollector", "err", err.Error()) - os.Exit(1) + } else { + prometheus.MustRegister(pe) } - prometheus.MustRegister(pe) http.Handle(*metricPath, promhttp.Handler()) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -132,6 +144,8 @@ func main() { w.Write(landingPage) // nolint: errcheck }) + http.HandleFunc("/probe", handleProbe(logger)) + level.Info(logger).Log("msg", "Listening on address", "address", *listenAddress) srv := &http.Server{Addr: *listenAddress} if err := web.ListenAndServe(srv, *webConfig, logger); err != nil { diff --git a/cmd/postgres_exporter/namespace.go b/cmd/postgres_exporter/namespace.go index ab1e74ea0..41674007d 100644 --- a/cmd/postgres_exporter/namespace.go +++ b/cmd/postgres_exporter/namespace.go @@ -19,7 +19,7 @@ import ( "fmt" "time" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/go-kit/log/level" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" diff --git a/cmd/postgres_exporter/pg_setting.go b/cmd/postgres_exporter/pg_setting.go index bfe4c6c52..28a08a70b 100644 --- a/cmd/postgres_exporter/pg_setting.go +++ b/cmd/postgres_exporter/pg_setting.go @@ -129,7 +129,7 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) { return case "ms", "s", "min", "h", "d": unit = "seconds" - case "B", "kB", "MB", "GB", "TB", "8kB", "16kB", "32kB", "16MB", "32MB", "64MB": + case "B", "kB", "MB", "GB", "TB", "4kB", "8kB", "16kB", "32kB", "16MB", "32MB", "64MB": unit = "bytes" default: err = fmt.Errorf("Unknown unit for runtime variable: %q", s.unit) @@ -158,6 +158,8 @@ func (s *pgSetting) normaliseUnit() (val float64, unit string, err error) { val *= math.Pow(2, 30) case "TB": val *= math.Pow(2, 40) + case "4kB": + val *= math.Pow(2, 12) case "8kB": val *= math.Pow(2, 13) case "16kB": diff --git a/cmd/postgres_exporter/postgres_exporter.go b/cmd/postgres_exporter/postgres_exporter.go index 2574e0252..af973d5d5 100644 --- a/cmd/postgres_exporter/postgres_exporter.go +++ b/cmd/postgres_exporter/postgres_exporter.go @@ -24,7 +24,7 @@ import ( "strings" "time" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) diff --git a/cmd/postgres_exporter/postgres_exporter_test.go b/cmd/postgres_exporter/postgres_exporter_test.go index 6017756a8..468aa39b9 100644 --- a/cmd/postgres_exporter/postgres_exporter_test.go +++ b/cmd/postgres_exporter/postgres_exporter_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" . "gopkg.in/check.v1" ) diff --git a/cmd/postgres_exporter/probe.go b/cmd/postgres_exporter/probe.go new file mode 100644 index 000000000..28d1395ee --- /dev/null +++ b/cmd/postgres_exporter/probe.go @@ -0,0 +1,123 @@ +// Copyright 2022 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 main + +import ( + "fmt" + "net/http" + "time" + + "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) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + conf := c.GetConfig() + params := r.URL.Query() + target := params.Get("target") + if target == "" { + http.Error(w, "target is required", http.StatusBadRequest) + return + } + var authModule config.AuthModule + authModuleName := params.Get("auth_module") + if authModuleName == "" { + level.Info(logger).Log("msg", "no auth_module specified, using default") + } else { + var ok bool + authModule, ok = conf.AuthModules[authModuleName] + if !ok { + http.Error(w, fmt.Sprintf("auth_module %s not found", authModuleName), http.StatusBadRequest) + return + } + if authModule.UserPass.Username == "" || authModule.UserPass.Password == "" { + http.Error(w, fmt.Sprintf("auth_module %s has no username or password", authModuleName), http.StatusBadRequest) + return + } + } + + dsn, err := authModule.ConfigureTarget(target) + if err != nil { + level.Error(logger).Log("msg", "failed to configure target", "err", err) + http.Error(w, fmt.Sprintf("could not configure dsn for target: %v", err), http.StatusBadRequest) + return + } + + // TODO(@sysadmind): Timeout + + probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "probe_success", + Help: "Displays whether or not the probe was a success", + }) + probeDurationGauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "probe_duration_seconds", + Help: "Returns how long the probe took to complete in seconds", + }) + + tl := log.With(logger, "target", target) + + start := time.Now() + registry := prometheus.NewRegistry() + registry.MustRegister(probeSuccessGauge) + registry.MustRegister(probeDurationGauge) + + opts := []ExporterOpt{ + DisableDefaultMetrics(*disableDefaultMetrics), + DisableSettingsMetrics(*disableSettingsMetrics), + AutoDiscoverDatabases(*autoDiscoverDatabases), + WithUserQueriesPath(*queriesPath), + WithConstantLabels(*constantLabelsList), + ExcludeDatabases(*excludeDatabases), + IncludeDatabases(*includeDatabases), + } + + dsns := []string{dsn.GetConnectionString()} + exporter := NewExporter(dsns, opts...) + defer func() { + exporter.servers.Close() + }() + registry.MustRegister(exporter) + + // Run the probe + pc, err := collector.NewProbeCollector(tl, registry, dsn) + if err != nil { + probeSuccessGauge.Set(0) + probeDurationGauge.Set(time.Since(start).Seconds()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // TODO(@sysadmind): Remove the registry.MustRegister() call below and instead handle the collection here. That will allow + // for the passing of context, handling of timeouts, and more control over the collection. + // The current NewProbeCollector() implementation relies on the MustNewConstMetric() call to create the metrics which is not + // ideal to use without the registry.MustRegister() call. + _ = ctx + + registry.MustRegister(pc) + + duration := time.Since(start).Seconds() + probeDurationGauge.Set(duration) + probeSuccessGauge.Set(1) + + // TODO check success, etc + h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + h.ServeHTTP(w, r) + } +} diff --git a/cmd/postgres_exporter/queries.go b/cmd/postgres_exporter/queries.go index e27480f4f..b17420d28 100644 --- a/cmd/postgres_exporter/queries.go +++ b/cmd/postgres_exporter/queries.go @@ -17,7 +17,7 @@ import ( "errors" "fmt" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/go-kit/log/level" "gopkg.in/yaml.v2" ) diff --git a/cmd/postgres_exporter/server.go b/cmd/postgres_exporter/server.go index 8747dffa0..bcfee6812 100644 --- a/cmd/postgres_exporter/server.go +++ b/cmd/postgres_exporter/server.go @@ -19,7 +19,7 @@ import ( "sync" "time" - "github.com/blang/semver" + "github.com/blang/semver/v4" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) diff --git a/collector/collector.go b/collector/collector.go index 58765b659..6d1a4dd11 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -15,6 +15,7 @@ package collector import ( "context" + "database/sql" "errors" "fmt" "sync" @@ -58,7 +59,7 @@ var ( ) type Collector interface { - Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error + Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error } func registerCollector(name string, isDefaultEnabled bool, createFunc func(logger log.Logger) (Collector, error)) { @@ -86,13 +87,13 @@ type PostgresCollector struct { Collectors map[string]Collector logger log.Logger - servers map[string]*server + db *sql.DB } type Option func(*PostgresCollector) error // NewPostgresCollector creates a new PostgresCollector. -func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, options ...Option) (*PostgresCollector, error) { +func NewPostgresCollector(logger log.Logger, dsn string, filters []string, options ...Option) (*PostgresCollector, error) { p := &PostgresCollector{ logger: logger, } @@ -136,17 +137,18 @@ func NewPostgresCollector(logger log.Logger, dsns []string, filters []string, op p.Collectors = collectors - servers := make(map[string]*server) - for _, dsn := range dsns { - s, err := makeServer(dsn) - if err != nil { - return nil, err - } + if dsn == "" { + return nil, errors.New("empty dsn") + } - servers[dsn] = s + db, err := sql.Open("postgres", dsn) + if err != nil { + return nil, err } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) - p.servers = servers + p.db = db return p, nil } @@ -160,32 +162,20 @@ func (p PostgresCollector) Describe(ch chan<- *prometheus.Desc) { // Collect implements the prometheus.Collector interface. func (p PostgresCollector) Collect(ch chan<- prometheus.Metric) { ctx := context.TODO() - wg := sync.WaitGroup{} - wg.Add(len(p.servers)) - for _, s := range p.servers { - go func(s *server) { - p.subCollect(ctx, s, ch) - wg.Done() - }(s) - } - wg.Wait() -} - -func (p PostgresCollector) subCollect(ctx context.Context, server *server, ch chan<- prometheus.Metric) { wg := sync.WaitGroup{} wg.Add(len(p.Collectors)) for name, c := range p.Collectors { go func(name string, c Collector) { - execute(ctx, name, c, server, ch, p.logger) + execute(ctx, name, c, p.db, ch, p.logger) wg.Done() }(name, c) } wg.Wait() } -func execute(ctx context.Context, name string, c Collector, s *server, ch chan<- prometheus.Metric, logger log.Logger) { +func execute(ctx context.Context, name string, c Collector, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) { begin := time.Now() - err := c.Update(ctx, s, ch) + err := c.Update(ctx, db, ch) duration := time.Since(begin) var success float64 diff --git a/collector/pg_database.go b/collector/pg_database.go index 5868f66d8..8fa4dab8e 100644 --- a/collector/pg_database.go +++ b/collector/pg_database.go @@ -15,6 +15,7 @@ package collector import ( "context" + "database/sql" "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" @@ -36,15 +37,11 @@ var pgDatabase = map[string]*prometheus.Desc{ "size_bytes": prometheus.NewDesc( "pg_database_size_bytes", "Disk space used by the database", - []string{"datname", "server"}, nil, + []string{"datname"}, nil, ), } -func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error { - db, err := server.GetDB() - if err != nil { - return err - } +func (PGDatabaseCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error { rows, err := db.QueryContext(ctx, `SELECT pg_database.datname ,pg_database_size(pg_database.datname) @@ -63,7 +60,7 @@ func (PGDatabaseCollector) Update(ctx context.Context, server *server, ch chan<- ch <- prometheus.MustNewConstMetric( pgDatabase["size_bytes"], - prometheus.GaugeValue, float64(size), datname, server.GetName(), + prometheus.GaugeValue, float64(size), datname, ) } if err := rows.Err(); err != nil { diff --git a/collector/pg_stat_bgwriter.go b/collector/pg_stat_bgwriter.go index 7e7d09c72..69c75653f 100644 --- a/collector/pg_stat_bgwriter.go +++ b/collector/pg_stat_bgwriter.go @@ -15,6 +15,7 @@ package collector import ( "context" + "database/sql" "time" "github.com/go-kit/log" @@ -38,77 +39,72 @@ var statBGWriter = map[string]*prometheus.Desc{ "checkpoints_timed": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_timed_total"), "Number of scheduled checkpoints that have been performed", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "checkpoints_req": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoints_req_total"), "Number of requested checkpoints that have been performed", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "checkpoint_write_time": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_write_time_total"), "Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk, in milliseconds", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "checkpoint_sync_time": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "checkpoint_sync_time_total"), "Total amount of time that has been spent in the portion of checkpoint processing where files are synchronized to disk, in milliseconds", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "buffers_checkpoint": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_checkpoint_total"), "Number of buffers written during checkpoints", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "buffers_clean": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_clean_total"), "Number of buffers written by the background writer", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "maxwritten_clean": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "maxwritten_clean_total"), "Number of times the background writer stopped a cleaning scan because it had written too many buffers", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "buffers_backend": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_backend_total"), "Number of buffers written directly by a backend", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "buffers_backend_fsync": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_backend_fsync_total"), "Number of times a backend had to execute its own fsync call (normally the background writer handles those even when the backend does its own write)", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "buffers_alloc": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "buffers_alloc_total"), "Number of buffers allocated", - []string{"server"}, + []string{}, prometheus.Labels{}, ), "stats_reset": prometheus.NewDesc( prometheus.BuildFQName(namespace, bgWriterSubsystem, "stats_reset_total"), "Time at which these statistics were last reset", - []string{"server"}, + []string{}, prometheus.Labels{}, ), } -func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch chan<- prometheus.Metric) error { - db, err := server.GetDB() - if err != nil { - return err - } - +func (PGStatBGWriterCollector) Update(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric) error { row := db.QueryRowContext(ctx, `SELECT checkpoints_timed @@ -126,8 +122,8 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch ch var cpt int var cpr int - var cpwt int - var cpst int + var cpwt float64 + var cpst float64 var bcp int var bc int var mwc int @@ -136,7 +132,7 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch ch var ba int var sr time.Time - err = row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr) + err := row.Scan(&cpt, &cpr, &cpwt, &cpst, &bcp, &bc, &mwc, &bb, &bbf, &ba, &sr) if err != nil { return err } @@ -145,67 +141,56 @@ func (PGStatBGWriterCollector) Update(ctx context.Context, server *server, ch ch statBGWriter["checkpoints_timed"], prometheus.CounterValue, float64(cpt), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["checkpoints_req"], prometheus.CounterValue, float64(cpr), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["checkpoint_write_time"], prometheus.CounterValue, float64(cpwt), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["checkpoint_sync_time"], prometheus.CounterValue, float64(cpst), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["buffers_checkpoint"], prometheus.CounterValue, float64(bcp), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["buffers_clean"], prometheus.CounterValue, float64(bc), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["maxwritten_clean"], prometheus.CounterValue, float64(mwc), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["buffers_backend"], prometheus.CounterValue, float64(bb), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["buffers_backend_fsync"], prometheus.CounterValue, float64(bbf), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["buffers_alloc"], prometheus.CounterValue, float64(ba), - server.GetName(), ) ch <- prometheus.MustNewConstMetric( statBGWriter["stats_reset"], prometheus.CounterValue, float64(sr.Unix()), - server.GetName(), ) return nil diff --git a/collector/probe.go b/collector/probe.go new file mode 100644 index 000000000..37cb151b9 --- /dev/null +++ b/collector/probe.go @@ -0,0 +1,85 @@ +// Copyright 2022 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" + "sync" + + "github.com/go-kit/log" + "github.com/prometheus-community/postgres_exporter/config" + "github.com/prometheus/client_golang/prometheus" +) + +type ProbeCollector struct { + registry *prometheus.Registry + collectors map[string]Collector + logger log.Logger + db *sql.DB +} + +func NewProbeCollector(logger log.Logger, registry *prometheus.Registry, dsn config.DSN) (*ProbeCollector, error) { + collectors := make(map[string]Collector) + initiatedCollectorsMtx.Lock() + defer initiatedCollectorsMtx.Unlock() + for key, enabled := range collectorState { + // TODO: Handle filters + // if !*enabled || (len(f) > 0 && !f[key]) { + // continue + // } + if !*enabled { + continue + } + if collector, ok := initiatedCollectors[key]; ok { + collectors[key] = collector + } else { + collector, err := factories[key](log.With(logger, "collector", key)) + if err != nil { + return nil, err + } + collectors[key] = collector + initiatedCollectors[key] = collector + } + } + + db, err := sql.Open("postgres", dsn.GetConnectionString()) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + return &ProbeCollector{ + registry: registry, + collectors: collectors, + logger: logger, + db: db, + }, nil +} + +func (pc *ProbeCollector) Describe(ch chan<- *prometheus.Desc) { +} + +func (pc *ProbeCollector) Collect(ch chan<- prometheus.Metric) { + wg := sync.WaitGroup{} + wg.Add(len(pc.collectors)) + for name, c := range pc.collectors { + go func(name string, c Collector) { + execute(context.TODO(), name, c, pc.db, ch, pc.logger) + wg.Done() + }(name, c) + } + wg.Wait() +} diff --git a/collector/server.go b/collector/server.go deleted file mode 100644 index fa490a2c6..000000000 --- a/collector/server.go +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2022 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 ( - "database/sql" - "fmt" - "strings" - - "github.com/lib/pq" -) - -type server struct { - dsn string - name string - db *sql.DB -} - -func makeServer(dsn string) (*server, error) { - name, err := parseServerName(dsn) - if err != nil { - return nil, err - } - return &server{ - dsn: dsn, - name: name, - }, nil -} - -func (s *server) GetDB() (*sql.DB, error) { - if s.db != nil { - return s.db, nil - } - - db, err := sql.Open("postgres", s.dsn) - if err != nil { - return nil, err - } - db.SetMaxOpenConns(1) - db.SetMaxIdleConns(1) - - s.db = db - - return s.db, nil -} - -func (s *server) GetName() string { - return s.name -} - -func (s *server) String() string { - return s.name -} - -func parseServerName(url string) (string, error) { - dsn, err := pq.ParseURL(url) - if err != nil { - dsn = url - } - - pairs := strings.Split(dsn, " ") - kv := make(map[string]string, len(pairs)) - for _, pair := range pairs { - splitted := strings.SplitN(pair, "=", 2) - if len(splitted) != 2 { - return "", fmt.Errorf("malformed dsn %q", dsn) - } - // Newer versions of pq.ParseURL quote values so trim them off if they exist - key := strings.Trim(splitted[0], "'\"") - value := strings.Trim(splitted[1], "'\"") - kv[key] = value - } - - var fingerprint string - - if host, ok := kv["host"]; ok { - fingerprint += host - } else { - fingerprint += "localhost" - } - - if port, ok := kv["port"]; ok { - fingerprint += ":" + port - } else { - fingerprint += ":5432" - } - - return fingerprint, nil -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..9e514f413 --- /dev/null +++ b/config/config.go @@ -0,0 +1,120 @@ +// Copyright 2022 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 config + +import ( + "fmt" + "os" + "sync" + + "github.com/go-kit/log" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "gopkg.in/yaml.v3" +) + +var ( + configReloadSuccess = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "postgres_exporter", + Name: "config_last_reload_successful", + Help: "Postgres exporter config loaded successfully.", + }) + + configReloadSeconds = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "postgres_exporter", + Name: "config_last_reload_success_timestamp_seconds", + Help: "Timestamp of the last successful configuration reload.", + }) +) + +type Config struct { + AuthModules map[string]AuthModule `yaml:"auth_modules"` +} + +type AuthModule struct { + Type string `yaml:"type"` + UserPass UserPass `yaml:"userpass,omitempty"` + // Add alternative auth modules here + Options map[string]string `yaml:"options"` +} + +type UserPass struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +type ConfigHandler struct { + sync.RWMutex + Config *Config +} + +func (ch *ConfigHandler) GetConfig() *Config { + ch.RLock() + defer ch.RUnlock() + return ch.Config +} + +func (ch *ConfigHandler) ReloadConfig(f string, logger log.Logger) error { + config := &Config{} + var err error + defer func() { + if err != nil { + configReloadSuccess.Set(0) + } else { + configReloadSuccess.Set(1) + configReloadSeconds.SetToCurrentTime() + } + }() + + yamlReader, err := os.Open(f) + if err != nil { + 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) + } + + ch.Lock() + ch.Config = config + ch.Unlock() + return nil +} + +func (m AuthModule) ConfigureTarget(target string) (DSN, error) { + dsn, err := dsnFromString(target) + if err != nil { + return DSN{}, err + } + + // Set the credentials from the authentication module + // TODO(@sysadmind): What should the order of precedence be? + if m.Type == "userpass" { + if m.UserPass.Username != "" { + dsn.username = m.UserPass.Username + } + if m.UserPass.Password != "" { + dsn.password = m.UserPass.Password + } + } + + for k, v := range m.Options { + dsn.query.Set(k, v) + } + + return dsn, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 000000000..63b932adb --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,58 @@ +// Copyright 2022 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 config + +import ( + "testing" +) + +func TestLoadConfig(t *testing.T) { + ch := &ConfigHandler{ + Config: &Config{}, + } + + err := ch.ReloadConfig("testdata/config-good.yaml", nil) + if err != nil { + t.Errorf("Error loading config: %s", err) + } +} + +func TestLoadBadConfigs(t *testing.T) { + ch := &ConfigHandler{ + Config: &Config{}, + } + + tests := []struct { + input string + want string + }{ + { + 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", + }, + { + 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", + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + got := ch.ReloadConfig(test.input, nil) + if got == nil || got.Error() != test.want { + t.Fatalf("ReloadConfig(%q) = %v, want %s", test.input, got, test.want) + } + }) + } +} diff --git a/config/dsn.go b/config/dsn.go new file mode 100644 index 000000000..78d798d5f --- /dev/null +++ b/config/dsn.go @@ -0,0 +1,238 @@ +// Copyright 2022 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 config + +import ( + "fmt" + "net/url" + "regexp" + "strings" + "unicode" +) + +// DSN represents a parsed datasource. It contains fields for the individual connection components. +type DSN struct { + scheme string + username string + password string + host string + path string + query url.Values +} + +// String makes a dsn safe to print by excluding any passwords. This allows dsn to be used in +// strings and log messages without needing to call a redaction function first. +func (d DSN) String() string { + if d.password != "" { + return fmt.Sprintf("%s://%s:******@%s%s?%s", d.scheme, d.username, d.host, d.path, d.query.Encode()) + } + + if d.username != "" { + return fmt.Sprintf("%s://%s@%s%s?%s", d.scheme, d.username, d.host, d.path, d.query.Encode()) + } + + return fmt.Sprintf("%s://%s%s?%s", d.scheme, d.host, d.path, d.query.Encode()) +} + +// GetConnectionString returns the URL to pass to the driver for database connections. This value should not be logged. +func (d DSN) GetConnectionString() string { + u := url.URL{ + Scheme: d.scheme, + Host: d.host, + Path: d.path, + RawQuery: d.query.Encode(), + } + + // Username and Password + if d.username != "" { + u.User = url.UserPassword(d.username, d.password) + } + + return u.String() +} + +// dsnFromString parses a connection string into a dsn. It will attempt to parse the string as +// a URL and as a set of key=value pairs. If both attempts fail, dsnFromString will return an error. +func dsnFromString(in string) (DSN, error) { + if strings.HasPrefix(in, "postgresql://") { + return dsnFromURL(in) + } + + // Try to parse as key=value pairs + d, err := dsnFromKeyValue(in) + if err == nil { + return d, nil + } + + // Parse the string as a URL, with the scheme prefixed + d, err = dsnFromURL(fmt.Sprintf("postgresql://%s", in)) + if err == nil { + return d, nil + } + + return DSN{}, fmt.Errorf("could not understand DSN") +} + +// dsnFromURL parses the input as a URL and returns the dsn representation. +func dsnFromURL(in string) (DSN, error) { + u, err := url.Parse(in) + if err != nil { + return DSN{}, err + } + pass, _ := u.User.Password() + user := u.User.Username() + + query := u.Query() + + if queryPass := query.Get("password"); queryPass != "" { + if pass == "" { + pass = queryPass + } + } + query.Del("password") + + if queryUser := query.Get("user"); queryUser != "" { + if user == "" { + user = queryUser + } + } + query.Del("user") + + d := DSN{ + scheme: u.Scheme, + username: user, + password: pass, + host: u.Host, + path: u.Path, + query: query, + } + + return d, nil +} + +// dsnFromKeyValue parses the input as a set of key=value pairs and returns the dsn representation. +func dsnFromKeyValue(in string) (DSN, error) { + // Attempt to confirm at least one key=value pair before starting the rune parser + connstringRe := regexp.MustCompile(`^ *[a-zA-Z0-9]+ *= *[^= ]+`) + if !connstringRe.MatchString(in) { + return DSN{}, fmt.Errorf("input is not a key-value DSN") + } + + // Anything other than known fields should be part of the querystring + query := url.Values{} + + pairs, err := parseKeyValue(in) + if err != nil { + return DSN{}, fmt.Errorf("failed to parse key-value DSN: %v", err) + } + + // Build the dsn from the key=value pairs + d := DSN{ + scheme: "postgresql", + } + + hostname := "" + port := "" + + for k, v := range pairs { + switch k { + case "host": + hostname = v + case "port": + port = v + case "user": + d.username = v + case "password": + d.password = v + default: + query.Set(k, v) + } + } + + if hostname == "" { + hostname = "localhost" + } + + if port == "" { + d.host = hostname + } else { + d.host = fmt.Sprintf("%s:%s", hostname, port) + } + + d.query = query + + return d, nil +} + +// parseKeyValue is a key=value parser. It loops over each rune to split out keys and values +// and attempting to honor quoted values. parseKeyValue will return an error if it is unable +// to properly parse the input. +func parseKeyValue(in string) (map[string]string, error) { + out := map[string]string{} + + inPart := false + inQuote := false + part := []rune{} + key := "" + for _, c := range in { + switch { + case unicode.In(c, unicode.Quotation_Mark): + if inQuote { + inQuote = false + } else { + inQuote = true + } + case unicode.In(c, unicode.White_Space): + if inPart { + if inQuote { + part = append(part, c) + } else { + // Are we finishing a key=value? + if key == "" { + return out, fmt.Errorf("invalid input") + } + out[key] = string(part) + inPart = false + part = []rune{} + } + } else { + // Are we finishing a key=value? + if key == "" { + return out, fmt.Errorf("invalid input") + } + out[key] = string(part) + inPart = false + part = []rune{} + // Do something with the value + } + case c == '=': + if inPart { + inPart = false + key = string(part) + part = []rune{} + } else { + return out, fmt.Errorf("invalid input") + } + default: + inPart = true + part = append(part, c) + } + } + + if key != "" && len(part) > 0 { + out[key] = string(part) + } + + return out, nil +} diff --git a/config/dsn_test.go b/config/dsn_test.go new file mode 100644 index 000000000..637a3568e --- /dev/null +++ b/config/dsn_test.go @@ -0,0 +1,215 @@ +// Copyright 2022 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 config + +import ( + "net/url" + "reflect" + "testing" +) + +// Test_dsn_String is designed to test different dsn combinations for their string representation. +// dsn.String() is designed to be safe to print, redacting any password information and these test +// cases are intended to cover known cases. +func Test_dsn_String(t *testing.T) { + type fields struct { + scheme string + username string + password string + host string + path string + query url.Values + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Without Password", + fields: fields{ + scheme: "postgresql", + username: "test", + host: "localhost:5432", + query: url.Values{}, + }, + want: "postgresql://test@localhost:5432?", + }, + { + name: "With Password", + fields: fields{ + scheme: "postgresql", + username: "test", + password: "supersecret", + host: "localhost:5432", + query: url.Values{}, + }, + want: "postgresql://test:******@localhost:5432?", + }, + { + name: "With Password and Query String", + fields: fields{ + scheme: "postgresql", + username: "test", + password: "supersecret", + host: "localhost:5432", + query: url.Values{ + "ssldisable": []string{"true"}, + }, + }, + want: "postgresql://test:******@localhost:5432?ssldisable=true", + }, + { + name: "With Password, Path, and Query String", + fields: fields{ + scheme: "postgresql", + username: "test", + password: "supersecret", + host: "localhost:5432", + path: "/somevalue", + query: url.Values{ + "ssldisable": []string{"true"}, + }, + }, + want: "postgresql://test:******@localhost:5432/somevalue?ssldisable=true", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := DSN{ + scheme: tt.fields.scheme, + username: tt.fields.username, + password: tt.fields.password, + host: tt.fields.host, + path: tt.fields.path, + query: tt.fields.query, + } + if got := d.String(); got != tt.want { + t.Errorf("dsn.String() = %v, want %v", got, tt.want) + } + }) + } +} + +// Test_dsnFromString tests the dsnFromString function with known variations +// of connection string inputs to ensure that it properly parses the input into +// a dsn. +func Test_dsnFromString(t *testing.T) { + + tests := []struct { + name string + input string + want DSN + wantErr bool + }{ + { + name: "Key value with password", + input: "host=host.example.com user=postgres port=5432 password=s3cr3t", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + username: "postgres", + password: "s3cr3t", + query: url.Values{}, + }, + wantErr: false, + }, + { + name: "Key value with quoted password and space", + input: "host=host.example.com user=postgres port=5432 password=\"s3cr 3t\"", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + username: "postgres", + password: "s3cr 3t", + query: url.Values{}, + }, + wantErr: false, + }, + { + name: "Key value with different order", + input: "password=abcde host=host.example.com user=postgres port=5432", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + username: "postgres", + password: "abcde", + query: url.Values{}, + }, + wantErr: false, + }, + { + name: "Key value with different order, quoted password, duplicate password", + input: "password=abcde host=host.example.com user=postgres port=5432 password=\"s3cr 3t\"", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + username: "postgres", + password: "s3cr 3t", + query: url.Values{}, + }, + wantErr: false, + }, + { + name: "URL with user in query string", + input: "postgresql://host.example.com:5432/tsdb?user=postgres", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + path: "/tsdb", + query: url.Values{}, + username: "postgres", + }, + wantErr: false, + }, + { + name: "URL with user and password", + input: "postgresql://user:s3cret@host.example.com:5432/tsdb?user=postgres", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + path: "/tsdb", + query: url.Values{}, + username: "user", + password: "s3cret", + }, + wantErr: false, + }, + { + name: "URL with user and password in query string", + input: "postgresql://host.example.com:5432/tsdb?user=postgres&password=s3cr3t", + want: DSN{ + scheme: "postgresql", + host: "host.example.com:5432", + path: "/tsdb", + query: url.Values{}, + username: "postgres", + password: "s3cr3t", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := dsnFromString(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("dsnFromString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("dsnFromString() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/config/testdata/config-bad-auth-module.yaml b/config/testdata/config-bad-auth-module.yaml new file mode 100644 index 000000000..8f718dd5a --- /dev/null +++ b/config/testdata/config-bad-auth-module.yaml @@ -0,0 +1,7 @@ +auth_modules: + foo: + pretendauth: + username: test + password: pass + options: + extra: "1" diff --git a/config/testdata/config-bad-extra-field.yaml b/config/testdata/config-bad-extra-field.yaml new file mode 100644 index 000000000..f6ff6d6cf --- /dev/null +++ b/config/testdata/config-bad-extra-field.yaml @@ -0,0 +1,8 @@ +auth_modules: + foo: + userpass: + username: test + password: pass + options: + extra: "1" + doesNotExist: test diff --git a/config/testdata/config-good.yaml b/config/testdata/config-good.yaml new file mode 100644 index 000000000..13453e26f --- /dev/null +++ b/config/testdata/config-good.yaml @@ -0,0 +1,8 @@ +auth_modules: + first: + type: userpass + userpass: + username: first + password: firstpass + options: + sslmode: disable diff --git a/go.mod b/go.mod index c2505f7fe..f8a0c26f0 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,17 @@ module github.com/prometheus-community/postgres_exporter go 1.17 require ( - github.com/blang/semver v3.5.1+incompatible + github.com/blang/semver/v4 v4.0.0 github.com/go-kit/log v0.2.1 - github.com/lib/pq v1.10.6 - github.com/prometheus/client_golang v1.12.2 + github.com/lib/pq v1.10.7 + github.com/prometheus/client_golang v1.13.0 github.com/prometheus/client_model v0.2.0 - github.com/prometheus/common v0.34.0 + github.com/prometheus/common v0.37.0 github.com/prometheus/exporter-toolkit v0.7.1 gopkg.in/alecthomas/kingpin.v2 v2.2.6 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -28,12 +29,12 @@ require ( github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/procfs v0.8.0 // indirect golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.26.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect ) diff --git a/go.sum b/go.sum index 8bc754b16..631253203 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 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 v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -116,8 +116,9 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -154,8 +155,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -177,8 +178,8 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= -github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -189,16 +190,17 @@ github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB8 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= -github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= +github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/exporter-toolkit v0.7.1 h1:c6RXaK8xBVercEeUQ4tRNL8UGWzDHfvj9dseo1FcK1Y= github.com/prometheus/exporter-toolkit v0.7.1/go.mod h1:ZUBIj498ePooX9t/2xtDjeQYwvRpiPP2lh5u4iblj2g= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -305,6 +307,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -342,8 +345,9 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -400,7 +404,6 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -477,8 +480,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -494,6 +498,8 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=