diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c033bf9b..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -version: 2.1 -orbs: - prometheus: prometheus/prometheus@0.16.0 -executors: - # Whenever the Go version is updated here, .promu.yml - # should also be updated. - golang: - docker: - - image: cimg/go:1.18 -jobs: - test: - executor: golang - steps: - - prometheus/setup_environment - - run: make check_license style staticcheck unused build test-short - - prometheus/store_artifact: - file: mysqld_exporter - integration: - docker: - - image: cimg/go:1.18 - - image: << parameters.mysql_image >> - environment: - MYSQL_ALLOW_EMPTY_PASSWORD: "yes" - MYSQL_ROOT_HOST: '%' - parameters: - mysql_image: - type: string - steps: - - checkout - - setup_remote_docker - - run: docker version - - run: docker-compose --version - - run: make build - - run: make test - codespell: - docker: - - image: cimg/python:3.10 - steps: - - checkout - - run: pip install codespell - - run: codespell --skip=".git,./vendor,ttar,Makefile.common" -L uint,ist,keypair - mixin: - executor: golang - steps: - - checkout - - run: go install github.com/monitoring-mixins/mixtool/cmd/mixtool@latest - - run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest - - run: make -C mysqld-mixin lint build -workflows: - version: 2 - mysqld_exporter: - jobs: - - test: - filters: - tags: - only: /.*/ - - integration: - matrix: - parameters: - mysql_image: - - percona:5.6 - - mysql/mysql-server:5.7.33 - - mysql/mysql-server:8.0 - - mariadb:10.3 - - mariadb:10.4 - - mariadb:10.5 - - mariadb:10.6 - - mariadb:10.7 - - 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: - ignore: /^v2(\.[0-9]+){2}(-.+|[^-.]*)$/ - branches: - ignore: /^(main|release-.*|.*build-all.*)$/ - - prometheus/build: - name: build_all - parallelism: 12 - filters: - branches: - only: /^(main|release-.*|.*build-all.*)$/ - tags: - only: /^v2(\.[0-9]+){2}(-.+|[^-.]*)$/ - - - codespell: - filters: - tags: - only: /.*/ - - mixin: - filters: - tags: - only: /.*/ - - prometheus/publish_main: - context: org-context - requires: - - test - - build_all - filters: - branches: - only: main - - prometheus/publish_release: - context: org-context - requires: - - test - - build_all - filters: - tags: - only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ - branches: - ignore: /.*/ diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9f30266d..e5f72131 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,20 +14,15 @@ jobs: strategy: matrix: mysql-image: - - mysql/mysql-server:5.5 - - mysql/mysql-server:5.6 - mysql/mysql-server:5.7 - mysql/mysql-server:8.0 - - mariadb:5.5 - - mariadb:10.0 - - mariadb:10.1 - - mariadb:10.2 - - mariadb:10.3 - - percona/percona-server:5.6 + - mariadb:10.5 + - mariadb:10.6 + - mariadb:10.11 + - mariadb:11.4 - percona/percona-server:5.7 - percona/percona-server:8.0 - - percona:5.5 - - percona:5.6 + - percona/percona-server:8.4 - percona:5.7 - percona:8.0 runs-on: ubuntu-latest @@ -43,8 +38,7 @@ jobs: - name: Run checks run: | - go build -modfile=tools/go.mod -o bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint - go build -modfile=tools/go.mod -o bin/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog + make init bin/golangci-lint run -c=.golangci-required.yml --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} bin/reviewdog -f=golangci-lint -level=error -reporter=github-pr-check bin/golangci-lint run -c=.golangci.yml --out-format=line-number | env REVIEWDOG_GITHUB_API_TOKEN=${{ secrets.GITHUB_TOKEN }} bin/reviewdog -f=golangci-lint -level=error -reporter=github-pr-review diff --git a/.gitignore b/.gitignore index d074be2a..eefa68d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ /.build +/bin /mysqld_exporter /.release /.tarballs -*.tar.gz +*.tar.{gz,xz} *.test *-stamp .idea diff --git a/.golangci-required.yml b/.golangci-required.yml index dd82e1de..5e643d98 100644 --- a/.golangci-required.yml +++ b/.golangci-required.yml @@ -1,4 +1,12 @@ --- +linters-settings: + depguard: + rules: + main: + deny: + - pkg: "github.com/pkg/errors" + desc: use the "errors" package instead + # The most valuable linters; they are required to pass for PR to be merged. linters: disable-all: true diff --git a/.golangci.yml b/.golangci.yml index 5876793a..1898b56f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,5 +2,7 @@ # Run only staticcheck for now. Additional linters will be enabled one-by-one. linters: enable: + - misspell - staticcheck + - sloglint disable-all: true diff --git a/.promu.yml b/.promu.yml index 01205763..8bbf10a2 100644 --- a/.promu.yml +++ b/.promu.yml @@ -1,11 +1,10 @@ go: # Whenever the Go version is updated here, .circle/config.yml should also # be updated. - version: 1.21 + version: 1.23 repository: path: github.com/percona/mysqld_exporter build: - flags: -a -tags netgo ldflags: | -X github.com/prometheus/common/version.Version={{.Version}} -X github.com/prometheus/common/version.Revision={{.Revision}} diff --git a/.yamllint b/.yamllint index 3878a31d..8d09c375 100644 --- a/.yamllint +++ b/.yamllint @@ -1,5 +1,7 @@ --- extends: default +ignore: | + **/node_modules rules: braces: @@ -20,9 +22,4 @@ rules: config/testdata/section_key_dup.bad.yml line-length: disable truthy: - ignore: | - .github/workflows/codeql-analysis.yml - .github/workflows/funcbench.yml - .github/workflows/fuzzing.yml - .github/workflows/prombench.yml - .github/workflows/golangci-lint.yml + check-keys: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c2743db..f77149b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,66 @@ Changes: * [ENHANCEMENT] * [BUGFIX] +## 0.16.0 / 2024-11-08 + +Changes: + +* [CHANGE] Replace logging library go-kit/log with slog #875 +* [FEATURE] Support for prometheus scrape timeout in probe endpoint #828 +* [ENHANCEMENT] Support MySQL 8.4 replicas syntax #837 +* [ENHANCEMENT] Fetch lock time and cpu time from performance schema #862 +* [ENHANCEMENT] Add the instance struct to handle connections #859 +* [ENHANCEMENT] Optimize code by using built-in constants in the standard lib #844 +* [BUGFIX] Fix fetching tmpTables vs tmpDiskTables from performance_schema #853 +* [BUGFIX] Skip SPACE_TYPE column for MariaDB >=10.5 #860 +* [BUGFIX] Fixed parsing of timestamps with non-zero padded days #841 +* [BUGFIX] Fix auto_increment metric collection errors caused by using collation in INFORMATION_SCHEMA searches #833 +* [BUGFIX] Fix race condition in ReloadConfig #760 +* [BUGFIX] Change processlist query to support ONLY_FULL_GROUP_BY sql_mode #684 +* [BUGFIX] replication_applier_status_by_worker requires mysql 8.0 #683 +* [BUGFIX] Update docker registry link in README.md #813 +* [BUGFIX] Fix Docker run command and update documentation for cnf file handling #843 +* [BUGFIX] info_schema_tables: do not collect the sys schema #879 + +## 0.15.1 / 2023-12-12 + +* Rebuild for dependency updates + +## 0.15.0 / 2023-06-16 + +BREAKING CHANGES: + +The exporter no longer supports the monolithic `DATA_SOURCE_NAME` environment variable. +To configure connections to MySQL you can either use a `my.cnf` style config file or command line arguments. + +For example: + + export MYSQLD_EXPORTER_PASSWORD=secret + mysqld_exporter --mysqld.address=localhost:3306 --mysqld.username=exporter + +We have also dropped some internal scrape metrics: +- `mysql_exporter_scrapes_total` +- `mysql_exporter_scrape_errors_total` +- `mysql_last_scrape_failed` + +The default client configuration file is now `.my.cnf` in the process working directory. Use `--config.my-cnf="$HOME/.my.cnf"` to retain the previous default. + +Changes: + +* [CHANGE] Allow `tlsCfg.InsecureSkipVerify` outside of mTLS #631 +* [CHANGE] Update to exporter-toolkit v0.8.1 #677 +* [CHANGE] Fix shared metrics between requests #722 +* [CHANGE] Allow empty passwords #742 +* [CHANGE] Don't use HOME env in the my-cnf config path. #745 +* [FEATURE] Add support for collecting metrics from `sys.user_summary` #628 +* [FEATURE] Support for multi-target mysqld probes #651 +* [FEATURE] Add MySQL TLS configurations #718 +* [FEATURE] Add config reload via /-/reload #734 +* [ENHANCEMENT] Add UNIX domain socket support for multi-target scraping #707 +* [ENHANCEMENT] Use `STRAIGHT_JOIN` in infoSchemaAutoIncrementQuery #726 +* [BUGFIX] Fix `infoSchemaInnodbMetricsEnabledColumnQuery` #687 +* [BUGFIX] Allow empty passwords #742 + ## 0.14.0 / 2022-01-05 BREAKING CHANGES: diff --git a/Makefile b/Makefile index 66431d49..ce24a88d 100644 --- a/Makefile +++ b/Makefile @@ -20,12 +20,16 @@ PREFIX ?= $(shell pwd) BIN_DIR ?= $(shell pwd) DOCKER_IMAGE_NAME ?= mysqld-exporter DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) -TMPDIR ?= $(shell dirname $(shell mktemp)/) +TMPDIR ?= $(shell dirname $(shell mktemp -d)/) default: help all: format build test-short +init: ## Install tools + rm -rf bin/* + cd tools && go generate -x -tags=tools + env-up: ## Start MySQL and copy ssl certificates to /tmp @docker-compose up -d @sleep 5 @@ -49,25 +53,26 @@ test: ## Run all tests @echo ">> running tests" @$(GO) test -race $(pkgs) +FILES = $(shell find . -type f -name '*.go') + format: ## Format the code @echo ">> formatting code" @$(GO) fmt $(pkgs) + @bin/goimports -local github.com/percona/pmm -l -w $(FILES) -FILES = $(shell find . -type f -name '*.go') - -fumpt: ## Format source code using fumpt and fumports. - @gofumpt -w -s $(FILES) - @gofumports -local github.com/percona/mysqld_exporter -l -w $(FILES) +fumpt: ## Format source code using fumpt and goimports. + bin/gofumpt -l -w $(FILES) + bin/goimports -local github.com/percona/pmm -l -w $(FILES) vet: ## Run vet @echo ">> vetting code" @$(GO) vet $(pkgs) -build: promu ## Build binaries +build: ## Build binaries @echo ">> building binaries" @$(PROMU) build --prefix $(PREFIX) -tarball: promu ## Build release tarball +tarball: ## Build release tarball @echo ">> building release tarball" @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) @@ -75,23 +80,23 @@ docker: ## Build docker image @echo ">> building docker image" @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . -promu: ## Install promu - @GOOS=$(shell uname -s | tr A-Z a-z) \ - GO111MODULE=on \ - GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \ - $(GO) build -modfile=tools/go.mod -o bin/promu github.com/prometheus/promu - help: ## Display this help message. @echo "$(TMPDIR)" @echo "Please use \`make \` where is one of:" @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | \ awk -F ':.*?## ' 'NF==2 {printf " %-26s%s\n", $$1, $$2}' -GO_BUILD_LDFLAGS = -X github.com/prometheus/common/version.Version=$(shell cat VERSION) -X github.com/prometheus/common/version.Revision=$(shell git rev-parse HEAD) -X github.com/prometheus/common/version.Branch=$(shell git describe --always --contains --all) -X github.com/prometheus/common/version.BuildUser= -X github.com/prometheus/common/version.BuildDate=$(shell date +%FT%T%z) -s -w +GO_BUILD_LDFLAGS = -ldflags " \ + -X github.com/prometheus/common/version.Version=$(shell cat VERSION) \ + -X github.com/prometheus/common/version.Revision=$(shell git rev-parse HEAD) \ + -X github.com/prometheus/common/version.Branch=$(shell git describe --always --contains --all) \ + -X github.com/prometheus/common/version.BuildUser= \ + -X github.com/prometheus/common/version.BuildDate=$(shell date +%FT%T%z) -s -w \ + " export PMM_RELEASE_PATH?=. release: - go build -ldflags="$(GO_BUILD_LDFLAGS)" -o $(PMM_RELEASE_PATH)/mysqld_exporter + go build $(GO_BUILD_LDFLAGS) -o $(PMM_RELEASE_PATH)/mysqld_exporter -.PHONY: all style format build test vet tarball docker promu env-up env-down help default +.PHONY: all init style format build test vet tarball docker env-up env-down help default diff --git a/README.md b/README.md index 4557e72b..efeebf02 100644 --- a/README.md +++ b/README.md @@ -26,19 +26,55 @@ NOTE: It is recommended to set a max connection limit for the user to avoid over ### Build - make + make build ### Running -Running using an environment variable: +##### Single exporter mode - export DATA_SOURCE_NAME='user:password@(hostname:3306)/' - ./mysqld_exporter - -Running using ~/.my.cnf: +Running using `.my.cnf` from the current directory: ./mysqld_exporter +##### Multi-target support + +This exporter supports the multi-target pattern. This allows running a single instance of this exporter for multiple MySQL targets. + +To use the multi-target functionality, send an http request to the endpoint `/probe?target=foo:3306` where target is set to the DSN of the MySQL instance to scrape metrics from. + +To avoid putting sensitive information like username and password in the URL, you can have multiple configurations in `config.my-cnf` file and match it by adding `&auth_module=
` to the request. + +Sample config file for multiple configurations + + [client] + user = foo + password = foo123 + [client.servers] + user = bar + password = bar123 + +On the prometheus side you can set a scrape config as follows + + - job_name: mysql # To get metrics about the mysql exporter’s targets + params: + # Not required. Will match value to child in config file. Default value is `client`. + auth_module: [client.servers] + static_configs: + - targets: + # All mysql hostnames or unix sockets to monitor. + - server1:3306 + - server2:3306 + - unix:///run/mysqld/mysqld.sock + relabel_configs: + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + # The mysqld_exporter host:port + replacement: localhost:9104 + +##### Flag format Example format for flags for version > 0.10.0: --collect.auto_increment.columns @@ -60,6 +96,10 @@ collect.engine_tokudb_status | 5.6 | C collect.global_status | 5.1 | Collect from SHOW GLOBAL STATUS (Enabled by default) collect.global_variables | 5.1 | Collect from SHOW GLOBAL VARIABLES (Enabled by default) collect.plugins | 5.1 | Collect from SHOW PLUGINS +collect.heartbeat | 5.1 | Collect from [heartbeat](#heartbeat). +collect.heartbeat.database | 5.1 | Database from where to collect heartbeat data. (default: heartbeat) +collect.heartbeat.table | 5.1 | Table from where to collect heartbeat data. (default: heartbeat) +collect.heartbeat.utc | 5.1 | Use UTC for timestamps of the current server (`pt-heartbeat` is called with `--utc`). (default: false) collect.info_schema.clientstats | 5.5 | If running with userstat=1, set to true to collect client statistics. collect.info_schema.innodb_metrics | 5.6 | Collect metrics from information_schema.innodb_metrics. collect.info_schema.innodb_tablespaces | 5.7 | Collect metrics from information_schema.innodb_sys_tablespaces. @@ -94,15 +134,14 @@ collect.perf_schema.replication_group_member_stats | 5.7 | C collect.perf_schema.replication_applier_status_by_worker | 5.7 | Collect metrics from performance_schema.replication_applier_status_by_worker. collect.slave_status | 5.1 | Collect from SHOW SLAVE STATUS (Enabled by default) collect.slave_hosts | 5.1 | Collect from SHOW SLAVE HOSTS -collect.heartbeat | 5.1 | Collect from [heartbeat](#heartbeat). -collect.heartbeat.database | 5.1 | Database from where to collect heartbeat data. (default: heartbeat) -collect.heartbeat.table | 5.1 | Table from where to collect heartbeat data. (default: heartbeat) -collect.heartbeat.utc | 5.1 | Use UTC for timestamps of the current server (`pt-heartbeat` is called with `--utc`). (default: false) +collect.sys.user_summary | 5.7 | Collect metrics from sys.x$user_summary (disabled by default). ### General Flags Name | Description -------------------------------------------|-------------------------------------------------------------------------------------------------- +mysqld.address | Hostname and port used for connecting to MySQL server, format: `host:port`. (default: `locahost:3306`) +mysqld.username | Username to be used for connecting to MySQL Server config.my-cnf | Path to .my.cnf file to read MySQL credentials from. (default: `~/.my.cnf`) log.level | Logging verbosity (default: info) exporter.lock_wait_timeout | Set a lock_wait_timeout (in seconds) on the connection to avoid long metadata locking. (default: 2) @@ -113,6 +152,15 @@ web.listen-address | Address to listen on for web interf web.telemetry-path | Path under which to expose metrics. version | Print the version information. +### Environment Variables +Name | Description +-------------------------------------------|-------------------------------------------------------------------------------------------------- +MYSQLD_EXPORTER_PASSWORD | Password to be used for connecting to MySQL Server + +### Configuration precedence + +If you have configured cli with both `mysqld` flags and a valid configuration file, the options in the configuration file will override the flags for `client` section. + ## TLS and basic authentication The MySQLd Exporter supports TLS and basic authentication. @@ -121,12 +169,6 @@ To use TLS and/or basic authentication, you need to pass a configuration file using the `--web.config.file` parameter. The format of the file is described [in the exporter-toolkit repository](https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md). -### Setting the MySQL server's data source name - -The MySQL server's [data source name](http://en.wikipedia.org/wiki/Data_source_name) -must be set via the `DATA_SOURCE_NAME` environment variable. -The format of this variable is described at https://github.com/go-sql-driver/mysql#dsn-data-source-name. - ## Customizing Configuration for a SSL Connection If The MySQL server supports SSL, you may need to specify a CA truststore to verify the server's chain-of-trust. You may also need to specify a SSL keypair for the client side of the SSL connection. To configure the mysqld exporter to use a custom CA certificate, add the following to the mysql cnf file: @@ -142,12 +184,10 @@ ssl-key=/path/to/ssl/client/key ssl-cert=/path/to/ssl/client/cert ``` -Customizing the SSL configuration is only supported in the mysql cnf file and is not supported if you set the mysql server's data source name in the environment variable DATA_SOURCE_NAME. - ## Using Docker -You can deploy this exporter using the [prom/mysqld-exporter](https://registry.hub.docker.com/r/prom/mysqld-exporter/) Docker image. +You can deploy this exporter using the [prom/mysqld-exporter](https://hub.docker.com/r/prom/mysqld-exporter/) Docker image. For example: @@ -157,8 +197,8 @@ docker pull prom/mysqld-exporter docker run -d \ -p 9104:9104 \ + -v /home/user/user_my.cnf:/.my.cnf \ --network my-mysql-network \ - -e DATA_SOURCE_NAME="user:password@(hostname:3306)/" \ prom/mysqld-exporter ``` diff --git a/VERSION b/VERSION index a803cc22..04a373ef 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.14.0 +0.16.0 diff --git a/collector/binlog.go b/collector/binlog.go index 419f4330..b012f4be 100644 --- a/collector/binlog.go +++ b/collector/binlog.go @@ -17,12 +17,11 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" "strconv" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -53,7 +52,7 @@ var ( ) ) -// ScrapeBinlogSize colects from `SHOW BINARY LOGS`. +// ScrapeBinlogSize collects from `SHOW BINARY LOGS`. type ScrapeBinlogSize struct{} // Name of the Scraper. Should be unique. @@ -72,8 +71,9 @@ func (ScrapeBinlogSize) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeBinlogSize) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeBinlogSize) Scrape(ctx context.Context, Instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var logBin uint8 + db := Instance.GetDB() err := db.QueryRowContext(ctx, logbinQuery).Scan(&logBin) if err != nil { return err diff --git a/collector/binlog_test.go b/collector/binlog_test.go index bfe8f382..8846bedf 100644 --- a/collector/binlog_test.go +++ b/collector/binlog_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -31,6 +31,8 @@ func TestScrapeBinlogSize(t *testing.T) { } defer db.Close() + inst := &Instance{db: db} + mock.ExpectQuery(logbinQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) columns := []string{"Log_name", "File_size"} @@ -42,7 +44,7 @@ func TestScrapeBinlogSize(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeBinlogSize{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeBinlogSize{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/collector.go b/collector/collector.go index dfe692fd..89324f0a 100644 --- a/collector/collector.go +++ b/collector/collector.go @@ -59,10 +59,10 @@ func parseStatus(data sql.RawBytes) (float64, bool) { case "non-primary", "disconnected": return 0, true } - if ts, err := time.Parse("Jan 02 15:04:05 2006 MST", string(data)); err == nil { + if ts, err := time.Parse("Jan _2 15:04:05 2006 MST", string(data)); err == nil { return float64(ts.Unix()), true } - if ts, err := time.Parse("2006-01-02 15:04:05", string(data)); err == nil { + if ts, err := time.Parse(time.DateTime, string(data)); err == nil { return float64(ts.Unix()), true } if logNum := logRE.Find(data); logNum != nil { diff --git a/collector/collector_percona_extensions.go b/collector/collector_percona_extensions.go index c13b4172..7280620e 100644 --- a/collector/collector_percona_extensions.go +++ b/collector/collector_percona_extensions.go @@ -15,6 +15,7 @@ package collector import ( "database/sql" + "github.com/prometheus/client_golang/prometheus" ) diff --git a/collector/engine_innodb.go b/collector/engine_innodb.go index 9b1a33c2..b4aaf0ba 100644 --- a/collector/engine_innodb.go +++ b/collector/engine_innodb.go @@ -17,12 +17,11 @@ package collector import ( "context" - "database/sql" + "log/slog" "regexp" "strconv" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -52,7 +51,8 @@ func (ScrapeEngineInnodbStatus) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeEngineInnodbStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeEngineInnodbStatus) Scrape(ctx context.Context, Instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := Instance.GetDB() rows, err := db.QueryContext(ctx, engineInnodbStatusQuery) if err != nil { return err diff --git a/collector/engine_innodb_test.go b/collector/engine_innodb_test.go index bf19caf6..42ef3a61 100644 --- a/collector/engine_innodb_test.go +++ b/collector/engine_innodb_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -152,10 +152,10 @@ END OF INNODB MONITOR OUTPUT rows := sqlmock.NewRows(columns).AddRow("InnoDB", "", sample) mock.ExpectQuery(sanitizeQuery(engineInnodbStatusQuery)).WillReturnRows(rows) - + inst := &Instance{db: db} ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeEngineInnodbStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeEngineInnodbStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/engine_tokudb.go b/collector/engine_tokudb.go index d4e89adc..1c8ab7bc 100644 --- a/collector/engine_tokudb.go +++ b/collector/engine_tokudb.go @@ -18,9 +18,9 @@ package collector import ( "context" "database/sql" + "log/slog" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -50,7 +50,8 @@ func (ScrapeEngineTokudbStatus) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeEngineTokudbStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeEngineTokudbStatus) Scrape(ctx context.Context, Instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := Instance.GetDB() tokudbRows, err := db.QueryContext(ctx, engineTokudbStatusQuery) if err != nil { return err diff --git a/collector/engine_tokudb_test.go b/collector/engine_tokudb_test.go index 627aa31f..dcf0c984 100644 --- a/collector/engine_tokudb_test.go +++ b/collector/engine_tokudb_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -46,6 +46,7 @@ func TestScrapeEngineTokudbStatus(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Type", "Name", "Status"} rows := sqlmock.NewRows(columns). @@ -59,7 +60,7 @@ func TestScrapeEngineTokudbStatus(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeEngineTokudbStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeEngineTokudbStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/exporter.go b/collector/exporter.go index 457071ae..87bdbabd 100644 --- a/collector/exporter.go +++ b/collector/exporter.go @@ -15,18 +15,15 @@ package collector import ( "context" - "database/sql" - "regexp" + "fmt" + "log/slog" "runtime/pprof" - "strconv" + "strings" "sync" "time" - "gopkg.in/alecthomas/kingpin.v2" - - "github.com/go-kit/log" - "github.com/go-kit/log/level" - _ "github.com/go-sql-driver/mysql" + "github.com/alecthomas/kingpin/v2" + "github.com/go-sql-driver/mysql" "github.com/prometheus/client_golang/prometheus" ) @@ -38,19 +35,39 @@ const ( // SQL Queries. const ( - versionQuery = `SELECT @@version` + // System variable params formatting. + // See: https://github.com/go-sql-driver/mysql#system-variables + sessionSettingsParam = `log_slow_filter=%27tmp_table_on_disk,filesort_on_disk%27` + timeoutParam = `lock_wait_timeout=%d` ) +// Tunable flags. var ( - versionRE = regexp.MustCompile(`^\d+\.\d+`) - collectorFailuresAsError = kingpin.Flag( - "collector.failures.error", - "Log collector failures as errors (Debug by default)").Bool() + exporterLockTimeout = kingpin.Flag( + "exporter.lock_wait_timeout", + "Set a lock_wait_timeout (in seconds) on the connection to avoid long metadata locking.", + ).Default("2").Int() + slowLogFilter = kingpin.Flag( + "exporter.log_slow_filter", + "Add a log_slow_filter to avoid slow query logging of scrapes. NOTE: Not supported by Oracle MySQL.", + ).Default("false").Bool() ) -// Metric descriptors. +// metric definition var ( - scrapeDurationDesc = prometheus.NewDesc( + mysqlUp = prometheus.NewDesc( + prometheus.BuildFQName(namespace, "", "up"), + "Whether the MySQL server is up.", + nil, + nil, + ) + mysqlScrapeCollectorSuccess = prometheus.NewDesc( + prometheus.BuildFQName(namespace, exporter, "collector_success"), + "mysqld_exporter: Whether a collector succeeded.", + []string{"collector"}, + nil, + ) + mysqlScrapeDurationSeconds = prometheus.NewDesc( prometheus.BuildFQName(namespace, exporter, "collector_duration_seconds"), "Collector time duration.", []string{"collector"}, nil, @@ -63,64 +80,70 @@ var _ prometheus.Collector = (*Exporter)(nil) // Exporter collects MySQL metrics. It implements prometheus.Collector. type Exporter struct { ctx context.Context - logger log.Logger - db *sql.DB + logger *slog.Logger + dsn string scrapers []Scraper - metrics Metrics + instance *Instance } // New returns a new MySQL exporter for the provided DSN. -func New(ctx context.Context, db *sql.DB, metrics Metrics, scrapers []Scraper, logger log.Logger) *Exporter { +func New(ctx context.Context, dsn string, scrapers []Scraper, logger *slog.Logger) *Exporter { + // Setup extra params for the DSN, default to having a lock timeout. + dsnParams := []string{fmt.Sprintf(timeoutParam, *exporterLockTimeout)} + + if *slowLogFilter { + dsnParams = append(dsnParams, sessionSettingsParam) + } + + if strings.Contains(dsn, "?") { + dsn = dsn + "&" + } else { + dsn = dsn + "?" + } + dsn += strings.Join(dsnParams, "&") + return &Exporter{ ctx: ctx, logger: logger, - db: db, + dsn: dsn, scrapers: scrapers, - metrics: metrics, } } // Describe implements prometheus.Collector. func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- e.metrics.TotalScrapes.Desc() - ch <- e.metrics.Error.Desc() - e.metrics.ScrapeErrors.Describe(ch) - ch <- e.metrics.MySQLUp.Desc() + ch <- mysqlUp + ch <- mysqlScrapeDurationSeconds + ch <- mysqlScrapeCollectorSuccess } // Collect implements prometheus.Collector. func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - e.scrape(e.ctx, ch) - - ch <- e.metrics.TotalScrapes - ch <- e.metrics.Error - e.metrics.ScrapeErrors.Collect(ch) - ch <- e.metrics.MySQLUp + up := e.scrape(e.ctx, ch) + ch <- prometheus.MustNewConstMetric(mysqlUp, prometheus.GaugeValue, up) } -func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) { - e.metrics.TotalScrapes.Inc() - +// scrape collects metrics from the target, returns an up metric value. +func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) float64 { + var err error scrapeTime := time.Now() - if err := e.db.PingContext(ctx); err != nil { - // BUG(arvenil): PMM-2726: When PingContext returns with context deadline exceeded - // the subsequent call will return `bad connection`. - // https://github.com/go-sql-driver/mysql/issues/858 - // The PingContext is called second time as a workaround for this issue. - if err = e.db.PingContext(ctx); err != nil { - level.Error(e.logger).Log("msg", "Error pinging mysqld", "err", err) - e.metrics.MySQLUp.Set(0) - e.metrics.Error.Set(1) - return - } + instance, err := newInstance(e.dsn) + if err != nil { + e.logger.Error("Error opening connection to database", "err", err) + return 0.0 + } + defer instance.Close() + e.instance = instance + + if err := instance.Ping(); err != nil { + e.logger.Error("Error pinging mysqld", "err", err) + return 0.0 } - e.metrics.MySQLUp.Set(1) - e.metrics.Error.Set(0) + ch <- prometheus.MustNewConstMetric(mysqlScrapeDurationSeconds, prometheus.GaugeValue, time.Since(scrapeTime).Seconds(), "connection") - ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, time.Since(scrapeTime).Seconds(), "connection") + version := instance.versionMajorMinor - version := getMySQLVersion(e.db, e.logger) var wg sync.WaitGroup defer wg.Wait() for _, scraper := range e.scrapers { @@ -138,82 +161,24 @@ func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) { label := "collect." + scraper.Name() scrapeTime := time.Now() - if err := scraper.Scrape(scrapeCtx, e.db, ch, log.With(e.logger, "scraper", scraper.Name())); err != nil { - if *collectorFailuresAsError { - level.Error(e.logger).Log("msg", "Error from scraper", "scraper", scraper.Name(), "err", err) - } else { - level.Debug(e.logger).Log("msg", "Error from scraper", "scraper", scraper.Name(), "err", err) - } - - e.metrics.ScrapeErrors.WithLabelValues(label).Inc() - e.metrics.Error.Set(1) + collectorSuccess := 1.0 + if err := scraper.Scrape(ctx, instance, ch, e.logger.With("scraper", scraper.Name())); err != nil { + e.logger.Error("Error from scraper", "scraper", scraper.Name(), "target", e.getTargetFromDsn(), "err", err) + collectorSuccess = 0.0 } - ch <- prometheus.MustNewConstMetric(scrapeDurationDesc, prometheus.GaugeValue, time.Since(scrapeTime).Seconds(), label) + ch <- prometheus.MustNewConstMetric(mysqlScrapeCollectorSuccess, prometheus.GaugeValue, collectorSuccess, label) + ch <- prometheus.MustNewConstMetric(mysqlScrapeDurationSeconds, prometheus.GaugeValue, time.Since(scrapeTime).Seconds(), label) }(scraper) } + return 1.0 } -func getMySQLVersion(db *sql.DB, logger log.Logger) float64 { - var versionStr string - var versionNum float64 - if err := db.QueryRow(versionQuery).Scan(&versionStr); err == nil { - versionNum, _ = strconv.ParseFloat(versionRE.FindString(versionStr), 64) - } else { - level.Debug(logger).Log("msg", "Error querying version", "err", err) +func (e *Exporter) getTargetFromDsn() string { + // Get target from DSN. + dsnConfig, err := mysql.ParseDSN(e.dsn) + if err != nil { + e.logger.Error("Error parsing DSN", "err", err) + return "" } - // If we can't match/parse the version, set it some big value that matches all versions. - if versionNum == 0 { - level.Debug(logger).Log("msg", "Error parsing version string", "version", versionStr) - versionNum = 999 - } - return versionNum -} - -// Metrics represents exporter metrics which values can be carried between http requests. -type Metrics struct { - TotalScrapes prometheus.Counter - ScrapeErrors *prometheus.CounterVec - Error prometheus.Gauge - MySQLUp prometheus.Gauge -} - -// NewMetrics creates new Metrics instance. -func NewMetrics(resolution string) Metrics { - subsystem := exporter - if resolution != "" { - subsystem = exporter + "_" + resolution - } - return Metrics{ - TotalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "scrapes_total", - Help: "Total number of times MySQL was scraped for metrics.", - }), - ScrapeErrors: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "scrape_errors_total", - Help: "Total number of times an error occurred scraping a MySQL.", - }, []string{"collector"}), - Error: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: subsystem, - Name: "last_scrape_error", - Help: "Whether the last scrape of metrics from MySQL resulted in an error (1 for error, 0 for success).", - }), - MySQLUp: prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: namespace, - Name: "up", - Help: "Whether the MySQL server is up.", - }), - } -} - -/* percona private accessors */ - -const VersionQuery = versionQuery - -func GetMySQLVersion(db *sql.DB, logger log.Logger) float64 { - return getMySQLVersion(db, logger) + return dsnConfig.Addr } diff --git a/collector/exporter_test.go b/collector/exporter_test.go index 59ed2a9a..3d924145 100644 --- a/collector/exporter_test.go +++ b/collector/exporter_test.go @@ -15,14 +15,11 @@ package collector import ( "context" - "database/sql" - "os" "testing" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/model" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -33,20 +30,13 @@ func TestExporter(t *testing.T) { t.Skip("-short is passed, skipping test") } - db, err := sql.Open("mysql", dsn) - if err != nil { - t.Fatal(err) - } - defer db.Close() - exporter := New( context.Background(), - db, - NewMetrics(""), + dsn, []Scraper{ ScrapeGlobalStatus{}, }, - log.NewNopLogger(), + promslog.NewNopLogger(), ) convey.Convey("Metrics describing", t, func() { @@ -81,14 +71,10 @@ func TestGetMySQLVersion(t *testing.T) { t.Skip("-short is passed, skipping test") } - logger := log.NewLogfmtLogger(os.Stderr) - logger = level.NewFilter(logger, level.AllowDebug()) - convey.Convey("Version parsing", t, func() { - db, err := sql.Open("mysql", dsn) + instance, err := newInstance(dsn) convey.So(err, convey.ShouldBeNil) - defer db.Close() - convey.So(getMySQLVersion(db, logger), convey.ShouldBeBetweenOrEqual, 5.5, 11.0) + convey.So(instance.versionMajorMinor, convey.ShouldBeBetweenOrEqual, 5.7, 11.4) }) } diff --git a/collector/global_status.go b/collector/global_status.go index 4b25c559..f8aa74fb 100644 --- a/collector/global_status.go +++ b/collector/global_status.go @@ -18,11 +18,11 @@ package collector import ( "context" "database/sql" + "log/slog" "regexp" "strconv" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -99,7 +99,8 @@ func (ScrapeGlobalStatus) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeGlobalStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeGlobalStatus) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() globalStatusRows, err := db.QueryContext(ctx, globalStatusQuery) if err != nil { return err @@ -194,11 +195,11 @@ func (ScrapeGlobalStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prom } evsMap := []evsValue{ - evsValue{name: "min_seconds", value: 0, index: 0, help: "PXC/Galera group communication latency. Min value."}, - evsValue{name: "avg_seconds", value: 0, index: 1, help: "PXC/Galera group communication latency. Avg value."}, - evsValue{name: "max_seconds", value: 0, index: 2, help: "PXC/Galera group communication latency. Max value."}, - evsValue{name: "stdev", value: 0, index: 3, help: "PXC/Galera group communication latency. Standard Deviation."}, - evsValue{name: "sample_size", value: 0, index: 4, help: "PXC/Galera group communication latency. Sample Size."}, + {name: "min_seconds", value: 0, index: 0, help: "PXC/Galera group communication latency. Min value."}, + {name: "avg_seconds", value: 0, index: 1, help: "PXC/Galera group communication latency. Avg value."}, + {name: "max_seconds", value: 0, index: 2, help: "PXC/Galera group communication latency. Max value."}, + {name: "stdev", value: 0, index: 3, help: "PXC/Galera group communication latency. Standard Deviation."}, + {name: "sample_size", value: 0, index: 4, help: "PXC/Galera group communication latency. Sample Size."}, } evsParsingSuccess := true diff --git a/collector/global_status_test.go b/collector/global_status_test.go index 14456c91..8ac969bf 100644 --- a/collector/global_status_test.go +++ b/collector/global_status_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeGlobalStatus(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Variable_name", "Value"} rows := sqlmock.NewRows(columns). @@ -63,7 +64,7 @@ func TestScrapeGlobalStatus(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeGlobalStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeGlobalStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/global_variables.go b/collector/global_variables.go index 3fba819d..e7e0a7e6 100644 --- a/collector/global_variables.go +++ b/collector/global_variables.go @@ -18,11 +18,11 @@ package collector import ( "context" "database/sql" + "log/slog" "regexp" "strconv" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -138,7 +138,8 @@ func (ScrapeGlobalVariables) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeGlobalVariables) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeGlobalVariables) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() globalVariablesRows, err := db.QueryContext(ctx, globalVariablesQuery) if err != nil { return err diff --git a/collector/global_variables_test.go b/collector/global_variables_test.go index 4f0ab36f..96ca8080 100644 --- a/collector/global_variables_test.go +++ b/collector/global_variables_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeGlobalVariables(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Variable_name", "Value"} rows := sqlmock.NewRows(columns). @@ -52,7 +53,7 @@ func TestScrapeGlobalVariables(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeGlobalVariables{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeGlobalVariables{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/heartbeat.go b/collector/heartbeat.go index a014219f..dbd086d6 100644 --- a/collector/heartbeat.go +++ b/collector/heartbeat.go @@ -19,11 +19,11 @@ import ( "context" "database/sql" "fmt" + "log/slog" "strconv" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const ( @@ -69,8 +69,10 @@ var ( // This is mainly targeting pt-heartbeat, but will work with any heartbeat // implementation that writes to a table with two columns: // CREATE TABLE heartbeat ( -// ts varchar(26) NOT NULL, -// server_id int unsigned NOT NULL PRIMARY KEY, +// +// ts varchar(26) NOT NULL, +// server_id int unsigned NOT NULL PRIMARY KEY, +// // ); type ScrapeHeartbeat struct{} @@ -98,7 +100,8 @@ func nowExpr() string { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeHeartbeat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeHeartbeat) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() query := fmt.Sprintf(heartbeatQuery, nowExpr(), *collectHeartbeatDatabase, *collectHeartbeatTable) heartbeatRows, err := db.QueryContext(ctx, query) if err != nil { diff --git a/collector/heartbeat_test.go b/collector/heartbeat_test.go index a8dfa979..20f096dd 100644 --- a/collector/heartbeat_test.go +++ b/collector/heartbeat_test.go @@ -19,11 +19,11 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "gopkg.in/alecthomas/kingpin.v2" ) type ScrapeHeartbeatTestCase struct { @@ -65,6 +65,7 @@ func TestScrapeHeartbeat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} rows := sqlmock.NewRows(tt.Columns). AddRow("1487597613.001320", "1487598113.448042", 1) @@ -72,7 +73,7 @@ func TestScrapeHeartbeat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeHeartbeat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeHeartbeat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_auto_increment.go b/collector/info_schema_auto_increment.go index 875722de..2d1729e9 100644 --- a/collector/info_schema_auto_increment.go +++ b/collector/info_schema_auto_increment.go @@ -17,15 +17,14 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) // https://jira.percona.com/browse/PMM-4001 explains STRAIGHT_JOIN usage. const infoSchemaAutoIncrementQuery = ` - SELECT t.table_schema, t.table_name, column_name, auto_increment, + SELECT c.table_schema, c.table_name, column_name, auto_increment, pow(2, case data_type when 'tinyint' then 7 when 'smallint' then 15 @@ -34,8 +33,7 @@ const infoSchemaAutoIncrementQuery = ` when 'bigint' then 63 end+(column_type like '% unsigned'))-1 as max_int FROM information_schema.columns c - STRAIGHT_JOIN information_schema.tables t - ON BINARY t.table_schema = c.table_schema AND BINARY t.table_name = c.table_name + STRAIGHT_JOIN information_schema.tables t ON (BINARY c.table_schema=t.table_schema AND BINARY c.table_name=t.table_name) WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL ` @@ -72,7 +70,8 @@ func (ScrapeAutoIncrementColumns) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeAutoIncrementColumns) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeAutoIncrementColumns) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() autoIncrementRows, err := db.QueryContext(ctx, infoSchemaAutoIncrementQuery) if err != nil { return err diff --git a/collector/info_schema_clientstats.go b/collector/info_schema_clientstats.go index 619fb4a0..ff02db4c 100644 --- a/collector/info_schema_clientstats.go +++ b/collector/info_schema_clientstats.go @@ -17,12 +17,10 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) @@ -161,15 +159,16 @@ func (ScrapeClientStat) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeClientStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeClientStat) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var varName, varVal string + db := instance.GetDB() err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) if err != nil { - level.Debug(logger).Log("msg", "Detailed client stats are not available.") + logger.Debug("Detailed client stats are not available.") return nil } if varVal == "OFF" { - level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) + logger.Debug("MySQL variable is OFF.", "var", varName) return nil } diff --git a/collector/info_schema_clientstats_test.go b/collector/info_schema_clientstats_test.go index c77e2401..b63c14ee 100644 --- a/collector/info_schema_clientstats_test.go +++ b/collector/info_schema_clientstats_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeClientStat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). AddRow("userstat", "ON")) @@ -41,7 +42,7 @@ func TestScrapeClientStat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeClientStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeClientStat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_innodb_cmp.go b/collector/info_schema_innodb_cmp.go index c79f9003..28672527 100644 --- a/collector/info_schema_innodb_cmp.go +++ b/collector/info_schema_innodb_cmp.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -77,7 +76,8 @@ func (ScrapeInnodbCmp) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInnodbCmp) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInnodbCmp) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() informationSchemaInnodbCmpRows, err := db.QueryContext(ctx, innodbCmpQuery) if err != nil { return err diff --git a/collector/info_schema_innodb_cmp_test.go b/collector/info_schema_innodb_cmp_test.go index 2143af09..869fa6a9 100644 --- a/collector/info_schema_innodb_cmp_test.go +++ b/collector/info_schema_innodb_cmp_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeInnodbCmp(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"page_size", "compress_ops", "compress_ops_ok", "compress_time", "uncompress_ops", "uncompress_time"} rows := sqlmock.NewRows(columns). @@ -38,7 +39,7 @@ func TestScrapeInnodbCmp(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeInnodbCmp{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeInnodbCmp{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_innodb_cmpmem.go b/collector/info_schema_innodb_cmpmem.go index 4e6d175c..62c801f9 100644 --- a/collector/info_schema_innodb_cmpmem.go +++ b/collector/info_schema_innodb_cmpmem.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -72,7 +71,8 @@ func (ScrapeInnodbCmpMem) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() informationSchemaInnodbCmpMemRows, err := db.QueryContext(ctx, innodbCmpMemQuery) if err != nil { return err diff --git a/collector/info_schema_innodb_cmpmem_test.go b/collector/info_schema_innodb_cmpmem_test.go index 346b2d23..17e9877f 100644 --- a/collector/info_schema_innodb_cmpmem_test.go +++ b/collector/info_schema_innodb_cmpmem_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeInnodbCmpMem(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"page_size", "buffer_pool", "pages_used", "pages_free", "relocation_ops", "relocation_time"} rows := sqlmock.NewRows(columns). @@ -38,7 +39,7 @@ func TestScrapeInnodbCmpMem(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeInnodbCmpMem{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeInnodbCmpMem{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_innodb_metrics.go b/collector/info_schema_innodb_metrics.go index f077dd5a..b6089db0 100644 --- a/collector/info_schema_innodb_metrics.go +++ b/collector/info_schema_innodb_metrics.go @@ -17,13 +17,11 @@ package collector import ( "context" - "database/sql" "errors" "fmt" + "log/slog" "regexp" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) @@ -93,10 +91,11 @@ func (ScrapeInnodbMetrics) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInnodbMetrics) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInnodbMetrics) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var enabledColumnName string var query string + db := instance.GetDB() err := db.QueryRowContext(ctx, infoSchemaInnodbMetricsEnabledColumnQuery).Scan(&enabledColumnName) if err != nil { return err @@ -132,7 +131,7 @@ func (ScrapeInnodbMetrics) Scrape(ctx context.Context, db *sql.DB, ch chan<- pro if subsystem == "buffer_page_io" { match := bufferPageRE.FindStringSubmatch(name) if len(match) != 3 { - level.Warn(logger).Log("msg", "innodb_metrics subsystem buffer_page_io returned an invalid name", "name", name) + logger.Warn("innodb_metrics subsystem buffer_page_io returned an invalid name", "name", name) continue } switch match[1] { diff --git a/collector/info_schema_innodb_metrics_test.go b/collector/info_schema_innodb_metrics_test.go index 43b0de24..fbb4b154 100644 --- a/collector/info_schema_innodb_metrics_test.go +++ b/collector/info_schema_innodb_metrics_test.go @@ -19,9 +19,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -31,6 +31,7 @@ func TestScrapeInnodbMetrics(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} enabledColumnName := []string{"COLUMN_NAME"} rows := sqlmock.NewRows(enabledColumnName). @@ -53,7 +54,7 @@ func TestScrapeInnodbMetrics(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeInnodbMetrics{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeInnodbMetrics{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_innodb_sys_tablespaces.go b/collector/info_schema_innodb_sys_tablespaces.go index 29218926..a15b5177 100644 --- a/collector/info_schema_innodb_sys_tablespaces.go +++ b/collector/info_schema_innodb_sys_tablespaces.go @@ -17,11 +17,11 @@ package collector import ( "context" - "database/sql" "errors" "fmt" + "log/slog" - "github.com/go-kit/log" + "github.com/blang/semver/v4" "github.com/prometheus/client_golang/prometheus" ) @@ -32,7 +32,8 @@ const innodbTablespacesTablenameQuery = ` WHERE table_name = 'INNODB_SYS_TABLESPACES' OR table_name = 'INNODB_TABLESPACES' ` -const innodbTablespacesQuery = ` + +const innodbTablespacesQueryMySQL = ` SELECT SPACE, NAME, @@ -47,6 +48,21 @@ const innodbTablespacesQuery = ` ALLOCATED_SIZE FROM information_schema.` + "`%s`" +// SPACE_TYPE has been removed in MariaDB 10.5 https://jira.mariadb.org/browse/MDEV-19940 +const innodbTablespacesQueryMariaDB = ` + SELECT + SPACE, + NAME, + ifnull((SELECT column_name + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = 'information_schema' + AND TABLE_NAME = ` + "'%s'" + ` + AND COLUMN_NAME = 'FILE_FORMAT' LIMIT 1), 'NONE') as FILE_FORMAT, + ifnull(ROW_FORMAT, 'NONE') as ROW_FORMAT, + FILE_SIZE, + ALLOCATED_SIZE + FROM information_schema.` + "`%s`" + // Metric descriptors. var ( infoSchemaInnodbTablesspaceInfoDesc = prometheus.NewDesc( @@ -85,9 +101,11 @@ func (ScrapeInfoSchemaInnodbTablespaces) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInfoSchemaInnodbTablespaces) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInfoSchemaInnodbTablespaces) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var tablespacesTablename string var query string + db := instance.GetDB() + err := db.QueryRowContext(ctx, innodbTablespacesTablenameQuery).Scan(&tablespacesTablename) if err != nil { return err @@ -95,7 +113,10 @@ func (ScrapeInfoSchemaInnodbTablespaces) Scrape(ctx context.Context, db *sql.DB, switch tablespacesTablename { case "INNODB_SYS_TABLESPACES", "INNODB_TABLESPACES": - query = fmt.Sprintf(innodbTablespacesQuery, tablespacesTablename, tablespacesTablename) + query = fmt.Sprintf(innodbTablespacesQueryMySQL, tablespacesTablename, tablespacesTablename) + if instance.flavor == FlavorMariaDB && instance.version.GTE(semver.MustParse("10.5.0")) { + query = fmt.Sprintf(innodbTablespacesQueryMariaDB, tablespacesTablename, tablespacesTablename) + } default: return errors.New("Couldn't find INNODB_SYS_TABLESPACES or INNODB_TABLESPACES in information_schema.") } @@ -117,15 +138,28 @@ func (ScrapeInfoSchemaInnodbTablespaces) Scrape(ctx context.Context, db *sql.DB, ) for tablespacesRows.Next() { - err = tablespacesRows.Scan( - &tableSpace, - &tableName, - &fileFormat, - &rowFormat, - &spaceType, - &fileSize, - &allocatedSize, - ) + var err error + if instance.flavor == FlavorMariaDB && instance.version.GTE(semver.MustParse("10.5.0")) { + err = tablespacesRows.Scan( + &tableSpace, + &tableName, + &fileFormat, + &rowFormat, + &fileSize, + &allocatedSize, + ) + } else { + err = tablespacesRows.Scan( + &tableSpace, + &tableName, + &fileFormat, + &rowFormat, + &spaceType, + &fileSize, + &allocatedSize, + ) + } + if err != nil { return err } diff --git a/collector/info_schema_innodb_sys_tablespaces_test.go b/collector/info_schema_innodb_sys_tablespaces_test.go index 05553287..a524ffe1 100644 --- a/collector/info_schema_innodb_sys_tablespaces_test.go +++ b/collector/info_schema_innodb_sys_tablespaces_test.go @@ -19,9 +19,10 @@ import ( "testing" "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,6 +32,10 @@ func TestScrapeInfoSchemaInnodbTablespaces(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{ + db: db, + flavor: FlavorMySQL, + } columns := []string{"TABLE_NAME"} rows := sqlmock.NewRows(columns). @@ -42,12 +47,12 @@ func TestScrapeInfoSchemaInnodbTablespaces(t *testing.T) { rows = sqlmock.NewRows(columns). AddRow(1, "sys/sys_config", "Barracuda", "Dynamic", "Single", 100, 100). AddRow(2, "db/compressed", "Barracuda", "Compressed", "Single", 300, 200) - query := fmt.Sprintf(innodbTablespacesQuery, tablespacesTablename, tablespacesTablename) + query := fmt.Sprintf(innodbTablespacesQueryMySQL, tablespacesTablename, tablespacesTablename) mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeInfoSchemaInnodbTablespaces{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeInfoSchemaInnodbTablespaces{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -67,6 +72,55 @@ func TestScrapeInfoSchemaInnodbTablespaces(t *testing.T) { convey.So(expect, convey.ShouldResemble, got) } }) +} + +func TestScrapeInfoSchemaInnodbTablespacesWithoutSpaceType(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error opening a stub database connection: %s", err) + } + defer db.Close() + inst := &Instance{ + db: db, + flavor: FlavorMariaDB, + version: semver.MustParse("10.5.0"), + } + + columns := []string{"TABLE_NAME"} + rows := sqlmock.NewRows(columns). + AddRow("INNODB_SYS_TABLESPACES") + mock.ExpectQuery(sanitizeQuery(innodbTablespacesTablenameQuery)).WillReturnRows(rows) + + tablespacesTablename := "INNODB_SYS_TABLESPACES" + columns = []string{"SPACE", "NAME", "FILE_FORMAT", "ROW_FORMAT", "FILE_SIZE", "ALLOCATED_SIZE"} + rows = sqlmock.NewRows(columns). + AddRow(1, "sys/sys_config", "Barracuda", "Dynamic", 100, 100). + AddRow(2, "db/compressed", "Barracuda", "Compressed", 300, 200) + query := fmt.Sprintf(innodbTablespacesQueryMariaDB, tablespacesTablename, tablespacesTablename) + mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + go func() { + if err = (ScrapeInfoSchemaInnodbTablespaces{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { + t.Errorf("error calling function on test: %s", err) + } + close(ch) + }() + + expected := []MetricResult{ + {labels: labelMap{"tablespace_name": "sys/sys_config", "file_format": "Barracuda", "row_format": "Dynamic", "space_type": ""}, value: 1, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"tablespace_name": "sys/sys_config"}, value: 100, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"tablespace_name": "sys/sys_config"}, value: 100, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"tablespace_name": "db/compressed", "file_format": "Barracuda", "row_format": "Compressed", "space_type": ""}, value: 2, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"tablespace_name": "db/compressed"}, value: 300, metricType: dto.MetricType_GAUGE}, + {labels: labelMap{"tablespace_name": "db/compressed"}, value: 200, metricType: dto.MetricType_GAUGE}, + } + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expected { + got := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, got) + } + }) // Ensure all SQL queries were executed if err := mock.ExpectationsWereMet(); err != nil { diff --git a/collector/info_schema_processlist.go b/collector/info_schema_processlist.go index 45e2a005..e352a708 100755 --- a/collector/info_schema_processlist.go +++ b/collector/info_schema_processlist.go @@ -17,15 +17,14 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" "reflect" "sort" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const infoSchemaProcesslistQuery = ` @@ -39,7 +38,7 @@ const infoSchemaProcesslistQuery = ` FROM information_schema.processlist WHERE ID != connection_id() AND TIME >= %d - GROUP BY user, SUBSTRING_INDEX(host, ':', 1), command, state + GROUP BY user, host, command, state ` // Tunable flags. @@ -97,11 +96,12 @@ func (ScrapeProcesslist) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeProcesslist) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeProcesslist) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { processQuery := fmt.Sprintf( infoSchemaProcesslistQuery, *processlistMinTime, ) + db := instance.GetDB() processlistRows, err := db.QueryContext(ctx, processQuery) if err != nil { return err diff --git a/collector/info_schema_processlist_test.go b/collector/info_schema_processlist_test.go index 8887ea7b..560bb784 100644 --- a/collector/info_schema_processlist_test.go +++ b/collector/info_schema_processlist_test.go @@ -19,11 +19,11 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "gopkg.in/alecthomas/kingpin.v2" ) func TestScrapeProcesslist(t *testing.T) { @@ -40,6 +40,7 @@ func TestScrapeProcesslist(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} query := fmt.Sprintf(infoSchemaProcesslistQuery, 0) columns := []string{"user", "host", "command", "state", "processes", "seconds"} @@ -56,7 +57,7 @@ func TestScrapeProcesslist(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeProcesslist{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeProcesslist{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_query_response_time.go b/collector/info_schema_query_response_time.go index bf5b14df..3031f7f6 100644 --- a/collector/info_schema_query_response_time.go +++ b/collector/info_schema_query_response_time.go @@ -17,12 +17,10 @@ package collector import ( "context" - "database/sql" + "log/slog" "strconv" "strings" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) @@ -56,7 +54,8 @@ var ( } ) -func processQueryResponseTimeTable(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, query string, i int) error { +func processQueryResponseTimeTable(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, query string, i int) error { + db := instance.GetDB() queryDistributionRows, err := db.QueryContext(ctx, query) if err != nil { return err @@ -119,20 +118,21 @@ func (ScrapeQueryResponseTime) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeQueryResponseTime) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeQueryResponseTime) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var queryStats uint8 + db := instance.GetDB() err := db.QueryRowContext(ctx, queryResponseCheckQuery).Scan(&queryStats) if err != nil { - level.Debug(logger).Log("msg", "Query response time distribution is not available.") + logger.Debug("Query response time distribution is not available.") return nil } if queryStats == 0 { - level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", "query_response_time_stats") + logger.Debug("MySQL variable is OFF.", "var", "query_response_time_stats") return nil } for i, query := range queryResponseTimeQueries { - err := processQueryResponseTimeTable(ctx, db, ch, query, i) + err := processQueryResponseTimeTable(ctx, instance, ch, query, i) // The first query should not fail if query_response_time_stats is ON, // unlike the other two when the read/write tables exist only with Percona Server 5.6/5.7. if i == 0 && err != nil { diff --git a/collector/info_schema_query_response_time_test.go b/collector/info_schema_query_response_time_test.go index 8c766686..efaac9e8 100644 --- a/collector/info_schema_query_response_time_test.go +++ b/collector/info_schema_query_response_time_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeQueryResponseTime(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} mock.ExpectQuery(queryResponseCheckQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) @@ -52,7 +53,7 @@ func TestScrapeQueryResponseTime(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeQueryResponseTime{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeQueryResponseTime{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_replica_host.go b/collector/info_schema_replica_host.go index 106bbbd0..12cebbdf 100644 --- a/collector/info_schema_replica_host.go +++ b/collector/info_schema_replica_host.go @@ -17,10 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" - "github.com/go-kit/log/level" MySQL "github.com/go-sql-driver/mysql" "github.com/prometheus/client_golang/prometheus" ) @@ -84,13 +82,14 @@ func (ScrapeReplicaHost) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeReplicaHost) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeReplicaHost) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() replicaHostRows, err := db.QueryContext(ctx, replicaHostQuery) if err != nil { if mysqlErr, ok := err.(*MySQL.MySQLError); ok { // Now the error number is accessible directly // Check for error 1109: Unknown table if mysqlErr.Number == 1109 { - level.Debug(logger).Log("msg", "information_schema.replica_host_status is not available.") + logger.Debug("information_schema.replica_host_status is not available.") return nil } } diff --git a/collector/info_schema_replica_host_test.go b/collector/info_schema_replica_host_test.go index 08d881b6..bcee600c 100644 --- a/collector/info_schema_replica_host_test.go +++ b/collector/info_schema_replica_host_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeReplicaHost(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"SERVER_ID", "ROLE", "CPU", "MASTER_SLAVE_LATENCY_IN_MICROSECONDS", "REPLICA_LAG_IN_MILLISECONDS", "LOG_STREAM_SPEED_IN_KiB_PER_SECOND", "CURRENT_REPLAY_LATENCY_IN_MICROSECONDS"} rows := sqlmock.NewRows(columns). @@ -39,7 +40,7 @@ func TestScrapeReplicaHost(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeReplicaHost{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeReplicaHost{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_schemastats.go b/collector/info_schema_schemastats.go index 64ff6528..a301bc56 100644 --- a/collector/info_schema_schemastats.go +++ b/collector/info_schema_schemastats.go @@ -17,10 +17,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" ) @@ -72,16 +70,17 @@ func (ScrapeSchemaStat) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeSchemaStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeSchemaStat) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var varName, varVal string + db := instance.GetDB() err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) if err != nil { - level.Debug(logger).Log("msg", "Detailed schema stats are not available.") + logger.Debug("Detailed schema stats are not available.") return nil } if varVal == "OFF" { - level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) + logger.Debug("MySQL variable is OFF.", "var", varName) return nil } diff --git a/collector/info_schema_schemastats_test.go b/collector/info_schema_schemastats_test.go index bc57fc6f..daa434f9 100644 --- a/collector/info_schema_schemastats_test.go +++ b/collector/info_schema_schemastats_test.go @@ -18,8 +18,8 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -29,6 +29,7 @@ func TestScrapeSchemaStat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). AddRow("userstat", "ON")) @@ -41,7 +42,7 @@ func TestScrapeSchemaStat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeSchemaStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeSchemaStat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_tables.go b/collector/info_schema_tables.go index d0e58235..d23167c4 100644 --- a/collector/info_schema_tables.go +++ b/collector/info_schema_tables.go @@ -17,13 +17,12 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const ( @@ -47,7 +46,7 @@ const ( SELECT SCHEMA_NAME FROM information_schema.schemata - WHERE SCHEMA_NAME NOT IN ('mysql', 'performance_schema', 'information_schema') + WHERE SCHEMA_NAME NOT IN ('mysql', 'performance_schema', 'information_schema', 'sys') ` ) @@ -97,28 +96,18 @@ func (ScrapeTableSchema) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeTableSchema) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) (e error) { - // PMM-5684 fix - conn, err := db.Conn(ctx) - if err != nil { - return err - } - defer func() { //nolint:wsl - err := conn.Close() - if err != nil && e == nil { - e = err - } - }() +func (ScrapeTableSchema) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + var dbList []string + db := instance.GetDB() // This query will affect only 8.0 and higher versions of MySQL, othervise it will be ignored - _, err = conn.ExecContext(ctx, "/*!80000 set session information_schema_stats_expiry=0 */") + _, err := db.ExecContext(ctx, "/*!80000 set session information_schema_stats_expiry=0 */") if err != nil { return err } - var dbList []string if *tableSchemaDatabases == "*" { - dbListRows, err := conn.QueryContext(ctx, dbListQuery) + dbListRows, err := db.QueryContext(ctx, dbListQuery) if err != nil { return err } @@ -139,7 +128,7 @@ func (ScrapeTableSchema) Scrape(ctx context.Context, db *sql.DB, ch chan<- prome } for _, database := range dbList { - tableSchemaRows, err := conn.QueryContext(ctx, fmt.Sprintf(tableSchemaQuery, database)) + tableSchemaRows, err := db.QueryContext(ctx, fmt.Sprintf(tableSchemaQuery, database)) if err != nil { return err } diff --git a/collector/info_schema_tables_test.go b/collector/info_schema_tables_test.go index 3eec7491..7a1584d5 100644 --- a/collector/info_schema_tables_test.go +++ b/collector/info_schema_tables_test.go @@ -18,10 +18,10 @@ package collector import ( "context" "database/sql" - "github.com/go-kit/log" "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -67,14 +67,17 @@ func TestScrapeTableSchema(t *testing.T) { //nolint:unused addRowAndCheckRowsCount(t, ctx, db, dbName, tableName, 2) } -func addRowAndCheckRowsCount(t *testing.T, ctx context.Context, db *sql.DB, dbName, tableName string, expectedRowsCount float64) { //nolint:go-lint +func addRowAndCheckRowsCount(t *testing.T, ctx context.Context, db *sql.DB, dbName, tableName string, expectedRowsCount float64) { _, err := db.Exec("INSERT INTO " + dbName + "." + tableName + " VALUES(50)") if err != nil { t.Fatal(err) } ch := make(chan prometheus.Metric) go func() { //nolint:wsl - if err = (ScrapeTableSchema{}).Scrape(ctx, db, ch, log.NewNopLogger()); err != nil { + instance := &Instance{ + db: db, + } + if err = (ScrapeTableSchema{}).Scrape(ctx, instance, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_tablestats.go b/collector/info_schema_tablestats.go index b4d964df..bb671d44 100644 --- a/collector/info_schema_tablestats.go +++ b/collector/info_schema_tablestats.go @@ -17,10 +17,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" ) @@ -72,15 +70,16 @@ func (ScrapeTableStat) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeTableStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeTableStat) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var varName, varVal string + db := instance.GetDB() err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) if err != nil { - level.Debug(logger).Log("msg", "Detailed table stats are not available.") + logger.Debug("Detailed table stats are not available.") return nil } if varVal == "OFF" { - level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) + logger.Debug("MySQL variable is OFF.", "var", varName) return nil } diff --git a/collector/info_schema_tablestats_test.go b/collector/info_schema_tablestats_test.go index 9b678641..a6da10ce 100644 --- a/collector/info_schema_tablestats_test.go +++ b/collector/info_schema_tablestats_test.go @@ -18,8 +18,8 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -29,6 +29,7 @@ func TestScrapeTableStat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). AddRow("userstat", "ON")) @@ -42,7 +43,7 @@ func TestScrapeTableStat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeTableStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeTableStat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/info_schema_userstats.go b/collector/info_schema_userstats.go index 329d3b0a..9115527b 100644 --- a/collector/info_schema_userstats.go +++ b/collector/info_schema_userstats.go @@ -17,12 +17,10 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" - "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" ) @@ -157,15 +155,16 @@ func (ScrapeUserStat) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeUserStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeUserStat) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var varName, varVal string + db := instance.GetDB() err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) if err != nil { - level.Debug(logger).Log("msg", "Detailed user stats are not available.") + logger.Debug("Detailed user stats are not available.") return nil } if varVal == "OFF" { - level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) + logger.Debug("MySQL variable is OFF.", "var", varName) return nil } diff --git a/collector/info_schema_userstats_test.go b/collector/info_schema_userstats_test.go index 99f54ebc..39ef13e9 100644 --- a/collector/info_schema_userstats_test.go +++ b/collector/info_schema_userstats_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeUserStat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). AddRow("userstat", "ON")) @@ -41,7 +42,7 @@ func TestScrapeUserStat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeUserStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeUserStat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/instance.go b/collector/instance.go new file mode 100644 index 00000000..770d1c0a --- /dev/null +++ b/collector/instance.go @@ -0,0 +1,140 @@ +// 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 ( + "database/sql" + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/alecthomas/kingpin/v2" + "github.com/blang/semver/v4" +) + +const ( + FlavorMySQL = "mysql" + FlavorMariaDB = "mariadb" + versionQuery = "SELECT @@version;" +) + +var ( + exporterMaxOpenConns = kingpin.Flag( + "exporter.max-open-conns", + "Maximum number of open connections to the database. https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns", + ).Default("3").Int() + exporterMaxIdleConns = kingpin.Flag( + "exporter.max-idle-conns", + "Maximum number of connections in the idle connection pool. https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns", + ).Default("3").Int() + exporterConnMaxLifetime = kingpin.Flag( + "exporter.conn-max-lifetime", + "Maximum amount of time a connection may be reused. https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime", + ).Default("1m").Duration() +) + +type Instance struct { + db *sql.DB + flavor string + version semver.Version + versionMajorMinor float64 +} + +func newInstance(dsn string) (*Instance, error) { + i := &Instance{} + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(*exporterMaxOpenConns) + db.SetMaxIdleConns(*exporterMaxIdleConns) + db.SetConnMaxLifetime(*exporterConnMaxLifetime) + + i.db = db + + version, versionString, err := queryVersion(db) + if err != nil { + db.Close() + return nil, err + } + + i.version = version + + versionMajorMinor, err := strconv.ParseFloat(fmt.Sprintf("%d.%d", i.version.Major, i.version.Minor), 64) + if err != nil { + db.Close() + return nil, err + } + + i.versionMajorMinor = versionMajorMinor + + if strings.Contains(strings.ToLower(versionString), "mariadb") { + i.flavor = FlavorMariaDB + } else { + i.flavor = FlavorMySQL + } + + return i, nil +} + +// GetDB returns the database connection for the instance. +func (i *Instance) GetDB() *sql.DB { + return i.db +} + +// SetDB sets the database connection for the instance. Used for testing only. +func (i *Instance) SetDB(db *sql.DB) { + i.db = db +} + +// Close closes the database connection. +func (i *Instance) Close() error { + return i.db.Close() +} + +// Ping checks connection availability and possibly invalidates the connection if it fails. +func (i *Instance) Ping() error { + if err := i.db.Ping(); err != nil { + if cerr := i.Close(); cerr != nil { + return err + } + return err + } + return nil +} + +// The result of SELECT version() is something like: +// for MariaDB: "10.5.17-MariaDB-1:10.5.17+maria~ubu2004-log" +// for MySQL: "8.0.36-28.1" +var versionRegex = regexp.MustCompile(`^((\d+)(\.\d+)(\.\d+))`) + +func queryVersion(db *sql.DB) (semver.Version, string, error) { + var version string + err := db.QueryRow(versionQuery).Scan(&version) + if err != nil { + return semver.Version{}, version, err + } + + matches := versionRegex.FindStringSubmatch(version) + if len(matches) > 1 { + parsedVersion, err := semver.ParseTolerant(matches[1]) + if err != nil { + return semver.Version{}, version, fmt.Errorf("could not parse version from %q", matches[1]) + } + return parsedVersion, version, nil + } + + return semver.Version{}, version, fmt.Errorf("could not parse version from %q", version) +} diff --git a/collector/instance_test.go b/collector/instance_test.go new file mode 100644 index 00000000..dc90d5dc --- /dev/null +++ b/collector/instance_test.go @@ -0,0 +1,70 @@ +// Copyright 2018 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 ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/blang/semver/v4" + + "github.com/smartystreets/goconvey/convey" +) + +func TestGetMySQLVersion_Percona(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error opening a stub database connection: %s", err) + } + defer db.Close() + + convey.Convey("MySQL version extract", t, func() { + var semVer semver.Version + var err error + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("")) + semVer, _, err = queryVersion(db) + convey.ShouldBeError(err) + convey.So(semVer.String(), convey.ShouldEqual, "0.0.0") + + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("something")) + semVer, _, err = queryVersion(db) + convey.ShouldBeNil(err) + convey.So(semVer.String(), convey.ShouldEqual, "0.0.0") + + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("10.1.17-MariaDB")) + semVer, _, err = queryVersion(db) + convey.ShouldBeNil(err) + convey.So(semVer.String(), convey.ShouldEqual, "10.1.17") + + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.7.13-6-log")) + semVer, _, err = queryVersion(db) + convey.ShouldBeNil(err) + convey.So(semVer.String(), convey.ShouldEqual, "5.7.13") + + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.6.30-76.3-56-log")) + semVer, _, err = queryVersion(db) + convey.ShouldBeNil(err) + convey.So(semVer.String(), convey.ShouldEqual, "5.6.30") + + mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.5.51-38.1")) + semVer, _, err = queryVersion(db) + convey.ShouldBeNil(err) + convey.So(semVer.String(), convey.ShouldEqual, "5.5.51") + }) + + // Ensure all SQL queries were executed + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} diff --git a/collector/mysql_user.go b/collector/mysql_user.go index 9b145c33..b1579aff 100644 --- a/collector/mysql_user.go +++ b/collector/mysql_user.go @@ -19,13 +19,16 @@ import ( "context" "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) +// mysqlSubsystem used for metric names. +const mysqlSubsystem = "mysql" + const mysqlUserQuery = ` SELECT user, @@ -81,19 +84,19 @@ var ( // Metric descriptors. var ( userMaxQuestionsDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, mysql, "max_questions"), + prometheus.BuildFQName(namespace, mysqlSubsystem, "max_questions"), "The number of max_questions by user.", labelNames, nil) userMaxUpdatesDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, mysql, "max_updates"), + prometheus.BuildFQName(namespace, mysqlSubsystem, "max_updates"), "The number of max_updates by user.", labelNames, nil) userMaxConnectionsDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, mysql, "max_connections"), + prometheus.BuildFQName(namespace, mysqlSubsystem, "max_connections"), "The number of max_connections by user.", labelNames, nil) userMaxUserConnectionsDesc = prometheus.NewDesc( - prometheus.BuildFQName(namespace, mysql, "max_user_connections"), + prometheus.BuildFQName(namespace, mysqlSubsystem, "max_user_connections"), "The number of max_user_connections by user.", labelNames, nil) ) @@ -103,7 +106,7 @@ type ScrapeUser struct{} // Name of the Scraper. Should be unique. func (ScrapeUser) Name() string { - return mysql + ".user" + return mysqlSubsystem + ".user" } // Help describes the role of the Scraper. @@ -117,7 +120,8 @@ func (ScrapeUser) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeUser) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeUser) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() var ( userRows *sql.Rows err error @@ -229,7 +233,7 @@ func (ScrapeUser) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.M if value, ok := parsePrivilege(*scanArgs[i].(*sql.RawBytes)); ok { // Silently skip unparsable values. ch <- prometheus.MustNewConstMetric( prometheus.NewDesc( - prometheus.BuildFQName(namespace, mysql, strings.ToLower(col)), + prometheus.BuildFQName(namespace, mysqlSubsystem, strings.ToLower(col)), col+" by user.", labelNames, nil, diff --git a/collector/perf_schema_events_statements.go b/collector/perf_schema_events_statements.go index a060b9ba..9d9979b8 100644 --- a/collector/perf_schema_events_statements.go +++ b/collector/perf_schema_events_statements.go @@ -17,12 +17,11 @@ package collector import ( "context" - "database/sql" "fmt" + "log/slog" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const perfEventsStatementsQuery = ` @@ -32,6 +31,8 @@ const perfEventsStatementsQuery = ` LEFT(DIGEST_TEXT, %d) as DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT, + SUM_LOCK_TIME, + SUM_CPU_TIME, SUM_ERRORS, SUM_WARNINGS, SUM_ROWS_AFFECTED, @@ -41,7 +42,10 @@ const perfEventsStatementsQuery = ` SUM_CREATED_TMP_TABLES, SUM_SORT_MERGE_PASSES, SUM_SORT_ROWS, - SUM_NO_INDEX_USED + SUM_NO_INDEX_USED, + QUANTILE_95, + QUANTILE_99, + QUANTILE_999 FROM ( SELECT * FROM performance_schema.events_statements_summary_by_digest @@ -55,6 +59,8 @@ const perfEventsStatementsQuery = ` Q.DIGEST_TEXT, Q.COUNT_STAR, Q.SUM_TIMER_WAIT, + Q.SUM_LOCK_TIME, + Q.SUM_CPU_TIME, Q.SUM_ERRORS, Q.SUM_WARNINGS, Q.SUM_ROWS_AFFECTED, @@ -64,7 +70,10 @@ const perfEventsStatementsQuery = ` Q.SUM_CREATED_TMP_TABLES, Q.SUM_SORT_MERGE_PASSES, Q.SUM_SORT_ROWS, - Q.SUM_NO_INDEX_USED + Q.SUM_NO_INDEX_USED, + Q.QUANTILE_95, + Q.QUANTILE_99, + Q.QUANTILE_999 ORDER BY SUM_TIMER_WAIT DESC LIMIT %d ` @@ -97,6 +106,16 @@ var ( "The total time of events statements by digest.", []string{"schema", "digest", "digest_text"}, nil, ) + performanceSchemaEventsStatementsLockTimeDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, performanceSchema, "events_statements_lock_time_seconds_total"), + "The total lock time of events statements by digest.", + []string{"schema", "digest", "digest_text"}, nil, + ) + performanceSchemaEventsStatementsCpuTimeDesc = prometheus.NewDesc( + prometheus.BuildFQName(namespace, performanceSchema, "events_statements_cpu_time_seconds_total"), + "The total cpu time of events statements by digest.", + []string{"schema", "digest", "digest_text"}, nil, + ) performanceSchemaEventsStatementsErrorsDesc = prometheus.NewDesc( prometheus.BuildFQName(namespace, performanceSchema, "events_statements_errors_total"), "The errors of events statements by digest.", @@ -147,6 +166,11 @@ var ( "The total number of statements that used full table scans by digest.", []string{"schema", "digest", "digest_text"}, nil, ) + performanceSchemaEventsStatementsLatency = prometheus.NewDesc( + prometheus.BuildFQName(namespace, performanceSchema, "events_statements_latency"), + "A summary of statement latency by digest", + []string{"schema", "digest", "digest_text"}, nil, + ) ) // ScrapePerfEventsStatements collects from `performance_schema.events_statements_summary_by_digest`. @@ -168,13 +192,14 @@ func (ScrapePerfEventsStatements) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfEventsStatements) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfEventsStatements) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { perfQuery := fmt.Sprintf( perfEventsStatementsQuery, *perfEventsStatementsDigestTextLimit, *perfEventsStatementsTimeLimit, *perfEventsStatementsLimit, ) + db := instance.GetDB() // Timers here are returned in picoseconds. perfSchemaEventsStatementsRows, err := db.QueryContext(ctx, perfQuery) if err != nil { @@ -184,15 +209,17 @@ func (ScrapePerfEventsStatements) Scrape(ctx context.Context, db *sql.DB, ch cha var ( schemaName, digest, digestText string - count, queryTime, errors, warnings uint64 + count, queryTime, lockTime, cpuTime uint64 + errors, warnings uint64 rowsAffected, rowsSent, rowsExamined uint64 tmpTables, tmpDiskTables uint64 sortMergePasses, sortRows uint64 noIndexUsed uint64 + quantile95, quantile99, quantile999 uint64 ) for perfSchemaEventsStatementsRows.Next() { if err := perfSchemaEventsStatementsRows.Scan( - &schemaName, &digest, &digestText, &count, &queryTime, &errors, &warnings, &rowsAffected, &rowsSent, &rowsExamined, &tmpTables, &tmpDiskTables, &sortMergePasses, &sortRows, &noIndexUsed, + &schemaName, &digest, &digestText, &count, &queryTime, &lockTime, &cpuTime, &errors, &warnings, &rowsAffected, &rowsSent, &rowsExamined, &tmpDiskTables, &tmpTables, &sortMergePasses, &sortRows, &noIndexUsed, &quantile95, &quantile99, &quantile999, ); err != nil { return err } @@ -204,6 +231,14 @@ func (ScrapePerfEventsStatements) Scrape(ctx context.Context, db *sql.DB, ch cha performanceSchemaEventsStatementsTimeDesc, prometheus.CounterValue, float64(queryTime)/picoSeconds, schemaName, digest, digestText, ) + ch <- prometheus.MustNewConstMetric( + performanceSchemaEventsStatementsLockTimeDesc, prometheus.CounterValue, float64(lockTime)/picoSeconds, + schemaName, digest, digestText, + ) + ch <- prometheus.MustNewConstMetric( + performanceSchemaEventsStatementsCpuTimeDesc, prometheus.CounterValue, float64(cpuTime)/picoSeconds, + schemaName, digest, digestText, + ) ch <- prometheus.MustNewConstMetric( performanceSchemaEventsStatementsErrorsDesc, prometheus.CounterValue, float64(errors), schemaName, digest, digestText, @@ -244,6 +279,11 @@ func (ScrapePerfEventsStatements) Scrape(ctx context.Context, db *sql.DB, ch cha performanceSchemaEventsStatementsNoIndexUsedDesc, prometheus.CounterValue, float64(noIndexUsed), schemaName, digest, digestText, ) + ch <- prometheus.MustNewConstSummary(performanceSchemaEventsStatementsLatency, count, float64(queryTime)/picoSeconds, map[float64]float64{ + 95: float64(quantile95) / picoSeconds, + 99: float64(quantile99) / picoSeconds, + 999: float64(quantile999) / picoSeconds, + }, schemaName, digest, digestText) } return nil } diff --git a/collector/perf_schema_events_statements_sum.go b/collector/perf_schema_events_statements_sum.go index e079199b..e08e03f8 100644 --- a/collector/perf_schema_events_statements_sum.go +++ b/collector/perf_schema_events_statements_sum.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -177,7 +176,8 @@ func (ScrapePerfEventsStatementsSum) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfEventsStatementsSum) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfEventsStatementsSum) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() // Timers here are returned in picoseconds. perfEventsStatementsSumRows, err := db.QueryContext(ctx, perfEventsStatementsSumQuery) if err != nil { diff --git a/collector/perf_schema_events_waits.go b/collector/perf_schema_events_waits.go index 30df7cfa..c120da25 100644 --- a/collector/perf_schema_events_waits.go +++ b/collector/perf_schema_events_waits.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -61,7 +60,8 @@ func (ScrapePerfEventsWaits) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfEventsWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfEventsWaits) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() // Timers here are returned in picoseconds. perfSchemaEventsWaitsRows, err := db.QueryContext(ctx, perfEventsWaitsQuery) if err != nil { diff --git a/collector/perf_schema_file_events.go b/collector/perf_schema_file_events.go index fa9b6ce1..abb77566 100644 --- a/collector/perf_schema_file_events.go +++ b/collector/perf_schema_file_events.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -70,7 +69,8 @@ func (ScrapePerfFileEvents) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfFileEvents) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfFileEvents) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() // Timers here are returned in picoseconds. perfSchemaFileEventsRows, err := db.QueryContext(ctx, perfFileEventsQuery) if err != nil { diff --git a/collector/perf_schema_file_instances.go b/collector/perf_schema_file_instances.go index 11f922a5..0806da9e 100644 --- a/collector/perf_schema_file_instances.go +++ b/collector/perf_schema_file_instances.go @@ -17,12 +17,11 @@ package collector import ( "context" - "database/sql" + "log/slog" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const perfFileInstancesQuery = ` @@ -80,7 +79,8 @@ func (ScrapePerfFileInstances) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfFileInstances) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfFileInstances) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() // Timers here are returned in picoseconds. perfSchemaFileInstancesRows, err := db.QueryContext(ctx, perfFileInstancesQuery, *performanceSchemaFileInstancesFilter) if err != nil { diff --git a/collector/perf_schema_file_instances_test.go b/collector/perf_schema_file_instances_test.go index 1689d8b0..2ef1a4e6 100644 --- a/collector/perf_schema_file_instances_test.go +++ b/collector/perf_schema_file_instances_test.go @@ -19,11 +19,11 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "gopkg.in/alecthomas/kingpin.v2" ) func TestScrapePerfFileInstances(t *testing.T) { @@ -37,6 +37,7 @@ func TestScrapePerfFileInstances(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"FILE_NAME", "EVENT_NAME", "COUNT_READ", "COUNT_WRITE", "SUM_NUMBER_OF_BYTES_READ", "SUM_NUMBER_OF_BYTES_WRITE"} @@ -48,7 +49,7 @@ func TestScrapePerfFileInstances(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfFileInstances{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfFileInstances{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { panic(fmt.Sprintf("error calling function on test: %s", err)) } close(ch) diff --git a/collector/perf_schema_index_io_waits.go b/collector/perf_schema_index_io_waits.go index 16cb203e..b537e067 100644 --- a/collector/perf_schema_index_io_waits.go +++ b/collector/perf_schema_index_io_waits.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -64,7 +63,8 @@ func (ScrapePerfIndexIOWaits) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfIndexIOWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfIndexIOWaits) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfSchemaIndexWaitsRows, err := db.QueryContext(ctx, perfIndexIOWaitsQuery) if err != nil { return err diff --git a/collector/perf_schema_index_io_waits_test.go b/collector/perf_schema_index_io_waits_test.go index 5ba9e512..64de2d3b 100644 --- a/collector/perf_schema_index_io_waits_test.go +++ b/collector/perf_schema_index_io_waits_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapePerfIndexIOWaits(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"OBJECT_SCHEMA", "OBJECT_NAME", "INDEX_NAME", "COUNT_FETCH", "COUNT_INSERT", "COUNT_UPDATE", "COUNT_DELETE", "SUM_TIMER_FETCH", "SUM_TIMER_INSERT", "SUM_TIMER_UPDATE", "SUM_TIMER_DELETE"} rows := sqlmock.NewRows(columns). @@ -40,7 +41,7 @@ func TestScrapePerfIndexIOWaits(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfIndexIOWaits{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfIndexIOWaits{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/perf_schema_memory_events.go b/collector/perf_schema_memory_events.go index 8350bed0..1f75be46 100644 --- a/collector/perf_schema_memory_events.go +++ b/collector/perf_schema_memory_events.go @@ -17,12 +17,11 @@ package collector import ( "context" - "database/sql" + "log/slog" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const perfMemoryEventsQuery = ` @@ -79,7 +78,8 @@ func (ScrapePerfMemoryEvents) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfMemoryEvents) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfMemoryEvents) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfSchemaMemoryEventsRows, err := db.QueryContext(ctx, perfMemoryEventsQuery) if err != nil { return err diff --git a/collector/perf_schema_memory_events_test.go b/collector/perf_schema_memory_events_test.go index 53e9d86b..96fece17 100644 --- a/collector/perf_schema_memory_events_test.go +++ b/collector/perf_schema_memory_events_test.go @@ -19,11 +19,11 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "gopkg.in/alecthomas/kingpin.v2" ) func TestScrapePerfMemoryEvents(t *testing.T) { @@ -37,6 +37,7 @@ func TestScrapePerfMemoryEvents(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{ "EVENT_NAME", @@ -54,7 +55,7 @@ func TestScrapePerfMemoryEvents(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfMemoryEvents{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfMemoryEvents{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { panic(fmt.Sprintf("error calling function on test: %s", err)) } close(ch) diff --git a/collector/perf_schema_replication_applier_status_by_worker.go b/collector/perf_schema_replication_applier_status_by_worker.go index c74571bc..275ba435 100644 --- a/collector/perf_schema_replication_applier_status_by_worker.go +++ b/collector/perf_schema_replication_applier_status_by_worker.go @@ -15,10 +15,9 @@ package collector import ( "context" - "database/sql" + "log/slog" "time" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -97,11 +96,12 @@ func (ScrapePerfReplicationApplierStatsByWorker) Help() string { // Version of MySQL from which scraper is available. func (ScrapePerfReplicationApplierStatsByWorker) Version() float64 { - return 5.7 + return 8.0 } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfReplicationApplierStatsByWorker) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfReplicationApplierStatsByWorker) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfReplicationApplierStatsByWorkerRows, err := db.QueryContext(ctx, perfReplicationApplierStatsByWorkerQuery) if err != nil { return err diff --git a/collector/perf_schema_replication_applier_status_by_worker_test.go b/collector/perf_schema_replication_applier_status_by_worker_test.go index cf638c0c..dc6974d7 100644 --- a/collector/perf_schema_replication_applier_status_by_worker_test.go +++ b/collector/perf_schema_replication_applier_status_by_worker_test.go @@ -19,9 +19,9 @@ import ( "time" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -31,6 +31,7 @@ func TestScrapePerfReplicationApplierStatsByWorker(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{ "CHANNEL_NAME", @@ -54,7 +55,7 @@ func TestScrapePerfReplicationApplierStatsByWorker(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfReplicationApplierStatsByWorker{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfReplicationApplierStatsByWorker{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/perf_schema_replication_group_member_stats.go b/collector/perf_schema_replication_group_member_stats.go index 32bec2ef..1f174f62 100644 --- a/collector/perf_schema_replication_group_member_stats.go +++ b/collector/perf_schema_replication_group_member_stats.go @@ -16,9 +16,9 @@ package collector import ( "context" "database/sql" + "log/slog" "strconv" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -79,7 +79,8 @@ func (ScrapePerfReplicationGroupMemberStats) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfReplicationGroupMemberStats) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfReplicationGroupMemberStats) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() rows, err := db.QueryContext(ctx, perfReplicationGroupMemberStatsQuery) if err != nil { return err diff --git a/collector/perf_schema_replication_group_member_stats_test.go b/collector/perf_schema_replication_group_member_stats_test.go index 0559d293..6a404385 100644 --- a/collector/perf_schema_replication_group_member_stats_test.go +++ b/collector/perf_schema_replication_group_member_stats_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapePerfReplicationGroupMemberStats(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{ "CHANNEL_NAME", @@ -66,7 +67,7 @@ func TestScrapePerfReplicationGroupMemberStats(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfReplicationGroupMemberStats{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfReplicationGroupMemberStats{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/perf_schema_replication_group_members.go b/collector/perf_schema_replication_group_members.go index a62db5e1..bb0c3980 100644 --- a/collector/perf_schema_replication_group_members.go +++ b/collector/perf_schema_replication_group_members.go @@ -16,9 +16,10 @@ package collector import ( "context" "database/sql" - "github.com/go-kit/log" - "github.com/prometheus/client_golang/prometheus" + "log/slog" "strings" + + "github.com/prometheus/client_golang/prometheus" ) const perfReplicationGroupMembersQuery = ` @@ -44,7 +45,8 @@ func (ScrapePerfReplicationGroupMembers) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfReplicationGroupMembers) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfReplicationGroupMembers) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfReplicationGroupMembersRows, err := db.QueryContext(ctx, perfReplicationGroupMembersQuery) if err != nil { return err diff --git a/collector/perf_schema_replication_group_members_test.go b/collector/perf_schema_replication_group_members_test.go index f660e3ae..843dacbd 100644 --- a/collector/perf_schema_replication_group_members_test.go +++ b/collector/perf_schema_replication_group_members_test.go @@ -15,12 +15,13 @@ package collector import ( "context" + "testing" + "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "testing" ) func TestScrapePerfReplicationGroupMembers(t *testing.T) { @@ -29,6 +30,7 @@ func TestScrapePerfReplicationGroupMembers(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{ "CHANNEL_NAME", @@ -49,7 +51,7 @@ func TestScrapePerfReplicationGroupMembers(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -82,6 +84,7 @@ func TestScrapePerfReplicationGroupMembersMySQL57(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{ "CHANNEL_NAME", @@ -100,7 +103,7 @@ func TestScrapePerfReplicationGroupMembersMySQL57(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/perf_schema_table_io_waits.go b/collector/perf_schema_table_io_waits.go index ccd9d372..77f5bc42 100644 --- a/collector/perf_schema_table_io_waits.go +++ b/collector/perf_schema_table_io_waits.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -65,7 +64,8 @@ func (ScrapePerfTableIOWaits) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfTableIOWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfTableIOWaits) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfSchemaTableWaitsRows, err := db.QueryContext(ctx, perfTableIOWaitsQuery) if err != nil { return err diff --git a/collector/perf_schema_table_lock_waits.go b/collector/perf_schema_table_lock_waits.go index 0ff8d0a2..8a29cff1 100644 --- a/collector/perf_schema_table_lock_waits.go +++ b/collector/perf_schema_table_lock_waits.go @@ -17,9 +17,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -94,7 +93,8 @@ func (ScrapePerfTableLockWaits) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapePerfTableLockWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePerfTableLockWaits) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() perfSchemaTableLockWaitsRows, err := db.QueryContext(ctx, perfTableLockWaitsQuery) if err != nil { return err diff --git a/collector/plugins.go b/collector/plugins.go index fc5640c2..2abf9785 100644 --- a/collector/plugins.go +++ b/collector/plugins.go @@ -3,8 +3,8 @@ package collector import ( "context" "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -46,7 +46,8 @@ func (ScrapePlugins) Version() float64 { return 5.1 } -func (ScrapePlugins) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapePlugins) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() showPluginsRows, err := db.QueryContext(ctx, pluginsQuery) if err != nil { return err diff --git a/collector/plugins_test.go b/collector/plugins_test.go index 523e93b9..27149483 100644 --- a/collector/plugins_test.go +++ b/collector/plugins_test.go @@ -5,9 +5,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -17,6 +17,7 @@ func TestScrapePlugins(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + instance := &Instance{db: db} columns := []string{"Name", "Status", "Type", "Library", "License"} rows := sqlmock.NewRows(columns). AddRow("INNODB_SYS_COLUMNS", "ACTIVE", "INFORMATION SCHEMA", nil, "GPL"). @@ -26,7 +27,7 @@ func TestScrapePlugins(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapePlugins{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapePlugins{}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/scraper.go b/collector/scraper.go index e77fd8a3..65ff4a62 100644 --- a/collector/scraper.go +++ b/collector/scraper.go @@ -15,9 +15,8 @@ package collector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" _ "github.com/go-sql-driver/mysql" "github.com/prometheus/client_golang/prometheus" ) @@ -35,5 +34,5 @@ type Scraper interface { Version() float64 // Scrape collects data from database connection and sends it over channel as prometheus metric. - Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error + Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error } diff --git a/collector/slave_hosts.go b/collector/slave_hosts.go index d473c3cd..5026c933 100644 --- a/collector/slave_hosts.go +++ b/collector/slave_hosts.go @@ -18,8 +18,8 @@ package collector import ( "context" "database/sql" + "log/slog" - "github.com/go-kit/log" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" ) @@ -31,7 +31,8 @@ const ( // timestamps. %s will be replaced by the database and table name. // The second column allows gets the server timestamp at the exact same // time the query is run. - slaveHostsQuery = "SHOW SLAVE HOSTS" + slaveHostsQuery = "SHOW SLAVE HOSTS" + showReplicasQuery = "SHOW REPLICAS" ) // Metric descriptors. @@ -62,10 +63,17 @@ func (ScrapeSlaveHosts) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeSlaveHosts) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { - slaveHostsRows, err := db.QueryContext(ctx, slaveHostsQuery) - if err != nil { - return err +func (ScrapeSlaveHosts) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + var ( + slaveHostsRows *sql.Rows + err error + ) + db := instance.GetDB() + // Try the both syntax for MySQL 8.0 and MySQL 8.4 + if slaveHostsRows, err = db.QueryContext(ctx, slaveHostsQuery); err != nil { + if slaveHostsRows, err = db.QueryContext(ctx, showReplicasQuery); err != nil { + return err + } } defer slaveHostsRows.Close() diff --git a/collector/slave_hosts_test.go b/collector/slave_hosts_test.go index e0d4e659..c086715e 100644 --- a/collector/slave_hosts_test.go +++ b/collector/slave_hosts_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeSlaveHostsOldFormat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Server_id", "Host", "Port", "Rpl_recovery_rank", "Master_id"} rows := sqlmock.NewRows(columns). @@ -39,7 +40,7 @@ func TestScrapeSlaveHostsOldFormat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -68,6 +69,7 @@ func TestScrapeSlaveHostsNewFormat(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Server_id", "Host", "Port", "Master_id", "Slave_UUID"} rows := sqlmock.NewRows(columns). @@ -77,7 +79,7 @@ func TestScrapeSlaveHostsNewFormat(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -106,6 +108,7 @@ func TestScrapeSlaveHostsWithoutSlaveUuid(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Server_id", "Host", "Port", "Master_id"} rows := sqlmock.NewRows(columns). @@ -115,7 +118,7 @@ func TestScrapeSlaveHostsWithoutSlaveUuid(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeSlaveHosts{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/slave_status.go b/collector/slave_status.go index 36dda335..4a1da610 100644 --- a/collector/slave_status.go +++ b/collector/slave_status.go @@ -19,9 +19,9 @@ import ( "context" "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" ) @@ -30,7 +30,7 @@ const ( slaveStatus = "slave_status" ) -var slaveStatusQueries = [2]string{"SHOW ALL SLAVES STATUS", "SHOW SLAVE STATUS"} +var slaveStatusQueries = [3]string{"SHOW ALL SLAVES STATUS", "SHOW SLAVE STATUS", "SHOW REPLICA STATUS"} var slaveStatusQuerySuffixes = [3]string{" NONBLOCKING", " NOLOCK", ""} func columnIndex(slaveCols []string, colName string) int { @@ -69,11 +69,12 @@ func (ScrapeSlaveStatus) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeSlaveStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeSlaveStatus) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { var ( slaveStatusRows *sql.Rows err error ) + db := instance.GetDB() // Try the both syntax for MySQL/Percona and MariaDB for _, query := range slaveStatusQueries { slaveStatusRows, err = db.QueryContext(ctx, query) @@ -113,7 +114,13 @@ func (ScrapeSlaveStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prome } masterUUID := columnValue(scanArgs, slaveCols, "Master_UUID") + if masterUUID == "" { + masterUUID = columnValue(scanArgs, slaveCols, "Source_UUID") + } masterHost := columnValue(scanArgs, slaveCols, "Master_Host") + if masterHost == "" { + masterHost = columnValue(scanArgs, slaveCols, "Source_Host") + } channelName := columnValue(scanArgs, slaveCols, "Channel_Name") // MySQL & Percona connectionName := columnValue(scanArgs, slaveCols, "Connection_name") // MariaDB diff --git a/collector/slave_status_test.go b/collector/slave_status_test.go index c3830bc9..a33d7fb3 100644 --- a/collector/slave_status_test.go +++ b/collector/slave_status_test.go @@ -18,9 +18,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" ) @@ -30,6 +30,7 @@ func TestScrapeSlaveStatus(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } defer db.Close() + inst := &Instance{db: db} columns := []string{"Master_Host", "Read_Master_Log_Pos", "Slave_IO_Running", "Slave_SQL_Running", "Seconds_Behind_Master"} rows := sqlmock.NewRows(columns). @@ -38,7 +39,7 @@ func TestScrapeSlaveStatus(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeSlaveStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + if err = (ScrapeSlaveStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) diff --git a/collector/mysql.go b/collector/sys.go similarity index 88% rename from collector/mysql.go rename to collector/sys.go index 5850748f..61b9d699 100644 --- a/collector/mysql.go +++ b/collector/sys.go @@ -1,4 +1,4 @@ -// Copyright 2018 The Prometheus Authors +// 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 @@ -13,5 +13,4 @@ package collector -// Subsystem. -const mysql = "mysql" +const sysSchema = "sys" diff --git a/collector/sys_user_summary.go b/collector/sys_user_summary.go new file mode 100644 index 00000000..8e84badd --- /dev/null +++ b/collector/sys_user_summary.go @@ -0,0 +1,158 @@ +// 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" + "log/slog" + + "github.com/prometheus/client_golang/prometheus" +) + +const sysUserSummaryQuery = ` + SELECT + user, + statements, + statement_latency, + table_scans, + file_ios, + file_io_latency, + current_connections, + total_connections, + unique_hosts, + current_memory, + total_memory_allocated + FROM + ` + sysSchema + `.x$user_summary +` + +var ( + sysUserSummaryStatements = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "statements_total"), + " The total number of statements for the user", + []string{"user"}, nil) + sysUserSummaryStatementLatency = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "statement_latency"), + "The total wait time of timed statements for the user", + []string{"user"}, nil) + sysUserSummaryTableScans = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "table_scans_total"), + "The total number of table scans for the user", + []string{"user"}, nil) + sysUserSummaryFileIOs = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "file_ios_total"), + "The total number of file I/O events for the user", + []string{"user"}, nil) + sysUserSummaryFileIOLatency = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "file_io_seconds_total"), + "The total wait time of timed file I/O events for the user", + []string{"user"}, nil) + sysUserSummaryCurrentConnections = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "current_connections"), + "The current number of connections for the user", + []string{"user"}, nil) + sysUserSummaryTotalConnections = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "connections_total"), + "The total number of connections for the user", + []string{"user"}, nil) + sysUserSummaryUniqueHosts = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "unique_hosts_total"), + "The number of distinct hosts from which connections for the user have originated", + []string{"user"}, nil) + sysUserSummaryCurrentMemory = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "current_memory_bytes"), + "The current amount of allocated memory for the user", + []string{"user"}, nil) + sysUserSummaryTotalMemoryAllocated = prometheus.NewDesc( + prometheus.BuildFQName(namespace, sysSchema, "memory_allocated_bytes_total"), + "The total amount of allocated memory for the user", + []string{"user"}, nil) +) + +type ScrapeSysUserSummary struct{} + +// Name of the Scraper. Should be unique. +func (ScrapeSysUserSummary) Name() string { + return sysSchema + ".user_summary" +} + +// Help describes the role of the Scraper. +func (ScrapeSysUserSummary) Help() string { + return "Collect per user metrics from sys.x$user_summary. See https://dev.mysql.com/doc/refman/5.7/en/sys-user-summary.html for details" +} + +// Version of MySQL from which scraper is available. +func (ScrapeSysUserSummary) Version() float64 { + return 5.7 +} + +// Scrape the information from sys.user_summary, creating a metric for each value of each row, labeled with the user +func (ScrapeSysUserSummary) Scrape(ctx context.Context, instance *Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + + db := instance.GetDB() + + userSummaryRows, err := db.QueryContext(ctx, sysUserSummaryQuery) + if err != nil { + return err + } + defer userSummaryRows.Close() + + var ( + user string + statements uint64 + statement_latency float64 + table_scans uint64 + file_ios uint64 + file_io_latency float64 + current_connections uint64 + total_connections uint64 + unique_hosts uint64 + current_memory uint64 + total_memory_allocated uint64 + ) + + for userSummaryRows.Next() { + err = userSummaryRows.Scan( + &user, + &statements, + &statement_latency, + &table_scans, + &file_ios, + &file_io_latency, + ¤t_connections, + &total_connections, + &unique_hosts, + ¤t_memory, + &total_memory_allocated, + ) + if err != nil { + return err + } + + ch <- prometheus.MustNewConstMetric(sysUserSummaryStatements, prometheus.CounterValue, float64(statements), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryStatementLatency, prometheus.CounterValue, float64(statement_latency)/picoSeconds, user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryTableScans, prometheus.CounterValue, float64(table_scans), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryFileIOs, prometheus.CounterValue, float64(file_ios), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryFileIOLatency, prometheus.CounterValue, float64(file_io_latency)/picoSeconds, user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryCurrentConnections, prometheus.GaugeValue, float64(current_connections), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryTotalConnections, prometheus.CounterValue, float64(total_connections), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryUniqueHosts, prometheus.CounterValue, float64(unique_hosts), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryCurrentMemory, prometheus.GaugeValue, float64(current_memory), user) + ch <- prometheus.MustNewConstMetric(sysUserSummaryTotalMemoryAllocated, prometheus.CounterValue, float64(total_memory_allocated), user) + + } + return nil +} + +var _ Scraper = ScrapeSysUserSummary{} diff --git a/collector/sys_user_summary_test.go b/collector/sys_user_summary_test.go new file mode 100644 index 00000000..cee3de42 --- /dev/null +++ b/collector/sys_user_summary_test.go @@ -0,0 +1,134 @@ +// 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/driver" + "regexp" + "strconv" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" + "github.com/smartystreets/goconvey/convey" +) + +func TestScrapeSysUserSummary(t *testing.T) { + + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error opening a stub database connection: %s", err) + } + defer db.Close() + inst := &Instance{db: db} + + columns := []string{ + "user", + "statemets", + "statement_latency", + "table_scans", + "file_ios", + "file_io_latency", + "current_connections", + "total_connections", + "unique_hosts", + "current_memory", + "total_memory_allocated", + } + rows := sqlmock.NewRows(columns) + queryResults := [][]driver.Value{ + { + "user1", + "110", + "120", + "140", + "150", + "160", + "170", + "180", + "190", + "110", + "111", + }, + { + "user2", + "210", + "220", + "240", + "250", + "260", + "270", + "280", + "290", + "210", + "211", + }, + } + expectedMetrics := []MetricResult{} + // Register the query results with mock SQL driver and assemble expected metric results list + for _, row := range queryResults { + rows.AddRow(row...) + user := row[0] + for i, metricsValue := range row { + if i == 0 { + continue + } + metricType := dto.MetricType_COUNTER + // Current Connections and Current Memory are gauges + if i == 6 || i == 9 { + metricType = dto.MetricType_GAUGE + } + value, err := strconv.ParseFloat(metricsValue.(string), 64) + if err != nil { + t.Errorf("Failed to parse result value as float64: %+v", err) + } + // Statement latency & IO latency are latencies in picoseconds, convert them to seconds + if i == 2 || i == 5 { + value = value / picoSeconds + } + expectedMetrics = append(expectedMetrics, MetricResult{ + labels: labelMap{"user": user.(string)}, + value: value, + metricType: metricType, + }) + } + } + + mock.ExpectQuery(sanitizeQuery(regexp.QuoteMeta(sysUserSummaryQuery))).WillReturnRows(rows) + + ch := make(chan prometheus.Metric) + + go func() { + if err = (ScrapeSysUserSummary{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { + t.Errorf("error calling function on test: %s", err) + } + close(ch) + }() + + // Ensure metrics look OK + convey.Convey("Metrics comparison", t, func() { + for _, expect := range expectedMetrics { + got := readMetric(<-ch) + convey.So(expect, convey.ShouldResemble, got) + } + }) + + // Ensure all SQL queries were executed + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled exceptions: %s", err) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..16602f8c --- /dev/null +++ b/config/config.go @@ -0,0 +1,256 @@ +// 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 ( + "crypto/tls" + "crypto/x509" + "fmt" + "log/slog" + "net" + "os" + "strconv" + "strings" + "sync" + + "github.com/go-sql-driver/mysql" + "github.com/prometheus/client_golang/prometheus" + + "gopkg.in/ini.v1" +) + +var ( + configReloadSuccess = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "mysqld_exporter", + Name: "config_last_reload_successful", + Help: "Mysqld exporter config loaded successfully.", + }) + + configReloadSeconds = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "mysqld_exporter", + Name: "config_last_reload_success_timestamp_seconds", + Help: "Timestamp of the last successful configuration reload.", + }) + + opts = ini.LoadOptions{ + // Do not error on nonexistent file to allow empty string as filename input + Loose: true, + // MySQL ini file can have boolean keys. + AllowBooleanKeys: true, + } + + err error +) + +type Config struct { + Sections map[string]MySqlConfig +} + +type MySqlConfig struct { + User string `ini:"user"` + Password string `ini:"password"` + Host string `ini:"host"` + Port int `ini:"port"` + Socket string `ini:"socket"` + SslCa string `ini:"ssl-ca"` + SslCert string `ini:"ssl-cert"` + SslKey string `ini:"ssl-key"` + TlsInsecureSkipVerify bool `ini:"ssl-skip-verfication"` //nolint:misspell + Tls string `ini:"tls"` +} + +type MySqlConfigHandler struct { + sync.RWMutex + TlsInsecureSkipVerify bool + Config *Config +} + +func (ch *MySqlConfigHandler) GetConfig() *Config { + ch.RLock() + defer ch.RUnlock() + return ch.Config +} + +func (ch *MySqlConfigHandler) ReloadConfig(filename string, mysqldAddress string, mysqldUser string, tlsInsecureSkipVerify bool, logger *slog.Logger) error { + var host, port string + defer func() { + if err != nil { + configReloadSuccess.Set(0) + } else { + configReloadSuccess.Set(1) + configReloadSeconds.SetToCurrentTime() + } + }() + + cfg, err := ini.LoadSources( + opts, + []byte("[client]\npassword = ${MYSQLD_EXPORTER_PASSWORD}\n"), + filename, + ) + if err != nil { + return fmt.Errorf("failed to load config from %s: %w", filename, err) + } + + if host, port, err = net.SplitHostPort(mysqldAddress); err != nil { + return fmt.Errorf("failed to parse address: %w", err) + } + + if clientSection := cfg.Section("client"); clientSection != nil { + if cfgHost := clientSection.Key("host"); cfgHost.String() == "" { + cfgHost.SetValue(host) + } + if cfgPort := clientSection.Key("port"); cfgPort.String() == "" { + cfgPort.SetValue(port) + } + if cfgUser := clientSection.Key("user"); cfgUser.String() == "" { + cfgUser.SetValue(mysqldUser) + } + } + + httpAuth := os.Getenv("HTTP_AUTH") + var user, password string + + if httpAuth != "" { + data := strings.SplitN(httpAuth, ":", 2) + if len(data) != 2 || data[0] == "" || data[1] == "" { + logger.Error("HTTP_AUTH should be formatted as user:password") + return fmt.Errorf("HTTP_AUTH should be formatted as user:password") + } + user = data[0] + password = data[1] + } + if user != "" && password != "" { + logger.Info("HTTP basic authentication is enabled") + } + + cfg.ValueMapper = os.ExpandEnv + config := &Config{} + m := make(map[string]MySqlConfig) + for _, sec := range cfg.Sections() { + sectionName := sec.Name() + + if sectionName == "DEFAULT" { + continue + } + + mysqlcfg := &MySqlConfig{ + TlsInsecureSkipVerify: tlsInsecureSkipVerify, + } + if user != "" { + mysqlcfg.User = user + mysqlcfg.Password = password + } + + err = sec.StrictMapTo(mysqlcfg) + if err != nil { + logger.Error("failed to parse config", "section", sectionName, "err", err) + continue + } + if err := mysqlcfg.validateConfig(); err != nil { + logger.Error("failed to validate config", "section", sectionName, "err", err) + continue + } + + m[sectionName] = *mysqlcfg + } + config.Sections = m + if len(config.Sections) == 0 { + return fmt.Errorf("no configuration found") + } + ch.Lock() + ch.Config = config + ch.Unlock() + return nil +} + +func (m MySqlConfig) validateConfig() error { + if m.User == "" { + return fmt.Errorf("no user specified in section or parent") + } + + return nil +} + +func (m MySqlConfig) FormDSN(target string) (string, error) { + config := mysql.NewConfig() + config.User = m.User + config.Passwd = m.Password + config.Net = "tcp" + if target == "" { + if m.Socket == "" { + host := "127.0.0.1" + if m.Host != "" { + host = m.Host + } + port := "3306" + if m.Port != 0 { + port = strconv.Itoa(m.Port) + } + config.Addr = net.JoinHostPort(host, port) + } else { + config.Net = "unix" + config.Addr = m.Socket + } + } else if prefix := "unix://"; strings.HasPrefix(target, prefix) { + config.Net = "unix" + config.Addr = target[len(prefix):] + } else { + if _, _, err = net.SplitHostPort(target); err != nil { + return "", fmt.Errorf("failed to parse target: %s", err) + } + config.Addr = target + } + + if m.TlsInsecureSkipVerify { + config.TLSConfig = "skip-verify" + } else { + config.TLSConfig = m.Tls + if m.SslCa != "" { + if err := m.CustomizeTLS(); err != nil { + err = fmt.Errorf("failed to register a custom TLS configuration for mysql dsn: %w", err) + return "", err + } + config.TLSConfig = "custom" + } + } + + return config.FormatDSN(), nil +} + +func (m MySqlConfig) CustomizeTLS() error { + var tlsCfg tls.Config + caBundle := x509.NewCertPool() + pemCA, err := os.ReadFile(m.SslCa) + if err != nil { + return err + } + if ok := caBundle.AppendCertsFromPEM(pemCA); ok { + tlsCfg.RootCAs = caBundle + } else { + return fmt.Errorf("failed parsing pem-encoded CA certificates from %s", m.SslCa) + } + if m.SslCert != "" && m.SslKey != "" { + certPairs := make([]tls.Certificate, 0, 1) + keypair, err := tls.LoadX509KeyPair(m.SslCert, m.SslKey) + if err != nil { + return fmt.Errorf("failed to parse pem-encoded SSL cert %s or SSL key %s: %w", + m.SslCert, m.SslKey, err) + } + certPairs = append(certPairs, keypair) + tlsCfg.Certificates = certPairs + } + tlsCfg.InsecureSkipVerify = m.TlsInsecureSkipVerify + mysql.RegisterTLSConfig("custom", &tlsCfg) + return nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..c1d21b07 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,256 @@ +// 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" + "testing" + + "github.com/prometheus/common/promslog" + "github.com/smartystreets/goconvey/convey" +) + +func TestValidateConfig(t *testing.T) { + convey.Convey("Working config validation", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + + convey.Convey("Valid configuration", func() { + cfg := c.GetConfig() + convey.So(cfg.Sections, convey.ShouldContainKey, "client") + convey.So(cfg.Sections, convey.ShouldContainKey, "client.server1") + + section, ok := cfg.Sections["client"] + convey.So(ok, convey.ShouldBeTrue) + convey.So(section.User, convey.ShouldEqual, "root") + convey.So(section.Password, convey.ShouldEqual, "abc") + + childSection, ok := cfg.Sections["client.server1"] + convey.So(ok, convey.ShouldBeTrue) + convey.So(childSection.User, convey.ShouldEqual, "test") + convey.So(childSection.Password, convey.ShouldEqual, "foo") + + }) + + convey.Convey("False on non-existent section", func() { + cfg := c.GetConfig() + _, ok := cfg.Sections["fakeclient"] + convey.So(ok, convey.ShouldBeFalse) + }) + }) + + convey.Convey("Inherit from parent section", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + if err := c.ReloadConfig("testdata/child_client.cnf", "localhost:3306", "", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + cfg := c.GetConfig() + section, _ := cfg.Sections["client.server1"] + convey.So(section.Password, convey.ShouldEqual, "abc") + }) + + convey.Convey("Environment variable / CLI flags", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") + if err := c.ReloadConfig("", "testhost:5000", "testuser", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + + cfg := c.GetConfig() + section := cfg.Sections["client"] + convey.So(section.Host, convey.ShouldEqual, "testhost") + convey.So(section.Port, convey.ShouldEqual, 5000) + convey.So(section.User, convey.ShouldEqual, "testuser") + convey.So(section.Password, convey.ShouldEqual, "supersecretpassword") + }) + + convey.Convey("Environment variable / CLI flags error without port", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") + err := c.ReloadConfig("", "testhost", "testuser", true, promslog.NewNopLogger()) + convey.So( + err, + convey.ShouldBeError, + ) + }) + + convey.Convey("Config file precedence over environment variables", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") + if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "fakeuser", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + + cfg := c.GetConfig() + section := cfg.Sections["client"] + convey.So(section.User, convey.ShouldEqual, "root") + convey.So(section.Password, convey.ShouldEqual, "abc") + }) + + convey.Convey("Client without user", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + os.Clearenv() + err := c.ReloadConfig("testdata/missing_user.cnf", "localhost:3306", "", true, promslog.NewNopLogger()) + convey.So( + err, + convey.ShouldResemble, + fmt.Errorf("no configuration found"), + ) + }) + + convey.Convey("Client without password", t, func() { + c := MySqlConfigHandler{ + Config: &Config{}, + } + os.Clearenv() + if err := c.ReloadConfig("testdata/missing_password.cnf", "localhost:3306", "", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + + cfg := c.GetConfig() + section := cfg.Sections["client"] + convey.So(section.User, convey.ShouldEqual, "abc") + convey.So(section.Password, convey.ShouldEqual, "") + }) +} + +func TestFormDSN(t *testing.T) { + var ( + c = MySqlConfigHandler{ + Config: &Config{}, + } + err error + dsn string + ) + + convey.Convey("Host exporter dsn", t, func() { + if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "", false, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + convey.Convey("Default Client", func() { + cfg := c.GetConfig() + section := cfg.Sections["client"] + if dsn, err = section.FormDSN(""); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "root:abc@tcp(server2:3306)/") + }) + convey.Convey("Target specific with explicit port", func() { + cfg := c.GetConfig() + section := cfg.Sections["client.server1"] + if dsn, err = section.FormDSN("server1:5000"); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "test:foo@tcp(server1:5000)/") + }) + convey.Convey("UNIX domain socket", func() { + cfg := c.GetConfig() + section := cfg.Sections["client.server1"] + if dsn, err = section.FormDSN("unix:///run/mysqld/mysqld.sock"); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "test:foo@unix(/run/mysqld/mysqld.sock)/") + }) + }) +} + +func TestFormDSNWithSslSkipVerify(t *testing.T) { + var ( + c = MySqlConfigHandler{ + Config: &Config{}, + } + err error + dsn string + ) + + convey.Convey("Host exporter dsn with tls skip verify", t, func() { + if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "", true, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + convey.Convey("Default Client", func() { + cfg := c.GetConfig() + section := cfg.Sections["client"] + if dsn, err = section.FormDSN(""); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "root:abc@tcp(server2:3306)/?tls=skip-verify") + }) + convey.Convey("Target specific with explicit port", func() { + cfg := c.GetConfig() + section := cfg.Sections["client.server1"] + if dsn, err = section.FormDSN("server1:5000"); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "test:foo@tcp(server1:5000)/?tls=skip-verify") + }) + }) +} + +func TestFormDSNWithCustomTls(t *testing.T) { + var ( + c = MySqlConfigHandler{ + Config: &Config{}, + } + err error + dsn string + ) + + convey.Convey("Host exporter dsn with custom tls", t, func() { + if err := c.ReloadConfig("testdata/client_custom_tls.cnf", "localhost:3306", "", false, promslog.NewNopLogger()); err != nil { + t.Error(err) + } + convey.Convey("Target tls enabled", func() { + cfg := c.GetConfig() + section := cfg.Sections["client_tls_true"] + if dsn, err = section.FormDSN(""); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "usr:pwd@tcp(server2:3306)/?tls=true") + }) + + convey.Convey("Target tls preferred", func() { + cfg := c.GetConfig() + section := cfg.Sections["client_tls_preferred"] + if dsn, err = section.FormDSN(""); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "usr:pwd@tcp(server3:3306)/?tls=preferred") + }) + + convey.Convey("Target tls skip-verify", func() { + cfg := c.GetConfig() + section := cfg.Sections["client_tls_skip_verify"] + if dsn, err = section.FormDSN(""); err != nil { + t.Error(err) + } + convey.So(dsn, convey.ShouldEqual, "usr:pwd@tcp(server3:3306)/?tls=skip-verify") + }) + + }) +} diff --git a/config/testdata/child_client.cnf b/config/testdata/child_client.cnf new file mode 100644 index 00000000..cd779742 --- /dev/null +++ b/config/testdata/child_client.cnf @@ -0,0 +1,5 @@ +[client] +user = root +password = abc +[client.server1] +user = root diff --git a/config/testdata/client.cnf b/config/testdata/client.cnf new file mode 100644 index 00000000..3416acca --- /dev/null +++ b/config/testdata/client.cnf @@ -0,0 +1,7 @@ +[client] +user = root +password = abc +host = server2 +[client.server1] +user = test +password = foo diff --git a/config/testdata/client_custom_tls.cnf b/config/testdata/client_custom_tls.cnf new file mode 100644 index 00000000..3f1d525e --- /dev/null +++ b/config/testdata/client_custom_tls.cnf @@ -0,0 +1,18 @@ +[client_tls_true] +host = server2 +port = 3306 +user = usr +password = pwd +tls=true +[client_tls_preferred] +host = server3 +port = 3306 +user = usr +password = pwd +tls=preferred +[client_tls_skip_verify] +host = server3 +port = 3306 +user = usr +password = pwd +tls=skip-verify diff --git a/config/testdata/missing_password.cnf b/config/testdata/missing_password.cnf new file mode 100644 index 00000000..04e48bec --- /dev/null +++ b/config/testdata/missing_password.cnf @@ -0,0 +1,2 @@ +[client] +user = abc diff --git a/config/testdata/missing_user.cnf b/config/testdata/missing_user.cnf new file mode 100644 index 00000000..dc78f1b9 --- /dev/null +++ b/config/testdata/missing_user.cnf @@ -0,0 +1,2 @@ +[client] +password = abc diff --git a/docker-compose.yml b/docker-compose.yml index 941a0a89..dc0afaad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,5 @@ # see CONTRIBUTING.md --- -version: '3' services: mysql: image: ${MYSQL_IMAGE:-mysql/mysql-server:8.0.32} diff --git a/go.mod b/go.mod index b0eb92ac..dc5f338f 100644 --- a/go.mod +++ b/go.mod @@ -4,47 +4,52 @@ go 1.23 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/go-kit/log v0.2.1 + github.com/alecthomas/kingpin/v2 v2.4.0 + github.com/blang/semver/v4 v4.0.0 github.com/go-sql-driver/mysql v1.8.1 + github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 github.com/montanaflynn/stats v0.7.1 - github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.12.2 + github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 - github.com/prometheus/common v0.37.0 - github.com/prometheus/exporter-toolkit v0.7.3 + github.com/prometheus/common v0.61.0 github.com/smartystreets/goconvey v1.8.1 github.com/stretchr/testify v1.10.0 github.com/tklauser/go-sysconf v0.3.14 golang.org/x/sys v0.29.0 - gopkg.in/alecthomas/kingpin.v2 v2.2.6 - gopkg.in/ini.v1 v1.66.6 + gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v2 v2.4.0 ) +require ( + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/vsock v1.2.1 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect +) + require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/golang/protobuf v1.5.2 // 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/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/exporter-toolkit v0.13.2 + github.com/prometheus/procfs v0.15.1 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/tklauser/numcpus v0.8.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/text v0.21.0 // indirect - google.golang.org/appengine v1.6.6 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 00a68418..a822cedd 100644 --- a/go.sum +++ b/go.sum @@ -1,228 +1,73 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +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/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= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -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.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -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/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -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.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -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/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= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -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_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= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 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.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -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.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -github.com/prometheus/exporter-toolkit v0.7.3 h1:IYBn0CTGi/nYxstdTUKysuSofUNJ3DQW3FmZ/Ub6rgU= -github.com/prometheus/exporter-toolkit v0.7.3/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/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= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/exporter-toolkit v0.13.2 h1:Z02fYtbqTMy2i/f+xZ+UK5jy/bl1Ex3ndzh06T/Q9DQ= +github.com/prometheus/exporter-toolkit v0.13.2/go.mod h1:tCqnfx21q6qN1KA4U3Bfb8uWzXfijIrJz3/kTIqMV7g= +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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -230,306 +75,29 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/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= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -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= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -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= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= -gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -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= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/mysqld-mixin/dashboards/mysql-overview.json b/mysqld-mixin/dashboards/mysql-overview.json index 089d6536..a96720b1 100644 --- a/mysqld-mixin/dashboards/mysql-overview.json +++ b/mysqld-mixin/dashboards/mysql-overview.json @@ -2496,7 +2496,7 @@ "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, - "expr": "mysql_info_schema_threads{job=~\"$job\", instance=~\"$instance\"}", + "expr": "mysql_info_schema_processlist_threads{job=~\"$job\", instance=~\"$instance\"}", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -2608,7 +2608,7 @@ "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, - "expr": "topk(5, avg_over_time(mysql_info_schema_threads{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))", + "expr": "topk(5, avg_over_time(mysql_info_schema_processlist_threads{job=~\"$job\", instance=~\"$instance\"}[$__rate_interval]))", "interval": "1m", "intervalFactor": 1, "legendFormat": "{{ state }}", @@ -3690,7 +3690,7 @@ }, "hide": 0, "includeAll": false, - "label": "Data Source", + "label": "Data source", "multi": false, "name": "datasource", "options": [], @@ -3713,12 +3713,12 @@ "definition": "label_values(mysql_up, job)", "hide": 0, "includeAll": true, - "label": "job", + "label": "Job", "multi": true, "name": "job", "options": [], "query": "label_values(mysql_up, job)", - "refresh": 1, + "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, @@ -3742,12 +3742,12 @@ "definition": "label_values(mysql_up, instance)", "hide": 0, "includeAll": true, - "label": "instance", + "label": "Instance", "multi": true, "name": "instance", "options": [], "query": "label_values(mysql_up, instance)", - "refresh": 1, + "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, diff --git a/mysqld_exporter.go b/mysqld_exporter.go index e17ca0b8..4427f1bf 100644 --- a/mysqld_exporter.go +++ b/mysqld_exporter.go @@ -15,47 +15,33 @@ package main import ( "context" - "crypto/tls" - "crypto/x509" - "database/sql" "fmt" + "log/slog" "net/http" "os" "path" - "path/filepath" "strconv" "strings" "sync/atomic" "time" - "github.com/go-kit/log" - "github.com/go-kit/log/level" - "github.com/go-sql-driver/mysql" + "github.com/alecthomas/kingpin/v2" "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" webflag "github.com/prometheus/exporter-toolkit/web/kingpinflag" - "gopkg.in/alecthomas/kingpin.v2" - "gopkg.in/ini.v1" "github.com/percona/mysqld_exporter/collector" + "github.com/percona/mysqld_exporter/config" pcl "github.com/percona/mysqld_exporter/percona/perconacollector" ) var ( - webConfig = webflag.AddFlags(kingpin.CommandLine) - webConfigFile = kingpin.Flag( - "web.config", - "[EXPERIMENTAL] Path to config yaml file that can enable TLS or authentication.", - ).Default("").String() - listenAddress = kingpin.Flag( - "web.listen-address", - "Address to listen on for web interface and telemetry.", - ).Default(":9104").String() - metricPath = kingpin.Flag( + metricsPath = kingpin.Flag( "web.telemetry-path", "Path under which to expose metrics.", ).Default("/metrics").String() @@ -67,92 +53,39 @@ var ( "config.my-cnf", "Path to .my.cnf file to read MySQL credentials from.", ).Default(path.Join(os.Getenv("HOME"), ".my.cnf")).String() - - exporterLockTimeout = kingpin.Flag( - "exporter.lock_wait_timeout", - "Set a lock_wait_timeout (in seconds) on the connection to avoid long metadata locking.", - ).Default("2").Int() - exporterLogSlowFilter = kingpin.Flag( - "exporter.log_slow_filter", - "Add a log_slow_filter to avoid slow query logging of scrapes. NOTE: Not supported by Oracle MySQL.", - ).Default("false").Bool() - exporterGlobalConnPool = kingpin.Flag( - "exporter.global-conn-pool", - "Use global connection pool instead of creating new pool for each http request.", - ).Default("true").Bool() - exporterMaxOpenConns = kingpin.Flag( - "exporter.max-open-conns", - "Maximum number of open connections to the database. https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns", - ).Default("3").Int() - exporterMaxIdleConns = kingpin.Flag( - "exporter.max-idle-conns", - "Maximum number of connections in the idle connection pool. https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns", - ).Default("3").Int() - exporterConnMaxLifetime = kingpin.Flag( - "exporter.conn-max-lifetime", - "Maximum amount of time a connection may be reused. https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime", - ).Default("1m").Duration() - collectAll = kingpin.Flag( - "collect.all", - "Collect all metrics.", - ).Default("false").Bool() - - mysqlSSLCAFile = kingpin.Flag( - "mysql.ssl-ca-file", - "SSL CA file for the MySQL connection", - ).ExistingFile() - - mysqlSSLCertFile = kingpin.Flag( - "mysql.ssl-cert-file", - "SSL Cert file for the MySQL connection", - ).ExistingFile() - - mysqlSSLKeyFile = kingpin.Flag( - "mysql.ssl-key-file", - "SSL Key file for the MySQL connection", - ).ExistingFile() - - mysqlSSLSkipVerify = kingpin.Flag( - "mysql.ssl-skip-verify", - "Skip cert verification when connection to MySQL", - ).Bool() + mysqldAddress = kingpin.Flag( + "mysqld.address", + "Address to use for connecting to MySQL", + ).Default("localhost:3306").String() + mysqldUser = kingpin.Flag( + "mysqld.username", + "Username to use for connecting to MySQL", + ).String() tlsInsecureSkipVerify = kingpin.Flag( "tls.insecure-skip-verify", "Ignore certificate and server verification when using a tls connection.", ).Bool() - dsn string -) - -// SQL queries and parameters. -const ( - versionQuery = `SELECT @@version` + collectAll = kingpin.Flag( + "collect.all", + "Collect all metrics.", + ).Default("false").Bool() - // System variable params formatting. - // See: https://github.com/go-sql-driver/mysql#system-variables - sessionSettingsParam = `log_slow_filter=%27tmp_table_on_disk,filesort_on_disk%27` - timeoutParam = `lock_wait_timeout=%d` + // This adds the following flags: `--web.listen-address`, `--web.config.file`, `--web.systemd-socket (linux-only)` + toolkitFlags = webflag.AddFlags(kingpin.CommandLine, ":9104") + c = config.MySqlConfigHandler{ + Config: &config.Config{}, + } ) -type webAuth struct { - User string `yaml:"server_user,omitempty"` - Password string `yaml:"server_password,omitempty"` +type errLogger struct { + logger *slog.Logger } -type basicAuthHandler struct { - handler http.HandlerFunc - user string - password string +func (el *errLogger) Println(v ...interface{}) { + el.logger.Error(fmt.Sprint(v...)) } -func (h *basicAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - user, password, ok := r.BasicAuth() - if !ok || password != h.password || user != h.user { - w.Header().Set("WWW-Authenticate", "Basic realm=\"metrics\"") - http.Error(w, "Invalid username or password", http.StatusUnauthorized) - return - } - h.handler(w, r) -} +var _ promhttp.Logger = &errLogger{} // scrapers lists all possible collection methods and if they should be enabled by default. var scrapers = map[collector.Scraper]bool{ @@ -181,6 +114,7 @@ var scrapers = map[collector.Scraper]bool{ collector.ScrapePerfReplicationGroupMembers{}: false, collector.ScrapePerfReplicationGroupMemberStats{}: false, collector.ScrapePerfReplicationApplierStatsByWorker{}: false, + collector.ScrapeSysUserSummary{}: false, collector.ScrapeUserStat{}: false, collector.ScrapeClientStat{}: false, collector.ScrapeTableStat{}: false, @@ -202,246 +136,188 @@ var scrapers = map[collector.Scraper]bool{ pcl.NewStandardProcess(): false, } -// TODO Remove -var scrapersHr = map[collector.Scraper]struct{}{ - pcl.ScrapeGlobalStatus{}: {}, - collector.ScrapeInnodbMetrics{}: {}, - pcl.ScrapeCustomQuery{Resolution: pcl.HR}: {}, -} - -// TODO Remove -var scrapersMr = map[collector.Scraper]struct{}{ - collector.ScrapeSlaveStatus{}: {}, - pcl.ScrapeProcesslist{}: {}, - collector.ScrapePerfEventsWaits{}: {}, - collector.ScrapePerfFileEvents{}: {}, - collector.ScrapePerfTableLockWaits{}: {}, - collector.ScrapeQueryResponseTime{}: {}, - collector.ScrapeEngineInnodbStatus{}: {}, - pcl.ScrapeInnodbCmp{}: {}, - pcl.ScrapeInnodbCmpMem{}: {}, - pcl.ScrapeCustomQuery{Resolution: pcl.MR}: {}, -} - -// TODO Remove -var scrapersLr = map[collector.Scraper]struct{}{ - collector.ScrapeGlobalVariables{}: {}, - collector.ScrapePlugins{}: {}, - collector.ScrapeTableSchema{}: {}, - collector.ScrapeAutoIncrementColumns{}: {}, - collector.ScrapeBinlogSize{}: {}, - collector.ScrapePerfTableIOWaits{}: {}, - collector.ScrapePerfIndexIOWaits{}: {}, - collector.ScrapePerfFileInstances{}: {}, - collector.ScrapeUserStat{}: {}, - collector.ScrapeTableStat{}: {}, - collector.ScrapePerfEventsStatements{}: {}, - collector.ScrapeClientStat{}: {}, - collector.ScrapeInfoSchemaInnodbTablespaces{}: {}, - collector.ScrapeEngineTokudbStatus{}: {}, - collector.ScrapeHeartbeat{}: {}, - pcl.ScrapeCustomQuery{Resolution: pcl.LR}: {}, -} +// // TODO Remove +// var scrapersHr = map[collector.Scraper]struct{}{ +// pcl.ScrapeGlobalStatus{}: {}, +// collector.ScrapeInnodbMetrics{}: {}, +// pcl.ScrapeCustomQuery{Resolution: pcl.HR}: {}, +// } + +// // TODO Remove +// var scrapersMr = map[collector.Scraper]struct{}{ +// collector.ScrapeSlaveStatus{}: {}, +// pcl.ScrapeProcesslist{}: {}, +// collector.ScrapePerfEventsWaits{}: {}, +// collector.ScrapePerfFileEvents{}: {}, +// collector.ScrapePerfTableLockWaits{}: {}, +// collector.ScrapeQueryResponseTime{}: {}, +// collector.ScrapeEngineInnodbStatus{}: {}, +// pcl.ScrapeInnodbCmp{}: {}, +// pcl.ScrapeInnodbCmpMem{}: {}, +// pcl.ScrapeCustomQuery{Resolution: pcl.MR}: {}, +// } + +// // TODO Remove +// var scrapersLr = map[collector.Scraper]struct{}{ +// collector.ScrapeGlobalVariables{}: {}, +// collector.ScrapePlugins{}: {}, +// collector.ScrapeTableSchema{}: {}, +// collector.ScrapeAutoIncrementColumns{}: {}, +// collector.ScrapeBinlogSize{}: {}, +// collector.ScrapePerfTableIOWaits{}: {}, +// collector.ScrapePerfIndexIOWaits{}: {}, +// collector.ScrapePerfFileInstances{}: {}, +// collector.ScrapeUserStat{}: {}, +// collector.ScrapeTableStat{}: {}, +// collector.ScrapePerfEventsStatements{}: {}, +// collector.ScrapeClientStat{}: {}, +// collector.ScrapeInfoSchemaInnodbTablespaces{}: {}, +// collector.ScrapeEngineTokudbStatus{}: {}, +// collector.ScrapeHeartbeat{}: {}, +// pcl.ScrapeCustomQuery{Resolution: pcl.LR}: {}, +// } + +func filterScrapers(scrapers []collector.Scraper, collectParams []string) []collector.Scraper { + var filteredScrapers []collector.Scraper + + // Check if we have some "collect[]" query parameters. + if len(collectParams) > 0 { + filters := make(map[string]bool) + for _, param := range collectParams { + filters[param] = true + } -func parseMycnf(config interface{}, logger log.Logger) (string, error) { - var dsn string - opts := ini.LoadOptions{ - // MySQL ini file can have boolean keys. - // PMM-2469: my.cnf can have boolean keys. - AllowBooleanKeys: true, - } - cfg, err := ini.LoadSources(opts, config) - if err != nil { - return dsn, fmt.Errorf("failed reading ini file: %s", err) - } - user := cfg.Section("client").Key("user").String() - password := cfg.Section("client").Key("password").String() - if user == "" { - return dsn, fmt.Errorf("no user specified under [client] in %s", config) - } - host := cfg.Section("client").Key("host").MustString("localhost") - port := cfg.Section("client").Key("port").MustUint(3306) - socket := cfg.Section("client").Key("socket").String() - sslCA := cfg.Section("client").Key("ssl-ca").String() - sslCert := cfg.Section("client").Key("ssl-cert").String() - sslKey := cfg.Section("client").Key("ssl-key").String() - passwordPart := "" - if password != "" { - passwordPart = ":" + password - } else { - if sslKey == "" { - return dsn, fmt.Errorf("password or ssl-key should be specified under [client] in %s", config) + for _, scraper := range scrapers { + if filters[scraper.Name()] { + filteredScrapers = append(filteredScrapers, scraper) + } } } - if socket != "" { - dsn = fmt.Sprintf("%s%s@unix(%s)/", user, passwordPart, socket) - } else { - dsn = fmt.Sprintf("%s%s@tcp(%s:%d)/", user, passwordPart, host, port) - } - if sslCA != "" { - if tlsErr := customizeTLS(sslCA, sslCert, sslKey); tlsErr != nil { - tlsErr = fmt.Errorf("failed to register a custom TLS configuration for mysql dsn: %s", tlsErr) - return dsn, tlsErr - } - dsn, err = setTLSConfig(dsn) - if err != nil { - return "", fmt.Errorf("cannot set TLS configuration: %s", err) - } + if len(filteredScrapers) == 0 { + return scrapers } - - level.Debug(logger).Log("dsn", dsn) - return dsn, nil + return filteredScrapers } -func customizeTLS(sslCA string, sslCert string, sslKey string) error { - var tlsCfg tls.Config - caBundle := x509.NewCertPool() - pemCA, err := os.ReadFile(filepath.Clean(sslCA)) - if err != nil { - return err - } - if ok := caBundle.AppendCertsFromPEM(pemCA); ok { - tlsCfg.RootCAs = caBundle - } else { - return fmt.Errorf("failed parse pem-encoded CA certificates from %s", sslCA) - } - if sslCert != "" && sslKey != "" { - certPairs := make([]tls.Certificate, 0, 1) - keypair, err := tls.LoadX509KeyPair(sslCert, sslKey) +func getScrapeTimeoutSeconds(r *http.Request, offset float64) (float64, error) { + var timeoutSeconds float64 + if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { + var err error + timeoutSeconds, err = strconv.ParseFloat(v, 64) if err != nil { - return fmt.Errorf("failed to parse pem-encoded SSL cert %s or SSL key %s: %s", - sslCert, sslKey, err) + return 0, fmt.Errorf("failed to parse timeout from Prometheus header: %v", err) } - certPairs = append(certPairs, keypair) - tlsCfg.Certificates = certPairs - } else { - return fmt.Errorf("missing certificates. Cannot specify only SSL CA file") } - - tlsCfg.InsecureSkipVerify = *mysqlSSLSkipVerify || *tlsInsecureSkipVerify - return mysql.RegisterTLSConfig("custom", &tlsCfg) -} - -func setTLSConfig(dsn string) (string, error) { - cfg, err := mysql.ParseDSN(dsn) - if err != nil { - return "", err + if timeoutSeconds == 0 { + return 0, nil + } + if timeoutSeconds < 0 { + return 0, fmt.Errorf("timeout value from Prometheus header is invalid: %f", timeoutSeconds) } - cfg.TLSConfig = "custom" - return cfg.FormatDSN(), nil + if offset >= timeoutSeconds { + // Ignore timeout offset if it doesn't leave time to scrape. + return 0, fmt.Errorf("timeout offset (%f) should be lower than prometheus scrape timeout (%f)", offset, timeoutSeconds) + } else { + // Subtract timeout offset from timeout. + timeoutSeconds -= offset + } + return timeoutSeconds, nil } func init() { - prometheus.MustRegister(version.NewCollector("mysqld_exporter")) + prometheus.MustRegister(versioncollector.NewCollector("mysqld_exporter")) } -func newHandler(cfg *webAuth, db *sql.DB, metrics collector.Metrics, scrapers []collector.Scraper, defaultGatherer bool, logger log.Logger) http.HandlerFunc { +func newHandler(scrapers []collector.Scraper, logger *slog.Logger) http.HandlerFunc { var processing_lr, processing_mr, processing_hr uint32 = 0, 0, 0 // default value is already 0, but for extra clarity return func(w http.ResponseWriter, r *http.Request) { start := time.Now() - query_collect := r.URL.Query().Get("collect[]") + q := r.URL.Query() + query_collect := q.Get("collect[]") + switch query_collect { case "custom_query.hr": if !atomic.CompareAndSwapUint32(&processing_hr, 0, 1) { - level.Warn(logger).Log("msg", "Received metrics HR request while previous still in progress: returning 429 Too Many Requests") + logger.Warn("Received metrics HR request while previous still in progress: returning 429 Too Many Requests") http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests) return } defer atomic.StoreUint32(&processing_hr, 0) case "custom_query.mr": if !atomic.CompareAndSwapUint32(&processing_mr, 0, 1) { - level.Warn(logger).Log("msg", "Received metrics MR request while previous still in progress: returning 429 Too Many Requests") + logger.Warn("Received metrics MR request while previous still in progress: returning 429 Too Many Requests") http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests) return } defer atomic.StoreUint32(&processing_mr, 0) case "custom_query.lr": if !atomic.CompareAndSwapUint32(&processing_lr, 0, 1) { - level.Warn(logger).Log("msg", "Received metrics LR request while previous still in progress: returning 429 Too Many Requests") + logger.Warn("Received metrics LR request while previous still in progress: returning 429 Too Many Requests") http.Error(w, "429 Too Many Requests", http.StatusTooManyRequests) return } defer atomic.StoreUint32(&processing_lr, 0) } defer func() { - level.Debug(logger).Log("msg", "Request elapsed time", "sinceStart", time.Since(start), "query_collect", query_collect) + logger.Debug("Request elapsed time", "sinceStart", time.Since(start), "query_collect", query_collect) }() - filteredScrapers := scrapers - params := r.URL.Query()["collect[]"] // Use request context for cancellation when connection gets closed. ctx := r.Context() // If a timeout is configured via the Prometheus header, add it to the context. - if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" { - timeoutSeconds, err := strconv.ParseFloat(v, 64) - if err != nil { - level.Error(logger).Log("msg", "Failed to parse timeout from Prometheus header", "err", err) - } else { - if *timeoutOffset >= timeoutSeconds { - // Ignore timeout offset if it doesn't leave time to scrape. - level.Error(logger).Log("msg", "Timeout offset should be lower than prometheus scrape timeout", "offset", *timeoutOffset, "prometheus_scrape_timeout", timeoutSeconds) - } else { - // Subtract timeout offset from timeout. - timeoutSeconds -= *timeoutOffset - } - // Create new timeout context with request context as parent. - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds*float64(time.Second))) - defer cancel() - // Overwrite request with timeout context. - r = r.WithContext(ctx) - } + timeoutSeconds, err := getScrapeTimeoutSeconds(r, *timeoutOffset) + if err != nil { + logger.Error("Error getting timeout from Prometheus header", "err", err) + } + if timeoutSeconds > 0 { + // Create new timeout context with request context as parent. + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds*float64(time.Second))) + defer cancel() + // Overwrite request with timeout context. + r = r.WithContext(ctx) } - level.Debug(logger).Log("msg", "collect[] params", "params", strings.Join(params, ",")) - // Check if we have some "collect[]" query parameters. - if len(params) > 0 { - filters := make(map[string]bool) - for _, param := range params { - filters[param] = true - } + collect := q["collect[]"] + // logger.Debug("msg", "collect[] params", strings.Join(collect, ",")) + if len(collect) > 0 { + logger.Info("msg", "collect[] params", strings.Join(collect, ",")) + } - filteredScrapers = nil - for _, scraper := range scrapers { - if filters[scraper.Name()] { - filteredScrapers = append(filteredScrapers, scraper) - } - } + filteredScrapers := filterScrapers(scrapers, collect) + + var dsn string + target := "" + if q.Has("target") { + target = q.Get("target") } - // Copy db as local variable, so the pointer passed to newHandler doesn't get updated. - db := db - // If there is no global connection pool then create new. - var err error - if db == nil { - db, err = newDB(dsn) - if err != nil { - level.Error(logger).Log("msg", "Error opening connection to database", "error", err) - return - } - defer db.Close() + cfg := c.GetConfig() + cfgsection, ok := cfg.Sections["client"] + if !ok { + logger.Error("Failed to parse section [client] from config file", "err", err) + } + if dsn, err = cfgsection.FormDSN(target); err != nil { + logger.Error("Failed to form dsn from section [client]", "err", err) } registry := prometheus.NewRegistry() - registry.MustRegister(collector.New(ctx, db, metrics, filteredScrapers, logger)) + registry.MustRegister(collector.New(ctx, dsn, filteredScrapers, logger)) - gatherers := prometheus.Gatherers{} - if defaultGatherer { - gatherers = append(gatherers, prometheus.DefaultGatherer) + gatherers := prometheus.Gatherers{ + prometheus.DefaultGatherer, + registry, } - gatherers = append(gatherers, registry) + eLogger := &errLogger{logger: logger} // Delegate http serving to Prometheus client library, which will call collector.Collect. h := promhttp.HandlerFor(gatherers, promhttp.HandlerOpts{ - // mysqld_exporter has multiple collectors, if one fails, - // we still should report metrics from collectors that succeeded. + // mysqld_exporter has multiple collectors; if one fails, + // we still should handle metrics from collectors that succeeded. ErrorHandling: promhttp.ContinueOnError, - //ErrorLog: logger., TODO TS + ErrorLog: eLogger, }) - if cfg.User != "" && cfg.Password != "" { - h = &basicAuthHandler{handler: h.ServeHTTP, user: cfg.User, password: cfg.Password} - } h.ServeHTTP(w, r) } } @@ -464,216 +340,89 @@ func main() { } // Parse flags. - promlogConfig := &promlog.Config{} - flag.AddFlags(kingpin.CommandLine, promlogConfig) + promslogConfig := &promslog.Config{} + flag.AddFlags(kingpin.CommandLine, promslogConfig) kingpin.Version(version.Print("mysqld_exporter")) kingpin.HelpFlag.Short('h') kingpin.Parse() - logger := promlog.New(promlogConfig) - - // landingPage contains the HTML served at '/'. - // TODO: Make this nicer and more informative. - var landingPage = []byte(` -MySQLd exporter - -

