├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── go.yml ├── .gitignore ├── .golangci-required.yml ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── NOTICE ├── README.md ├── SECURITY.md ├── VERSION ├── collector ├── binlog.go ├── binlog_test.go ├── collector.go ├── collector_percona_extensions.go ├── collector_test.go ├── engine_innodb.go ├── engine_innodb_test.go ├── engine_tokudb.go ├── engine_tokudb_test.go ├── exporter.go ├── exporter_test.go ├── global_status.go ├── global_status_test.go ├── global_variables.go ├── global_variables_test.go ├── heartbeat.go ├── heartbeat_test.go ├── info_schema.go ├── info_schema_auto_increment.go ├── info_schema_clientstats.go ├── info_schema_clientstats_test.go ├── info_schema_innodb_cmp.go ├── info_schema_innodb_cmp_test.go ├── info_schema_innodb_cmpmem.go ├── info_schema_innodb_cmpmem_test.go ├── info_schema_innodb_metrics.go ├── info_schema_innodb_metrics_test.go ├── info_schema_innodb_sys_tablespaces.go ├── info_schema_innodb_sys_tablespaces_test.go ├── info_schema_processlist.go ├── info_schema_processlist_test.go ├── info_schema_query_response_time.go ├── info_schema_query_response_time_test.go ├── info_schema_replica_host.go ├── info_schema_replica_host_test.go ├── info_schema_schemastats.go ├── info_schema_schemastats_test.go ├── info_schema_tables.go ├── info_schema_tables_test.go ├── info_schema_tablestats.go ├── info_schema_tablestats_test.go ├── info_schema_userstats.go ├── info_schema_userstats_test.go ├── mysql.go ├── mysql_user.go ├── perf_schema.go ├── perf_schema_events_statements.go ├── perf_schema_events_statements_sum.go ├── perf_schema_events_waits.go ├── perf_schema_file_events.go ├── perf_schema_file_instances.go ├── perf_schema_file_instances_test.go ├── perf_schema_index_io_waits.go ├── perf_schema_index_io_waits_test.go ├── perf_schema_memory_events.go ├── perf_schema_memory_events_test.go ├── perf_schema_replication_applier_status_by_worker.go ├── perf_schema_replication_applier_status_by_worker_test.go ├── perf_schema_replication_group_member_stats.go ├── perf_schema_replication_group_member_stats_test.go ├── perf_schema_replication_group_members.go ├── perf_schema_replication_group_members_test.go ├── perf_schema_table_io_waits.go ├── perf_schema_table_lock_waits.go ├── plugins.go ├── plugins_test.go ├── scraper.go ├── slave_hosts.go ├── slave_hosts_test.go ├── slave_status.go └── slave_status_test.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── mysqld-mixin ├── .gitignore ├── Makefile ├── README.md ├── alerts │ ├── galera.yaml │ └── general.yaml ├── dashboards │ └── mysql-overview.json ├── mixin.libsonnet └── rules │ └── rules.yaml ├── mysqld_exporter.go ├── mysqld_exporter_test.go ├── percona ├── perconacollector │ ├── custom_query.go │ ├── custom_query_test.go │ ├── exporter_test.go │ ├── global_status.go │ ├── info_schema_innodb_cmp.go │ ├── info_schema_innodb_cmpmem.go │ ├── info_schema_process_list.go │ └── standard.go └── tests │ ├── Makefile │ ├── assets │ ├── mysql-compose.yml │ ├── mysqld_exporter_percona.tar.xz │ └── test.exporter-flags.txt │ ├── custom-queries │ ├── high-resolution │ │ ├── queries-mysqld-group-replication.yml │ │ └── queries-mysqld.yml │ ├── low-resolution │ │ └── queries-mysqld.yml │ └── medium-resolution │ │ └── queries-mysqld.yml │ ├── env_prepare_test.go │ ├── metrics_test.go │ ├── performance_test.go │ ├── readme.md │ └── utils_test.go ├── queries-mysqld-group-replication.yml ├── queries-mysqld.yml ├── test_image.sh └── tools ├── go.mod ├── go.sum └── tools.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: prometheus/prometheus@0.16.0 5 | executors: 6 | # Whenever the Go version is updated here, .promu.yml 7 | # should also be updated. 8 | golang: 9 | docker: 10 | - image: cimg/go:1.18 11 | jobs: 12 | test: 13 | executor: golang 14 | steps: 15 | - prometheus/setup_environment 16 | - run: make check_license style staticcheck unused build test-short 17 | - prometheus/store_artifact: 18 | file: mysqld_exporter 19 | integration: 20 | docker: 21 | - image: cimg/go:1.18 22 | - image: << parameters.mysql_image >> 23 | environment: 24 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 25 | MYSQL_ROOT_HOST: '%' 26 | parameters: 27 | mysql_image: 28 | type: string 29 | steps: 30 | - checkout 31 | - setup_remote_docker 32 | - run: docker version 33 | - run: docker-compose --version 34 | - run: make build 35 | - run: make test 36 | codespell: 37 | docker: 38 | - image: cimg/python:3.10 39 | steps: 40 | - checkout 41 | - run: pip install codespell 42 | - run: codespell --skip=".git,./vendor,ttar,Makefile.common" -L uint,ist,keypair 43 | mixin: 44 | executor: golang 45 | steps: 46 | - checkout 47 | - run: go install github.com/monitoring-mixins/mixtool/cmd/mixtool@latest 48 | - run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest 49 | - run: make -C mysqld-mixin lint build 50 | workflows: 51 | version: 2 52 | mysqld_exporter: 53 | jobs: 54 | - test: 55 | filters: 56 | tags: 57 | only: /.*/ 58 | - integration: 59 | matrix: 60 | parameters: 61 | mysql_image: 62 | - percona:5.6 63 | - mysql/mysql-server:5.7.33 64 | - mysql/mysql-server:8.0 65 | - mariadb:10.3 66 | - mariadb:10.4 67 | - mariadb:10.5 68 | - mariadb:10.6 69 | - mariadb:10.7 70 | - prometheus/build: 71 | name: build 72 | parallelism: 3 73 | promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386" 74 | filters: 75 | tags: 76 | ignore: /^v2(\.[0-9]+){2}(-.+|[^-.]*)$/ 77 | branches: 78 | ignore: /^(main|release-.*|.*build-all.*)$/ 79 | - prometheus/build: 80 | name: build_all 81 | parallelism: 12 82 | filters: 83 | branches: 84 | only: /^(main|release-.*|.*build-all.*)$/ 85 | tags: 86 | only: /^v2(\.[0-9]+){2}(-.+|[^-.]*)$/ 87 | 88 | - codespell: 89 | filters: 90 | tags: 91 | only: /.*/ 92 | - mixin: 93 | filters: 94 | tags: 95 | only: /.*/ 96 | - prometheus/publish_main: 97 | context: org-context 98 | requires: 99 | - test 100 | - build_all 101 | filters: 102 | branches: 103 | only: main 104 | - prometheus/publish_release: 105 | context: org-context 106 | requires: 107 | - test 108 | - build_all 109 | filters: 110 | tags: 111 | only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ 112 | branches: 113 | ignore: /.*/ 114 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @percona/pmm-review-exporters 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | ### Host operating system: output of `uname -a` 13 | 14 | ### mysqld_exporter version: output of `mysqld_exporter --version` 15 | 16 | 17 | ### MySQL server version 18 | 19 | ### mysqld_exporter command line flags 20 | 21 | 22 | ### What did you do that produced an error? 23 | 24 | ### What did you expect to see? 25 | 26 | ### What did you see instead? 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "gomod" 9 | directory: "/tools" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | 21 | # v3 22 | - package-ecosystem: "gomod" 23 | directory: "/" 24 | target-branch: "v3" 25 | schedule: 26 | interval: "weekly" 27 | - package-ecosystem: "gomod" 28 | directory: "/tools" 29 | target-branch: "v3" 30 | schedule: 31 | interval: "weekly" 32 | - package-ecosystem: "docker" 33 | directory: "/" 34 | target-branch: "v3" 35 | schedule: 36 | interval: "weekly" 37 | - package-ecosystem: "github-actions" 38 | directory: "/" 39 | target-branch: "v3" 40 | schedule: 41 | interval: "weekly" 42 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v[0-9]+.[0-9]+.[0-9]+* 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | strategy: 15 | matrix: 16 | mysql-image: 17 | - mysql/mysql-server:5.5 18 | - mysql/mysql-server:5.6 19 | - mysql/mysql-server:5.7 20 | - mysql/mysql-server:8.0 21 | - mariadb:5.5 22 | - mariadb:10.0 23 | - mariadb:10.1 24 | - mariadb:10.2 25 | - mariadb:10.3 26 | - percona/percona-server:5.6 27 | - percona/percona-server:5.7 28 | - percona/percona-server:8.0 29 | - percona:5.5 30 | - percona:5.6 31 | - percona:5.7 32 | - percona:8.0 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v4 38 | 39 | - name: Set up Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version-file: ${{ github.workspace }}/go.mod 43 | 44 | - name: Run checks 45 | run: | 46 | go build -modfile=tools/go.mod -o bin/golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint 47 | go build -modfile=tools/go.mod -o bin/reviewdog github.com/reviewdog/reviewdog/cmd/reviewdog 48 | 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 49 | 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 50 | 51 | - name: Run Tests 52 | run: | 53 | docker compose up -d 54 | make all 55 | make test 56 | env: 57 | MYSQL_IMAGE: ${{ matrix.mysql-image }} 58 | 59 | - name: Run debug commands on failure 60 | if: ${{ failure() }} 61 | run: | 62 | env | sort 63 | go env | sort 64 | git status 65 | docker --version 66 | docker compose --version 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.build 2 | /mysqld_exporter 3 | /.release 4 | /.tarballs 5 | 6 | *.tar.gz 7 | *.test 8 | *-stamp 9 | .idea 10 | *.iml 11 | /vendor 12 | /percona/tests/assets/mysqld_exporter 13 | /percona/tests/assets/mysqld_exporter_percona 14 | /percona/tests/assets/metrics.names.new.txt 15 | /percona/tests/assets/metrics.names.old.txt 16 | /percona/tests/assets/metrics.new.txt 17 | /percona/tests/assets/metrics.old.txt 18 | -------------------------------------------------------------------------------- /.golangci-required.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # The most valuable linters; they are required to pass for PR to be merged. 3 | linters: 4 | disable-all: true 5 | enable: 6 | - depguard 7 | - goimports 8 | - ineffassign 9 | - govet 10 | - staticcheck 11 | 12 | issues: 13 | exclude-use-default: false 14 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Run only staticcheck for now. Additional linters will be enabled one-by-one. 3 | linters: 4 | enable: 5 | - staticcheck 6 | disable-all: true 7 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, .circle/config.yml should also 3 | # be updated. 4 | version: 1.21 5 | repository: 6 | path: github.com/percona/mysqld_exporter 7 | build: 8 | flags: -a -tags netgo 9 | ldflags: | 10 | -X github.com/prometheus/common/version.Version={{.Version}} 11 | -X github.com/prometheus/common/version.Revision={{.Revision}} 12 | -X github.com/prometheus/common/version.Branch={{.Branch}} 13 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 14 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 15 | tarball: 16 | files: 17 | - LICENSE 18 | - NOTICE 19 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | braces: 6 | max-spaces-inside: 1 7 | level: error 8 | brackets: 9 | max-spaces-inside: 1 10 | level: error 11 | commas: disable 12 | comments: disable 13 | comments-indentation: disable 14 | document-start: disable 15 | indentation: 16 | spaces: consistent 17 | indent-sequences: consistent 18 | key-duplicates: 19 | ignore: | 20 | config/testdata/section_key_dup.bad.yml 21 | line-length: disable 22 | truthy: 23 | ignore: | 24 | .github/workflows/codeql-analysis.yml 25 | .github/workflows/funcbench.yml 26 | .github/workflows/fuzzing.yml 27 | .github/workflows/prombench.yml 28 | .github/workflows/golangci-lint.yml 29 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Prometheus uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | * If you plan to do something more involved, first discuss your ideas 10 | on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | * Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | 20 | 21 | ## Local setup 22 | 23 | The easiest way to make a local development setup is to use Docker Compose. 24 | 25 | ``` 26 | docker-compose up 27 | make 28 | make test 29 | ``` 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/mysqld_exporter /bin/mysqld_exporter 9 | 10 | EXPOSE 9104 11 | USER nobody 12 | ENTRYPOINT [ "/bin/mysqld_exporter" ] 13 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Ben Kochie 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | GO := go 15 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell $(GO) env GOPATH))) 16 | PROMU := bin/promu 17 | pkgs = $(shell $(GO) list ./...) 18 | 19 | PREFIX ?= $(shell pwd) 20 | BIN_DIR ?= $(shell pwd) 21 | DOCKER_IMAGE_NAME ?= mysqld-exporter 22 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD)) 23 | TMPDIR ?= $(shell dirname $(shell mktemp)/) 24 | 25 | default: help 26 | 27 | all: format build test-short 28 | 29 | env-up: ## Start MySQL and copy ssl certificates to /tmp 30 | @docker-compose up -d 31 | @sleep 5 32 | @docker container cp mysqld_exporter_db:/var/lib/mysql/client-cert.pem $(TMPDIR) 33 | @docker container cp mysqld_exporter_db:/var/lib/mysql/client-key.pem $(TMPDIR) 34 | @docker container cp mysqld_exporter_db:/var/lib/mysql/ca.pem $(TMPDIR) 35 | 36 | env-down: ## Stop MySQL and clean up certs 37 | @docker-compose down 38 | @rm ${TMPDIR}/client-cert.pem ${TMPDIR}/client-key.pem ${TMPDIR}/ca.pem 39 | 40 | style: ## Check the code style 41 | @echo ">> checking code style" 42 | @! gofmt -d $(shell find . -name '*.go' -print) | grep '^' 43 | 44 | test-short: ## Run short tests 45 | @echo ">> running short tests" 46 | @$(GO) test -short -race $(pkgs) 47 | 48 | test: ## Run all tests 49 | @echo ">> running tests" 50 | @$(GO) test -race $(pkgs) 51 | 52 | format: ## Format the code 53 | @echo ">> formatting code" 54 | @$(GO) fmt $(pkgs) 55 | 56 | FILES = $(shell find . -type f -name '*.go') 57 | 58 | fumpt: ## Format source code using fumpt and fumports. 59 | @gofumpt -w -s $(FILES) 60 | @gofumports -local github.com/percona/mysqld_exporter -l -w $(FILES) 61 | 62 | vet: ## Run vet 63 | @echo ">> vetting code" 64 | @$(GO) vet $(pkgs) 65 | 66 | build: promu ## Build binaries 67 | @echo ">> building binaries" 68 | @$(PROMU) build --prefix $(PREFIX) 69 | 70 | tarball: promu ## Build release tarball 71 | @echo ">> building release tarball" 72 | @$(PROMU) tarball --prefix $(PREFIX) $(BIN_DIR) 73 | 74 | docker: ## Build docker image 75 | @echo ">> building docker image" 76 | @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" . 77 | 78 | promu: ## Install promu 79 | @GOOS=$(shell uname -s | tr A-Z a-z) \ 80 | GO111MODULE=on \ 81 | GOARCH=$(subst x86_64,amd64,$(patsubst i%86,386,$(shell uname -m))) \ 82 | $(GO) build -modfile=tools/go.mod -o bin/promu github.com/prometheus/promu 83 | 84 | help: ## Display this help message. 85 | @echo "$(TMPDIR)" 86 | @echo "Please use \`make \` where is one of:" 87 | @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | \ 88 | awk -F ':.*?## ' 'NF==2 {printf " %-26s%s\n", $$1, $$2}' 89 | 90 | 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 91 | 92 | export PMM_RELEASE_PATH?=. 93 | 94 | release: 95 | go build -ldflags="$(GO_BUILD_LDFLAGS)" -o $(PMM_RELEASE_PATH)/mysqld_exporter 96 | 97 | .PHONY: all style format build test vet tarball docker promu env-up env-down help default 98 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Exporter for MySQL daemon. 2 | Copyright 2015 The Prometheus Authors 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.14.0 2 | -------------------------------------------------------------------------------- /collector/binlog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW BINARY LOGS` 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/go-kit/log" 26 | "github.com/prometheus/client_golang/prometheus" 27 | ) 28 | 29 | const ( 30 | // Subsystem. 31 | binlog = "binlog" 32 | // Queries. 33 | logbinQuery = `SELECT @@log_bin` 34 | binlogQuery = `SHOW BINARY LOGS` 35 | ) 36 | 37 | // Metric descriptors. 38 | var ( 39 | binlogSizeDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, binlog, "size_bytes"), 41 | "Combined size of all registered binlog files.", 42 | []string{}, nil, 43 | ) 44 | binlogFilesDesc = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, binlog, "files"), 46 | "Number of registered binlog files.", 47 | []string{}, nil, 48 | ) 49 | binlogFileNumberDesc = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, binlog, "file_number"), 51 | "The last binlog file number.", 52 | []string{}, nil, 53 | ) 54 | ) 55 | 56 | // ScrapeBinlogSize colects from `SHOW BINARY LOGS`. 57 | type ScrapeBinlogSize struct{} 58 | 59 | // Name of the Scraper. Should be unique. 60 | func (ScrapeBinlogSize) Name() string { 61 | return "binlog_size" 62 | } 63 | 64 | // Help describes the role of the Scraper. 65 | func (ScrapeBinlogSize) Help() string { 66 | return "Collect the current size of all registered binlog files" 67 | } 68 | 69 | // Version of MySQL from which scraper is available. 70 | func (ScrapeBinlogSize) Version() float64 { 71 | return 5.1 72 | } 73 | 74 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 75 | func (ScrapeBinlogSize) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 76 | var logBin uint8 77 | err := db.QueryRowContext(ctx, logbinQuery).Scan(&logBin) 78 | if err != nil { 79 | return err 80 | } 81 | // If log_bin is OFF, do not run SHOW BINARY LOGS which explicitly produces MySQL error 82 | if logBin == 0 { 83 | return nil 84 | } 85 | 86 | masterLogRows, err := db.QueryContext(ctx, binlogQuery) 87 | if err != nil { 88 | return err 89 | } 90 | defer masterLogRows.Close() 91 | 92 | var ( 93 | size uint64 94 | count uint64 95 | filename string 96 | filesize uint64 97 | encrypted string 98 | ) 99 | size = 0 100 | count = 0 101 | 102 | columns, err := masterLogRows.Columns() 103 | if err != nil { 104 | return err 105 | } 106 | columnCount := len(columns) 107 | 108 | for masterLogRows.Next() { 109 | switch columnCount { 110 | case 2: 111 | if err := masterLogRows.Scan(&filename, &filesize); err != nil { 112 | return nil 113 | } 114 | case 3: 115 | if err := masterLogRows.Scan(&filename, &filesize, &encrypted); err != nil { 116 | return nil 117 | } 118 | default: 119 | return fmt.Errorf("invalid number of columns: %q", columnCount) 120 | } 121 | 122 | size += filesize 123 | count++ 124 | } 125 | 126 | ch <- prometheus.MustNewConstMetric( 127 | binlogSizeDesc, prometheus.GaugeValue, float64(size), 128 | ) 129 | ch <- prometheus.MustNewConstMetric( 130 | binlogFilesDesc, prometheus.GaugeValue, float64(count), 131 | ) 132 | // The last row contains the last binlog file number. 133 | value, _ := strconv.ParseFloat(strings.Split(filename, ".")[1], 64) 134 | ch <- prometheus.MustNewConstMetric( 135 | binlogFileNumberDesc, prometheus.GaugeValue, value, 136 | ) 137 | 138 | return nil 139 | } 140 | 141 | // check interface 142 | var _ Scraper = ScrapeBinlogSize{} 143 | -------------------------------------------------------------------------------- /collector/binlog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeBinlogSize(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | mock.ExpectQuery(logbinQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) 35 | 36 | columns := []string{"Log_name", "File_size"} 37 | rows := sqlmock.NewRows(columns). 38 | AddRow("centos6-bin.000001", "1813"). 39 | AddRow("centos6-bin.000002", "120"). 40 | AddRow("centos6-bin.000444", "573009") 41 | mock.ExpectQuery(sanitizeQuery(binlogQuery)).WillReturnRows(rows) 42 | 43 | ch := make(chan prometheus.Metric) 44 | go func() { 45 | if err = (ScrapeBinlogSize{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 46 | t.Errorf("error calling function on test: %s", err) 47 | } 48 | close(ch) 49 | }() 50 | 51 | counterExpected := []MetricResult{ 52 | {labels: labelMap{}, value: 574942, metricType: dto.MetricType_GAUGE}, 53 | {labels: labelMap{}, value: 3, metricType: dto.MetricType_GAUGE}, 54 | {labels: labelMap{}, value: 444, metricType: dto.MetricType_GAUGE}, 55 | } 56 | convey.Convey("Metrics comparison", t, func() { 57 | for _, expect := range counterExpected { 58 | got := readMetric(<-ch) 59 | convey.So(got, convey.ShouldResemble, expect) 60 | } 61 | }) 62 | 63 | // Ensure all SQL queries were executed 64 | if err := mock.ExpectationsWereMet(); err != nil { 65 | t.Errorf("there were unfulfilled exceptions: %s", err) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /collector/collector.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "bytes" 18 | "database/sql" 19 | "regexp" 20 | "strconv" 21 | "strings" 22 | "time" 23 | 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const ( 28 | // Exporter namespace. 29 | namespace = "mysql" 30 | // Math constant for picoseconds to seconds. 31 | picoSeconds = 1e12 32 | // Query to check whether user/table/client stats are enabled. 33 | userstatCheckQuery = `SHOW GLOBAL VARIABLES WHERE Variable_Name='userstat' 34 | OR Variable_Name='userstat_running'` 35 | ) 36 | 37 | var logRE = regexp.MustCompile(`.+\.(\d+)$`) 38 | 39 | func newDesc(subsystem, name, help string) *prometheus.Desc { 40 | return prometheus.NewDesc( 41 | prometheus.BuildFQName(namespace, subsystem, name), 42 | help, nil, nil, 43 | ) 44 | } 45 | 46 | func parseStatus(data sql.RawBytes) (float64, bool) { 47 | dataString := strings.ToLower(string(data)) 48 | switch dataString { 49 | case "yes", "on": 50 | return 1, true 51 | case "no", "off", "disabled": 52 | return 0, true 53 | // SHOW SLAVE STATUS Slave_IO_Running can return "Connecting" which is a non-running state. 54 | case "connecting": 55 | return 0, true 56 | // SHOW GLOBAL STATUS like 'wsrep_cluster_status' can return "Primary" or "non-Primary"/"Disconnected" 57 | case "primary": 58 | return 1, true 59 | case "non-primary", "disconnected": 60 | return 0, true 61 | } 62 | if ts, err := time.Parse("Jan 02 15:04:05 2006 MST", string(data)); err == nil { 63 | return float64(ts.Unix()), true 64 | } 65 | if ts, err := time.Parse("2006-01-02 15:04:05", string(data)); err == nil { 66 | return float64(ts.Unix()), true 67 | } 68 | if logNum := logRE.Find(data); logNum != nil { 69 | value, err := strconv.ParseFloat(string(logNum), 64) 70 | return value, err == nil 71 | } 72 | value, err := strconv.ParseFloat(string(data), 64) 73 | return value, err == nil 74 | } 75 | 76 | func parsePrivilege(data sql.RawBytes) (float64, bool) { 77 | if bytes.Equal(data, []byte("Y")) { 78 | return 1, true 79 | } 80 | if bytes.Equal(data, []byte("N")) { 81 | return 0, true 82 | } 83 | return -1, false 84 | } 85 | -------------------------------------------------------------------------------- /collector/collector_percona_extensions.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "database/sql" 18 | "github.com/prometheus/client_golang/prometheus" 19 | ) 20 | 21 | /* percona private accessors */ 22 | 23 | const InformationSchema = informationSchema 24 | const Namespace = namespace 25 | 26 | func NewDesc(subsystem, name, help string) *prometheus.Desc { 27 | return newDesc(subsystem, name, help) 28 | } 29 | 30 | func ParseStatus(data sql.RawBytes) (float64, bool) { 31 | return parseStatus(data) 32 | } 33 | 34 | func ValidPrometheusName(s string) string { 35 | return validPrometheusName(s) 36 | } 37 | -------------------------------------------------------------------------------- /collector/collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "strings" 18 | 19 | "github.com/prometheus/client_golang/prometheus" 20 | dto "github.com/prometheus/client_model/go" 21 | ) 22 | 23 | type labelMap map[string]string 24 | 25 | type MetricResult struct { 26 | labels labelMap 27 | value float64 28 | metricType dto.MetricType 29 | } 30 | 31 | func readMetric(m prometheus.Metric) MetricResult { 32 | pb := &dto.Metric{} 33 | m.Write(pb) 34 | labels := make(labelMap, len(pb.Label)) 35 | for _, v := range pb.Label { 36 | labels[v.GetName()] = v.GetValue() 37 | } 38 | if pb.Gauge != nil { 39 | return MetricResult{labels: labels, value: pb.GetGauge().GetValue(), metricType: dto.MetricType_GAUGE} 40 | } 41 | if pb.Counter != nil { 42 | return MetricResult{labels: labels, value: pb.GetCounter().GetValue(), metricType: dto.MetricType_COUNTER} 43 | } 44 | if pb.Untyped != nil { 45 | return MetricResult{labels: labels, value: pb.GetUntyped().GetValue(), metricType: dto.MetricType_UNTYPED} 46 | } 47 | panic("Unsupported metric type") 48 | } 49 | 50 | func sanitizeQuery(q string) string { 51 | q = strings.Join(strings.Fields(q), " ") 52 | q = strings.Replace(q, "(", "\\(", -1) 53 | q = strings.Replace(q, ")", "\\)", -1) 54 | q = strings.Replace(q, "*", "\\*", -1) 55 | return q 56 | } 57 | -------------------------------------------------------------------------------- /collector/engine_innodb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW ENGINE INNODB STATUS`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/go-kit/log" 26 | "github.com/prometheus/client_golang/prometheus" 27 | ) 28 | 29 | const ( 30 | // Subsystem. 31 | innodb = "engine_innodb" 32 | // Query. 33 | engineInnodbStatusQuery = `SHOW ENGINE INNODB STATUS` 34 | ) 35 | 36 | // ScrapeEngineInnodbStatus scrapes from `SHOW ENGINE INNODB STATUS`. 37 | type ScrapeEngineInnodbStatus struct{} 38 | 39 | // Name of the Scraper. Should be unique. 40 | func (ScrapeEngineInnodbStatus) Name() string { 41 | return "engine_innodb_status" 42 | } 43 | 44 | // Help describes the role of the Scraper. 45 | func (ScrapeEngineInnodbStatus) Help() string { 46 | return "Collect from SHOW ENGINE INNODB STATUS" 47 | } 48 | 49 | // Version of MySQL from which scraper is available. 50 | func (ScrapeEngineInnodbStatus) Version() float64 { 51 | return 5.1 52 | } 53 | 54 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 55 | func (ScrapeEngineInnodbStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 56 | rows, err := db.QueryContext(ctx, engineInnodbStatusQuery) 57 | if err != nil { 58 | return err 59 | } 60 | defer rows.Close() 61 | 62 | var typeCol, nameCol, statusCol string 63 | // First row should contain the necessary info. If many rows returned then it's unknown case. 64 | if rows.Next() { 65 | if err := rows.Scan(&typeCol, &nameCol, &statusCol); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | // 0 queries inside InnoDB, 0 queries in queue 71 | // 0 read views open inside InnoDB 72 | rQueries, _ := regexp.Compile(`(\d+) queries inside InnoDB, (\d+) queries in queue`) 73 | rViews, _ := regexp.Compile(`(\d+) read views open inside InnoDB`) 74 | 75 | for _, line := range strings.Split(statusCol, "\n") { 76 | if data := rQueries.FindStringSubmatch(line); data != nil { 77 | value, _ := strconv.ParseFloat(data[1], 64) 78 | ch <- prometheus.MustNewConstMetric( 79 | newDesc(innodb, "queries_inside_innodb", "Queries inside InnoDB."), 80 | prometheus.GaugeValue, 81 | value, 82 | ) 83 | value, _ = strconv.ParseFloat(data[2], 64) 84 | ch <- prometheus.MustNewConstMetric( 85 | newDesc(innodb, "queries_in_queue", "Queries in queue."), 86 | prometheus.GaugeValue, 87 | value, 88 | ) 89 | } else if data := rViews.FindStringSubmatch(line); data != nil { 90 | value, _ := strconv.ParseFloat(data[1], 64) 91 | ch <- prometheus.MustNewConstMetric( 92 | newDesc(innodb, "read_views_open_inside_innodb", "Read views open inside InnoDB."), 93 | prometheus.GaugeValue, 94 | value, 95 | ) 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // check interface 103 | var _ Scraper = ScrapeEngineInnodbStatus{} 104 | -------------------------------------------------------------------------------- /collector/engine_tokudb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW ENGINE TOKUDB STATUS`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "strings" 22 | 23 | "github.com/go-kit/log" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const ( 28 | // Subsystem. 29 | tokudb = "engine_tokudb" 30 | // Query. 31 | engineTokudbStatusQuery = `SHOW ENGINE TOKUDB STATUS` 32 | ) 33 | 34 | // ScrapeEngineTokudbStatus scrapes from `SHOW ENGINE TOKUDB STATUS`. 35 | type ScrapeEngineTokudbStatus struct{} 36 | 37 | // Name of the Scraper. Should be unique. 38 | func (ScrapeEngineTokudbStatus) Name() string { 39 | return "engine_tokudb_status" 40 | } 41 | 42 | // Help describes the role of the Scraper. 43 | func (ScrapeEngineTokudbStatus) Help() string { 44 | return "Collect from SHOW ENGINE TOKUDB STATUS" 45 | } 46 | 47 | // Version of MySQL from which scraper is available. 48 | func (ScrapeEngineTokudbStatus) Version() float64 { 49 | return 5.6 50 | } 51 | 52 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 53 | func (ScrapeEngineTokudbStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 54 | tokudbRows, err := db.QueryContext(ctx, engineTokudbStatusQuery) 55 | if err != nil { 56 | return err 57 | } 58 | defer tokudbRows.Close() 59 | 60 | var temp, key string 61 | var val sql.RawBytes 62 | 63 | for tokudbRows.Next() { 64 | if err := tokudbRows.Scan(&temp, &key, &val); err != nil { 65 | return err 66 | } 67 | key = strings.ToLower(key) 68 | if floatVal, ok := parseStatus(val); ok { 69 | ch <- prometheus.MustNewConstMetric( 70 | newDesc(tokudb, sanitizeTokudbMetric(key), "Generic metric from SHOW ENGINE TOKUDB STATUS."), 71 | prometheus.UntypedValue, 72 | floatVal, 73 | ) 74 | } 75 | } 76 | return nil 77 | } 78 | 79 | func sanitizeTokudbMetric(metricName string) string { 80 | replacements := map[string]string{ 81 | ">": "", 82 | ",": "", 83 | ":": "", 84 | "(": "", 85 | ")": "", 86 | " ": "_", 87 | "-": "_", 88 | "+": "and", 89 | "/": "and", 90 | } 91 | for r := range replacements { 92 | metricName = strings.Replace(metricName, r, replacements[r], -1) 93 | } 94 | return metricName 95 | } 96 | 97 | // check interface 98 | var _ Scraper = ScrapeEngineTokudbStatus{} 99 | -------------------------------------------------------------------------------- /collector/engine_tokudb_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestSanitizeTokudbMetric(t *testing.T) { 28 | samples := map[string]string{ 29 | "loader: number of calls to loader->close() that failed": "loader_number_of_calls_to_loader_close_that_failed", 30 | "ft: promotion: stopped anyway, after locking the child": "ft_promotion_stopped_anyway_after_locking_the_child", 31 | "ft: basement nodes deserialized with fixed-keysize": "ft_basement_nodes_deserialized_with_fixed_keysize", 32 | "memory: number of bytes used (requested + overhead)": "memory_number_of_bytes_used_requested_and_overhead", 33 | "ft: uncompressed / compressed bytes written (overall)": "ft_uncompressed_and_compressed_bytes_written_overall", 34 | } 35 | convey.Convey("Replacement tests", t, func() { 36 | for metric := range samples { 37 | got := sanitizeTokudbMetric(metric) 38 | convey.So(got, convey.ShouldEqual, samples[metric]) 39 | } 40 | }) 41 | } 42 | 43 | func TestScrapeEngineTokudbStatus(t *testing.T) { 44 | db, mock, err := sqlmock.New() 45 | if err != nil { 46 | t.Fatalf("error opening a stub database connection: %s", err) 47 | } 48 | defer db.Close() 49 | 50 | columns := []string{"Type", "Name", "Status"} 51 | rows := sqlmock.NewRows(columns). 52 | AddRow("TokuDB", "indexer: number of calls to indexer->build() succeeded", "1"). 53 | AddRow("TokuDB", "ft: promotion: stopped anyway, after locking the child", "45316247"). 54 | AddRow("TokuDB", "memory: mallocator version", "3.3.1-0-g9ef9d9e8c271cdf14f664b871a8f98c827714784"). 55 | AddRow("TokuDB", "filesystem: most recent disk full", "Thu Jan 1 00:00:00 1970"). 56 | AddRow("TokuDB", "locktree: time spent ending the STO early (seconds)", "9115.904484") 57 | 58 | mock.ExpectQuery(sanitizeQuery(engineTokudbStatusQuery)).WillReturnRows(rows) 59 | 60 | ch := make(chan prometheus.Metric) 61 | go func() { 62 | if err = (ScrapeEngineTokudbStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 63 | t.Errorf("error calling function on test: %s", err) 64 | } 65 | close(ch) 66 | }() 67 | 68 | metricsExpected := []MetricResult{ 69 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_UNTYPED}, 70 | {labels: labelMap{}, value: 45316247, metricType: dto.MetricType_UNTYPED}, 71 | {labels: labelMap{}, value: 9115.904484, metricType: dto.MetricType_UNTYPED}, 72 | } 73 | convey.Convey("Metrics comparison", t, func() { 74 | for _, expect := range metricsExpected { 75 | got := readMetric(<-ch) 76 | convey.So(got, convey.ShouldResemble, expect) 77 | } 78 | }) 79 | 80 | // Ensure all SQL queries were executed 81 | if err := mock.ExpectationsWereMet(); err != nil { 82 | t.Errorf("there were unfulfilled exceptions: %s", err) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /collector/exporter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "database/sql" 19 | "os" 20 | "testing" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/go-kit/log/level" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "github.com/prometheus/common/model" 26 | "github.com/smartystreets/goconvey/convey" 27 | ) 28 | 29 | const dsn = "root@/mysql" 30 | 31 | func TestExporter(t *testing.T) { 32 | if testing.Short() { 33 | t.Skip("-short is passed, skipping test") 34 | } 35 | 36 | db, err := sql.Open("mysql", dsn) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | defer db.Close() 41 | 42 | exporter := New( 43 | context.Background(), 44 | db, 45 | NewMetrics(""), 46 | []Scraper{ 47 | ScrapeGlobalStatus{}, 48 | }, 49 | log.NewNopLogger(), 50 | ) 51 | 52 | convey.Convey("Metrics describing", t, func() { 53 | ch := make(chan *prometheus.Desc) 54 | go func() { 55 | exporter.Describe(ch) 56 | close(ch) 57 | }() 58 | 59 | for range ch { 60 | } 61 | }) 62 | 63 | convey.Convey("Metrics collection", t, func() { 64 | ch := make(chan prometheus.Metric) 65 | go func() { 66 | exporter.Collect(ch) 67 | close(ch) 68 | }() 69 | 70 | for m := range ch { 71 | got := readMetric(m) 72 | if got.labels[model.MetricNameLabel] == "mysql_up" { 73 | convey.So(got.value, convey.ShouldEqual, 1) 74 | } 75 | } 76 | }) 77 | } 78 | 79 | func TestGetMySQLVersion(t *testing.T) { 80 | if testing.Short() { 81 | t.Skip("-short is passed, skipping test") 82 | } 83 | 84 | logger := log.NewLogfmtLogger(os.Stderr) 85 | logger = level.NewFilter(logger, level.AllowDebug()) 86 | 87 | convey.Convey("Version parsing", t, func() { 88 | db, err := sql.Open("mysql", dsn) 89 | convey.So(err, convey.ShouldBeNil) 90 | defer db.Close() 91 | 92 | convey.So(getMySQLVersion(db, logger), convey.ShouldBeBetweenOrEqual, 5.5, 11.0) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /collector/heartbeat.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape heartbeat data. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "strconv" 23 | 24 | "github.com/go-kit/log" 25 | "github.com/prometheus/client_golang/prometheus" 26 | "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | const ( 30 | // heartbeat is the Metric subsystem we use. 31 | heartbeat = "heartbeat" 32 | // heartbeatQuery is the query used to fetch the stored and current 33 | // timestamps. %s will be replaced by the database and table name. 34 | // The second column allows gets the server timestamp at the exact same 35 | // time the query is run. 36 | heartbeatQuery = "SELECT UNIX_TIMESTAMP(ts), UNIX_TIMESTAMP(%s), server_id from `%s`.`%s`" 37 | ) 38 | 39 | var ( 40 | collectHeartbeatDatabase = kingpin.Flag( 41 | "collect.heartbeat.database", 42 | "Database from where to collect heartbeat data", 43 | ).Default("heartbeat").String() 44 | collectHeartbeatTable = kingpin.Flag( 45 | "collect.heartbeat.table", 46 | "Table from where to collect heartbeat data", 47 | ).Default("heartbeat").String() 48 | collectHeartbeatUtc = kingpin.Flag( 49 | "collect.heartbeat.utc", 50 | "Use UTC for timestamps of the current server (`pt-heartbeat` is called with `--utc`)", 51 | ).Bool() 52 | ) 53 | 54 | // Metric descriptors. 55 | var ( 56 | HeartbeatStoredDesc = prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, heartbeat, "stored_timestamp_seconds"), 58 | "Timestamp stored in the heartbeat table.", 59 | []string{"server_id"}, nil, 60 | ) 61 | HeartbeatNowDesc = prometheus.NewDesc( 62 | prometheus.BuildFQName(namespace, heartbeat, "now_timestamp_seconds"), 63 | "Timestamp of the current server.", 64 | []string{"server_id"}, nil, 65 | ) 66 | ) 67 | 68 | // ScrapeHeartbeat scrapes from the heartbeat table. 69 | // This is mainly targeting pt-heartbeat, but will work with any heartbeat 70 | // implementation that writes to a table with two columns: 71 | // CREATE TABLE heartbeat ( 72 | // ts varchar(26) NOT NULL, 73 | // server_id int unsigned NOT NULL PRIMARY KEY, 74 | // ); 75 | type ScrapeHeartbeat struct{} 76 | 77 | // Name of the Scraper. Should be unique. 78 | func (ScrapeHeartbeat) Name() string { 79 | return "heartbeat" 80 | } 81 | 82 | // Help describes the role of the Scraper. 83 | func (ScrapeHeartbeat) Help() string { 84 | return "Collect from heartbeat" 85 | } 86 | 87 | // Version of MySQL from which scraper is available. 88 | func (ScrapeHeartbeat) Version() float64 { 89 | return 5.1 90 | } 91 | 92 | // nowExpr returns a current timestamp expression. 93 | func nowExpr() string { 94 | if *collectHeartbeatUtc { 95 | return "UTC_TIMESTAMP(6)" 96 | } 97 | return "NOW(6)" 98 | } 99 | 100 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 101 | func (ScrapeHeartbeat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 102 | query := fmt.Sprintf(heartbeatQuery, nowExpr(), *collectHeartbeatDatabase, *collectHeartbeatTable) 103 | heartbeatRows, err := db.QueryContext(ctx, query) 104 | if err != nil { 105 | return err 106 | } 107 | defer heartbeatRows.Close() 108 | 109 | var ( 110 | now, ts sql.RawBytes 111 | serverId int 112 | ) 113 | 114 | for heartbeatRows.Next() { 115 | if err := heartbeatRows.Scan(&ts, &now, &serverId); err != nil { 116 | return err 117 | } 118 | 119 | tsFloatVal, err := strconv.ParseFloat(string(ts), 64) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | nowFloatVal, err := strconv.ParseFloat(string(now), 64) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | serverId := strconv.Itoa(serverId) 130 | 131 | ch <- prometheus.MustNewConstMetric( 132 | HeartbeatNowDesc, 133 | prometheus.GaugeValue, 134 | nowFloatVal, 135 | serverId, 136 | ) 137 | ch <- prometheus.MustNewConstMetric( 138 | HeartbeatStoredDesc, 139 | prometheus.GaugeValue, 140 | tsFloatVal, 141 | serverId, 142 | ) 143 | } 144 | 145 | return nil 146 | } 147 | 148 | // check interface 149 | var _ Scraper = ScrapeHeartbeat{} 150 | -------------------------------------------------------------------------------- /collector/heartbeat_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | type ScrapeHeartbeatTestCase struct { 30 | Args []string 31 | Columns []string 32 | Query string 33 | } 34 | 35 | var ScrapeHeartbeatTestCases = []ScrapeHeartbeatTestCase{ 36 | { 37 | []string{ 38 | "--collect.heartbeat.database", "heartbeat-test", 39 | "--collect.heartbeat.table", "heartbeat-test", 40 | }, 41 | []string{"UNIX_TIMESTAMP(ts)", "UNIX_TIMESTAMP(NOW(6))", "server_id"}, 42 | "SELECT UNIX_TIMESTAMP(ts), UNIX_TIMESTAMP(NOW(6)), server_id from `heartbeat-test`.`heartbeat-test`", 43 | }, 44 | { 45 | []string{ 46 | "--collect.heartbeat.database", "heartbeat-test", 47 | "--collect.heartbeat.table", "heartbeat-test", 48 | "--collect.heartbeat.utc", 49 | }, 50 | []string{"UNIX_TIMESTAMP(ts)", "UNIX_TIMESTAMP(UTC_TIMESTAMP(6))", "server_id"}, 51 | "SELECT UNIX_TIMESTAMP(ts), UNIX_TIMESTAMP(UTC_TIMESTAMP(6)), server_id from `heartbeat-test`.`heartbeat-test`", 52 | }, 53 | } 54 | 55 | func TestScrapeHeartbeat(t *testing.T) { 56 | for _, tt := range ScrapeHeartbeatTestCases { 57 | t.Run(fmt.Sprint(tt.Args), func(t *testing.T) { 58 | _, err := kingpin.CommandLine.Parse(tt.Args) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | db, mock, err := sqlmock.New() 64 | if err != nil { 65 | t.Fatalf("error opening a stub database connection: %s", err) 66 | } 67 | defer db.Close() 68 | 69 | rows := sqlmock.NewRows(tt.Columns). 70 | AddRow("1487597613.001320", "1487598113.448042", 1) 71 | mock.ExpectQuery(sanitizeQuery(tt.Query)).WillReturnRows(rows) 72 | 73 | ch := make(chan prometheus.Metric) 74 | go func() { 75 | if err = (ScrapeHeartbeat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 76 | t.Errorf("error calling function on test: %s", err) 77 | } 78 | close(ch) 79 | }() 80 | 81 | counterExpected := []MetricResult{ 82 | {labels: labelMap{"server_id": "1"}, value: 1487598113.448042, metricType: dto.MetricType_GAUGE}, 83 | {labels: labelMap{"server_id": "1"}, value: 1487597613.00132, metricType: dto.MetricType_GAUGE}, 84 | } 85 | convey.Convey("Metrics comparison", t, func() { 86 | for _, expect := range counterExpected { 87 | got := readMetric(<-ch) 88 | convey.So(got, convey.ShouldResemble, expect) 89 | } 90 | }) 91 | 92 | // Ensure all SQL queries were executed 93 | if err := mock.ExpectationsWereMet(); err != nil { 94 | t.Errorf("there were unfulfilled exceptions: %s", err) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /collector/info_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | // Subsystem. 17 | const informationSchema = "info_schema" 18 | -------------------------------------------------------------------------------- /collector/info_schema_auto_increment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape auto_increment column information. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | // https://jira.percona.com/browse/PMM-4001 explains STRAIGHT_JOIN usage. 27 | const infoSchemaAutoIncrementQuery = ` 28 | SELECT t.table_schema, t.table_name, column_name, auto_increment, 29 | pow(2, case data_type 30 | when 'tinyint' then 7 31 | when 'smallint' then 15 32 | when 'mediumint' then 23 33 | when 'int' then 31 34 | when 'bigint' then 63 35 | end+(column_type like '% unsigned'))-1 as max_int 36 | FROM information_schema.columns c 37 | STRAIGHT_JOIN information_schema.tables t 38 | ON BINARY t.table_schema = c.table_schema AND BINARY t.table_name = c.table_name 39 | WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL 40 | ` 41 | 42 | // Metric descriptors. 43 | var ( 44 | globalInfoSchemaAutoIncrementDesc = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column"), 46 | "The current value of an auto_increment column from information_schema.", 47 | []string{"schema", "table", "column"}, nil, 48 | ) 49 | globalInfoSchemaAutoIncrementMaxDesc = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column_max"), 51 | "The max value of an auto_increment column from information_schema.", 52 | []string{"schema", "table", "column"}, nil, 53 | ) 54 | ) 55 | 56 | // ScrapeAutoIncrementColumns collects auto_increment column information. 57 | type ScrapeAutoIncrementColumns struct{} 58 | 59 | // Name of the Scraper. Should be unique. 60 | func (ScrapeAutoIncrementColumns) Name() string { 61 | return "auto_increment.columns" 62 | } 63 | 64 | // Help describes the role of the Scraper. 65 | func (ScrapeAutoIncrementColumns) Help() string { 66 | return "Collect auto_increment columns and max values from information_schema" 67 | } 68 | 69 | // Version of MySQL from which scraper is available. 70 | func (ScrapeAutoIncrementColumns) Version() float64 { 71 | return 5.1 72 | } 73 | 74 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 75 | func (ScrapeAutoIncrementColumns) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 76 | autoIncrementRows, err := db.QueryContext(ctx, infoSchemaAutoIncrementQuery) 77 | if err != nil { 78 | return err 79 | } 80 | defer autoIncrementRows.Close() 81 | 82 | var ( 83 | schema, table, column string 84 | value, max float64 85 | ) 86 | 87 | for autoIncrementRows.Next() { 88 | if err := autoIncrementRows.Scan( 89 | &schema, &table, &column, &value, &max, 90 | ); err != nil { 91 | return err 92 | } 93 | ch <- prometheus.MustNewConstMetric( 94 | globalInfoSchemaAutoIncrementDesc, prometheus.GaugeValue, value, 95 | schema, table, column, 96 | ) 97 | ch <- prometheus.MustNewConstMetric( 98 | globalInfoSchemaAutoIncrementMaxDesc, prometheus.GaugeValue, max, 99 | schema, table, column, 100 | ) 101 | } 102 | return nil 103 | } 104 | 105 | // check interface 106 | var _ Scraper = ScrapeAutoIncrementColumns{} 107 | -------------------------------------------------------------------------------- /collector/info_schema_clientstats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeClientStat(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 35 | AddRow("userstat", "ON")) 36 | 37 | columns := []string{"CLIENT", "TOTAL_CONNECTIONS", "CONCURRENT_CONNECTIONS", "CONNECTED_TIME", "BUSY_TIME", "CPU_TIME", "BYTES_RECEIVED", "BYTES_SENT", "BINLOG_BYTES_WRITTEN", "ROWS_READ", "ROWS_SENT", "ROWS_DELETED", "ROWS_INSERTED", "ROWS_UPDATED", "SELECT_COMMANDS", "UPDATE_COMMANDS", "OTHER_COMMANDS", "COMMIT_TRANSACTIONS", "ROLLBACK_TRANSACTIONS", "DENIED_CONNECTIONS", "LOST_CONNECTIONS", "ACCESS_DENIED", "EMPTY_QUERIES"} 38 | rows := sqlmock.NewRows(columns). 39 | AddRow("localhost", 1002, 0, 127027, 286, 245, float64(2565104853), 21090856, float64(2380108042), 767691, 1764, 8778, 1210741, 0, 1764, 1214416, 293, 2430888, 0, 0, 0, 0, 0) 40 | mock.ExpectQuery(sanitizeQuery(clientStatQuery)).WillReturnRows(rows) 41 | 42 | ch := make(chan prometheus.Metric) 43 | go func() { 44 | if err = (ScrapeClientStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 45 | t.Errorf("error calling function on test: %s", err) 46 | } 47 | close(ch) 48 | }() 49 | 50 | expected := []MetricResult{ 51 | {labels: labelMap{"client": "localhost"}, value: 1002, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_GAUGE}, 53 | {labels: labelMap{"client": "localhost"}, value: 127027, metricType: dto.MetricType_COUNTER}, 54 | {labels: labelMap{"client": "localhost"}, value: 286, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"client": "localhost"}, value: 245, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"client": "localhost"}, value: float64(2565104853), metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"client": "localhost"}, value: 21090856, metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"client": "localhost"}, value: float64(2380108042), metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"client": "localhost"}, value: 767691, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"client": "localhost"}, value: 1764, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"client": "localhost"}, value: 8778, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"client": "localhost"}, value: 1210741, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"client": "localhost"}, value: 1764, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"client": "localhost"}, value: 1214416, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"client": "localhost"}, value: 293, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"client": "localhost"}, value: 2430888, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 70 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 71 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 72 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 73 | } 74 | convey.Convey("Metrics comparison", t, func() { 75 | for _, expect := range expected { 76 | got := readMetric(<-ch) 77 | convey.So(expect, convey.ShouldResemble, got) 78 | } 79 | }) 80 | 81 | // Ensure all SQL queries were executed 82 | if err := mock.ExpectationsWereMet(); err != nil { 83 | t.Errorf("there were unfulfilled exceptions: %s", err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmp.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.INNODB_CMP`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const innodbCmpQuery = ` 27 | SELECT 28 | page_size, compress_ops, compress_ops_ok, compress_time, uncompress_ops, uncompress_time 29 | FROM information_schema.innodb_cmp 30 | ` 31 | 32 | // Metric descriptors. 33 | var ( 34 | infoSchemaInnodbCmpCompressOps = prometheus.NewDesc( 35 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_ops_total"), 36 | "Number of times a B-tree page of the size PAGE_SIZE has been compressed.", 37 | []string{"page_size"}, nil, 38 | ) 39 | infoSchemaInnodbCmpCompressOpsOk = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_ops_ok_total"), 41 | "Number of times a B-tree page of the size PAGE_SIZE has been successfully compressed.", 42 | []string{"page_size"}, nil, 43 | ) 44 | infoSchemaInnodbCmpCompressTime = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_time_seconds_total"), 46 | "Total time in seconds spent in attempts to compress B-tree pages.", 47 | []string{"page_size"}, nil, 48 | ) 49 | infoSchemaInnodbCmpUncompressOps = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_uncompress_ops_total"), 51 | "Number of times a B-tree page of the size PAGE_SIZE has been uncompressed.", 52 | []string{"page_size"}, nil, 53 | ) 54 | infoSchemaInnodbCmpUncompressTime = prometheus.NewDesc( 55 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_uncompress_time_seconds_total"), 56 | "Total time in seconds spent in uncompressing B-tree pages.", 57 | []string{"page_size"}, nil, 58 | ) 59 | ) 60 | 61 | // ScrapeInnodbCmp collects from `information_schema.innodb_cmp`. 62 | type ScrapeInnodbCmp struct{} 63 | 64 | // Name of the Scraper. Should be unique. 65 | func (ScrapeInnodbCmp) Name() string { 66 | return informationSchema + ".innodb_cmp.prometheus" 67 | } 68 | 69 | // Help describes the role of the Scraper. 70 | func (ScrapeInnodbCmp) Help() string { 71 | return "Collect metrics from information_schema.innodb_cmp" 72 | } 73 | 74 | // Version of MySQL from which scraper is available. 75 | func (ScrapeInnodbCmp) Version() float64 { 76 | return 5.5 77 | } 78 | 79 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 80 | func (ScrapeInnodbCmp) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 81 | informationSchemaInnodbCmpRows, err := db.QueryContext(ctx, innodbCmpQuery) 82 | if err != nil { 83 | return err 84 | } 85 | defer informationSchemaInnodbCmpRows.Close() 86 | 87 | var ( 88 | page_size string 89 | compress_ops, compress_ops_ok, compress_time, uncompress_ops, uncompress_time float64 90 | ) 91 | 92 | for informationSchemaInnodbCmpRows.Next() { 93 | if err := informationSchemaInnodbCmpRows.Scan( 94 | &page_size, &compress_ops, &compress_ops_ok, &compress_time, &uncompress_ops, &uncompress_time, 95 | ); err != nil { 96 | return err 97 | } 98 | 99 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressOps, prometheus.CounterValue, compress_ops, page_size) 100 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressOpsOk, prometheus.CounterValue, compress_ops_ok, page_size) 101 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressTime, prometheus.CounterValue, compress_time, page_size) 102 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpUncompressOps, prometheus.CounterValue, uncompress_ops, page_size) 103 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpUncompressTime, prometheus.CounterValue, uncompress_time, page_size) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | // check interface 110 | var _ Scraper = ScrapeInnodbCmp{} 111 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeInnodbCmp(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | columns := []string{"page_size", "compress_ops", "compress_ops_ok", "compress_time", "uncompress_ops", "uncompress_time"} 35 | rows := sqlmock.NewRows(columns). 36 | AddRow("1024", 10, 20, 30, 40, 50) 37 | mock.ExpectQuery(sanitizeQuery(innodbCmpQuery)).WillReturnRows(rows) 38 | 39 | ch := make(chan prometheus.Metric) 40 | go func() { 41 | if err = (ScrapeInnodbCmp{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 42 | t.Errorf("error calling function on test: %s", err) 43 | } 44 | close(ch) 45 | }() 46 | 47 | expected := []MetricResult{ 48 | {labels: labelMap{"page_size": "1024"}, value: 10, metricType: dto.MetricType_COUNTER}, 49 | {labels: labelMap{"page_size": "1024"}, value: 20, metricType: dto.MetricType_COUNTER}, 50 | {labels: labelMap{"page_size": "1024"}, value: 30, metricType: dto.MetricType_COUNTER}, 51 | {labels: labelMap{"page_size": "1024"}, value: 40, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"page_size": "1024"}, value: 50, metricType: dto.MetricType_COUNTER}, 53 | } 54 | convey.Convey("Metrics comparison", t, func() { 55 | for _, expect := range expected { 56 | got := readMetric(<-ch) 57 | convey.So(expect, convey.ShouldResemble, got) 58 | } 59 | }) 60 | 61 | // Ensure all SQL queries were executed 62 | if err := mock.ExpectationsWereMet(); err != nil { 63 | t.Errorf("there were unfulfilled exceptions: %s", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmpmem.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.INNODB_CMPMEM`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const innodbCmpMemQuery = ` 27 | SELECT 28 | page_size, buffer_pool_instance, pages_used, pages_free, relocation_ops, relocation_time 29 | FROM information_schema.innodb_cmpmem 30 | ` 31 | 32 | // Metric descriptors. 33 | var ( 34 | infoSchemaInnodbCmpMemPagesRead = prometheus.NewDesc( 35 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_pages_used_total"), 36 | "Number of blocks of the size PAGE_SIZE that are currently in use.", 37 | []string{"page_size", "buffer_pool"}, nil, 38 | ) 39 | infoSchemaInnodbCmpMemPagesFree = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_pages_free_total"), 41 | "Number of blocks of the size PAGE_SIZE that are currently available for allocation.", 42 | []string{"page_size", "buffer_pool"}, nil, 43 | ) 44 | infoSchemaInnodbCmpMemRelocationOps = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_relocation_ops_total"), 46 | "Number of times a block of the size PAGE_SIZE has been relocated.", 47 | []string{"page_size", "buffer_pool"}, nil, 48 | ) 49 | infoSchemaInnodbCmpMemRelocationTime = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_relocation_time_seconds_total"), 51 | "Total time in seconds spent in relocating blocks.", 52 | []string{"page_size", "buffer_pool"}, nil, 53 | ) 54 | ) 55 | 56 | // ScrapeInnodbCmp collects from `information_schema.innodb_cmp`. 57 | type ScrapeInnodbCmpMem struct{} 58 | 59 | // Name of the Scraper. Should be unique. 60 | func (ScrapeInnodbCmpMem) Name() string { 61 | return informationSchema + ".innodb_cmpmem.prometheus" 62 | } 63 | 64 | // Help describes the role of the Scraper. 65 | func (ScrapeInnodbCmpMem) Help() string { 66 | return "Collect metrics from information_schema.innodb_cmpmem" 67 | } 68 | 69 | // Version of MySQL from which scraper is available. 70 | func (ScrapeInnodbCmpMem) Version() float64 { 71 | return 5.5 72 | } 73 | 74 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 75 | func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 76 | informationSchemaInnodbCmpMemRows, err := db.QueryContext(ctx, innodbCmpMemQuery) 77 | if err != nil { 78 | return err 79 | } 80 | defer informationSchemaInnodbCmpMemRows.Close() 81 | 82 | var ( 83 | page_size, buffer_pool string 84 | pages_used, pages_free, relocation_ops, relocation_time float64 85 | ) 86 | 87 | for informationSchemaInnodbCmpMemRows.Next() { 88 | if err := informationSchemaInnodbCmpMemRows.Scan( 89 | &page_size, &buffer_pool, &pages_used, &pages_free, &relocation_ops, &relocation_time, 90 | ); err != nil { 91 | return err 92 | } 93 | 94 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesRead, prometheus.CounterValue, pages_used, page_size, buffer_pool) 95 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesFree, prometheus.CounterValue, pages_free, page_size, buffer_pool) 96 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationOps, prometheus.CounterValue, relocation_ops, page_size, buffer_pool) 97 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationTime, prometheus.CounterValue, (relocation_time / 1000), page_size, buffer_pool) 98 | } 99 | return nil 100 | } 101 | 102 | // check interface 103 | var _ Scraper = ScrapeInnodbCmpMem{} 104 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmpmem_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeInnodbCmpMem(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | columns := []string{"page_size", "buffer_pool", "pages_used", "pages_free", "relocation_ops", "relocation_time"} 35 | rows := sqlmock.NewRows(columns). 36 | AddRow("1024", "0", 30, 40, 50, 6000) 37 | mock.ExpectQuery(sanitizeQuery(innodbCmpMemQuery)).WillReturnRows(rows) 38 | 39 | ch := make(chan prometheus.Metric) 40 | go func() { 41 | if err = (ScrapeInnodbCmpMem{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 42 | t.Errorf("error calling function on test: %s", err) 43 | } 44 | close(ch) 45 | }() 46 | 47 | expected := []MetricResult{ 48 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 30, metricType: dto.MetricType_COUNTER}, 49 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 40, metricType: dto.MetricType_COUNTER}, 50 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 50, metricType: dto.MetricType_COUNTER}, 51 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 6, metricType: dto.MetricType_COUNTER}, 52 | } 53 | convey.Convey("Metrics comparison", t, func() { 54 | for _, expect := range expected { 55 | got := readMetric(<-ch) 56 | convey.So(expect, convey.ShouldResemble, got) 57 | } 58 | }) 59 | 60 | // Ensure all SQL queries were executed 61 | if err := mock.ExpectationsWereMet(); err != nil { 62 | t.Errorf("there were unfulfilled exceptions: %s", err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_metrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | ) 27 | 28 | func TestScrapeInnodbMetrics(t *testing.T) { 29 | db, mock, err := sqlmock.New() 30 | if err != nil { 31 | t.Fatalf("error opening a stub database connection: %s", err) 32 | } 33 | defer db.Close() 34 | 35 | enabledColumnName := []string{"COLUMN_NAME"} 36 | rows := sqlmock.NewRows(enabledColumnName). 37 | AddRow("STATUS") 38 | mock.ExpectQuery(sanitizeQuery(infoSchemaInnodbMetricsEnabledColumnQuery)).WillReturnRows(rows) 39 | 40 | columns := []string{"name", "subsystem", "type", "comment", "count"} 41 | rows = sqlmock.NewRows(columns). 42 | AddRow("lock_timeouts", "lock", "counter", "Number of lock timeouts", 0). 43 | AddRow("buffer_pool_reads", "buffer", "status_counter", "Number of reads directly from disk (innodb_buffer_pool_reads)", 1). 44 | AddRow("buffer_pool_size", "server", "value", "Server buffer pool size (all buffer pools) in bytes", 2). 45 | AddRow("buffer_page_read_system_page", "buffer_page_io", "counter", "Number of System Pages read", 3). 46 | AddRow("buffer_page_written_undo_log", "buffer_page_io", "counter", "Number of Undo Log Pages written", 4). 47 | AddRow("buffer_pool_pages_dirty", "buffer", "gauge", "Number of dirt buffer pool pages", 5). 48 | AddRow("buffer_pool_pages_data", "buffer", "gauge", "Number of data buffer pool pages", 6). 49 | AddRow("buffer_pool_pages_total", "buffer", "gauge", "Number of total buffer pool pages", 7). 50 | AddRow("NOPE", "buffer_page_io", "counter", "An invalid buffer_page_io metric", 999) 51 | query := fmt.Sprintf(infoSchemaInnodbMetricsQuery, "status", "enabled") 52 | mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) 53 | 54 | ch := make(chan prometheus.Metric) 55 | go func() { 56 | if err = (ScrapeInnodbMetrics{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 57 | t.Errorf("error calling function on test: %s", err) 58 | } 59 | close(ch) 60 | }() 61 | 62 | metricExpected := []MetricResult{ 63 | {labels: labelMap{}, value: 0, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{}, value: 2, metricType: dto.MetricType_GAUGE}, 66 | {labels: labelMap{"type": "system_page"}, value: 3, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"type": "undo_log"}, value: 4, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{}, value: 5, metricType: dto.MetricType_GAUGE}, 69 | {labels: labelMap{"state": "data"}, value: 6, metricType: dto.MetricType_GAUGE}, 70 | } 71 | convey.Convey("Metrics comparison", t, func() { 72 | for _, expect := range metricExpected { 73 | got := readMetric(<-ch) 74 | convey.So(got, convey.ShouldResemble, expect) 75 | } 76 | }) 77 | 78 | // Ensure all SQL queries were executed 79 | if err := mock.ExpectationsWereMet(); err != nil { 80 | t.Errorf("there were unfulfilled exceptions: %s", err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_sys_tablespaces_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | ) 27 | 28 | func TestScrapeInfoSchemaInnodbTablespaces(t *testing.T) { 29 | db, mock, err := sqlmock.New() 30 | if err != nil { 31 | t.Fatalf("error opening a stub database connection: %s", err) 32 | } 33 | defer db.Close() 34 | 35 | columns := []string{"TABLE_NAME"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("INNODB_SYS_TABLESPACES") 38 | mock.ExpectQuery(sanitizeQuery(innodbTablespacesTablenameQuery)).WillReturnRows(rows) 39 | 40 | tablespacesTablename := "INNODB_SYS_TABLESPACES" 41 | columns = []string{"SPACE", "NAME", "FILE_FORMAT", "ROW_FORMAT", "SPACE_TYPE", "FILE_SIZE", "ALLOCATED_SIZE"} 42 | rows = sqlmock.NewRows(columns). 43 | AddRow(1, "sys/sys_config", "Barracuda", "Dynamic", "Single", 100, 100). 44 | AddRow(2, "db/compressed", "Barracuda", "Compressed", "Single", 300, 200) 45 | query := fmt.Sprintf(innodbTablespacesQuery, tablespacesTablename, tablespacesTablename) 46 | mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) 47 | 48 | ch := make(chan prometheus.Metric) 49 | go func() { 50 | if err = (ScrapeInfoSchemaInnodbTablespaces{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 51 | t.Errorf("error calling function on test: %s", err) 52 | } 53 | close(ch) 54 | }() 55 | 56 | expected := []MetricResult{ 57 | {labels: labelMap{"tablespace_name": "sys/sys_config", "file_format": "Barracuda", "row_format": "Dynamic", "space_type": "Single"}, value: 1, metricType: dto.MetricType_GAUGE}, 58 | {labels: labelMap{"tablespace_name": "sys/sys_config"}, value: 100, metricType: dto.MetricType_GAUGE}, 59 | {labels: labelMap{"tablespace_name": "sys/sys_config"}, value: 100, metricType: dto.MetricType_GAUGE}, 60 | {labels: labelMap{"tablespace_name": "db/compressed", "file_format": "Barracuda", "row_format": "Compressed", "space_type": "Single"}, value: 2, metricType: dto.MetricType_GAUGE}, 61 | {labels: labelMap{"tablespace_name": "db/compressed"}, value: 300, metricType: dto.MetricType_GAUGE}, 62 | {labels: labelMap{"tablespace_name": "db/compressed"}, value: 200, metricType: dto.MetricType_GAUGE}, 63 | } 64 | convey.Convey("Metrics comparison", t, func() { 65 | for _, expect := range expected { 66 | got := readMetric(<-ch) 67 | convey.So(expect, convey.ShouldResemble, got) 68 | } 69 | }) 70 | 71 | // Ensure all SQL queries were executed 72 | if err := mock.ExpectationsWereMet(); err != nil { 73 | t.Errorf("there were unfulfilled exceptions: %s", err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /collector/info_schema_processlist_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | func TestScrapeProcesslist(t *testing.T) { 30 | _, err := kingpin.CommandLine.Parse([]string{ 31 | "--collect.info_schema.processlist.processes_by_user", 32 | "--collect.info_schema.processlist.processes_by_host", 33 | }) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | db, mock, err := sqlmock.New() 39 | if err != nil { 40 | t.Fatalf("error opening a stub database connection: %s", err) 41 | } 42 | defer db.Close() 43 | 44 | query := fmt.Sprintf(infoSchemaProcesslistQuery, 0) 45 | columns := []string{"user", "host", "command", "state", "processes", "seconds"} 46 | rows := sqlmock.NewRows(columns). 47 | AddRow("manager", "10.0.7.234", "Sleep", "", 10, 87). 48 | AddRow("feedback", "10.0.7.154", "Sleep", "", 8, 842). 49 | AddRow("root", "10.0.7.253", "Sleep", "", 1, 20). 50 | AddRow("feedback", "10.0.7.179", "Sleep", "", 2, 14). 51 | AddRow("system user", "", "Connect", "waiting for handler commit", 1, 7271248). 52 | AddRow("manager", "10.0.7.234", "Sleep", "", 4, 62). 53 | AddRow("system user", "", "Query", "Slave has read all relay log; waiting for more updates", 1, 7271248). 54 | AddRow("event_scheduler", "localhost", "Daemon", "Waiting on empty queue", 1, 7271248) 55 | mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) 56 | 57 | ch := make(chan prometheus.Metric) 58 | go func() { 59 | if err = (ScrapeProcesslist{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 60 | t.Errorf("error calling function on test: %s", err) 61 | } 62 | close(ch) 63 | }() 64 | 65 | expected := []MetricResult{ 66 | {labels: labelMap{"command": "connect", "state": "waiting_for_handler_commit"}, value: 1, metricType: dto.MetricType_GAUGE}, 67 | {labels: labelMap{"command": "connect", "state": "waiting_for_handler_commit"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 68 | {labels: labelMap{"command": "daemon", "state": "waiting_on_empty_queue"}, value: 1, metricType: dto.MetricType_GAUGE}, 69 | {labels: labelMap{"command": "daemon", "state": "waiting_on_empty_queue"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 70 | {labels: labelMap{"command": "query", "state": "slave_has_read_all_relay_log_waiting_for_more_updates"}, value: 1, metricType: dto.MetricType_GAUGE}, 71 | {labels: labelMap{"command": "query", "state": "slave_has_read_all_relay_log_waiting_for_more_updates"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 72 | {labels: labelMap{"command": "sleep", "state": "unknown"}, value: 25, metricType: dto.MetricType_GAUGE}, 73 | {labels: labelMap{"command": "sleep", "state": "unknown"}, value: 1025, metricType: dto.MetricType_GAUGE}, 74 | {labels: labelMap{"client_host": "10.0.7.154"}, value: 8, metricType: dto.MetricType_GAUGE}, 75 | {labels: labelMap{"client_host": "10.0.7.179"}, value: 2, metricType: dto.MetricType_GAUGE}, 76 | {labels: labelMap{"client_host": "10.0.7.234"}, value: 14, metricType: dto.MetricType_GAUGE}, 77 | {labels: labelMap{"client_host": "10.0.7.253"}, value: 1, metricType: dto.MetricType_GAUGE}, 78 | {labels: labelMap{"client_host": "localhost"}, value: 1, metricType: dto.MetricType_GAUGE}, 79 | {labels: labelMap{"client_host": "unknown"}, value: 2, metricType: dto.MetricType_GAUGE}, 80 | {labels: labelMap{"mysql_user": "event_scheduler"}, value: 1, metricType: dto.MetricType_GAUGE}, 81 | {labels: labelMap{"mysql_user": "feedback"}, value: 10, metricType: dto.MetricType_GAUGE}, 82 | {labels: labelMap{"mysql_user": "manager"}, value: 14, metricType: dto.MetricType_GAUGE}, 83 | {labels: labelMap{"mysql_user": "root"}, value: 1, metricType: dto.MetricType_GAUGE}, 84 | {labels: labelMap{"mysql_user": "system user"}, value: 2, metricType: dto.MetricType_GAUGE}, 85 | } 86 | convey.Convey("Metrics comparison", t, func() { 87 | for _, expect := range expected { 88 | got := readMetric(<-ch) 89 | convey.So(expect, convey.ShouldResemble, got) 90 | } 91 | }) 92 | 93 | // Ensure all SQL queries were executed 94 | if err := mock.ExpectationsWereMet(); err != nil { 95 | t.Errorf("there were unfulfilled exceptions: %s", err) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /collector/info_schema_query_response_time.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.query_response_time*` tables. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/go-kit/log" 25 | "github.com/go-kit/log/level" 26 | "github.com/prometheus/client_golang/prometheus" 27 | ) 28 | 29 | const queryResponseCheckQuery = `SELECT @@query_response_time_stats` 30 | 31 | var ( 32 | // Use uppercase for table names, otherwise read/write split will return the same results as total 33 | // due to the bug. 34 | queryResponseTimeQueries = [3]string{ 35 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME", 36 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME_READ", 37 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME_WRITE", 38 | } 39 | 40 | infoSchemaQueryResponseTimeCountDescs = [3]*prometheus.Desc{ 41 | prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, informationSchema, "query_response_time_seconds"), 43 | "The number of all queries by duration they took to execute.", 44 | []string{}, nil, 45 | ), 46 | prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, informationSchema, "read_query_response_time_seconds"), 48 | "The number of read queries by duration they took to execute.", 49 | []string{}, nil, 50 | ), 51 | prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, informationSchema, "write_query_response_time_seconds"), 53 | "The number of write queries by duration they took to execute.", 54 | []string{}, nil, 55 | ), 56 | } 57 | ) 58 | 59 | func processQueryResponseTimeTable(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, query string, i int) error { 60 | queryDistributionRows, err := db.QueryContext(ctx, query) 61 | if err != nil { 62 | return err 63 | } 64 | defer queryDistributionRows.Close() 65 | 66 | var ( 67 | length string 68 | count uint64 69 | total string 70 | histogramCnt uint64 71 | histogramSum float64 72 | countBuckets = map[float64]uint64{} 73 | ) 74 | 75 | for queryDistributionRows.Next() { 76 | err = queryDistributionRows.Scan( 77 | &length, 78 | &count, 79 | &total, 80 | ) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | length, _ := strconv.ParseFloat(strings.TrimSpace(length), 64) 86 | total, _ := strconv.ParseFloat(strings.TrimSpace(total), 64) 87 | histogramCnt += count 88 | histogramSum += total 89 | // Special case for "TOO LONG" row where we take into account the count field which is the only available 90 | // and do not add it as a part of histogram or metric 91 | if length == 0 { 92 | continue 93 | } 94 | countBuckets[length] = histogramCnt 95 | } 96 | // Create histogram with query counts 97 | ch <- prometheus.MustNewConstHistogram( 98 | infoSchemaQueryResponseTimeCountDescs[i], histogramCnt, histogramSum, countBuckets, 99 | ) 100 | return nil 101 | } 102 | 103 | // ScrapeQueryResponseTime collects from `information_schema.query_response_time`. 104 | type ScrapeQueryResponseTime struct{} 105 | 106 | // Name of the Scraper. Should be unique. 107 | func (ScrapeQueryResponseTime) Name() string { 108 | return "info_schema.query_response_time" 109 | } 110 | 111 | // Help describes the role of the Scraper. 112 | func (ScrapeQueryResponseTime) Help() string { 113 | return "Collect query response time distribution if query_response_time_stats is ON." 114 | } 115 | 116 | // Version of MySQL from which scraper is available. 117 | func (ScrapeQueryResponseTime) Version() float64 { 118 | return 5.5 119 | } 120 | 121 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 122 | func (ScrapeQueryResponseTime) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 123 | var queryStats uint8 124 | err := db.QueryRowContext(ctx, queryResponseCheckQuery).Scan(&queryStats) 125 | if err != nil { 126 | level.Debug(logger).Log("msg", "Query response time distribution is not available.") 127 | return nil 128 | } 129 | if queryStats == 0 { 130 | level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", "query_response_time_stats") 131 | return nil 132 | } 133 | 134 | for i, query := range queryResponseTimeQueries { 135 | err := processQueryResponseTimeTable(ctx, db, ch, query, i) 136 | // The first query should not fail if query_response_time_stats is ON, 137 | // unlike the other two when the read/write tables exist only with Percona Server 5.6/5.7. 138 | if i == 0 && err != nil { 139 | return err 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | // check interface 146 | var _ Scraper = ScrapeQueryResponseTime{} 147 | -------------------------------------------------------------------------------- /collector/info_schema_query_response_time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeQueryResponseTime(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | mock.ExpectQuery(queryResponseCheckQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) 35 | 36 | rows := sqlmock.NewRows([]string{"TIME", "COUNT", "TOTAL"}). 37 | AddRow(0.000001, 124, 0.000000). 38 | AddRow(0.000010, 179, 0.000797). 39 | AddRow(0.000100, 2859, 0.107321). 40 | AddRow(0.001000, 1085, 0.335395). 41 | AddRow(0.010000, 269, 0.522264). 42 | AddRow(0.100000, 11, 0.344209). 43 | AddRow(1.000000, 1, 0.267369). 44 | AddRow(10.000000, 0, 0.000000). 45 | AddRow(100.000000, 0, 0.000000). 46 | AddRow(1000.000000, 0, 0.000000). 47 | AddRow(10000.000000, 0, 0.000000). 48 | AddRow(100000.000000, 0, 0.000000). 49 | AddRow(1000000.000000, 0, 0.000000). 50 | AddRow("TOO LONG", 0, "TOO LONG") 51 | mock.ExpectQuery(sanitizeQuery(queryResponseTimeQueries[0])).WillReturnRows(rows) 52 | 53 | ch := make(chan prometheus.Metric) 54 | go func() { 55 | if err = (ScrapeQueryResponseTime{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 56 | t.Errorf("error calling function on test: %s", err) 57 | } 58 | close(ch) 59 | }() 60 | 61 | // Test histogram 62 | expectCounts := map[float64]uint64{ 63 | 1e-06: 124, 64 | 1e-05: 303, 65 | 0.0001: 3162, 66 | 0.001: 4247, 67 | 0.01: 4516, 68 | 0.1: 4527, 69 | 1: 4528, 70 | 10: 4528, 71 | 100: 4528, 72 | 1000: 4528, 73 | 10000: 4528, 74 | 100000: 4528, 75 | 1e+06: 4528, 76 | } 77 | expectHistogram := prometheus.MustNewConstHistogram(infoSchemaQueryResponseTimeCountDescs[0], 78 | 4528, 1.5773549999999998, expectCounts) 79 | expectPb := &dto.Metric{} 80 | expectHistogram.Write(expectPb) 81 | 82 | gotPb := &dto.Metric{} 83 | gotHistogram := <-ch // read the last item from channel 84 | gotHistogram.Write(gotPb) 85 | convey.Convey("Histogram comparison", t, func() { 86 | convey.So(expectPb.Histogram, convey.ShouldResemble, gotPb.Histogram) 87 | }) 88 | 89 | // Ensure all SQL queries were executed 90 | if err := mock.ExpectationsWereMet(); err != nil { 91 | t.Errorf("there were unfulfilled exceptions: %s", err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /collector/info_schema_replica_host.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.replica_host_status`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/go-kit/log/level" 24 | MySQL "github.com/go-sql-driver/mysql" 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const replicaHostQuery = ` 29 | SELECT SERVER_ID 30 | , if(SESSION_ID='MASTER_SESSION_ID','writer','reader') AS ROLE 31 | , CPU 32 | , MASTER_SLAVE_LATENCY_IN_MICROSECONDS 33 | , REPLICA_LAG_IN_MILLISECONDS 34 | , LOG_STREAM_SPEED_IN_KiB_PER_SECOND 35 | , CURRENT_REPLAY_LATENCY_IN_MICROSECONDS 36 | FROM information_schema.replica_host_status 37 | ` 38 | 39 | // Metric descriptors. 40 | var ( 41 | infoSchemaReplicaHostCpuDesc = prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_cpu_percent"), 43 | "The CPU usage as a percentage.", 44 | []string{"server_id", "role"}, nil, 45 | ) 46 | infoSchemaReplicaHostReplicaLatencyDesc = prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_replica_latency_seconds"), 48 | "The source-replica latency in seconds.", 49 | []string{"server_id", "role"}, nil, 50 | ) 51 | infoSchemaReplicaHostLagDesc = prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_lag_seconds"), 53 | "The replica lag in seconds.", 54 | []string{"server_id", "role"}, nil, 55 | ) 56 | infoSchemaReplicaHostLogStreamSpeedDesc = prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_log_stream_speed"), 58 | "The log stream speed in kilobytes per second.", 59 | []string{"server_id", "role"}, nil, 60 | ) 61 | infoSchemaReplicaHostReplayLatencyDesc = prometheus.NewDesc( 62 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_replay_latency_seconds"), 63 | "The current replay latency in seconds.", 64 | []string{"server_id", "role"}, nil, 65 | ) 66 | ) 67 | 68 | // ScrapeReplicaHost collects from `information_schema.replica_host_status`. 69 | type ScrapeReplicaHost struct{} 70 | 71 | // Name of the Scraper. Should be unique. 72 | func (ScrapeReplicaHost) Name() string { 73 | return "info_schema.replica_host" 74 | } 75 | 76 | // Help describes the role of the Scraper. 77 | func (ScrapeReplicaHost) Help() string { 78 | return "Collect metrics from information_schema.replica_host_status" 79 | } 80 | 81 | // Version of MySQL from which scraper is available. 82 | func (ScrapeReplicaHost) Version() float64 { 83 | return 5.6 84 | } 85 | 86 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 87 | func (ScrapeReplicaHost) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 88 | replicaHostRows, err := db.QueryContext(ctx, replicaHostQuery) 89 | if err != nil { 90 | if mysqlErr, ok := err.(*MySQL.MySQLError); ok { // Now the error number is accessible directly 91 | // Check for error 1109: Unknown table 92 | if mysqlErr.Number == 1109 { 93 | level.Debug(logger).Log("msg", "information_schema.replica_host_status is not available.") 94 | return nil 95 | } 96 | } 97 | return err 98 | } 99 | defer replicaHostRows.Close() 100 | 101 | var ( 102 | serverId string 103 | role string 104 | cpu float64 105 | replicaLatency uint64 106 | replicaLag float64 107 | logStreamSpeed float64 108 | replayLatency uint64 109 | ) 110 | for replicaHostRows.Next() { 111 | if err := replicaHostRows.Scan( 112 | &serverId, 113 | &role, 114 | &cpu, 115 | &replicaLatency, 116 | &replicaLag, 117 | &logStreamSpeed, 118 | &replayLatency, 119 | ); err != nil { 120 | return err 121 | } 122 | ch <- prometheus.MustNewConstMetric( 123 | infoSchemaReplicaHostCpuDesc, prometheus.GaugeValue, cpu, 124 | serverId, role, 125 | ) 126 | ch <- prometheus.MustNewConstMetric( 127 | infoSchemaReplicaHostReplicaLatencyDesc, prometheus.GaugeValue, float64(replicaLatency)*0.000001, 128 | serverId, role, 129 | ) 130 | ch <- prometheus.MustNewConstMetric( 131 | infoSchemaReplicaHostLagDesc, prometheus.GaugeValue, replicaLag*0.001, 132 | serverId, role, 133 | ) 134 | ch <- prometheus.MustNewConstMetric( 135 | infoSchemaReplicaHostLogStreamSpeedDesc, prometheus.GaugeValue, logStreamSpeed, 136 | serverId, role, 137 | ) 138 | ch <- prometheus.MustNewConstMetric( 139 | infoSchemaReplicaHostReplayLatencyDesc, prometheus.GaugeValue, float64(replayLatency)*0.000001, 140 | serverId, role, 141 | ) 142 | } 143 | return nil 144 | } 145 | 146 | // check interface 147 | var _ Scraper = ScrapeReplicaHost{} 148 | -------------------------------------------------------------------------------- /collector/info_schema_replica_host_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeReplicaHost(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | 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"} 35 | rows := sqlmock.NewRows(columns). 36 | AddRow("dbtools-cluster-us-west-2c", "reader", 1.2531328201293945, 250000, 20.069000244140625, 2.0368164549078225, 500000). 37 | AddRow("dbtools-cluster-writer", "writer", 1.9607843160629272, 250000, 0, 2.0368164549078225, 0) 38 | mock.ExpectQuery(sanitizeQuery(replicaHostQuery)).WillReturnRows(rows) 39 | 40 | ch := make(chan prometheus.Metric) 41 | go func() { 42 | if err = (ScrapeReplicaHost{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 43 | t.Errorf("error calling function on test: %s", err) 44 | } 45 | close(ch) 46 | }() 47 | 48 | expected := []MetricResult{ 49 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 1.2531328201293945, metricType: dto.MetricType_GAUGE}, 50 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.25, metricType: dto.MetricType_GAUGE}, 51 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.020069000244140625, metricType: dto.MetricType_GAUGE}, 52 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 2.0368164549078225, metricType: dto.MetricType_GAUGE}, 53 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.5, metricType: dto.MetricType_GAUGE}, 54 | 55 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 1.9607843160629272, metricType: dto.MetricType_GAUGE}, 56 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.25, metricType: dto.MetricType_GAUGE}, 57 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.0, metricType: dto.MetricType_GAUGE}, 58 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 2.0368164549078225, metricType: dto.MetricType_GAUGE}, 59 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.0, metricType: dto.MetricType_GAUGE}, 60 | } 61 | convey.Convey("Metrics comparison", t, func() { 62 | for _, expect := range expected { 63 | got := readMetric(<-ch) 64 | convey.So(expect, convey.ShouldResemble, got) 65 | } 66 | }) 67 | 68 | // Ensure all SQL queries were executed 69 | if err := mock.ExpectationsWereMet(); err != nil { 70 | t.Errorf("there were unfulfilled exceptions: %s", err) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /collector/info_schema_schemastats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.table_statistics`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/go-kit/log/level" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const schemaStatQuery = ` 28 | SELECT 29 | TABLE_SCHEMA, 30 | SUM(ROWS_READ) AS ROWS_READ, 31 | SUM(ROWS_CHANGED) AS ROWS_CHANGED, 32 | SUM(ROWS_CHANGED_X_INDEXES) AS ROWS_CHANGED_X_INDEXES 33 | FROM information_schema.TABLE_STATISTICS 34 | GROUP BY TABLE_SCHEMA; 35 | ` 36 | 37 | // Metric descriptors. 38 | var ( 39 | infoSchemaStatsRowsReadDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_read_total"), 41 | "The number of rows read from the schema.", 42 | []string{"schema"}, nil, 43 | ) 44 | infoSchemaStatsRowsChangedDesc = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_changed_total"), 46 | "The number of rows changed in the schema.", 47 | []string{"schema"}, nil, 48 | ) 49 | infoSchemaStatsRowsChangedXIndexesDesc = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_changed_x_indexes_total"), 51 | "The number of rows changed in the schema, multiplied by the number of indexes changed.", 52 | []string{"schema"}, nil, 53 | ) 54 | ) 55 | 56 | // ScrapeSchemaStat collects from `information_schema.table_statistics` grouped by schema. 57 | type ScrapeSchemaStat struct{} 58 | 59 | // Name of the Scraper. Should be unique. 60 | func (ScrapeSchemaStat) Name() string { 61 | return "info_schema.schemastats" 62 | } 63 | 64 | // Help describes the role of the Scraper. 65 | func (ScrapeSchemaStat) Help() string { 66 | return "If running with userstat=1, set to true to collect schema statistics" 67 | } 68 | 69 | // Version of MySQL from which scraper is available. 70 | func (ScrapeSchemaStat) Version() float64 { 71 | return 5.1 72 | } 73 | 74 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 75 | func (ScrapeSchemaStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 76 | var varName, varVal string 77 | 78 | err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) 79 | if err != nil { 80 | level.Debug(logger).Log("msg", "Detailed schema stats are not available.") 81 | return nil 82 | } 83 | if varVal == "OFF" { 84 | level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) 85 | return nil 86 | } 87 | 88 | informationSchemaTableStatisticsRows, err := db.QueryContext(ctx, schemaStatQuery) 89 | if err != nil { 90 | return err 91 | } 92 | defer informationSchemaTableStatisticsRows.Close() 93 | 94 | var ( 95 | tableSchema string 96 | rowsRead uint64 97 | rowsChanged uint64 98 | rowsChangedXIndexes uint64 99 | ) 100 | 101 | for informationSchemaTableStatisticsRows.Next() { 102 | err = informationSchemaTableStatisticsRows.Scan( 103 | &tableSchema, 104 | &rowsRead, 105 | &rowsChanged, 106 | &rowsChangedXIndexes, 107 | ) 108 | 109 | if err != nil { 110 | return err 111 | } 112 | ch <- prometheus.MustNewConstMetric( 113 | infoSchemaStatsRowsReadDesc, prometheus.CounterValue, float64(rowsRead), 114 | tableSchema, 115 | ) 116 | ch <- prometheus.MustNewConstMetric( 117 | infoSchemaStatsRowsChangedDesc, prometheus.CounterValue, float64(rowsChanged), 118 | tableSchema, 119 | ) 120 | ch <- prometheus.MustNewConstMetric( 121 | infoSchemaStatsRowsChangedXIndexesDesc, prometheus.CounterValue, float64(rowsChangedXIndexes), 122 | tableSchema, 123 | ) 124 | } 125 | return nil 126 | } 127 | 128 | // check interface 129 | var _ Scraper = ScrapeSchemaStat{} 130 | -------------------------------------------------------------------------------- /collector/info_schema_schemastats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/smartystreets/goconvey/convey" 24 | ) 25 | 26 | func TestScrapeSchemaStat(t *testing.T) { 27 | db, mock, err := sqlmock.New() 28 | if err != nil { 29 | t.Fatalf("error opening a stub database connection: %s", err) 30 | } 31 | defer db.Close() 32 | 33 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 34 | AddRow("userstat", "ON")) 35 | 36 | columns := []string{"TABLE_SCHEMA", "ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES"} 37 | rows := sqlmock.NewRows(columns). 38 | AddRow("mysql", 238, 0, 8). 39 | AddRow("default", 99, 1, 0) 40 | mock.ExpectQuery(sanitizeQuery(schemaStatQuery)).WillReturnRows(rows) 41 | 42 | ch := make(chan prometheus.Metric) 43 | go func() { 44 | if err = (ScrapeSchemaStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 45 | t.Errorf("error calling function on test: %s", err) 46 | } 47 | close(ch) 48 | }() 49 | 50 | expected := []MetricResult{ 51 | {labels: labelMap{"schema": "mysql"}, value: 238}, 52 | {labels: labelMap{"schema": "mysql"}, value: 0}, 53 | {labels: labelMap{"schema": "mysql"}, value: 8}, 54 | {labels: labelMap{"schema": "default"}, value: 99}, 55 | {labels: labelMap{"schema": "default"}, value: 1}, 56 | {labels: labelMap{"schema": "default"}, value: 0}, 57 | } 58 | convey.Convey("Metrics comparison", t, func() { 59 | for _, expect := range expected { 60 | got := readMetric(<-ch) 61 | convey.So(expect, convey.ShouldResemble, got) 62 | } 63 | }) 64 | 65 | // Ensure all SQL queries were executed 66 | if err := mock.ExpectationsWereMet(); err != nil { 67 | t.Errorf("there were unfulfilled exceptions: %s", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /collector/info_schema_tables_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.tables`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "github.com/go-kit/log" 22 | "testing" 23 | 24 | "github.com/prometheus/client_golang/prometheus" 25 | "github.com/smartystreets/goconvey/convey" 26 | ) 27 | 28 | func TestScrapeTableSchema(t *testing.T) { //nolint:unused 29 | db, err := sql.Open("mysql", "root@tcp(127.0.0.1:3306)/") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer db.Close() 34 | 35 | dbName := "test_cache_db" 36 | tableName := "test_cache_table" 37 | *tableSchemaDatabases = dbName 38 | 39 | _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + dbName) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer func() { //nolint:wsl 44 | _, err = db.Exec("DROP DATABASE " + dbName) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | }() 49 | 50 | _, err = db.Exec("CREATE TABLE IF NOT EXISTS " + dbName + "." + tableName + " (id int(64))") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | defer func() { //nolint:wsl 55 | _, err = db.Exec("DROP TABLE " + dbName + "." + tableName) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | }() 60 | _, err = db.Exec("TRUNCATE " + dbName + "." + tableName) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | ctx := context.Background() 66 | addRowAndCheckRowsCount(t, ctx, db, dbName, tableName, 1) 67 | addRowAndCheckRowsCount(t, ctx, db, dbName, tableName, 2) 68 | } 69 | 70 | func addRowAndCheckRowsCount(t *testing.T, ctx context.Context, db *sql.DB, dbName, tableName string, expectedRowsCount float64) { //nolint:go-lint 71 | _, err := db.Exec("INSERT INTO " + dbName + "." + tableName + " VALUES(50)") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | ch := make(chan prometheus.Metric) 76 | go func() { //nolint:wsl 77 | if err = (ScrapeTableSchema{}).Scrape(ctx, db, ch, log.NewNopLogger()); err != nil { 78 | t.Errorf("error calling function on test: %s", err) 79 | } 80 | close(ch) 81 | }() 82 | 83 | // For test is important only second receive to channel. 84 | // Others can be ignored. 85 | <-ch 86 | got := readMetric(<-ch) 87 | <-ch 88 | <-ch 89 | <-ch 90 | 91 | expect := MetricResult{ 92 | labels: labelMap{ 93 | "schema": dbName, 94 | "table": tableName, 95 | }, 96 | value: expectedRowsCount, 97 | metricType: 1, 98 | } 99 | // Variable got.value contains actual rows count in table. 100 | // Should be equal to count of calling this method. 101 | convey.ShouldEqual(got, expect) 102 | } 103 | -------------------------------------------------------------------------------- /collector/info_schema_tablestats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.table_statistics`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/go-kit/log/level" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const tableStatQuery = ` 28 | SELECT 29 | TABLE_SCHEMA, 30 | TABLE_NAME, 31 | ROWS_READ, 32 | ROWS_CHANGED, 33 | ROWS_CHANGED_X_INDEXES 34 | FROM information_schema.table_statistics 35 | ` 36 | 37 | // Metric descriptors. 38 | var ( 39 | infoSchemaTableStatsRowsReadDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_read_total"), 41 | "The number of rows read from the table.", 42 | []string{"schema", "table"}, nil, 43 | ) 44 | infoSchemaTableStatsRowsChangedDesc = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_changed_total"), 46 | "The number of rows changed in the table.", 47 | []string{"schema", "table"}, nil, 48 | ) 49 | infoSchemaTableStatsRowsChangedXIndexesDesc = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_changed_x_indexes_total"), 51 | "The number of rows changed in the table, multiplied by the number of indexes changed.", 52 | []string{"schema", "table"}, nil, 53 | ) 54 | ) 55 | 56 | // ScrapeTableStat collects from `information_schema.table_statistics`. 57 | type ScrapeTableStat struct{} 58 | 59 | // Name of the Scraper. Should be unique. 60 | func (ScrapeTableStat) Name() string { 61 | return "info_schema.tablestats" 62 | } 63 | 64 | // Help describes the role of the Scraper. 65 | func (ScrapeTableStat) Help() string { 66 | return "If running with userstat=1, set to true to collect table statistics" 67 | } 68 | 69 | // Version of MySQL from which scraper is available. 70 | func (ScrapeTableStat) Version() float64 { 71 | return 5.1 72 | } 73 | 74 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 75 | func (ScrapeTableStat) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 76 | var varName, varVal string 77 | err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) 78 | if err != nil { 79 | level.Debug(logger).Log("msg", "Detailed table stats are not available.") 80 | return nil 81 | } 82 | if varVal == "OFF" { 83 | level.Debug(logger).Log("msg", "MySQL variable is OFF.", "var", varName) 84 | return nil 85 | } 86 | 87 | informationSchemaTableStatisticsRows, err := db.QueryContext(ctx, tableStatQuery) 88 | if err != nil { 89 | return err 90 | } 91 | defer informationSchemaTableStatisticsRows.Close() 92 | 93 | var ( 94 | tableSchema string 95 | tableName string 96 | rowsRead uint64 97 | rowsChanged uint64 98 | rowsChangedXIndexes uint64 99 | ) 100 | 101 | for informationSchemaTableStatisticsRows.Next() { 102 | err = informationSchemaTableStatisticsRows.Scan( 103 | &tableSchema, 104 | &tableName, 105 | &rowsRead, 106 | &rowsChanged, 107 | &rowsChangedXIndexes, 108 | ) 109 | if err != nil { 110 | return err 111 | } 112 | ch <- prometheus.MustNewConstMetric( 113 | infoSchemaTableStatsRowsReadDesc, prometheus.CounterValue, float64(rowsRead), 114 | tableSchema, tableName, 115 | ) 116 | ch <- prometheus.MustNewConstMetric( 117 | infoSchemaTableStatsRowsChangedDesc, prometheus.CounterValue, float64(rowsChanged), 118 | tableSchema, tableName, 119 | ) 120 | ch <- prometheus.MustNewConstMetric( 121 | infoSchemaTableStatsRowsChangedXIndexesDesc, prometheus.CounterValue, float64(rowsChangedXIndexes), 122 | tableSchema, tableName, 123 | ) 124 | } 125 | return nil 126 | } 127 | 128 | // check interface 129 | var _ Scraper = ScrapeTableStat{} 130 | -------------------------------------------------------------------------------- /collector/info_schema_tablestats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/smartystreets/goconvey/convey" 24 | ) 25 | 26 | func TestScrapeTableStat(t *testing.T) { 27 | db, mock, err := sqlmock.New() 28 | if err != nil { 29 | t.Fatalf("error opening a stub database connection: %s", err) 30 | } 31 | defer db.Close() 32 | 33 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 34 | AddRow("userstat", "ON")) 35 | 36 | columns := []string{"TABLE_SCHEMA", "TABLE_NAME", "ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES"} 37 | rows := sqlmock.NewRows(columns). 38 | AddRow("mysql", "db", 238, 0, 8). 39 | AddRow("mysql", "proxies_priv", 99, 1, 0). 40 | AddRow("mysql", "user", 1064, 2, 5) 41 | mock.ExpectQuery(sanitizeQuery(tableStatQuery)).WillReturnRows(rows) 42 | 43 | ch := make(chan prometheus.Metric) 44 | go func() { 45 | if err = (ScrapeTableStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 46 | t.Errorf("error calling function on test: %s", err) 47 | } 48 | close(ch) 49 | }() 50 | 51 | expected := []MetricResult{ 52 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 238}, 53 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 0}, 54 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 8}, 55 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 99}, 56 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 1}, 57 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 0}, 58 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 1064}, 59 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 2}, 60 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 5}, 61 | } 62 | convey.Convey("Metrics comparison", t, func() { 63 | for _, expect := range expected { 64 | got := readMetric(<-ch) 65 | convey.So(expect, convey.ShouldResemble, got) 66 | } 67 | }) 68 | 69 | // Ensure all SQL queries were executed 70 | if err := mock.ExpectationsWereMet(); err != nil { 71 | t.Errorf("there were unfulfilled exceptions: %s", err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /collector/info_schema_userstats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeUserStat(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 35 | AddRow("userstat", "ON")) 36 | 37 | columns := []string{"USER", "TOTAL_CONNECTIONS", "CONCURRENT_CONNECTIONS", "CONNECTED_TIME", "BUSY_TIME", "CPU_TIME", "BYTES_RECEIVED", "BYTES_SENT", "BINLOG_BYTES_WRITTEN", "ROWS_READ", "ROWS_SENT", "ROWS_DELETED", "ROWS_INSERTED", "ROWS_UPDATED", "SELECT_COMMANDS", "UPDATE_COMMANDS", "OTHER_COMMANDS", "COMMIT_TRANSACTIONS", "ROLLBACK_TRANSACTIONS", "DENIED_CONNECTIONS", "LOST_CONNECTIONS", "ACCESS_DENIED", "EMPTY_QUERIES"} 38 | rows := sqlmock.NewRows(columns). 39 | AddRow("user_test", 1002, 0, 127027, 286, 245, float64(2565104853), 21090856, float64(2380108042), 767691, 1764, 8778, 1210741, 0, 1764, 1214416, 293, 2430888, 0, 0, 0, 0, 0) 40 | mock.ExpectQuery(sanitizeQuery(userStatQuery)).WillReturnRows(rows) 41 | 42 | ch := make(chan prometheus.Metric) 43 | go func() { 44 | if err = (ScrapeUserStat{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 45 | t.Errorf("error calling function on test: %s", err) 46 | } 47 | close(ch) 48 | }() 49 | 50 | expected := []MetricResult{ 51 | {labels: labelMap{"user": "user_test"}, value: 1002, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_GAUGE}, 53 | {labels: labelMap{"user": "user_test"}, value: 127027, metricType: dto.MetricType_COUNTER}, 54 | {labels: labelMap{"user": "user_test"}, value: 286, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"user": "user_test"}, value: 245, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"user": "user_test"}, value: float64(2565104853), metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"user": "user_test"}, value: 21090856, metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"user": "user_test"}, value: float64(2380108042), metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"user": "user_test"}, value: 767691, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"user": "user_test"}, value: 1764, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"user": "user_test"}, value: 8778, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"user": "user_test"}, value: 1210741, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"user": "user_test"}, value: 1764, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"user": "user_test"}, value: 1214416, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"user": "user_test"}, value: 293, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"user": "user_test"}, value: 2430888, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 70 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 71 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 72 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 73 | } 74 | convey.Convey("Metrics comparison", t, func() { 75 | for _, expect := range expected { 76 | got := readMetric(<-ch) 77 | convey.So(expect, convey.ShouldResemble, got) 78 | } 79 | }) 80 | 81 | // Ensure all SQL queries were executed 82 | if err := mock.ExpectationsWereMet(); err != nil { 83 | t.Errorf("there were unfulfilled exceptions: %s", err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /collector/mysql.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | // Subsystem. 17 | const mysql = "mysql" 18 | -------------------------------------------------------------------------------- /collector/perf_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | // Subsystem. 17 | const performanceSchema = "perf_schema" 18 | -------------------------------------------------------------------------------- /collector/perf_schema_events_waits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.events_waits_summary_global_by_event_name`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const perfEventsWaitsQuery = ` 27 | SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT 28 | FROM performance_schema.events_waits_summary_global_by_event_name 29 | ` 30 | 31 | // Metric descriptors. 32 | var ( 33 | performanceSchemaEventsWaitsDesc = prometheus.NewDesc( 34 | prometheus.BuildFQName(namespace, performanceSchema, "events_waits_total"), 35 | "The total events waits by event name.", 36 | []string{"event_name"}, nil, 37 | ) 38 | performanceSchemaEventsWaitsTimeDesc = prometheus.NewDesc( 39 | prometheus.BuildFQName(namespace, performanceSchema, "events_waits_seconds_total"), 40 | "The total seconds of events waits by event name.", 41 | []string{"event_name"}, nil, 42 | ) 43 | ) 44 | 45 | // ScrapePerfEventsWaits collects from `performance_schema.events_waits_summary_global_by_event_name`. 46 | type ScrapePerfEventsWaits struct{} 47 | 48 | // Name of the Scraper. Should be unique. 49 | func (ScrapePerfEventsWaits) Name() string { 50 | return "perf_schema.eventswaits" 51 | } 52 | 53 | // Help describes the role of the Scraper. 54 | func (ScrapePerfEventsWaits) Help() string { 55 | return "Collect metrics from performance_schema.events_waits_summary_global_by_event_name" 56 | } 57 | 58 | // Version of MySQL from which scraper is available. 59 | func (ScrapePerfEventsWaits) Version() float64 { 60 | return 5.5 61 | } 62 | 63 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 64 | func (ScrapePerfEventsWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 65 | // Timers here are returned in picoseconds. 66 | perfSchemaEventsWaitsRows, err := db.QueryContext(ctx, perfEventsWaitsQuery) 67 | if err != nil { 68 | return err 69 | } 70 | defer perfSchemaEventsWaitsRows.Close() 71 | 72 | var ( 73 | eventName string 74 | count, time uint64 75 | ) 76 | 77 | for perfSchemaEventsWaitsRows.Next() { 78 | if err := perfSchemaEventsWaitsRows.Scan( 79 | &eventName, &count, &time, 80 | ); err != nil { 81 | return err 82 | } 83 | ch <- prometheus.MustNewConstMetric( 84 | performanceSchemaEventsWaitsDesc, prometheus.CounterValue, float64(count), 85 | eventName, 86 | ) 87 | ch <- prometheus.MustNewConstMetric( 88 | performanceSchemaEventsWaitsTimeDesc, prometheus.CounterValue, float64(time)/picoSeconds, 89 | eventName, 90 | ) 91 | } 92 | return nil 93 | } 94 | 95 | // check interface 96 | var _ Scraper = ScrapePerfEventsWaits{} 97 | -------------------------------------------------------------------------------- /collector/perf_schema_file_events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.file_summary_by_event_name`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const perfFileEventsQuery = ` 27 | SELECT 28 | EVENT_NAME, 29 | COUNT_READ, SUM_TIMER_READ, SUM_NUMBER_OF_BYTES_READ, 30 | COUNT_WRITE, SUM_TIMER_WRITE, SUM_NUMBER_OF_BYTES_WRITE, 31 | COUNT_MISC, SUM_TIMER_MISC 32 | FROM performance_schema.file_summary_by_event_name 33 | ` 34 | 35 | // Metric descriptors. 36 | var ( 37 | performanceSchemaFileEventsDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_total"), 39 | "The total file events by event name/mode.", 40 | []string{"event_name", "mode"}, nil, 41 | ) 42 | performanceSchemaFileEventsTimeDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_seconds_total"), 44 | "The total seconds of file events by event name/mode.", 45 | []string{"event_name", "mode"}, nil, 46 | ) 47 | performanceSchemaFileEventsBytesDesc = prometheus.NewDesc( 48 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_bytes_total"), 49 | "The total bytes of file events by event name/mode.", 50 | []string{"event_name", "mode"}, nil, 51 | ) 52 | ) 53 | 54 | // ScrapePerfFileEvents collects from `performance_schema.file_summary_by_event_name`. 55 | type ScrapePerfFileEvents struct{} 56 | 57 | // Name of the Scraper. Should be unique. 58 | func (ScrapePerfFileEvents) Name() string { 59 | return "perf_schema.file_events" 60 | } 61 | 62 | // Help describes the role of the Scraper. 63 | func (ScrapePerfFileEvents) Help() string { 64 | return "Collect metrics from performance_schema.file_summary_by_event_name" 65 | } 66 | 67 | // Version of MySQL from which scraper is available. 68 | func (ScrapePerfFileEvents) Version() float64 { 69 | return 5.6 70 | } 71 | 72 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 73 | func (ScrapePerfFileEvents) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 74 | // Timers here are returned in picoseconds. 75 | perfSchemaFileEventsRows, err := db.QueryContext(ctx, perfFileEventsQuery) 76 | if err != nil { 77 | return err 78 | } 79 | defer perfSchemaFileEventsRows.Close() 80 | 81 | var ( 82 | eventName string 83 | countRead, timeRead, bytesRead uint64 84 | countWrite, timeWrite, bytesWrite uint64 85 | countMisc, timeMisc uint64 86 | ) 87 | for perfSchemaFileEventsRows.Next() { 88 | if err := perfSchemaFileEventsRows.Scan( 89 | &eventName, 90 | &countRead, &timeRead, &bytesRead, 91 | &countWrite, &timeWrite, &bytesWrite, 92 | &countMisc, &timeMisc, 93 | ); err != nil { 94 | return err 95 | } 96 | ch <- prometheus.MustNewConstMetric( 97 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countRead), 98 | eventName, "read", 99 | ) 100 | ch <- prometheus.MustNewConstMetric( 101 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeRead)/picoSeconds, 102 | eventName, "read", 103 | ) 104 | ch <- prometheus.MustNewConstMetric( 105 | performanceSchemaFileEventsBytesDesc, prometheus.CounterValue, float64(bytesRead), 106 | eventName, "read", 107 | ) 108 | ch <- prometheus.MustNewConstMetric( 109 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countWrite), 110 | eventName, "write", 111 | ) 112 | ch <- prometheus.MustNewConstMetric( 113 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeWrite)/picoSeconds, 114 | eventName, "write", 115 | ) 116 | ch <- prometheus.MustNewConstMetric( 117 | performanceSchemaFileEventsBytesDesc, prometheus.CounterValue, float64(bytesWrite), 118 | eventName, "write", 119 | ) 120 | ch <- prometheus.MustNewConstMetric( 121 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countMisc), 122 | eventName, "misc", 123 | ) 124 | ch <- prometheus.MustNewConstMetric( 125 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeMisc)/picoSeconds, 126 | eventName, "misc", 127 | ) 128 | } 129 | return nil 130 | } 131 | 132 | // check interface 133 | var _ Scraper = ScrapePerfFileEvents{} 134 | -------------------------------------------------------------------------------- /collector/perf_schema_file_instances.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.file_summary_by_instance`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "strings" 22 | 23 | "github.com/go-kit/log" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "gopkg.in/alecthomas/kingpin.v2" 26 | ) 27 | 28 | const perfFileInstancesQuery = ` 29 | SELECT 30 | FILE_NAME, EVENT_NAME, 31 | COUNT_READ, COUNT_WRITE, 32 | SUM_NUMBER_OF_BYTES_READ, SUM_NUMBER_OF_BYTES_WRITE 33 | FROM performance_schema.file_summary_by_instance 34 | where FILE_NAME REGEXP ? 35 | ` 36 | 37 | // Tunable flags. 38 | var ( 39 | performanceSchemaFileInstancesFilter = kingpin.Flag( 40 | "collect.perf_schema.file_instances.filter", 41 | "RegEx file_name filter for performance_schema.file_summary_by_instance", 42 | ).Default(".*").String() 43 | 44 | performanceSchemaFileInstancesRemovePrefix = kingpin.Flag( 45 | "collect.perf_schema.file_instances.remove_prefix", 46 | "Remove path prefix in performance_schema.file_summary_by_instance", 47 | ).Default("/var/lib/mysql/").String() 48 | ) 49 | 50 | // Metric descriptors. 51 | var ( 52 | performanceSchemaFileInstancesBytesDesc = prometheus.NewDesc( 53 | prometheus.BuildFQName(namespace, performanceSchema, "file_instances_bytes"), 54 | "The number of bytes processed by file read/write operations.", 55 | []string{"file_name", "event_name", "mode"}, nil, 56 | ) 57 | performanceSchemaFileInstancesCountDesc = prometheus.NewDesc( 58 | prometheus.BuildFQName(namespace, performanceSchema, "file_instances_total"), 59 | "The total number of file read/write operations.", 60 | []string{"file_name", "event_name", "mode"}, nil, 61 | ) 62 | ) 63 | 64 | // ScrapePerfFileInstances collects from `performance_schema.file_summary_by_instance`. 65 | type ScrapePerfFileInstances struct{} 66 | 67 | // Name of the Scraper. Should be unique. 68 | func (ScrapePerfFileInstances) Name() string { 69 | return "perf_schema.file_instances" 70 | } 71 | 72 | // Help describes the role of the Scraper. 73 | func (ScrapePerfFileInstances) Help() string { 74 | return "Collect metrics from performance_schema.file_summary_by_instance" 75 | } 76 | 77 | // Version of MySQL from which scraper is available. 78 | func (ScrapePerfFileInstances) Version() float64 { 79 | return 5.5 80 | } 81 | 82 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 83 | func (ScrapePerfFileInstances) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 84 | // Timers here are returned in picoseconds. 85 | perfSchemaFileInstancesRows, err := db.QueryContext(ctx, perfFileInstancesQuery, *performanceSchemaFileInstancesFilter) 86 | if err != nil { 87 | return err 88 | } 89 | defer perfSchemaFileInstancesRows.Close() 90 | 91 | var ( 92 | fileName, eventName string 93 | countRead, countWrite uint64 94 | sumBytesRead, sumBytesWritten uint64 95 | ) 96 | 97 | for perfSchemaFileInstancesRows.Next() { 98 | if err := perfSchemaFileInstancesRows.Scan( 99 | &fileName, &eventName, 100 | &countRead, &countWrite, 101 | &sumBytesRead, &sumBytesWritten, 102 | ); err != nil { 103 | return err 104 | } 105 | 106 | fileName = strings.TrimPrefix(fileName, *performanceSchemaFileInstancesRemovePrefix) 107 | ch <- prometheus.MustNewConstMetric( 108 | performanceSchemaFileInstancesCountDesc, prometheus.CounterValue, float64(countRead), 109 | fileName, eventName, "read", 110 | ) 111 | ch <- prometheus.MustNewConstMetric( 112 | performanceSchemaFileInstancesCountDesc, prometheus.CounterValue, float64(countWrite), 113 | fileName, eventName, "write", 114 | ) 115 | ch <- prometheus.MustNewConstMetric( 116 | performanceSchemaFileInstancesBytesDesc, prometheus.CounterValue, float64(sumBytesRead), 117 | fileName, eventName, "read", 118 | ) 119 | ch <- prometheus.MustNewConstMetric( 120 | performanceSchemaFileInstancesBytesDesc, prometheus.CounterValue, float64(sumBytesWritten), 121 | fileName, eventName, "write", 122 | ) 123 | 124 | } 125 | return nil 126 | } 127 | 128 | // check interface 129 | var _ Scraper = ScrapePerfFileInstances{} 130 | -------------------------------------------------------------------------------- /collector/perf_schema_file_instances_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | func TestScrapePerfFileInstances(t *testing.T) { 30 | _, err := kingpin.CommandLine.Parse([]string{"--collect.perf_schema.file_instances.filter", ""}) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | db, mock, err := sqlmock.New() 36 | if err != nil { 37 | t.Fatalf("error opening a stub database connection: %s", err) 38 | } 39 | defer db.Close() 40 | 41 | columns := []string{"FILE_NAME", "EVENT_NAME", "COUNT_READ", "COUNT_WRITE", "SUM_NUMBER_OF_BYTES_READ", "SUM_NUMBER_OF_BYTES_WRITE"} 42 | 43 | rows := sqlmock.NewRows(columns). 44 | AddRow("/var/lib/mysql/db1/file", "event1", "3", "4", "725", "128"). 45 | AddRow("/var/lib/mysql/db2/file", "event2", "23", "12", "3123", "967"). 46 | AddRow("db3/file", "event3", "45", "32", "1337", "326") 47 | mock.ExpectQuery(sanitizeQuery(perfFileInstancesQuery)).WillReturnRows(rows) 48 | 49 | ch := make(chan prometheus.Metric) 50 | go func() { 51 | if err = (ScrapePerfFileInstances{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 52 | panic(fmt.Sprintf("error calling function on test: %s", err)) 53 | } 54 | close(ch) 55 | }() 56 | 57 | metricExpected := []MetricResult{ 58 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "read"}, value: 3, metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "write"}, value: 4, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "read"}, value: 725, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "write"}, value: 128, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "read"}, value: 23, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "write"}, value: 12, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "read"}, value: 3123, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "write"}, value: 967, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "read"}, value: 45, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "write"}, value: 32, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "read"}, value: 1337, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "write"}, value: 326, metricType: dto.MetricType_COUNTER}, 70 | } 71 | convey.Convey("Metrics comparison", t, func() { 72 | for _, expect := range metricExpected { 73 | got := readMetric(<-ch) 74 | convey.So(got, convey.ShouldResemble, expect) 75 | } 76 | }) 77 | 78 | // Ensure all SQL queries were executed 79 | if err := mock.ExpectationsWereMet(); err != nil { 80 | t.Errorf("there were unfulfilled exceptions: %s", err) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /collector/perf_schema_index_io_waits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapePerfIndexIOWaits(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | 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"} 35 | rows := sqlmock.NewRows(columns). 36 | // Note, timers are in picoseconds. 37 | AddRow("database", "table", "index", "10", "11", "12", "13", "14000000000000", "15000000000000", "16000000000000", "17000000000000"). 38 | AddRow("database", "table", "NONE", "20", "21", "22", "23", "24000000000000", "25000000000000", "26000000000000", "27000000000000") 39 | mock.ExpectQuery(sanitizeQuery(perfIndexIOWaitsQuery)).WillReturnRows(rows) 40 | 41 | ch := make(chan prometheus.Metric) 42 | go func() { 43 | if err = (ScrapePerfIndexIOWaits{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 44 | t.Errorf("error calling function on test: %s", err) 45 | } 46 | close(ch) 47 | }() 48 | 49 | metricExpected := []MetricResult{ 50 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "fetch"}, value: 10, metricType: dto.MetricType_COUNTER}, 51 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "update"}, value: 12, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "delete"}, value: 13, metricType: dto.MetricType_COUNTER}, 53 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "fetch"}, value: 14, metricType: dto.MetricType_COUNTER}, 54 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "update"}, value: 16, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "delete"}, value: 17, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "fetch"}, value: 20, metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "insert"}, value: 21, metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "update"}, value: 22, metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "delete"}, value: 23, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "fetch"}, value: 24, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "insert"}, value: 25, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "update"}, value: 26, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "delete"}, value: 27, metricType: dto.MetricType_COUNTER}, 64 | } 65 | convey.Convey("Metrics comparison", t, func() { 66 | for _, expect := range metricExpected { 67 | got := readMetric(<-ch) 68 | convey.So(got, convey.ShouldResemble, expect) 69 | } 70 | }) 71 | 72 | // Ensure all SQL queries were executed 73 | if err := mock.ExpectationsWereMet(); err != nil { 74 | t.Errorf("there were unfulfilled exceptions: %s", err) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /collector/perf_schema_memory_events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.memory_summary_global_by_event_name`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "strings" 22 | 23 | "github.com/go-kit/log" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "gopkg.in/alecthomas/kingpin.v2" 26 | ) 27 | 28 | const perfMemoryEventsQuery = ` 29 | SELECT 30 | EVENT_NAME, SUM_NUMBER_OF_BYTES_ALLOC, SUM_NUMBER_OF_BYTES_FREE, 31 | CURRENT_NUMBER_OF_BYTES_USED 32 | FROM performance_schema.memory_summary_global_by_event_name 33 | where COUNT_ALLOC > 0; 34 | ` 35 | 36 | // Tunable flags. 37 | var ( 38 | performanceSchemaMemoryEventsRemovePrefix = kingpin.Flag( 39 | "collect.perf_schema.memory_events.remove_prefix", 40 | "Remove instrument prefix in performance_schema.memory_summary_global_by_event_name", 41 | ).Default("memory/").String() 42 | ) 43 | 44 | // Metric descriptors. 45 | var ( 46 | performanceSchemaMemoryBytesAllocDesc = prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_alloc_bytes_total"), 48 | "The total number of bytes allocated by events.", 49 | []string{"event_name"}, nil, 50 | ) 51 | performanceSchemaMemoryBytesFreeDesc = prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_free_bytes_total"), 53 | "The total number of bytes freed by events.", 54 | []string{"event_name"}, nil, 55 | ) 56 | perforanceSchemaMemoryUsedBytesDesc = prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_used_bytes"), 58 | "The number of bytes currently allocated by events.", 59 | []string{"event_name"}, nil, 60 | ) 61 | ) 62 | 63 | // ScrapePerfMemoryEvents collects from `performance_schema.memory_summary_global_by_event_name`. 64 | type ScrapePerfMemoryEvents struct{} 65 | 66 | // Name of the Scraper. Should be unique. 67 | func (ScrapePerfMemoryEvents) Name() string { 68 | return "perf_schema.memory_events" 69 | } 70 | 71 | // Help describes the role of the Scraper. 72 | func (ScrapePerfMemoryEvents) Help() string { 73 | return "Collect metrics from performance_schema.memory_summary_global_by_event_name" 74 | } 75 | 76 | // Version of MySQL from which scraper is available. 77 | func (ScrapePerfMemoryEvents) Version() float64 { 78 | return 5.7 79 | } 80 | 81 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 82 | func (ScrapePerfMemoryEvents) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 83 | perfSchemaMemoryEventsRows, err := db.QueryContext(ctx, perfMemoryEventsQuery) 84 | if err != nil { 85 | return err 86 | } 87 | defer perfSchemaMemoryEventsRows.Close() 88 | 89 | var ( 90 | eventName string 91 | bytesAlloc uint64 92 | bytesFree uint64 93 | currentBytes int64 94 | ) 95 | 96 | for perfSchemaMemoryEventsRows.Next() { 97 | if err := perfSchemaMemoryEventsRows.Scan( 98 | &eventName, &bytesAlloc, &bytesFree, ¤tBytes, 99 | ); err != nil { 100 | return err 101 | } 102 | 103 | eventName := strings.TrimPrefix(eventName, *performanceSchemaMemoryEventsRemovePrefix) 104 | ch <- prometheus.MustNewConstMetric( 105 | performanceSchemaMemoryBytesAllocDesc, prometheus.CounterValue, float64(bytesAlloc), eventName, 106 | ) 107 | ch <- prometheus.MustNewConstMetric( 108 | performanceSchemaMemoryBytesFreeDesc, prometheus.CounterValue, float64(bytesFree), eventName, 109 | ) 110 | ch <- prometheus.MustNewConstMetric( 111 | perforanceSchemaMemoryUsedBytesDesc, prometheus.GaugeValue, float64(currentBytes), eventName, 112 | ) 113 | } 114 | return nil 115 | } 116 | 117 | // check interface 118 | var _ Scraper = ScrapePerfMemoryEvents{} 119 | -------------------------------------------------------------------------------- /collector/perf_schema_memory_events_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | func TestScrapePerfMemoryEvents(t *testing.T) { 30 | _, err := kingpin.CommandLine.Parse([]string{}) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | db, mock, err := sqlmock.New() 36 | if err != nil { 37 | t.Fatalf("error opening a stub database connection: %s", err) 38 | } 39 | defer db.Close() 40 | 41 | columns := []string{ 42 | "EVENT_NAME", 43 | "SUM_NUMBER_OF_BYTES_ALLOC", 44 | "SUM_NUMBER_OF_BYTES_FREE", 45 | "CURRENT_NUMBER_OF_BYTES_USED", 46 | } 47 | 48 | rows := sqlmock.NewRows(columns). 49 | AddRow("memory/innodb/event1", "1001", "500", "501"). 50 | AddRow("memory/performance_schema/event1", "6000", "7", "-83904"). 51 | AddRow("memory/innodb/event2", "2002", "1000", "1002"). 52 | AddRow("memory/sql/event1", "30", "4", "26") 53 | mock.ExpectQuery(sanitizeQuery(perfMemoryEventsQuery)).WillReturnRows(rows) 54 | 55 | ch := make(chan prometheus.Metric) 56 | go func() { 57 | if err = (ScrapePerfMemoryEvents{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 58 | panic(fmt.Sprintf("error calling function on test: %s", err)) 59 | } 60 | close(ch) 61 | }() 62 | 63 | metricExpected := []MetricResult{ 64 | {labels: labelMap{"event_name": "innodb/event1"}, value: 1001, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"event_name": "innodb/event1"}, value: 500, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"event_name": "innodb/event1"}, value: 501, metricType: dto.MetricType_GAUGE}, 67 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: 6000, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: 7, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: -83904, metricType: dto.MetricType_GAUGE}, 70 | {labels: labelMap{"event_name": "innodb/event2"}, value: 2002, metricType: dto.MetricType_COUNTER}, 71 | {labels: labelMap{"event_name": "innodb/event2"}, value: 1000, metricType: dto.MetricType_COUNTER}, 72 | {labels: labelMap{"event_name": "innodb/event2"}, value: 1002, metricType: dto.MetricType_GAUGE}, 73 | {labels: labelMap{"event_name": "sql/event1"}, value: 30, metricType: dto.MetricType_COUNTER}, 74 | {labels: labelMap{"event_name": "sql/event1"}, value: 4, metricType: dto.MetricType_COUNTER}, 75 | {labels: labelMap{"event_name": "sql/event1"}, value: 26, metricType: dto.MetricType_GAUGE}, 76 | } 77 | convey.Convey("Metrics comparison", t, func() { 78 | for _, expect := range metricExpected { 79 | got := readMetric(<-ch) 80 | convey.So(got, convey.ShouldResemble, expect) 81 | } 82 | }) 83 | 84 | // Ensure all SQL queries were executed 85 | if err := mock.ExpectationsWereMet(); err != nil { 86 | t.Errorf("there were unfulfilled exceptions: %s", err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /collector/perf_schema_replication_applier_status_by_worker_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | "time" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/smartystreets/goconvey/convey" 26 | ) 27 | 28 | func TestScrapePerfReplicationApplierStatsByWorker(t *testing.T) { 29 | db, mock, err := sqlmock.New() 30 | if err != nil { 31 | t.Fatalf("error opening a stub database connection: %s", err) 32 | } 33 | defer db.Close() 34 | 35 | columns := []string{ 36 | "CHANNEL_NAME", 37 | "WORKER_ID", 38 | "LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP", 39 | "LAST_APPLIED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP", 40 | "LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP", 41 | "LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP", 42 | "APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP", 43 | "APPLYING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP", 44 | "APPLYING_TRANSACTION_START_APPLY_TIMESTAMP", 45 | } 46 | 47 | timeZero := "0000-00-00 00:00:00.000000" 48 | 49 | stubTime := time.Date(2019, 3, 14, 0, 0, 0, int(time.Millisecond), time.UTC) 50 | rows := sqlmock.NewRows(columns). 51 | AddRow("dummy_0", "0", timeZero, timeZero, timeZero, timeZero, timeZero, timeZero, timeZero). 52 | AddRow("dummy_1", "1", stubTime.Format(timeLayout), stubTime.Add(1*time.Minute).Format(timeLayout), stubTime.Add(2*time.Minute).Format(timeLayout), stubTime.Add(3*time.Minute).Format(timeLayout), stubTime.Add(4*time.Minute).Format(timeLayout), stubTime.Add(5*time.Minute).Format(timeLayout), stubTime.Add(6*time.Minute).Format(timeLayout)) 53 | mock.ExpectQuery(sanitizeQuery(perfReplicationApplierStatsByWorkerQuery)).WillReturnRows(rows) 54 | 55 | ch := make(chan prometheus.Metric) 56 | go func() { 57 | if err = (ScrapePerfReplicationApplierStatsByWorker{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 58 | t.Errorf("error calling function on test: %s", err) 59 | } 60 | close(ch) 61 | }() 62 | 63 | metricExpected := []MetricResult{ 64 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 65 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 66 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 67 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 68 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 69 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 70 | {labels: labelMap{"channel_name": "dummy_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 71 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521600001e+9, metricType: dto.MetricType_GAUGE}, 72 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521660001e+9, metricType: dto.MetricType_GAUGE}, 73 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521720001e+9, metricType: dto.MetricType_GAUGE}, 74 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521780001e+9, metricType: dto.MetricType_GAUGE}, 75 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521840001e+9, metricType: dto.MetricType_GAUGE}, 76 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521900001e+9, metricType: dto.MetricType_GAUGE}, 77 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521960001e+9, metricType: dto.MetricType_GAUGE}, 78 | } 79 | convey.Convey("Metrics comparison", t, func() { 80 | for _, expect := range metricExpected { 81 | got := readMetric(<-ch) 82 | convey.So(got, convey.ShouldResemble, expect) 83 | } 84 | }) 85 | 86 | // Ensure all SQL queries were executed 87 | if err := mock.ExpectationsWereMet(); err != nil { 88 | t.Errorf("there were unfulfilled exceptions: %s", err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /collector/perf_schema_replication_group_member_stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapePerfReplicationGroupMemberStats(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | columns := []string{ 35 | "CHANNEL_NAME", 36 | "VIEW_ID", 37 | "MEMBER_ID", 38 | "COUNT_TRANSACTIONS_IN_QUEUE", 39 | "COUNT_TRANSACTIONS_CHECKED", 40 | "COUNT_CONFLICTS_DETECTED", 41 | "COUNT_TRANSACTIONS_ROWS_VALIDATING", 42 | "TRANSACTIONS_COMMITTED_ALL_MEMBERS", 43 | "LAST_CONFLICT_FREE_TRANSACTION", 44 | "COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE", 45 | "COUNT_TRANSACTIONS_REMOTE_APPLIED", 46 | "COUNT_TRANSACTIONS_LOCAL_PROPOSED", 47 | "COUNT_TRANSACTIONS_LOCAL_ROLLBACK", 48 | } 49 | rows := sqlmock.NewRows(columns). 50 | AddRow( 51 | "group_replication_applier", 52 | "15813535259046852:43", 53 | "e14c4f71-025f-11ea-b800-0620049edbec", 54 | float64(0), 55 | float64(7389775), 56 | float64(1), 57 | float64(48), 58 | "0515b3c2-f59f-11e9-881b-0620049edbec:1-15270987,\n8f782839-34f7-11e7-a774-060ac4f023ae:4-39:2387-161606", 59 | "0515b3c2-f59f-11e9-881b-0620049edbec:15271011", 60 | float64(2), 61 | float64(22), 62 | float64(7389759), 63 | float64(7), 64 | ) 65 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMemberStatsQuery)).WillReturnRows(rows) 66 | 67 | ch := make(chan prometheus.Metric) 68 | go func() { 69 | if err = (ScrapePerfReplicationGroupMemberStats{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 70 | t.Errorf("error calling function on test: %s", err) 71 | } 72 | close(ch) 73 | }() 74 | 75 | expected := []MetricResult{ 76 | {labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE}, 77 | {labels: labelMap{}, value: float64(7389775), metricType: dto.MetricType_COUNTER}, 78 | {labels: labelMap{}, value: float64(1), metricType: dto.MetricType_COUNTER}, 79 | {labels: labelMap{}, value: float64(48), metricType: dto.MetricType_COUNTER}, 80 | {labels: labelMap{}, value: 2, metricType: dto.MetricType_GAUGE}, 81 | {labels: labelMap{}, value: float64(22), metricType: dto.MetricType_COUNTER}, 82 | {labels: labelMap{}, value: float64(7389759), metricType: dto.MetricType_COUNTER}, 83 | {labels: labelMap{}, value: float64(7), metricType: dto.MetricType_COUNTER}, 84 | } 85 | convey.Convey("Metrics comparison", t, func() { 86 | for _, expect := range expected { 87 | got := readMetric(<-ch) 88 | convey.So(expect, convey.ShouldResemble, got) 89 | } 90 | }) 91 | 92 | // Ensure all SQL queries were executed 93 | if err := mock.ExpectationsWereMet(); err != nil { 94 | t.Errorf("there were unfulfilled exceptions: %s", err) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /collector/perf_schema_replication_group_members.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "database/sql" 19 | "github.com/go-kit/log" 20 | "github.com/prometheus/client_golang/prometheus" 21 | "strings" 22 | ) 23 | 24 | const perfReplicationGroupMembersQuery = ` 25 | SELECT * FROM performance_schema.replication_group_members 26 | ` 27 | 28 | // ScrapeReplicationGroupMembers collects from `performance_schema.replication_group_members`. 29 | type ScrapePerfReplicationGroupMembers struct{} 30 | 31 | // Name of the Scraper. Should be unique. 32 | func (ScrapePerfReplicationGroupMembers) Name() string { 33 | return performanceSchema + ".replication_group_members" 34 | } 35 | 36 | // Help describes the role of the Scraper. 37 | func (ScrapePerfReplicationGroupMembers) Help() string { 38 | return "Collect metrics from performance_schema.replication_group_members" 39 | } 40 | 41 | // Version of MySQL from which scraper is available. 42 | func (ScrapePerfReplicationGroupMembers) Version() float64 { 43 | return 5.7 44 | } 45 | 46 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 47 | func (ScrapePerfReplicationGroupMembers) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 48 | perfReplicationGroupMembersRows, err := db.QueryContext(ctx, perfReplicationGroupMembersQuery) 49 | if err != nil { 50 | return err 51 | } 52 | defer perfReplicationGroupMembersRows.Close() 53 | 54 | var columnNames []string 55 | if columnNames, err = perfReplicationGroupMembersRows.Columns(); err != nil { 56 | return err 57 | } 58 | 59 | var scanArgs = make([]interface{}, len(columnNames)) 60 | for i := range scanArgs { 61 | scanArgs[i] = &sql.RawBytes{} 62 | } 63 | 64 | for perfReplicationGroupMembersRows.Next() { 65 | if err := perfReplicationGroupMembersRows.Scan(scanArgs...); err != nil { 66 | return err 67 | } 68 | 69 | var labelNames = make([]string, len(columnNames)) 70 | var values = make([]string, len(columnNames)) 71 | for i, columnName := range columnNames { 72 | labelNames[i] = strings.ToLower(columnName) 73 | values[i] = string(*scanArgs[i].(*sql.RawBytes)) 74 | } 75 | 76 | var performanceSchemaReplicationGroupMembersMemberDesc = prometheus.NewDesc( 77 | prometheus.BuildFQName(namespace, performanceSchema, "replication_group_member_info"), 78 | "Information about the replication group member: "+ 79 | "channel_name, member_id, member_host, member_port, member_state. "+ 80 | "(member_role and member_version where available)", 81 | labelNames, nil, 82 | ) 83 | 84 | ch <- prometheus.MustNewConstMetric(performanceSchemaReplicationGroupMembersMemberDesc, 85 | prometheus.GaugeValue, 1, values...) 86 | } 87 | return nil 88 | 89 | } 90 | 91 | // check interface 92 | var _ Scraper = ScrapePerfReplicationGroupMembers{} 93 | -------------------------------------------------------------------------------- /collector/perf_schema_table_io_waits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.table_io_waits_summary_by_table`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const perfTableIOWaitsQuery = ` 27 | SELECT 28 | OBJECT_SCHEMA, OBJECT_NAME, 29 | COUNT_FETCH, COUNT_INSERT, COUNT_UPDATE, COUNT_DELETE, 30 | SUM_TIMER_FETCH, SUM_TIMER_INSERT, SUM_TIMER_UPDATE, SUM_TIMER_DELETE 31 | FROM performance_schema.table_io_waits_summary_by_table 32 | WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema') 33 | ` 34 | 35 | // Metric descriptors. 36 | var ( 37 | performanceSchemaTableWaitsDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, performanceSchema, "table_io_waits_total"), 39 | "The total number of table I/O wait events for each table and operation.", 40 | []string{"schema", "name", "operation"}, nil, 41 | ) 42 | performanceSchemaTableWaitsTimeDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, performanceSchema, "table_io_waits_seconds_total"), 44 | "The total time of table I/O wait events for each table and operation.", 45 | []string{"schema", "name", "operation"}, nil, 46 | ) 47 | ) 48 | 49 | // ScrapePerfTableIOWaits collects from `performance_schema.table_io_waits_summary_by_table`. 50 | type ScrapePerfTableIOWaits struct{} 51 | 52 | // Name of the Scraper. Should be unique. 53 | func (ScrapePerfTableIOWaits) Name() string { 54 | return "perf_schema.tableiowaits" 55 | } 56 | 57 | // Help describes the role of the Scraper. 58 | func (ScrapePerfTableIOWaits) Help() string { 59 | return "Collect metrics from performance_schema.table_io_waits_summary_by_table" 60 | } 61 | 62 | // Version of MySQL from which scraper is available. 63 | func (ScrapePerfTableIOWaits) Version() float64 { 64 | return 5.6 65 | } 66 | 67 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 68 | func (ScrapePerfTableIOWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 69 | perfSchemaTableWaitsRows, err := db.QueryContext(ctx, perfTableIOWaitsQuery) 70 | if err != nil { 71 | return err 72 | } 73 | defer perfSchemaTableWaitsRows.Close() 74 | 75 | var ( 76 | objectSchema, objectName string 77 | countFetch, countInsert, countUpdate, countDelete uint64 78 | timeFetch, timeInsert, timeUpdate, timeDelete uint64 79 | ) 80 | 81 | for perfSchemaTableWaitsRows.Next() { 82 | if err := perfSchemaTableWaitsRows.Scan( 83 | &objectSchema, &objectName, &countFetch, &countInsert, &countUpdate, &countDelete, 84 | &timeFetch, &timeInsert, &timeUpdate, &timeDelete, 85 | ); err != nil { 86 | return err 87 | } 88 | ch <- prometheus.MustNewConstMetric( 89 | performanceSchemaTableWaitsDesc, prometheus.CounterValue, float64(countFetch), 90 | objectSchema, objectName, "fetch", 91 | ) 92 | ch <- prometheus.MustNewConstMetric( 93 | performanceSchemaTableWaitsDesc, prometheus.CounterValue, float64(countInsert), 94 | objectSchema, objectName, "insert", 95 | ) 96 | ch <- prometheus.MustNewConstMetric( 97 | performanceSchemaTableWaitsDesc, prometheus.CounterValue, float64(countUpdate), 98 | objectSchema, objectName, "update", 99 | ) 100 | ch <- prometheus.MustNewConstMetric( 101 | performanceSchemaTableWaitsDesc, prometheus.CounterValue, float64(countDelete), 102 | objectSchema, objectName, "delete", 103 | ) 104 | ch <- prometheus.MustNewConstMetric( 105 | performanceSchemaTableWaitsTimeDesc, prometheus.CounterValue, float64(timeFetch)/picoSeconds, 106 | objectSchema, objectName, "fetch", 107 | ) 108 | ch <- prometheus.MustNewConstMetric( 109 | performanceSchemaTableWaitsTimeDesc, prometheus.CounterValue, float64(timeInsert)/picoSeconds, 110 | objectSchema, objectName, "insert", 111 | ) 112 | ch <- prometheus.MustNewConstMetric( 113 | performanceSchemaTableWaitsTimeDesc, prometheus.CounterValue, float64(timeUpdate)/picoSeconds, 114 | objectSchema, objectName, "update", 115 | ) 116 | ch <- prometheus.MustNewConstMetric( 117 | performanceSchemaTableWaitsTimeDesc, prometheus.CounterValue, float64(timeDelete)/picoSeconds, 118 | objectSchema, objectName, "delete", 119 | ) 120 | } 121 | return nil 122 | } 123 | 124 | // check interface 125 | var _ Scraper = ScrapePerfTableIOWaits{} 126 | -------------------------------------------------------------------------------- /collector/plugins.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | 7 | "github.com/go-kit/log" 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | const ( 12 | pluginsQuery = `SHOW PLUGINS` 13 | ) 14 | 15 | var ( 16 | pluginDescription = prometheus.NewDesc( 17 | prometheus.BuildFQName(namespace, "", "plugin"), 18 | "MySQL plugin", 19 | []string{"name", "status", "type", "library", "license"}, nil, 20 | ) 21 | ) 22 | 23 | type plugin struct { 24 | Name sql.NullString 25 | Status sql.NullString 26 | Type sql.NullString 27 | Library sql.NullString 28 | License sql.NullString 29 | } 30 | 31 | // ScrapePlugins collects from `SHOW PLUGINS`. 32 | type ScrapePlugins struct{} 33 | 34 | // Name of the Scraper. Should be unique. 35 | func (ScrapePlugins) Name() string { 36 | return "plugins" 37 | } 38 | 39 | // Help describes the role of the Scraper. 40 | func (ScrapePlugins) Help() string { 41 | return "Collect from SHOW PLUGINS" 42 | } 43 | 44 | // Version of MySQL from which scraper is available. 45 | func (ScrapePlugins) Version() float64 { 46 | return 5.1 47 | } 48 | 49 | func (ScrapePlugins) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 50 | showPluginsRows, err := db.QueryContext(ctx, pluginsQuery) 51 | if err != nil { 52 | return err 53 | } 54 | defer showPluginsRows.Close() 55 | for showPluginsRows.Next() { 56 | var pluginVal plugin 57 | if err := showPluginsRows.Scan(&pluginVal.Name, &pluginVal.Status, &pluginVal.Type, &pluginVal.Library, &pluginVal.License); err != nil { 58 | return err 59 | } 60 | ch <- prometheus.MustNewConstMetric( 61 | pluginDescription, prometheus.GaugeValue, 1, 62 | pluginVal.Name.String, pluginVal.Status.String, pluginVal.Type.String, pluginVal.Library.String, pluginVal.License.String, 63 | ) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // check interface 70 | var _ Scraper = ScrapePlugins{} 71 | -------------------------------------------------------------------------------- /collector/plugins_test.go: -------------------------------------------------------------------------------- 1 | package collector 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/DATA-DOG/go-sqlmock" 8 | "github.com/go-kit/log" 9 | "github.com/prometheus/client_golang/prometheus" 10 | dto "github.com/prometheus/client_model/go" 11 | "github.com/smartystreets/goconvey/convey" 12 | ) 13 | 14 | func TestScrapePlugins(t *testing.T) { 15 | db, mock, err := sqlmock.New() 16 | if err != nil { 17 | t.Fatalf("error opening a stub database connection: %s", err) 18 | } 19 | defer db.Close() 20 | columns := []string{"Name", "Status", "Type", "Library", "License"} 21 | rows := sqlmock.NewRows(columns). 22 | AddRow("INNODB_SYS_COLUMNS", "ACTIVE", "INFORMATION SCHEMA", nil, "GPL"). 23 | AddRow("MRG_MYISAM", "ACTIVE", "STORAGE ENGINE", nil, "GPL"). 24 | AddRow(nil, nil, nil, nil, nil) 25 | mock.ExpectQuery(sanitizeQuery(pluginsQuery)).WillReturnRows(rows) 26 | 27 | ch := make(chan prometheus.Metric) 28 | go func() { 29 | if err = (ScrapePlugins{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 30 | t.Errorf("error calling function on test: %s", err) 31 | } 32 | close(ch) 33 | }() 34 | counterExpected := []MetricResult{ 35 | {labels: labelMap{"name": "INNODB_SYS_COLUMNS", "status": "ACTIVE", "type": "INFORMATION SCHEMA", "library": "", "license": "GPL"}, value: 1, metricType: dto.MetricType_GAUGE}, 36 | {labels: labelMap{"name": "MRG_MYISAM", "status": "ACTIVE", "type": "STORAGE ENGINE", "library": "", "license": "GPL"}, value: 1, metricType: dto.MetricType_GAUGE}, 37 | {labels: labelMap{"name": "", "status": "", "type": "", "library": "", "license": ""}, value: 1, metricType: dto.MetricType_GAUGE}, 38 | } 39 | convey.Convey("Metrics comparison", t, func() { 40 | for _, expect := range counterExpected { 41 | got := readMetric(<-ch) 42 | convey.So(got, convey.ShouldResemble, expect) 43 | } 44 | }) 45 | 46 | // Ensure all SQL queries were executed 47 | if err := mock.ExpectationsWereMet(); err != nil { 48 | t.Errorf("there were unfulfilled exceptions: %s", err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /collector/scraper.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "database/sql" 19 | 20 | "github.com/go-kit/log" 21 | _ "github.com/go-sql-driver/mysql" 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | // Scraper is minimal interface that let's you add new prometheus metrics to mysqld_exporter. 26 | type Scraper interface { 27 | // Name of the Scraper. Should be unique. 28 | Name() string 29 | 30 | // Help describes the role of the Scraper. 31 | // Example: "Collect from SHOW ENGINE INNODB STATUS" 32 | Help() string 33 | 34 | // Version of MySQL from which scraper is available. 35 | Version() float64 36 | 37 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 38 | Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error 39 | } 40 | -------------------------------------------------------------------------------- /collector/slave_hosts.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape heartbeat data. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | "github.com/google/uuid" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const ( 28 | // slavehosts is the Metric subsystem we use. 29 | slavehosts = "slave_hosts" 30 | // heartbeatQuery is the query used to fetch the stored and current 31 | // timestamps. %s will be replaced by the database and table name. 32 | // The second column allows gets the server timestamp at the exact same 33 | // time the query is run. 34 | slaveHostsQuery = "SHOW SLAVE HOSTS" 35 | ) 36 | 37 | // Metric descriptors. 38 | var ( 39 | SlaveHostsInfo = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, heartbeat, "mysql_slave_hosts_info"), 41 | "Information about running slaves", 42 | []string{"server_id", "slave_host", "port", "master_id", "slave_uuid"}, nil, 43 | ) 44 | ) 45 | 46 | // ScrapeSlaveHosts scrapes metrics about the replicating slaves. 47 | type ScrapeSlaveHosts struct{} 48 | 49 | // Name of the Scraper. Should be unique. 50 | func (ScrapeSlaveHosts) Name() string { 51 | return slavehosts 52 | } 53 | 54 | // Help describes the role of the Scraper. 55 | func (ScrapeSlaveHosts) Help() string { 56 | return "Scrape information from 'SHOW SLAVE HOSTS'" 57 | } 58 | 59 | // Version of MySQL from which scraper is available. 60 | func (ScrapeSlaveHosts) Version() float64 { 61 | return 5.1 62 | } 63 | 64 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 65 | func (ScrapeSlaveHosts) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 66 | slaveHostsRows, err := db.QueryContext(ctx, slaveHostsQuery) 67 | if err != nil { 68 | return err 69 | } 70 | defer slaveHostsRows.Close() 71 | 72 | // fields of row 73 | var serverId string 74 | var host string 75 | var port string 76 | var rrrOrMasterId string 77 | var slaveUuidOrMasterId string 78 | 79 | // Depends on the version of MySQL being scraped 80 | var masterId string 81 | var slaveUuid string 82 | 83 | columnNames, err := slaveHostsRows.Columns() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | for slaveHostsRows.Next() { 89 | // Newer versions of mysql have the following 90 | // Server_id, Host, Port, Master_id, Slave_UUID 91 | // Older versions of mysql have the following 92 | // Server_id, Host, Port, Rpl_recovery_rank, Master_id 93 | // MySQL 5.5 and MariaDB 10.5 have the following 94 | // Server_id, Host, Port, Master_id 95 | if len(columnNames) == 5 { 96 | err = slaveHostsRows.Scan(&serverId, &host, &port, &rrrOrMasterId, &slaveUuidOrMasterId) 97 | } else { 98 | err = slaveHostsRows.Scan(&serverId, &host, &port, &rrrOrMasterId) 99 | } 100 | if err != nil { 101 | return err 102 | } 103 | 104 | // if a Slave_UUID or Rpl_recovery_rank field is present 105 | if len(columnNames) == 5 { 106 | // Check to see if slaveUuidOrMasterId resembles a UUID or not 107 | // to find out if we are using an old version of MySQL 108 | if _, err = uuid.Parse(slaveUuidOrMasterId); err != nil { 109 | // We are running an older version of MySQL with no slave UUID 110 | slaveUuid = "" 111 | masterId = slaveUuidOrMasterId 112 | } else { 113 | // We are running a more recent version of MySQL 114 | slaveUuid = slaveUuidOrMasterId 115 | masterId = rrrOrMasterId 116 | } 117 | } else { 118 | slaveUuid = "" 119 | masterId = rrrOrMasterId 120 | } 121 | 122 | ch <- prometheus.MustNewConstMetric( 123 | SlaveHostsInfo, 124 | prometheus.GaugeValue, 125 | 1, 126 | serverId, 127 | host, 128 | port, 129 | masterId, 130 | slaveUuid, 131 | ) 132 | } 133 | 134 | return nil 135 | } 136 | 137 | // check interface 138 | var _ Scraper = ScrapeSlaveHosts{} 139 | -------------------------------------------------------------------------------- /collector/slave_status.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW SLAVE STATUS`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/go-kit/log" 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const ( 29 | // Subsystem. 30 | slaveStatus = "slave_status" 31 | ) 32 | 33 | var slaveStatusQueries = [2]string{"SHOW ALL SLAVES STATUS", "SHOW SLAVE STATUS"} 34 | var slaveStatusQuerySuffixes = [3]string{" NONBLOCKING", " NOLOCK", ""} 35 | 36 | func columnIndex(slaveCols []string, colName string) int { 37 | for idx := range slaveCols { 38 | if slaveCols[idx] == colName { 39 | return idx 40 | } 41 | } 42 | return -1 43 | } 44 | 45 | func columnValue(scanArgs []interface{}, slaveCols []string, colName string) string { 46 | var columnIndex = columnIndex(slaveCols, colName) 47 | if columnIndex == -1 { 48 | return "" 49 | } 50 | return string(*scanArgs[columnIndex].(*sql.RawBytes)) 51 | } 52 | 53 | // ScrapeSlaveStatus collects from `SHOW SLAVE STATUS`. 54 | type ScrapeSlaveStatus struct{} 55 | 56 | // Name of the Scraper. Should be unique. 57 | func (ScrapeSlaveStatus) Name() string { 58 | return slaveStatus 59 | } 60 | 61 | // Help describes the role of the Scraper. 62 | func (ScrapeSlaveStatus) Help() string { 63 | return "Collect from SHOW SLAVE STATUS" 64 | } 65 | 66 | // Version of MySQL from which scraper is available. 67 | func (ScrapeSlaveStatus) Version() float64 { 68 | return 5.1 69 | } 70 | 71 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 72 | func (ScrapeSlaveStatus) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 73 | var ( 74 | slaveStatusRows *sql.Rows 75 | err error 76 | ) 77 | // Try the both syntax for MySQL/Percona and MariaDB 78 | for _, query := range slaveStatusQueries { 79 | slaveStatusRows, err = db.QueryContext(ctx, query) 80 | if err != nil { // MySQL/Percona 81 | // Leverage lock-free SHOW SLAVE STATUS by guessing the right suffix 82 | for _, suffix := range slaveStatusQuerySuffixes { 83 | slaveStatusRows, err = db.QueryContext(ctx, fmt.Sprint(query, suffix)) 84 | if err == nil { 85 | break 86 | } 87 | } 88 | } else { // MariaDB 89 | break 90 | } 91 | } 92 | if err != nil { 93 | return err 94 | } 95 | defer slaveStatusRows.Close() 96 | 97 | slaveCols, err := slaveStatusRows.Columns() 98 | if err != nil { 99 | return err 100 | } 101 | 102 | for slaveStatusRows.Next() { 103 | // As the number of columns varies with mysqld versions, 104 | // and sql.Scan requires []interface{}, we need to create a 105 | // slice of pointers to the elements of slaveData. 106 | scanArgs := make([]interface{}, len(slaveCols)) 107 | for i := range scanArgs { 108 | scanArgs[i] = &sql.RawBytes{} 109 | } 110 | 111 | if err := slaveStatusRows.Scan(scanArgs...); err != nil { 112 | return err 113 | } 114 | 115 | masterUUID := columnValue(scanArgs, slaveCols, "Master_UUID") 116 | masterHost := columnValue(scanArgs, slaveCols, "Master_Host") 117 | channelName := columnValue(scanArgs, slaveCols, "Channel_Name") // MySQL & Percona 118 | connectionName := columnValue(scanArgs, slaveCols, "Connection_name") // MariaDB 119 | 120 | for i, col := range slaveCols { 121 | if value, ok := parseStatus(*scanArgs[i].(*sql.RawBytes)); ok { // Silently skip unparsable values. 122 | ch <- prometheus.MustNewConstMetric( 123 | prometheus.NewDesc( 124 | prometheus.BuildFQName(namespace, slaveStatus, strings.ToLower(col)), 125 | "Generic metric from SHOW SLAVE STATUS.", 126 | []string{"master_host", "master_uuid", "channel_name", "connection_name"}, 127 | nil, 128 | ), 129 | prometheus.UntypedValue, 130 | value, 131 | masterHost, masterUUID, channelName, connectionName, 132 | ) 133 | } 134 | } 135 | } 136 | return nil 137 | } 138 | 139 | // check interface 140 | var _ Scraper = ScrapeSlaveStatus{} 141 | -------------------------------------------------------------------------------- /collector/slave_status_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapeSlaveStatus(t *testing.T) { 28 | db, mock, err := sqlmock.New() 29 | if err != nil { 30 | t.Fatalf("error opening a stub database connection: %s", err) 31 | } 32 | defer db.Close() 33 | 34 | columns := []string{"Master_Host", "Read_Master_Log_Pos", "Slave_IO_Running", "Slave_SQL_Running", "Seconds_Behind_Master"} 35 | rows := sqlmock.NewRows(columns). 36 | AddRow("127.0.0.1", "1", "Connecting", "Yes", "2") 37 | mock.ExpectQuery(sanitizeQuery("SHOW SLAVE STATUS")).WillReturnRows(rows) 38 | 39 | ch := make(chan prometheus.Metric) 40 | go func() { 41 | if err = (ScrapeSlaveStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 42 | t.Errorf("error calling function on test: %s", err) 43 | } 44 | close(ch) 45 | }() 46 | 47 | counterExpected := []MetricResult{ 48 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 1, metricType: dto.MetricType_UNTYPED}, 49 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 0, metricType: dto.MetricType_UNTYPED}, 50 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 1, metricType: dto.MetricType_UNTYPED}, 51 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 2, metricType: dto.MetricType_UNTYPED}, 52 | } 53 | convey.Convey("Metrics comparison", t, func() { 54 | for _, expect := range counterExpected { 55 | got := readMetric(<-ch) 56 | convey.So(got, convey.ShouldResemble, expect) 57 | } 58 | }) 59 | 60 | // Ensure all SQL queries were executed 61 | if err := mock.ExpectationsWereMet(); err != nil { 62 | t.Errorf("there were unfulfilled exceptions: %s", err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # see CONTRIBUTING.md 2 | --- 3 | version: '3' 4 | services: 5 | mysql: 6 | image: ${MYSQL_IMAGE:-mysql/mysql-server:8.0.32} 7 | container_name: mysqld_exporter_db 8 | environment: 9 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes 10 | - MYSQL_ROOT_HOST=% 11 | - MYSQL_ROOT_PASSWORD= 12 | - INIT_ROCKSDB=1 13 | ports: 14 | - "3306:3306" 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/percona/mysqld_exporter 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/DATA-DOG/go-sqlmock v1.5.2 9 | github.com/go-kit/log v0.2.1 10 | github.com/go-sql-driver/mysql v1.9.2 11 | github.com/google/uuid v1.6.0 12 | github.com/montanaflynn/stats v0.7.1 13 | github.com/pkg/errors v0.9.1 14 | github.com/prometheus/client_golang v1.12.2 15 | github.com/prometheus/client_model v0.6.2 16 | github.com/prometheus/common v0.37.0 17 | github.com/prometheus/exporter-toolkit v0.7.3 18 | github.com/smartystreets/goconvey v1.8.1 19 | github.com/stretchr/testify v1.10.0 20 | github.com/tklauser/go-sysconf v0.3.15 21 | golang.org/x/sys v0.33.0 22 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 23 | gopkg.in/ini.v1 v1.66.6 24 | gopkg.in/yaml.v2 v2.4.0 25 | ) 26 | 27 | require ( 28 | filippo.io/edwards25519 v1.1.0 // indirect 29 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 30 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 33 | github.com/davecgh/go-spew v1.1.1 // indirect 34 | github.com/go-logfmt/logfmt v0.5.1 // indirect 35 | github.com/golang/protobuf v1.5.2 // indirect 36 | github.com/gopherjs/gopherjs v1.17.2 // indirect 37 | github.com/jpillora/backoff v1.0.0 // indirect 38 | github.com/jtolds/gls v4.20.0+incompatible // indirect 39 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 40 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/prometheus/procfs v0.7.3 // indirect 43 | github.com/smarty/assertions v1.15.0 // indirect 44 | github.com/tklauser/numcpus v0.10.0 // indirect 45 | golang.org/x/crypto v0.36.0 // indirect 46 | golang.org/x/net v0.38.0 // indirect 47 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 48 | golang.org/x/text v0.23.0 // indirect 49 | google.golang.org/appengine v1.6.6 // indirect 50 | google.golang.org/protobuf v1.36.6 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /mysqld-mixin/.gitignore: -------------------------------------------------------------------------------- 1 | /alerts.yaml 2 | /rules.yaml 3 | dashboards_out 4 | -------------------------------------------------------------------------------- /mysqld-mixin/Makefile: -------------------------------------------------------------------------------- 1 | JSONNET_FMT := jsonnetfmt -n 2 --max-blank-lines 2 --string-style s --comment-style s 2 | 3 | default: build 4 | 5 | all: fmt lint build clean 6 | 7 | fmt: 8 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 9 | xargs -n 1 -- $(JSONNET_FMT) -i 10 | 11 | lint: 12 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 13 | while read f; do \ 14 | $(JSONNET_FMT) "$$f" | diff -u "$$f" -; \ 15 | done 16 | 17 | mixtool lint mixin.libsonnet 18 | 19 | build: 20 | mixtool generate all mixin.libsonnet 21 | 22 | clean: 23 | rm -rf dashboards_out alerts.yaml rules.yaml 24 | -------------------------------------------------------------------------------- /mysqld-mixin/README.md: -------------------------------------------------------------------------------- 1 | # MySQLd Mixin 2 | 3 | The MySQLd Mixin is a set of configurable, reusable, and extensible alerts and 4 | dashboards based on the metrics exported by the MySQLd Exporter. The mixin creates 5 | recording and alerting rules for Prometheus and suitable dashboard descriptions 6 | for Grafana. 7 | 8 | To use them, you need to have `mixtool` and `jsonnetfmt` installed. If you 9 | have a working Go development environment, it's easiest to run the following: 10 | ```bash 11 | $ go get github.com/monitoring-mixins/mixtool/cmd/mixtool 12 | $ go get github.com/google/go-jsonnet/cmd/jsonnetfmt 13 | ``` 14 | 15 | You can then build the Prometheus rules files `alerts.yaml` and 16 | `rules.yaml` and a directory `dashboard_out` with the JSON dashboard files 17 | for Grafana: 18 | ```bash 19 | $ make build 20 | ``` 21 | 22 | For more advanced uses of mixins, see 23 | https://github.com/monitoring-mixins/docs. 24 | -------------------------------------------------------------------------------- /mysqld-mixin/alerts/galera.yaml: -------------------------------------------------------------------------------- 1 | ### 2 | # Sample prometheus rules/alerts for mysqld. 3 | # 4 | # NOTE: Please review these carefully as thresholds and behavior may not meet 5 | # your SLOs or labels. 6 | # 7 | ### 8 | 9 | groups: 10 | - name: GaleraAlerts 11 | rules: 12 | - alert: MySQLGaleraNotReady 13 | expr: mysql_global_status_wsrep_ready != 1 14 | for: 5m 15 | labels: 16 | severity: warning 17 | annotations: 18 | description: '{{$labels.job}} on {{$labels.instance}} is not ready.' 19 | summary: Galera cluster node not ready. 20 | - alert: MySQLGaleraOutOfSync 21 | expr: (mysql_global_status_wsrep_local_state != 4 and mysql_global_variables_wsrep_desync 22 | == 0) 23 | for: 5m 24 | labels: 25 | severity: warning 26 | annotations: 27 | description: '{{$labels.job}} on {{$labels.instance}} is not in sync ({{$value}} 28 | != 4).' 29 | summary: Galera cluster node out of sync. 30 | - alert: MySQLGaleraDonorFallingBehind 31 | expr: (mysql_global_status_wsrep_local_state == 2 and mysql_global_status_wsrep_local_recv_queue 32 | > 100) 33 | for: 5m 34 | labels: 35 | severity: warning 36 | annotations: 37 | description: '{{$labels.job}} on {{$labels.instance}} is a donor (hotbackup) 38 | and is falling behind (queue size {{$value}}).' 39 | summary: XtraDB cluster donor node falling behind. 40 | - alert: MySQLReplicationNotRunning 41 | expr: mysql_slave_status_slave_io_running == 0 or mysql_slave_status_slave_sql_running 42 | == 0 43 | for: 2m 44 | labels: 45 | severity: critical 46 | annotations: 47 | description: "Replication on {{$labels.instance}} (IO or SQL) has been down for more than 2 minutes." 48 | summary: Replication is not running. 49 | - alert: MySQLReplicationLag 50 | expr: (instance:mysql_slave_lag_seconds > 30) and on(instance) (predict_linear(instance:mysql_slave_lag_seconds[5m], 51 | 60 * 2) > 0) 52 | for: 1m 53 | labels: 54 | severity: critical 55 | annotations: 56 | description: "Replication on {{$labels.instance}} has fallen behind and is not recovering." 57 | summary: MySQL slave replication is lagging. 58 | - alert: MySQLHeartbeatLag 59 | expr: (instance:mysql_heartbeat_lag_seconds > 30) and on(instance) (predict_linear(instance:mysql_heartbeat_lag_seconds[5m], 60 | 60 * 2) > 0) 61 | for: 1m 62 | labels: 63 | severity: critical 64 | annotations: 65 | description: "The heartbeat is lagging on {{$labels.instance}} and is not recovering." 66 | summary: MySQL heartbeat is lagging. 67 | - alert: MySQLInnoDBLogWaits 68 | expr: rate(mysql_global_status_innodb_log_waits[15m]) > 10 69 | labels: 70 | severity: warning 71 | annotations: 72 | description: The innodb logs are waiting for disk at a rate of {{$value}} / 73 | second 74 | summary: MySQL innodb log writes stalling. 75 | -------------------------------------------------------------------------------- /mysqld-mixin/alerts/general.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: MySQLdAlerts 3 | rules: 4 | - alert: MySQLDown 5 | expr: mysql_up != 1 6 | for: 5m 7 | labels: 8 | severity: critical 9 | annotations: 10 | description: 'MySQL {{$labels.job}} on {{$labels.instance}} is not up.' 11 | summary: MySQL not up. 12 | -------------------------------------------------------------------------------- /mysqld-mixin/mixin.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | grafanaDashboards: { 3 | 'mysql-overview.json': (import 'dashboards/mysql-overview.json'), 4 | }, 5 | 6 | // Helper function to ensure that we don't override other rules, by forcing 7 | // the patching of the groups list, and not the overall rules object. 8 | local importRules(rules) = { 9 | groups+: std.native('parseYaml')(rules)[0].groups, 10 | }, 11 | 12 | prometheusRules+: importRules(importstr 'rules/rules.yaml'), 13 | 14 | prometheusAlerts+: 15 | importRules(importstr 'alerts/general.yaml') + 16 | importRules(importstr 'alerts/galera.yaml'), 17 | } 18 | -------------------------------------------------------------------------------- /mysqld-mixin/rules/rules.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mysqld_rules 3 | rules: 4 | 5 | # Record slave lag seconds for pre-computed timeseries that takes 6 | # `mysql_slave_status_sql_delay` into account 7 | - record: instance:mysql_slave_lag_seconds 8 | expr: mysql_slave_status_seconds_behind_master - mysql_slave_status_sql_delay 9 | 10 | # Record slave lag via heartbeat method 11 | - record: instance:mysql_heartbeat_lag_seconds 12 | expr: mysql_heartbeat_now_timestamp_seconds - mysql_heartbeat_stored_timestamp_seconds 13 | 14 | - record: job:mysql_transactions:rate5m 15 | expr: sum without (command) (rate(mysql_global_status_commands_total{command=~"(commit|rollback)"}[5m])) 16 | -------------------------------------------------------------------------------- /percona/perconacollector/exporter_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package perconacollector 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/DATA-DOG/go-sqlmock" 20 | "github.com/go-kit/log" 21 | cl "github.com/percona/mysqld_exporter/collector" 22 | "github.com/smartystreets/goconvey/convey" 23 | ) 24 | 25 | func TestGetMySQLVersion_Percona(t *testing.T) { 26 | if testing.Short() { 27 | t.Skip("-short is passed, skipping test") 28 | } 29 | 30 | db, mock, err := sqlmock.New() 31 | if err != nil { 32 | t.Fatalf("error opening a stub database connection: %s", err) 33 | } 34 | defer db.Close() 35 | 36 | logger := log.NewNopLogger() 37 | convey.Convey("MySQL version extract", t, func() { 38 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("")) 39 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 999) 40 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("something")) 41 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 999) 42 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("10.1.17-MariaDB")) 43 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 10.1) 44 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.7.13-6-log")) 45 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.7) 46 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.6.30-76.3-56-log")) 47 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.6) 48 | mock.ExpectQuery(cl.VersionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.5.51-38.1")) 49 | convey.So(cl.GetMySQLVersion(db, logger), convey.ShouldEqual, 5.5) 50 | }) 51 | 52 | // Ensure all SQL queries were executed 53 | if err := mock.ExpectationsWereMet(); err != nil { 54 | t.Errorf("there were unfulfilled expections: %s", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /percona/perconacollector/info_schema_innodb_cmpmem.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `information_schema.INNODB_CMPMEM`. 15 | 16 | package perconacollector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | 22 | "github.com/go-kit/log" 23 | cl "github.com/percona/mysqld_exporter/collector" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const innodbCmpMemQuery = ` 28 | SELECT 29 | page_size, buffer_pool_instance, pages_used, pages_free, relocation_ops, relocation_time 30 | FROM information_schema.innodb_cmpmem 31 | ` 32 | 33 | // Metric descriptors. 34 | var ( 35 | infoSchemaInnodbCmpMemPagesRead = prometheus.NewDesc( 36 | prometheus.BuildFQName(cl.Namespace, cl.InformationSchema, "innodb_cmpmem_pages_used_total"), 37 | "Number of blocks of the size PAGE_SIZE that are currently in use.", 38 | []string{"page_size", "buffer"}, nil, 39 | ) 40 | infoSchemaInnodbCmpMemPagesFree = prometheus.NewDesc( 41 | prometheus.BuildFQName(cl.Namespace, cl.InformationSchema, "innodb_cmpmem_pages_free_total"), 42 | "Number of blocks of the size PAGE_SIZE that are currently available for allocation.", 43 | []string{"page_size", "buffer"}, nil, 44 | ) 45 | infoSchemaInnodbCmpMemRelocationOps = prometheus.NewDesc( 46 | prometheus.BuildFQName(cl.Namespace, cl.InformationSchema, "innodb_cmpmem_relocation_ops_total"), 47 | "Number of times a block of the size PAGE_SIZE has been relocated.", 48 | []string{"page_size", "buffer"}, nil, 49 | ) 50 | infoSchemaInnodbCmpMemRelocationTime = prometheus.NewDesc( 51 | prometheus.BuildFQName(cl.Namespace, cl.InformationSchema, "innodb_cmpmem_relocation_time_seconds_total"), 52 | "Total time in seconds spent in relocating blocks.", 53 | []string{"page_size", "buffer"}, nil, 54 | ) 55 | ) 56 | 57 | // ScrapeInnodbCmp collects from `information_schema.innodb_cmp`. 58 | type ScrapeInnodbCmpMem struct{} 59 | 60 | // Name of the Scraper. Should be unique. 61 | func (ScrapeInnodbCmpMem) Name() string { 62 | return cl.InformationSchema + ".innodb_cmpmem" 63 | } 64 | 65 | // Help describes the role of the Scraper. 66 | func (ScrapeInnodbCmpMem) Help() string { 67 | return "Collect metrics from information_schema.innodb_cmpmem" 68 | } 69 | 70 | // Version of MySQL from which scraper is available. 71 | func (ScrapeInnodbCmpMem) Version() float64 { 72 | return 5.5 73 | } 74 | 75 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 76 | func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 77 | informationSchemaInnodbCmpMemRows, err := db.QueryContext(ctx, innodbCmpMemQuery) 78 | if err != nil { 79 | return err 80 | } 81 | defer informationSchemaInnodbCmpMemRows.Close() 82 | 83 | var ( 84 | page_size, buffer_pool string 85 | pages_used, pages_free, relocation_ops, relocation_time float64 86 | ) 87 | 88 | for informationSchemaInnodbCmpMemRows.Next() { 89 | if err := informationSchemaInnodbCmpMemRows.Scan( 90 | &page_size, &buffer_pool, &pages_used, &pages_free, &relocation_ops, &relocation_time, 91 | ); err != nil { 92 | return err 93 | } 94 | 95 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesRead, prometheus.CounterValue, pages_used, page_size, buffer_pool) 96 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesFree, prometheus.CounterValue, pages_free, page_size, buffer_pool) 97 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationOps, prometheus.CounterValue, relocation_ops, page_size, buffer_pool) 98 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationTime, prometheus.CounterValue, (relocation_time / 1000), page_size, buffer_pool) 99 | } 100 | return nil 101 | } 102 | 103 | // check interface 104 | var _ cl.Scraper = ScrapeInnodbCmpMem{} 105 | -------------------------------------------------------------------------------- /percona/perconacollector/standard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package perconacollector 15 | 16 | import ( 17 | "context" 18 | "database/sql" 19 | 20 | "github.com/go-kit/log" 21 | cl "github.com/percona/mysqld_exporter/collector" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/collectors" 24 | ) 25 | 26 | type standardGo struct { 27 | c prometheus.Collector 28 | } 29 | 30 | func NewStandardGo() cl.Scraper { 31 | return standardGo{ 32 | c: collectors.NewGoCollector(), 33 | } 34 | } 35 | 36 | // Name of the Scraper. 37 | func (standardGo) Name() string { 38 | return "standard.go" 39 | } 40 | 41 | // Help returns additional information about Scraper. 42 | func (standardGo) Help() string { 43 | return "Collect exporter Go process metrics" 44 | } 45 | 46 | // Version of MySQL from which scraper is available. 47 | func (standardGo) Version() float64 { 48 | return 0 49 | } 50 | 51 | // Scrape collects data. 52 | func (s standardGo) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 53 | s.c.Collect(ch) 54 | return nil 55 | } 56 | 57 | type standardProcess struct { 58 | c prometheus.Collector 59 | } 60 | 61 | func NewStandardProcess() cl.Scraper { 62 | return standardProcess{ 63 | c: collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), 64 | } 65 | } 66 | 67 | // Name of the Scraper. 68 | func (standardProcess) Name() string { 69 | return "standard.process" 70 | } 71 | 72 | // Help returns additional information about Scraper. 73 | func (standardProcess) Help() string { 74 | return "Collect exporter process metrics" 75 | } 76 | 77 | // Version of MySQL from which scraper is available. 78 | func (standardProcess) Version() float64 { 79 | return 0 80 | } 81 | 82 | // Scrape collects data. 83 | func (s standardProcess) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 84 | s.c.Collect(ch) 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /percona/tests/Makefile: -------------------------------------------------------------------------------- 1 | ######################### 2 | ### tests 3 | 4 | # measures avg scrape time and compares old vs new exporters 5 | test-performance: 6 | go test -v -run '^TestPerformance$$' -args -doRun=true 7 | 8 | extraMetrics = false 9 | multipleLabels = false 10 | dumpMetrics = false 11 | 12 | test-metrics: 13 | go test -v -run '^TestMissingMetrics$$' -args -doRun=true 14 | 15 | test-labels: 16 | go test -v -run '^TestMissingLabels$$' -args -doRun=true 17 | 18 | test-resolutions-duplicates: 19 | go test -v -run '^TestResolutionsMetricDuplicates$$' -args -doRun=true 20 | 21 | test-resolutions: 22 | go test -v -run '^TestResolutions$$' -args -doRun=true 23 | 24 | dump-metrics: 25 | go test -v -run '^TestDumpMetrics$$' -args -doRun=true -extraMetrics=$(extraMetrics) -multipleLabels=$(multipleLabels) -dumpMetrics=true 26 | 27 | test-consistency: test-metrics test-resolutions test-resolutions-duplicates 28 | 29 | ######################### 30 | ### env preparation 31 | 32 | # download exporter from provided feature build's client binary url 33 | prepare-exporter: 34 | go test -v -run '^TestPrepareUpdatedExporter$\' -args -doRun=true -url=$(url) 35 | 36 | prepare-exporter-from-repo: 37 | make -C ../../ build && cp ../../mysqld_exporter assets/mysqld_exporter 38 | 39 | prepare-base-exporter: 40 | tar -xf assets/mysqld_exporter_percona.tar.xz -C assets/ 41 | 42 | start-mysql-db: 43 | docker-compose -f assets/mysql-compose.yml up -d --force-recreate --renew-anon-volumes --remove-orphans 44 | 45 | stop-mysql-db: 46 | docker-compose -f assets/mysql-compose.yml down 47 | 48 | prepare-env-from-repo: prepare-exporter-from-repo prepare-base-exporter start-mysql-db 49 | -------------------------------------------------------------------------------- /percona/tests/assets/mysql-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.7' 3 | services: 4 | db: 5 | image: mysql:5.7 6 | container_name: mysql-test-srv 7 | environment: 8 | - MYSQL_DATABASE=db 9 | - MYSQL_USER=mysqlcred 10 | - MYSQL_PASSWORD=mysqlcred 11 | - MYSQL_ROOT_PASSWORD=mysqlcred 12 | ports: 13 | - "127.0.0.1:3306:3306" 14 | volumes: 15 | - mysql-test-srv-vol:/var/lib/mysql 16 | networks: 17 | - mysql-test-srv-net 18 | 19 | volumes: 20 | mysql-test-srv-vol: 21 | 22 | networks: 23 | mysql-test-srv-net: 24 | -------------------------------------------------------------------------------- /percona/tests/assets/mysqld_exporter_percona.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/percona/mysqld_exporter/fb7182ebfebb201a6e5e805835c3c23abc51009a/percona/tests/assets/mysqld_exporter_percona.tar.xz -------------------------------------------------------------------------------- /percona/tests/assets/test.exporter-flags.txt: -------------------------------------------------------------------------------- 1 | --collect.binlog_size 2 | --collect.engine_tokudb_status 3 | --collect.global_variables 4 | --collect.heartbeat 5 | --collect.info_schema.clientstats 6 | --collect.info_schema.userstats 7 | --collect.perf_schema.eventsstatements 8 | --collect.custom_query.lr 9 | --collect.engine_innodb_status 10 | --collect.info_schema.innodb_cmp 11 | --collect.info_schema.innodb_cmpmem 12 | --collect.info_schema.processlist 13 | --collect.info_schema.query_response_time 14 | --collect.perf_schema.eventswaits 15 | --collect.perf_schema.file_events 16 | --collect.slave_status 17 | --collect.custom_query.mr 18 | --collect.global_status 19 | --collect.info_schema.innodb_metrics 20 | --collect.custom_query.hr 21 | --collect.standard.go 22 | --collect.standard.process 23 | --exporter.max-idle-conns=3 24 | --exporter.max-open-conns=3 25 | --exporter.conn-max-lifetime=55s 26 | --exporter.global-conn-pool 27 | --collect.info_schema.innodb_tablespaces 28 | --collect.auto_increment.columns 29 | --collect.info_schema.tables 30 | --collect.info_schema.tablestats 31 | --collect.perf_schema.indexiowaits 32 | --collect.perf_schema.tableiowaits 33 | --collect.perf_schema.file_instances 34 | --collect.perf_schema.tablelocks -------------------------------------------------------------------------------- /percona/tests/custom-queries/high-resolution/queries-mysqld.yml: -------------------------------------------------------------------------------- 1 | ## Custom query example. 2 | #mysql_performance_schema: ## The namespace (prefix of the metric name) for the custom query. See https://prometheus.io/docs/practices/naming/#metric-names for details. 3 | # query: "SELECT event_name, current_count, high_count FROM sys.memory_global_by_current_bytes WHERE current_count > 0;" 4 | # metrics: ## List of metrics. 5 | # - event_name: ## The alias mapped to a value returned by a query to the Prometheus label https://prometheus.io/docs/practices/naming/#labels 6 | # usage: "LABEL" ## If usage is LABEL this value will be used as Prometheus dimension. 7 | # description: "Performance Schema Event Name" 8 | # - current_count: ## The name of the metric. See https://prometheus.io/docs/practices/naming/#metric-names 9 | # usage: "GAUGE" ## The type of the metric. It should be be the one from the following: COUNTER, GAUGE, MAPPEDMETRIC, DURATION, DISCARD. 10 | # description: "Memory currently allocated to the Event" 11 | # - high_count: 12 | # usage: "GAUGE" 13 | # description: "High Water Mark of Memory allocated to the Event" ## Description of the metric. 14 | 15 | -------------------------------------------------------------------------------- /percona/tests/custom-queries/low-resolution/queries-mysqld.yml: -------------------------------------------------------------------------------- 1 | ## Custom query example. 2 | #mysql_performance_schema: ## The namespace (prefix of the metric name) for the custom query. See https://prometheus.io/docs/practices/naming/#metric-names for details. 3 | # query: "SELECT event_name, current_count, high_count FROM sys.memory_global_by_current_bytes WHERE current_count > 0;" 4 | # metrics: ## List of metrics. 5 | # - event_name: ## The alias mapped to a value returned by a query to the Prometheus label https://prometheus.io/docs/practices/naming/#labels 6 | # usage: "LABEL" ## If usage is LABEL this value will be used as Prometheus dimension. 7 | # description: "Performance Schema Event Name" 8 | # - current_count: ## The name of the metric. See https://prometheus.io/docs/practices/naming/#metric-names 9 | # usage: "GAUGE" ## The type of the metric. It should be be the one from the following: COUNTER, GAUGE, MAPPEDMETRIC, DURATION, DISCARD. 10 | # description: "Memory currently allocated to the Event" 11 | # - high_count: 12 | # usage: "GAUGE" 13 | # description: "High Water Mark of Memory allocated to the Event" ## Description of the metric. 14 | 15 | -------------------------------------------------------------------------------- /percona/tests/custom-queries/medium-resolution/queries-mysqld.yml: -------------------------------------------------------------------------------- 1 | ## Custom query example. 2 | #mysql_performance_schema: ## The namespace (prefix of the metric name) for the custom query. See https://prometheus.io/docs/practices/naming/#metric-names for details. 3 | # query: "SELECT event_name, current_count, high_count FROM sys.memory_global_by_current_bytes WHERE current_count > 0;" 4 | # metrics: ## List of metrics. 5 | # - event_name: ## The alias mapped to a value returned by a query to the Prometheus label https://prometheus.io/docs/practices/naming/#labels 6 | # usage: "LABEL" ## If usage is LABEL this value will be used as Prometheus dimension. 7 | # description: "Performance Schema Event Name" 8 | # - current_count: ## The name of the metric. See https://prometheus.io/docs/practices/naming/#metric-names 9 | # usage: "GAUGE" ## The type of the metric. It should be be the one from the following: COUNTER, GAUGE, MAPPEDMETRIC, DURATION, DISCARD. 10 | # description: "Memory currently allocated to the Event" 11 | # - high_count: 12 | # usage: "GAUGE" 13 | # description: "High Water Mark of Memory allocated to the Event" ## Description of the metric. 14 | 15 | -------------------------------------------------------------------------------- /percona/tests/env_prepare_test.go: -------------------------------------------------------------------------------- 1 | package percona_tests 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | // TestPrepareExporters extracts exporter from client binary's tar.gz 16 | func TestPrepareUpdatedExporter(t *testing.T) { 17 | if doRun == nil || !*doRun { 18 | t.Skip("For manual runs only through make") 19 | return 20 | } 21 | 22 | if url == nil || *url == "" { 23 | t.Error("URL not defined") 24 | return 25 | } 26 | 27 | prepareExporter(*url, updatedExporterFileName) 28 | } 29 | 30 | func extractExporter(gzipStream io.Reader, fileName string) { 31 | uncompressedStream, err := gzip.NewReader(gzipStream) 32 | if err != nil { 33 | log.Fatal("ExtractTarGz: NewReader failed") 34 | } 35 | 36 | tarReader := tar.NewReader(uncompressedStream) 37 | 38 | exporterFound := false 39 | for !exporterFound { 40 | header, err := tarReader.Next() 41 | 42 | if err == io.EOF { 43 | break 44 | } 45 | 46 | if err != nil { 47 | log.Fatalf("ExtractTarGz: Next() failed: %s", err.Error()) 48 | } 49 | 50 | switch header.Typeflag { 51 | case tar.TypeDir: 52 | continue 53 | case tar.TypeReg: 54 | if strings.HasSuffix(header.Name, "postgres_exporter") { 55 | outFile, err := os.Create(fileName) 56 | if err != nil { 57 | log.Fatalf("ExtractTarGz: Create() failed: %s", err.Error()) 58 | } 59 | defer outFile.Close() 60 | if _, err := io.Copy(outFile, tarReader); err != nil { 61 | log.Fatalf("ExtractTarGz: Copy() failed: %s", err.Error()) 62 | } 63 | 64 | exporterFound = true 65 | } 66 | default: 67 | log.Fatalf( 68 | "ExtractTarGz: uknown type: %d in %s", 69 | header.Typeflag, 70 | header.Name) 71 | } 72 | } 73 | } 74 | 75 | func prepareExporter(url, fileName string) { 76 | resp, err := http.Get(url) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | defer resp.Body.Close() 82 | 83 | extractExporter(resp.Body, fileName) 84 | 85 | err = exec.Command("chmod", "+x", fileName).Run() 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /percona/tests/readme.md: -------------------------------------------------------------------------------- 1 | ### integration tests for exporter update 2 | 3 | basic usage: 4 | 5 | 1. unpack original exporter 6 | 7 | 8 | make prepare-base-exporter 9 | 10 | 2.a. download updated exporter from specific feature build 11 | 12 | make prepare-exporter url="" 13 | 14 | 2.b. or use current repo as updated exporter 15 | 16 | make prepare-exporter-from-repo 17 | 18 | 3. start test postgres_server 19 | 20 | 21 | make start-postgres-db 22 | 23 | 4. run basic performance comparison test 24 | 25 | 26 | make test-performance 27 | 28 | 5. run metrics list compatibility test 29 | 30 | 31 | make test-metrics 32 | 33 | -------------------------------------------------------------------------------- /queries-mysqld.yml: -------------------------------------------------------------------------------- 1 | ## Custom query example. 2 | #mysql_performance_schema: ## The namespace (prefix of the metric name) for the custom query. See https://prometheus.io/docs/practices/naming/#metric-names for details. 3 | # query: "SELECT event_name, current_count, high_count FROM sys.memory_global_by_current_bytes WHERE current_count > 0;" 4 | # metrics: ## List of metrics. 5 | # - event_name: ## The alias mapped to a value returned by a query to the Prometheus label https://prometheus.io/docs/practices/naming/#labels 6 | # usage: "LABEL" ## If usage is LABEL this value will be used as Prometheus dimension. 7 | # description: "Performance Schema Event Name" 8 | # - current_count: ## The name of the metric. See https://prometheus.io/docs/practices/naming/#metric-names 9 | # usage: "GAUGE" ## The type of the metric. It should be be the one from the following: COUNTER, GAUGE, MAPPEDMETRIC, DURATION, DISCARD. 10 | # description: "Memory currently allocated to the Event" 11 | # - high_count: 12 | # usage: "GAUGE" 13 | # description: "High Water Mark of Memory allocated to the Event" ## Description of the metric. 14 | 15 | -------------------------------------------------------------------------------- /test_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -exo pipefail 3 | 4 | docker_image=$1 5 | port=$2 6 | 7 | container_id='' 8 | 9 | wait_start() { 10 | for in in {1..10}; do 11 | if /usr/bin/curl -s -m 5 -f "http://localhost:${port}/metrics" > /dev/null; then 12 | docker_cleanup 13 | exit 0 14 | else 15 | sleep 1 16 | fi 17 | done 18 | 19 | exit 1 20 | } 21 | 22 | docker_start() { 23 | container_id=$(docker run -d --network mysql-test -e DATA_SOURCE_NAME="root:secret@(mysql-test:3306)/" -p "${port}":"${port}" "${docker_image}") 24 | } 25 | 26 | docker_cleanup() { 27 | docker kill "${container_id}" 28 | } 29 | 30 | if [[ "$#" -ne 2 ]] ; then 31 | echo "Usage: $0 quay.io/prometheus/mysqld-exporter:v0.10.0 9104" >&2 32 | exit 1 33 | fi 34 | 35 | docker_start 36 | wait_start 37 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // mysql_exporter 2 | 3 | //go:build tools 4 | // +build tools 5 | 6 | package tools 7 | 8 | import ( 9 | _ "github.com/golangci/golangci-lint/cmd/golangci-lint" 10 | _ "github.com/prometheus/promu" 11 | _ "github.com/reviewdog/reviewdog/cmd/reviewdog" 12 | ) 13 | --------------------------------------------------------------------------------