MySQL 3-in-1 exporter

- -

MySQL exporter

- - - -`) - - level.Info(logger).Log("msg", "Starting mysqld_exporter", "version", version.Info()) - level.Info(logger).Log("msg", "Build context", version.BuildContext()) - - dsn = os.Getenv("DATA_SOURCE_NAME") - if len(dsn) == 0 { - var err error - if dsn, err = parseMycnf(*configMycnf, logger); err != nil { - level.Error(logger).Log("msg", "Error parsing my.cnf", "file", *configMycnf, "err", err) - os.Exit(1) - } - } - - // Setup extra params for the DSN, default to having a lock timeout. - dsnParams := []string{fmt.Sprintf(timeoutParam, *exporterLockTimeout)} - if *exporterLogSlowFilter { - dsnParams = append(dsnParams, sessionSettingsParam) - } - - // The parseMycnf function will set the TLS config in case certificates are being defined in - // the config file. If the user also specified command line parameters, these parameters should - // override the ones from the cnf file. - if *mysqlSSLCAFile != "" || (*mysqlSSLCertFile != "" && *mysqlSSLKeyFile != "") { - if err := customizeTLS(*mysqlSSLCAFile, *mysqlSSLCertFile, *mysqlSSLKeyFile); err != nil { - level.Error(logger).Log("msg", "failed to register a custom TLS configuration for mysql dsn", "error", err) - } - var err error - dsn, err = setTLSConfig(dsn) - if err != nil { - level.Error(logger).Log("msg", "failed to register a custom TLS configuration for mysql dsn", "error", err) - os.Exit(1) - } - } - - // This could be improved using the driver's DSN parse and config format functions but this is - // how upstream does it. - if strings.Contains(dsn, "?") { - dsn += "&" - } else { - dsn += "?" - } - dsn += strings.Join(dsnParams, "&") - - // Open global connection pool if requested. - var db *sql.DB - - var err error + logger := promslog.New(promslogConfig) - if *exporterGlobalConnPool { - db, err = newDB(dsn) - if err != nil { - level.Error(logger).Log("msg", "Error opening connection to database", "error", err) - return - } - defer db.Close() - } + logger.Info("Starting mysqld_exporter", "version", version.Info()) + logger.Info("Build context", "build_context", version.BuildContext()) - cfg := &webAuth{} - httpAuth := os.Getenv("HTTP_AUTH") - - if httpAuth != "" { - data := strings.SplitN(httpAuth, ":", 2) - if len(data) != 2 || data[0] == "" || data[1] == "" { - level.Error(logger).Log("msg", "HTTP_AUTH should be formatted as user:password") - return - } - cfg.User = data[0] - cfg.Password = data[1] - } - if cfg.User != "" && cfg.Password != "" { - level.Info(logger).Log("msg", "HTTP basic authentication is enabled") + if err := c.ReloadConfig(*configMycnf, *mysqldAddress, *mysqldUser, *tlsInsecureSkipVerify, logger); err != nil { + logger.Info("Error parsing host config", "file", *configMycnf, "err", err) + os.Exit(1) } // Use default mux for /debug/vars and /debug/pprof mux := http.DefaultServeMux // Defines what to scrape in each resolution. - all, hr, mr, lr := enabledScrapers(scraperFlags) - - // TODO: Remove later. It's here for backward compatibility. See: https://jira.percona.com/browse/PMM-2180. - mux.Handle(*metricPath+"-hr", newHandler(cfg, db, collector.NewMetrics("hr"), hr, true, logger)) - mux.Handle(*metricPath+"-mr", newHandler(cfg, db, collector.NewMetrics("mr"), mr, false, logger)) - mux.Handle(*metricPath+"-lr", newHandler(cfg, db, collector.NewMetrics("lr"), lr, false, logger)) + all := enabledScrapers(scraperFlags, logger) // Handle all metrics on one endpoint. - mux.Handle(*metricPath, newHandler(cfg, db, collector.NewMetrics(""), all, false, logger)) - - // Log which scrapers are enabled. - if len(hr) > 0 { - level.Info(logger).Log("msg", "Enabled High Resolution scrapers:") - for _, scraper := range hr { - var v = fmt.Sprintf(" --collect.%s", scraper.Name()) - level.Info(logger).Log("msg", v) - } - } - if len(mr) > 0 { - level.Info(logger).Log("msg", "Enabled Medium Resolution scrapers:") - for _, scraper := range mr { - var v = fmt.Sprintf(" --collect.%s", scraper.Name()) - level.Info(logger).Log("msg", v) - } - } - if len(lr) > 0 { - level.Info(logger).Log("msg", "Enabled Low Resolution scrapers:") - for _, scraper := range lr { - var v = fmt.Sprintf(" --collect.%s", scraper.Name()) - level.Info(logger).Log("msg", v) - } - } - if len(all) > 0 { - level.Info(logger).Log("msg", "Enabled Resolution Independent scrapers:") - for _, scraper := range all { - var v = fmt.Sprintf(" --collect.%s", scraper.Name()) - level.Info(logger).Log("msg", v) - } - } + mux.Handle(*metricsPath, newHandler(all, logger)) srv := &http.Server{ - Addr: *listenAddress, Handler: mux, } - level.Info(logger).Log("msg", "Listening on", "address", *listenAddress) - - if *webConfigFile != "" && *webConfig != "" { - level.Error(logger).Log("msg", "Should specify only one web-config file") - os.Exit(1) - } - - webCfg := "" - if *webConfigFile != "" { - webCfg = *webConfigFile - } - if *webConfig != "" { - webCfg = *webConfig + // Register only scrapers enabled by flag, or all if --collect.all is set. + // enabledScrapers := []collector.Scraper{} + // for scraper, enabled := range scraperFlags { + // if *enabled || *collectAll{ + // logger.Info("Scraper enabled", "scraper", scraper.Name()) + // enabledScrapers = append(enabledScrapers, scraper) + // } + // } + // handlerFunc := newHandler(enabledScrapers, logger) + // http.Handle(*metricsPath, promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, handlerFunc)) + + if *metricsPath != "/" && *metricsPath != "" { + landingConfig := web.LandingConfig{ + Name: "MySQLd Exporter", + Description: "Prometheus Exporter for MySQL servers", + Version: version.Info(), + Links: []web.LandingLinks{ + { + Address: *metricsPath, + Text: "Metrics", + }, + }, + } + landingPage, err := web.NewLandingPage(landingConfig) + if err != nil { + logger.Error("Error creating landing page", "err", err) + os.Exit(1) + } + mux.Handle("/", landingPage) } - if webCfg != "" { - // https - mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") - w.Write(landingPage) - }) - - level.Error(logger).Log("error", web.ListenAndServe(srv, webCfg, logger)) - } else { - // http - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Write(landingPage) - }) + mux.HandleFunc("/probe", handleProbe(all, logger)) + mux.HandleFunc("/-/reload", func(w http.ResponseWriter, r *http.Request) { + if err := c.ReloadConfig(*configMycnf, *mysqldAddress, *mysqldUser, *tlsInsecureSkipVerify, logger); err != nil { + logger.Warn("Error reloading host config", "file", *configMycnf, "error", err) + return + } + _, _ = w.Write([]byte(`ok`)) + }) - level.Error(logger).Log("error", srv.ListenAndServe()) + if err := web.ListenAndServe(srv, toolkitFlags, logger); err != nil { + logger.Error("Error starting HTTP server", "err", err) + os.Exit(1) } } -func enabledScrapers(scraperFlags map[collector.Scraper]*bool) (all, hr, mr, lr []collector.Scraper) { +func enabledScrapers(scraperFlags map[collector.Scraper]*bool, logger *slog.Logger) (all []collector.Scraper) { for scraper, enabled := range scraperFlags { if *collectAll || *enabled { if _, ok := scrapers[scraper]; ok { all = append(all, scraper) - } - if _, ok := scrapersHr[scraper]; ok { - hr = append(hr, scraper) - } - if _, ok := scrapersMr[scraper]; ok { - mr = append(mr, scraper) - } - if _, ok := scrapersLr[scraper]; ok { - lr = append(lr, scraper) + logger.Info("Scraper enabled", "scraper", scraper.Name()) } } } - return all, hr, mr, lr -} - -func newDB(dsn string) (*sql.DB, error) { - // Validate DSN, and open connection pool. - db, err := sql.Open("mysql", dsn) - if err != nil { - return nil, err - } - db.SetMaxOpenConns(*exporterMaxOpenConns) - db.SetMaxIdleConns(*exporterMaxIdleConns) - db.SetConnMaxLifetime(*exporterConnMaxLifetime) - - return db, nil + return all } diff --git a/mysqld_exporter_test.go b/mysqld_exporter_test.go index 6258a035..12205531 100644 --- a/mysqld_exporter_test.go +++ b/mysqld_exporter_test.go @@ -29,124 +29,10 @@ import ( "testing" "time" - "github.com/go-kit/log" - "github.com/smartystreets/goconvey/convey" + "github.com/google/go-cmp/cmp" + "github.com/percona/mysqld_exporter/collector" ) -func TestParseMycnf(t *testing.T) { - const ( - tcpConfig = ` - [client] - user = root - password = abc123 - ` - tcpConfig2 = ` - [client] - user = root - password = abc123 - port = 3308 - ` - clientAuthConfig = ` - [client] - user = root - port = 3308 - ssl-ca = ca.crt - ssl-cert = tls.crt - ssl-key = tls.key - ` - socketConfig = ` - [client] - user = user - password = pass - socket = /var/lib/mysql/mysql.sock - ` - socketConfig2 = ` - [client] - user = dude - password = nopassword - # host and port will not be used because of socket presence - host = 1.2.3.4 - port = 3307 - socket = /var/lib/mysql/mysql.sock - ` - remoteConfig = ` - [client] - user = dude - password = nopassword - host = 1.2.3.4 - port = 3307 - ` - ignoreBooleanKeys = ` - [client] - user = root - password = abc123 - - [mysql] - skip-auto-rehash - ` - badConfig = ` - [client] - user = root - ` - badConfig2 = ` - [client] - password = abc123 - socket = /var/lib/mysql/mysql.sock - ` - badConfig3 = ` - [hello] - world = ismine - ` - badConfig4 = `[hello` - ) - convey.Convey("Various .my.cnf configurations", t, func() { - convey.Convey("Local tcp connection", func() { - dsn, _ := parseMycnf([]byte(tcpConfig), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "root:abc123@tcp(localhost:3306)/") - }) - convey.Convey("Local tcp connection on non-default port", func() { - dsn, _ := parseMycnf([]byte(tcpConfig2), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "root:abc123@tcp(localhost:3308)/") - }) - convey.Convey("Authentication with client certificate and no password", func() { - dsn, _ := parseMycnf([]byte(clientAuthConfig), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "root@tcp(localhost:3308)/") - }) - convey.Convey("Socket connection", func() { - dsn, _ := parseMycnf([]byte(socketConfig), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "user:pass@unix(/var/lib/mysql/mysql.sock)/") - }) - convey.Convey("Socket connection ignoring defined host", func() { - dsn, _ := parseMycnf([]byte(socketConfig2), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "dude:nopassword@unix(/var/lib/mysql/mysql.sock)/") - }) - convey.Convey("Remote connection", func() { - dsn, _ := parseMycnf([]byte(remoteConfig), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "dude:nopassword@tcp(1.2.3.4:3307)/") - }) - convey.Convey("Ignore boolean keys", func() { - dsn, _ := parseMycnf([]byte(ignoreBooleanKeys), log.NewNopLogger()) - convey.So(dsn, convey.ShouldEqual, "root:abc123@tcp(localhost:3306)/") - }) - convey.Convey("Missed user", func() { - _, err := parseMycnf([]byte(badConfig), log.NewNopLogger()) - convey.So(err, convey.ShouldBeError, fmt.Errorf("password or ssl-key should be specified under [client] in %s", badConfig)) - }) - convey.Convey("Missed password", func() { - _, err := parseMycnf([]byte(badConfig2), log.NewNopLogger()) - convey.So(err, convey.ShouldBeError, fmt.Errorf("no user specified under [client] in %s", badConfig2)) - }) - convey.Convey("No [client] section", func() { - _, err := parseMycnf([]byte(badConfig3), log.NewNopLogger()) - convey.So(err, convey.ShouldBeError, fmt.Errorf("no user specified under [client] in %s", badConfig3)) - }) - convey.Convey("Invalid config", func() { - _, err := parseMycnf([]byte(badConfig4), log.NewNopLogger()) - convey.So(err, convey.ShouldBeError, fmt.Errorf("failed reading ini file: unclosed section: %s", badConfig4)) - }) - }) -} - // bin stores information about path of executable and attached port type bin struct { path string @@ -169,7 +55,7 @@ func TestBin(t *testing.T) { } }() - importpath := "github.com/prometheus/mysqld_exporter/vendor/github.com/prometheus/common" + importpath := "github.com/prometheus/common" path := binDir + "/" + binName xVariables := map[string]string{ importpath + "/version.Version": "gotest-version", @@ -196,7 +82,8 @@ func TestBin(t *testing.T) { } tests := []func(*testing.T, bin){ - testLandingPage, + testLanding, + testProbe, } portStart := 56000 @@ -217,7 +104,7 @@ func TestBin(t *testing.T) { }) } -func testLandingPage(t *testing.T, data bin) { +func testLanding(t *testing.T, data bin) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -226,10 +113,8 @@ func testLandingPage(t *testing.T, data bin) { ctx, data.path, "--web.listen-address", fmt.Sprintf(":%d", data.port), + "--config.my-cnf=test_exporter.cnf", ) - cmd.Env = append(os.Environ(), "DATA_SOURCE_NAME=tcp:(127.0.0.1:3306)/") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { t.Fatal(err) } @@ -244,22 +129,41 @@ func testLandingPage(t *testing.T, data bin) { } got := string(body) - expected := ` -MySQLd exporter - -

MySQL 3-in-1 exporter

- -

MySQL exporter

- - - + expected := ` +

Prometheus Exporter for MySQL servers

` + if !strings.Contains(got, expected) { + t.Fatalf("the web page does not contain expected content: \n%s", cmp.Diff(got, expected)) + } +} + +func testProbe(t *testing.T, data bin) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Run exporter. + cmd := exec.CommandContext( + ctx, + data.path, + "--web.listen-address", fmt.Sprintf(":%d", data.port), + "--config.my-cnf=test_exporter.cnf", + ) + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer cmd.Wait() + defer cmd.Process.Kill() + + // Get the main page. + urlToGet := fmt.Sprintf("http://127.0.0.1:%d/probe", data.port) + body, err := waitForBody(urlToGet) + if err != nil { + t.Fatal(err) + } + got := strings.TrimSpace(string(body)) + + expected := `target is required` + if got != expected { t.Fatalf("got '%s' but expected '%s'", got, expected) } @@ -312,3 +216,126 @@ func getBody(urlToGet string) ([]byte, error) { return body, nil } + +func Test_filterScrapers(t *testing.T) { + type args struct { + scrapers []collector.Scraper + collectParams []string + } + tests := []struct { + name string + args args + want []collector.Scraper + }{ + {"args_appears_in_collector", + args{ + []collector.Scraper{collector.ScrapeGlobalStatus{}}, + []string{collector.ScrapeGlobalStatus{}.Name()}, + }, + []collector.Scraper{ + collector.ScrapeGlobalStatus{}, + }}, + {"args_absent_in_collector", + args{ + []collector.Scraper{collector.ScrapeGlobalStatus{}}, + []string{collector.ScrapeGlobalVariables{}.Name()}, + }, + []collector.Scraper{collector.ScrapeGlobalStatus{}}}, + {"respect_params", + args{ + []collector.Scraper{ + collector.ScrapeGlobalStatus{}, + collector.ScrapeGlobalVariables{}, + }, + []string{collector.ScrapeGlobalStatus{}.Name()}, + }, + []collector.Scraper{ + collector.ScrapeGlobalStatus{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filterScrapers(tt.args.scrapers, tt.args.collectParams); !reflect.DeepEqual(got, tt.want) { + t.Errorf("filterScrapers() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getScrapeTimeoutSeconds(t *testing.T) { + type args struct { + timeoutHeader string + offset float64 + } + tests := []struct { + name string + args args + wantTimeout float64 + wantErr bool + }{ + {"no_timeout_header", + args{}, + 0, false, + }, + {"zero_timeout_header", + args{ + timeoutHeader: "0", + }, + 0, false, + }, + {"negative_timeout_header", + args{ + timeoutHeader: "-5", + }, + 0, true, + }, + {"offset_greater_than_timeout", + args{ + timeoutHeader: "5", + offset: 6, + }, + 0, true, + }, + {"offset_equal_timeout", + args{ + timeoutHeader: "5", + offset: 5, + }, + 0, true, + }, + {"offset_less_than_timeout", + args{ + timeoutHeader: "5", + offset: 1, + }, + 4, false, + }, + {"no_offset", + args{ + timeoutHeader: "5", + }, + 5, false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "", nil) + if err != nil { + t.Fatalf("unexpected error creating http request: %v", err) + } + request.Header.Set("X-Prometheus-Scrape-Timeout-Seconds", tt.args.timeoutHeader) + + timeout, err := getScrapeTimeoutSeconds(request, tt.args.offset) + if err != nil && !tt.wantErr { + t.Fatalf("unexpected error: %v", err) + } + if err == nil && tt.wantErr { + t.Fatal("expecting an error, got nil") + } + if timeout != tt.wantTimeout { + t.Fatalf("unexpected timeout, got '%f' but expected '%f'", timeout, tt.wantTimeout) + } + }) + } +} diff --git a/percona/perconacollector/custom_query.go b/percona/perconacollector/custom_query.go index 36453414..bfdfec78 100644 --- a/percona/perconacollector/custom_query.go +++ b/percona/perconacollector/custom_query.go @@ -20,18 +20,18 @@ import ( "database/sql" "errors" "fmt" - "io/ioutil" + "log/slog" "math" + "os" "path/filepath" "strconv" "strings" "sync" "time" - "github.com/go-kit/log" - "github.com/go-kit/log/level" + "github.com/alecthomas/kingpin/v2" + cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" "gopkg.in/yaml.v2" ) @@ -121,7 +121,7 @@ func (scq ScrapeCustomQuery) Version() float64 { } // Scrape collects data. -func (scq ScrapeCustomQuery) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (scq ScrapeCustomQuery) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { cq := CustomQuery{ customMetricMap: make(map[string]MetricMapNamespace), customQueryMap: make(map[string]string), @@ -133,7 +133,7 @@ func (scq ScrapeCustomQuery) Scrape(ctx context.Context, db *sql.DB, ch chan<- p HR: collectCustomQueryHrDirectory, } - fi, err := ioutil.ReadDir(*dirs[scq.Resolution]) + fi, err := os.ReadDir(*dirs[scq.Resolution]) if err != nil { return fmt.Errorf("failed read dir %q for custom query. reason: %s", *dirs[scq.Resolution], err) } @@ -145,7 +145,7 @@ func (scq ScrapeCustomQuery) Scrape(ctx context.Context, db *sql.DB, ch chan<- p if filepath.Ext(v.Name()) == ".yml" || filepath.Ext(v.Name()) == ".yaml" { path := filepath.Join(*dirs[scq.Resolution], v.Name()) - userQueriesData, err := ioutil.ReadFile(path) + userQueriesData, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to open custom queries:%s", err.Error()) } @@ -161,6 +161,7 @@ func (scq ScrapeCustomQuery) Scrape(ctx context.Context, db *sql.DB, ch chan<- p cq.mappingMtx.RLock() defer cq.mappingMtx.RUnlock() + db := instance.GetDB() errMap := queryNamespaceMappings(ctx, ch, db, cq.customMetricMap, cq.customQueryMap, logger) if len(errMap) > 0 { errs := make([]string, 0, len(errMap)) @@ -197,7 +198,7 @@ func (cm *ColumnMapping) UnmarshalYAML(unmarshal func(interface{}) error) error // addQueries metricMap and customQueryMap to contain the new queries. // Added queries do not respect version requirements, because it is assumed that // the user knows what they are doing with their version of mysql. -func addQueries(content []byte, exporterMap map[string]MetricMapNamespace, customQueryMap map[string]string, logger log.Logger) error { +func addQueries(content []byte, exporterMap map[string]MetricMapNamespace, customQueryMap map[string]string, logger *slog.Logger) error { var extra map[string]interface{} err := yaml.Unmarshal(content, &extra) if err != nil { @@ -206,7 +207,7 @@ func addQueries(content []byte, exporterMap map[string]MetricMapNamespace, custo // Stores the loaded map representation. metricMaps := make(map[string]map[string]ColumnMapping) for metric, specs := range extra { - level.Debug(logger).Log("msg", "New user metric namespace from YAML", "metric", metric) + logger.Debug("msg", "New user metric namespace from YAML", metric) specMap, ok := specs.(map[interface{}]interface{}) if !ok { return fmt.Errorf("incorrect yaml format for %+v", specs) @@ -260,7 +261,7 @@ func addQueries(content []byte, exporterMap map[string]MetricMapNamespace, custo } // Turn the MetricMap column mapping into a prometheus descriptor mapping. -func makeDescMap(metricMaps map[string]map[string]ColumnMapping, exporterMap map[string]MetricMapNamespace, logger log.Logger) { +func makeDescMap(metricMaps map[string]map[string]ColumnMapping, exporterMap map[string]MetricMapNamespace, logger *slog.Logger) { metricMap := make(map[string]MetricMapNamespace) for namespace, mappings := range metricMaps { thisMap := make(map[string]MetricMap) @@ -331,7 +332,7 @@ func makeDescMap(metricMaps map[string]map[string]ColumnMapping, exporterMap map case string: durationString = t default: - level.Error(logger).Log("msg", "DURATION conversion metric was not a string") + logger.Error("msg", "DURATION conversion metric was not a string", durationString) return math.NaN(), false } @@ -341,7 +342,7 @@ func makeDescMap(metricMaps map[string]map[string]ColumnMapping, exporterMap map d, err := time.ParseDuration(durationString) if err != nil { - level.Error(logger).Log("msg", "Failed converting result to metric", "columnName", columnName, "in", in, "error", err) + logger.Error("msg", "Failed converting result to metric", "columnName", columnName, "in", in, "error", err) return math.NaN(), false } return float64(d / time.Millisecond), true @@ -377,7 +378,7 @@ func stringToColumnUsage(s string) (ColumnUsage, error) { // Convert "database/sql value" types to float64s for Prometheus consumption. // Null types are mapped to NaN. string and []byte types are mapped as NaN and !ok. -func dbToFloat64(t interface{}, logger log.Logger) (float64, bool) { +func dbToFloat64(t interface{}, logger *slog.Logger) (float64, bool) { switch v := t.(type) { case int64: return float64(v), true @@ -390,14 +391,14 @@ func dbToFloat64(t interface{}, logger log.Logger) (float64, bool) { strV := string(v) result, err := strconv.ParseFloat(strV, 64) if err != nil { - level.Warn(logger).Log("msg", "Could not parse []byte", "error", err) + logger.Warn("Could not parse []byte", "error", err) return math.NaN(), false } return result, true case string: result, err := strconv.ParseFloat(v, 64) if err != nil { - level.Warn(logger).Log("msg", "Could not parse string", "error", err) + logger.Warn("Could not parse string", "error", err) return math.NaN(), false } return result, true @@ -432,7 +433,7 @@ func dbToString(t interface{}) (string, bool) { // the scrape fails, and a slice of errors if they were non-fatal. func queryNamespaceMapping(ctx context.Context, ch chan<- prometheus.Metric, db *sql.DB, namespace string, mapping MetricMapNamespace, - customQueries map[string]string, logger log.Logger) ([]error, error) { + customQueries map[string]string, logger *slog.Logger) ([]error, error) { // Check for a query override for this namespace. query, found := customQueries[namespace] @@ -481,7 +482,7 @@ func queryNamespaceMapping(ctx context.Context, ch chan<- prometheus.Metric, for idx, columnName := range mapping.labels { labels[idx], ok = dbToString(columnData[columnIdx[columnName]]) if !ok { - level.Info(logger).Log("msg", "converted NULL to an empty string") + logger.Error("converted NULL to an empty string") } } @@ -527,7 +528,7 @@ func queryNamespaceMapping(ctx context.Context, ch chan<- prometheus.Metric, // Iterate through all the namespace mappings in the exporter and run their queries. func queryNamespaceMappings(ctx context.Context, ch chan<- prometheus.Metric, db *sql.DB, metricMap map[string]MetricMapNamespace, customQueries map[string]string, - logger log.Logger) map[string]error { + logger *slog.Logger) map[string]error { // Return a map of namespace -> errors. namespaceErrors := make(map[string]error) for namespace, mapping := range metricMap { @@ -539,7 +540,7 @@ func queryNamespaceMappings(ctx context.Context, ch chan<- prometheus.Metric, // Non-serious errors - likely version or parsing problems. if len(nonFatalErrors) > 0 { for _, err := range nonFatalErrors { - level.Info(logger).Log(err.Error()) + logger.Info(err.Error()) } } } diff --git a/percona/perconacollector/custom_query_test.go b/percona/perconacollector/custom_query_test.go index 0d842f54..307669d8 100644 --- a/percona/perconacollector/custom_query_test.go +++ b/percona/perconacollector/custom_query_test.go @@ -16,18 +16,18 @@ package perconacollector import ( "context" "fmt" - "github.com/go-kit/log" - "io/ioutil" "os" "path/filepath" "strings" "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/alecthomas/kingpin/v2" + cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/prometheus/common/promslog" "github.com/smartystreets/goconvey/convey" - "gopkg.in/alecthomas/kingpin.v2" ) const customQueryCounter = ` @@ -108,7 +108,9 @@ func TestScrapeCustomQueriesCounter(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + instance := &cl.Instance{} + instance.SetDB(db) + if err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -171,7 +173,9 @@ func TestScrapeCustomQueriesDuration(t *testing.T) { ch := make(chan prometheus.Metric) go func() { - if err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { + instance := &cl.Instance{} + instance.SetDB(db) + if err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()); err != nil { t.Errorf("error calling function on test: %s", err) } close(ch) @@ -231,7 +235,9 @@ func TestScrapeCustomQueriesDbError(t *testing.T) { expectedErr := "experiment_garden:error running query on database: experiment_garden, ERROR 1049 (42000): Unknown database 'non_existed_experiment'" convey.Convey("Should raise error ", func() { - err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), db, ch, log.NewNopLogger()) + instance := &cl.Instance{} + instance.SetDB(db) + err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()) convey.So(err, convey.ShouldBeError, expectedErr) }) close(ch) @@ -259,7 +265,9 @@ func TestScrapeCustomQueriesIncorrectYaml(t *testing.T) { ch := make(chan prometheus.Metric) convey.Convey("Should raise error ", func() { - err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), db, ch, log.NewNopLogger()) + instance := &cl.Instance{} + instance.SetDB(db) + err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()) convey.So(err, convey.ShouldBeError, "failed to add custom queries:incorrect yaml format for bar") }) close(ch) @@ -275,7 +283,9 @@ func TestScrapeCustomQueriesNoFile(t *testing.T) { t.Fatalf("error opening a stub database connection: %s", err) } ch := make(chan prometheus.Metric) - err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), db, ch, log.NewNopLogger()) + instance := &cl.Instance{} + instance.SetDB(db) + err = (ScrapeCustomQuery{Resolution: HR}).Scrape(context.Background(), instance, ch, promslog.NewNopLogger()) close(ch) convey.So(err, convey.ShouldBeError, "failed read dir \"/wrong/path\" for custom query. reason: open /wrong/path: no such file or directory") }) @@ -288,7 +298,7 @@ func createTmpFile(t *testing.T, resolution, content string) string { if err != nil { t.Fatalf("Cannot create temporary directory: %s", err) } - tmpFile, err := ioutil.TempFile(tempDir, "custom_queries.*.yaml") + tmpFile, err := os.CreateTemp(tempDir, "custom_queries.*.yaml") if err != nil { t.Fatalf("Cannot create temporary file: %s", err) } diff --git a/percona/perconacollector/exporter_test.go b/percona/perconacollector/exporter_test.go deleted file mode 100644 index 655ce1cb..00000000 --- a/percona/perconacollector/exporter_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2018 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 perconacollector - -import ( - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/go-kit/log" - cl "github.com/percona/mysqld_exporter/collector" - "github.com/smartystreets/goconvey/convey" -) - -func TestGetMySQLVersion_Percona(t *testing.T) { - if testing.Short() { - t.Skip("-short is passed, skipping test") - } - - db, mock, err := sqlmock.New() - if err != nil { - t.Fatalf("error opening a stub database connection: %s", err) - } - defer db.Close() - - logger := log.NewNopLogger() - convey.Convey("MySQL version extract", t, func() { - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 999) - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("something")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 999) - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("10.1.17-MariaDB")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 10.1) - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.7.13-6-log")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.7) - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.6.30-76.3-56-log")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.6) - mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.5.51-38.1")) - convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.5) - }) - - // Ensure all SQL queries were executed - if err := mock.ExpectationsWereMet(); err != nil { - t.Errorf("there were unfulfilled expections: %s", err) - } -} diff --git a/percona/perconacollector/global_status.go b/percona/perconacollector/global_status.go index 7765a628..c00c21c7 100644 --- a/percona/perconacollector/global_status.go +++ b/percona/perconacollector/global_status.go @@ -18,11 +18,11 @@ package perconacollector import ( "context" "database/sql" + "log/slog" "regexp" "strconv" "strings" - "github.com/go-kit/log" cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" ) @@ -100,7 +100,8 @@ func (ScrapeGlobalStatus) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeGlobalStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeGlobalStatus) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() globalStatusRows, err := db.QueryContext(ctx, globalStatusQuery) if err != nil { return err @@ -146,10 +147,14 @@ func (ScrapeGlobalStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prom ) case "innodb_buffer_pool_pages": switch match[2] { - case "data", "dirty", "free", "misc", "old", "total": + case "data", "free", "misc", "old", "total": ch <- prometheus.MustNewConstMetric( globalBufferPoolPagesDesc, prometheus.GaugeValue, floatVal, match[2], ) + case "dirty": + ch <- prometheus.MustNewConstMetric( + globalBufferPoolDirtyPagesDesc, prometheus.GaugeValue, floatVal, match[2], + ) default: ch <- prometheus.MustNewConstMetric( globalBufferPoolPageChangesDesc, prometheus.CounterValue, floatVal, match[2], @@ -189,11 +194,11 @@ func (ScrapeGlobalStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prom } evsMap := []evsValue{ - evsValue{name: "min_seconds", value: 0, index: 0, help: "PXC/Galera group communication latency. Min value."}, - evsValue{name: "avg_seconds", value: 0, index: 1, help: "PXC/Galera group communication latency. Avg value."}, - evsValue{name: "max_seconds", value: 0, index: 2, help: "PXC/Galera group communication latency. Max value."}, - evsValue{name: "stdev", value: 0, index: 3, help: "PXC/Galera group communication latency. Standard Deviation."}, - evsValue{name: "sample_size", value: 0, index: 4, help: "PXC/Galera group communication latency. Sample Size."}, + {name: "min_seconds", value: 0, index: 0, help: "PXC/Galera group communication latency. Min value."}, + {name: "avg_seconds", value: 0, index: 1, help: "PXC/Galera group communication latency. Avg value."}, + {name: "max_seconds", value: 0, index: 2, help: "PXC/Galera group communication latency. Max value."}, + {name: "stdev", value: 0, index: 3, help: "PXC/Galera group communication latency. Standard Deviation."}, + {name: "sample_size", value: 0, index: 4, help: "PXC/Galera group communication latency. Sample Size."}, } evsParsingSuccess := true diff --git a/percona/perconacollector/info_schema_innodb_cmp.go b/percona/perconacollector/info_schema_innodb_cmp.go index 2de80f8e..ee30bad6 100644 --- a/percona/perconacollector/info_schema_innodb_cmp.go +++ b/percona/perconacollector/info_schema_innodb_cmp.go @@ -17,12 +17,10 @@ package perconacollector import ( "context" - "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" - "github.com/go-kit/log/level" cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" ) @@ -90,10 +88,11 @@ func (ScrapeInnodbCmp) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInnodbCmp) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInnodbCmp) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() informationSchemaInnodbCmpRows, err := db.QueryContext(ctx, innodbCmpQuery) if err != nil { - level.Debug(logger).Log("msg", "INNODB_CMP stats are not available.", "error", err) + logger.Debug("msg", "INNODB_CMP stats are not available.", err) return err } defer informationSchemaInnodbCmpRows.Close() @@ -104,7 +103,7 @@ func (ScrapeInnodbCmp) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometh // To map metrics to names therefore we always range over columnNames[1:] columnNames, err := informationSchemaInnodbCmpRows.Columns() if err != nil { - level.Debug(logger).Log("msg", "INNODB_CMP stats are not available.", "error", err) + logger.Debug("msg", "INNODB_CMP stats are not available.", err) return err } diff --git a/percona/perconacollector/info_schema_innodb_cmpmem.go b/percona/perconacollector/info_schema_innodb_cmpmem.go index b8324dbf..eb769f17 100644 --- a/percona/perconacollector/info_schema_innodb_cmpmem.go +++ b/percona/perconacollector/info_schema_innodb_cmpmem.go @@ -17,9 +17,8 @@ package perconacollector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" ) @@ -73,7 +72,8 @@ func (ScrapeInnodbCmpMem) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { + db := instance.GetDB() informationSchemaInnodbCmpMemRows, err := db.QueryContext(ctx, innodbCmpMemQuery) if err != nil { return err diff --git a/percona/perconacollector/info_schema_process_list.go b/percona/perconacollector/info_schema_process_list.go index 71cffa8b..09897a7e 100644 --- a/percona/perconacollector/info_schema_process_list.go +++ b/percona/perconacollector/info_schema_process_list.go @@ -17,14 +17,13 @@ package perconacollector import ( "context" - "database/sql" "fmt" + "log/slog" "strings" - "github.com/go-kit/log" + "github.com/alecthomas/kingpin/v2" cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" - "gopkg.in/alecthomas/kingpin.v2" ) const infoSchemaProcesslistQuery = ` @@ -186,11 +185,12 @@ func (ScrapeProcesslist) Version() float64 { } // Scrape collects data from database connection and sends it over channel as prometheus metric. -func (ScrapeProcesslist) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (ScrapeProcesslist) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { processQuery := fmt.Sprintf( infoSchemaProcesslistQuery, *processlistMinTime, ) + db := instance.GetDB() processlistRows, err := db.QueryContext(ctx, processQuery) if err != nil { return err diff --git a/percona/perconacollector/standard.go b/percona/perconacollector/standard.go index 6c91528b..300a7684 100644 --- a/percona/perconacollector/standard.go +++ b/percona/perconacollector/standard.go @@ -15,9 +15,8 @@ package perconacollector import ( "context" - "database/sql" + "log/slog" - "github.com/go-kit/log" cl "github.com/percona/mysqld_exporter/collector" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" @@ -49,7 +48,7 @@ func (standardGo) Version() float64 { } // Scrape collects data. -func (s standardGo) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (s standardGo) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { s.c.Collect(ch) return nil } @@ -80,7 +79,7 @@ func (standardProcess) Version() float64 { } // Scrape collects data. -func (s standardProcess) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { +func (s standardProcess) Scrape(ctx context.Context, instance *cl.Instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { s.c.Collect(ch) return nil } diff --git a/percona/tests/assets/mysqld_exporter_percona.tar.xz b/percona/tests/assets/mysqld_exporter_percona.tar.xz deleted file mode 100644 index 04947db3..00000000 Binary files a/percona/tests/assets/mysqld_exporter_percona.tar.xz and /dev/null differ diff --git a/percona/tests/metrics_test.go b/percona/tests/metrics_test.go index 3774df75..16c25763 100644 --- a/percona/tests/metrics_test.go +++ b/percona/tests/metrics_test.go @@ -7,8 +7,6 @@ import ( "sort" "strings" "testing" - - "github.com/pkg/errors" ) var dumpMetricsFlag = flag.Bool("dumpMetrics", false, "") @@ -523,17 +521,17 @@ func getMetrics(fileName string) (string, error) { func getMetricsFrom(fileName, endpoint string) (string, error) { cmd, port, collectOutput, err := launchExporter(fileName) if err != nil { - return "", errors.Wrap(err, "Failed to launch exporter") + return "", fmt.Errorf("Failed to launch exporter: %w", err) } metrics, err := tryGetMetricsFrom(port, endpoint) if err != nil { - return "", errors.Wrap(err, "Failed to get metrics") + return "", fmt.Errorf("Failed to get metrics: %w", err) } err = stopExporter(cmd, collectOutput) if err != nil { - return "", errors.Wrap(err, "Failed to stop exporter") + return "", fmt.Errorf("Failed to stop exporter: %w", err) } return metrics, nil diff --git a/percona/tests/performance_test.go b/percona/tests/performance_test.go index 8b9340ea..38e3ba5c 100644 --- a/percona/tests/performance_test.go +++ b/percona/tests/performance_test.go @@ -3,14 +3,13 @@ package percona_tests import ( "flag" "fmt" - "io/ioutil" + "os" "strconv" "strings" "testing" "time" "github.com/montanaflynn/stats" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/tklauser/go-sysconf" ) @@ -133,7 +132,7 @@ func doTest(iterations int, fileName string) (cpu, hwm, data int64, _ error) { for i := 0; i < iterations; i++ { _, err = tryGetMetrics(port) if err != nil { - return 0, 0, 0, errors.Wrapf(err, "Failed to perform test iteration %d.%s", i, collectOutput()) + return 0, 0, 0, fmt.Errorf("Failed to perform test iteration %d.%s\n Error: %w", i, collectOutput(), err) } time.Sleep(1 * time.Millisecond) @@ -152,7 +151,7 @@ func doTest(iterations int, fileName string) (cpu, hwm, data int64, _ error) { } func getCPUMem(pid int) (hwm, data int64) { - contents, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) + contents, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) if err != nil { return 0, 0 } @@ -176,7 +175,7 @@ func getCPUMem(pid int) (hwm, data int64) { } func getCPUTime(pid int) (total int64) { - contents, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) + contents, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) if err != nil { return } diff --git a/percona/tests/readme.md b/percona/tests/readme.md index 3fab7af1..50a2a51b 100644 --- a/percona/tests/readme.md +++ b/percona/tests/readme.md @@ -4,7 +4,6 @@ basic usage: 1. unpack original exporter - make prepare-base-exporter 2.a. download updated exporter from specific feature build @@ -15,18 +14,14 @@ basic usage: make prepare-exporter-from-repo -3. start test postgres_server - +3. start test mysql_server - make start-postgres-db + make start-mysql-db 4. run basic performance comparison test - make test-performance 5. run metrics list compatibility test - make test-metrics - diff --git a/percona/tests/utils_test.go b/percona/tests/utils_test.go index 0f111831..73e7c165 100644 --- a/percona/tests/utils_test.go +++ b/percona/tests/utils_test.go @@ -13,7 +13,6 @@ import ( "strings" "time" - "github.com/pkg/errors" "golang.org/x/sys/unix" ) @@ -39,7 +38,7 @@ func getBool(val *bool) bool { func launchExporter(fileName string) (cmd *exec.Cmd, port int, collectOutput func() string, _ error) { lines, err := os.ReadFile("assets/test.exporter-flags.txt") if err != nil { - return nil, 0, nil, errors.Wrapf(err, "Unable to read exporter args file") + return nil, 0, nil, fmt.Errorf("Unable to read exporter args file: %w", err) } port = -1 @@ -51,7 +50,7 @@ func launchExporter(fileName string) (cmd *exec.Cmd, port int, collectOutput fun } if port == -1 { - return nil, 0, nil, errors.Wrapf(err, "Failed to find free port in range [%d..%d]", portRangeStart, portRangeEnd) + return nil, 0, nil, fmt.Errorf("Failed to find free port in range [%d..%d]", portRangeStart, portRangeEnd) } linesStr := string(lines) @@ -94,12 +93,12 @@ func launchExporter(fileName string) (cmd *exec.Cmd, port int, collectOutput fun err = cmd.Start() if err != nil { - return nil, 0, nil, errors.Wrapf(err, "Failed to start exporter.%s", collectOutput()) + return nil, 0, nil, fmt.Errorf("Failed to start exporter. %s\n Error: %w", collectOutput(), err) } err = waitForExporter(port) if err != nil { - return nil, 0, nil, errors.Wrapf(err, "Failed to wait for exporter.%s", collectOutput()) + return nil, 0, nil, fmt.Errorf("Failed to wait for exporter. %s\n Error: %w", collectOutput(), err) } return cmd, port, collectOutput, nil @@ -108,12 +107,12 @@ func launchExporter(fileName string) (cmd *exec.Cmd, port int, collectOutput fun func stopExporter(cmd *exec.Cmd, collectOutput func() string) error { err := cmd.Process.Signal(unix.SIGINT) if err != nil { - return errors.Wrapf(err, "Failed to send SIGINT to exporter process.%s\n", collectOutput()) + return fmt.Errorf("Failed to send SIGINT to exporter process. %s\n Error: %w", collectOutput(), err) } err = cmd.Wait() if err != nil && err.Error() != "signal: interrupt" { - return errors.Wrapf(err, "Failed to wait for exporter process termination.%s\n", collectOutput()) + return fmt.Errorf("Failed to wait for exporter process termination. %s\n Error: %w", collectOutput(), err) } return nil diff --git a/probe.go b/probe.go new file mode 100644 index 00000000..6cad6ffe --- /dev/null +++ b/probe.go @@ -0,0 +1,80 @@ +// 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 ( + "context" + "fmt" + "log/slog" + "net/http" + "time" + + "github.com/percona/mysqld_exporter/collector" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func handleProbe(scrapers []collector.Scraper, logger *slog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + params := r.URL.Query() + target := params.Get("target") + if target == "" { + http.Error(w, "target is required", http.StatusBadRequest) + return + } + collectParams := r.URL.Query()["collect[]"] + + authModule := params.Get("auth_module") + if authModule == "" { + authModule = "client" + } + + cfg := c.GetConfig() + cfgsection, ok := cfg.Sections[authModule] + if !ok { + logger.Error(fmt.Sprintf("Could not find section [%s] from config file", authModule)) + http.Error(w, fmt.Sprintf("Could not find config section [%s]", authModule), http.StatusBadRequest) + return + } + dsn, err := cfgsection.FormDSN(target) + if err != nil { + logger.Error(fmt.Sprintf("Failed to form dsn from section [%s]", authModule), "err", err) + http.Error(w, fmt.Sprintf("Error forming dsn from config section [%s]", authModule), http.StatusBadRequest) + return + } + + // If a timeout is configured via the Prometheus header, add it to the context. + timeoutSeconds, err := getScrapeTimeoutSeconds(r, *timeoutOffset) + if err != nil { + logger.Error("Error getting timeout from Prometheus header", "err", err) + } + if timeoutSeconds > 0 { + // Create new timeout context with request context as parent. + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds*float64(time.Second))) + defer cancel() + // Overwrite request with timeout context. + r = r.WithContext(ctx) + } + + filteredScrapers := filterScrapers(scrapers, collectParams) + + registry := prometheus.NewRegistry() + registry.MustRegister(collector.New(ctx, dsn, filteredScrapers, logger)) + + h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) + h.ServeHTTP(w, r) + } +} diff --git a/test_exporter.cnf b/test_exporter.cnf new file mode 100644 index 00000000..f71c7e5d --- /dev/null +++ b/test_exporter.cnf @@ -0,0 +1,9 @@ +[client] +host=localhost +port=3306 +socket=/var/run/mysqld/mysqld.sock +user=foo +password=bar +[client.server1] +user = bar +password = bar123 diff --git a/test_image.sh b/test_image.sh index c3cfe278..99f2efe9 100755 --- a/test_image.sh +++ b/test_image.sh @@ -15,12 +15,12 @@ wait_start() { sleep 1 fi done - + exit 1 } docker_start() { - container_id=$(docker run -d --network mysql-test -e DATA_SOURCE_NAME="root:secret@(mysql-test:3306)/" -p "${port}":"${port}" "${docker_image}") + container_id=$(docker run -d --network mysql-test -p "${port}":"${port}" "${docker_image}" --config.my-cnf=test_exporter.cnf) } docker_cleanup() { diff --git a/tools/go.mod b/tools/go.mod index 1510e040..2bca653f 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -1,6 +1,6 @@ module github.com/percona/mysqld_exporter/tools -go 1.22.6 +go 1.23 require ( github.com/golangci/golangci-lint v1.63.4 diff --git a/tools/go.sum b/tools/go.sum index 67ccae17..5cea6814 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -283,6 +283,7 @@ github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3 github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= +github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -404,6 +405,8 @@ github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5co github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= @@ -521,7 +524,9 @@ github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/tdakkota/asciicheck v0.3.0 h1:LqDGgZdholxZMaJgpM6b0U9CFIjDCbFdUF00bDnBKOQ= github.com/tdakkota/asciicheck v0.3.0/go.mod h1:KoJKXuX/Z/lt6XzLo8WMBfQGzak0SrAKZlvRr4tg8Ac= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.4.20 h1:z/p8Ek55UdNvzt4TFn2zx2KscpW4rWqcnUrdmvWJj7E= github.com/tetafro/godot v1.4.20/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= diff --git a/tools/tools.go b/tools/tools.go index 772f9629..51a7af95 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -1,7 +1,19 @@ -// mysql_exporter +// 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 . //go:build tools -// +build tools package tools @@ -9,4 +21,13 @@ import ( _ "github.com/golangci/golangci-lint/cmd/golangci-lint" _ "github.com/prometheus/promu" _ "github.com/reviewdog/reviewdog/cmd/reviewdog" + _ "golang.org/x/tools/cmd/goimports" + _ "mvdan.cc/gofumpt" ) + +// tools +//go:generate go build -o ../bin/gofumpt mvdan.cc/gofumpt +//go:generate go build -o ../bin/goimports golang.org/x/tools/cmd/goimports +//go:generate go build -o ../bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint +//go:generate go build -o ../bin/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog +//go:generate go build -o ../bin/promu github.com/prometheus/promu