├── VERSION ├── MAINTAINERS.md ├── config ├── testdata │ ├── missing_password.cnf │ ├── missing_user.cnf │ ├── child_client.cnf │ └── client.cnf └── config_test.go ├── mysqld-mixin ├── .gitignore ├── alerts │ ├── general.yaml │ └── galera.yaml ├── mixin.libsonnet ├── Makefile ├── rules │ └── rules.yaml └── README.md ├── NOTICE ├── .github ├── dependabot.yml ├── workflows │ └── golangci-lint.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── .golangci.yml ├── CODE_OF_CONDUCT.md ├── test_exporter.cnf ├── SECURITY.md ├── Dockerfile ├── .promu.yml ├── collector ├── mysql.go ├── info_schema.go ├── perf_schema.go ├── scraper.go ├── collector_test.go ├── exporter_test.go ├── binlog_test.go ├── info_schema_innodb_cmpmem_test.go ├── info_schema_schemastats_test.go ├── info_schema_innodb_cmp_test.go ├── collector.go ├── slave_status_test.go ├── engine_tokudb.go ├── info_schema_tablestats_test.go ├── info_schema_query_response_time_test.go ├── perf_schema_events_waits.go ├── info_schema_innodb_sys_tablespaces_test.go ├── heartbeat_test.go ├── perf_schema_replication_group_members.go ├── engine_innodb.go ├── engine_tokudb_test.go ├── perf_schema_replication_group_member_stats_test.go ├── info_schema_replica_host_test.go ├── perf_schema_memory_events_test.go ├── info_schema_innodb_metrics_test.go ├── info_schema_auto_increment.go ├── perf_schema_file_instances_test.go ├── binlog.go ├── perf_schema_memory_events.go ├── info_schema_innodb_cmpmem.go ├── info_schema_schemastats.go ├── info_schema_tablestats.go ├── slave_hosts.go ├── perf_schema_index_io_waits_test.go ├── slave_status.go ├── info_schema_innodb_cmp.go ├── info_schema_userstats_test.go ├── perf_schema_replication_applier_status_by_worker_test.go ├── heartbeat.go ├── info_schema_clientstats_test.go ├── perf_schema_file_instances.go ├── info_schema_processlist_test.go ├── perf_schema_file_events.go ├── perf_schema_table_io_waits.go ├── info_schema_query_response_time.go ├── info_schema_replica_host.go ├── info_schema_innodb_sys_tablespaces.go ├── perf_schema_index_io_waits.go ├── perf_schema_replication_group_members_test.go ├── perf_schema_replication_group_member_stats.go └── global_status_test.go ├── .yamllint ├── test_image.sh ├── Makefile ├── CONTRIBUTING.md ├── go.mod ├── probe.go └── .circleci └── config.yml /VERSION: -------------------------------------------------------------------------------- 1 | 0.14.0 2 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Ben Kochie 2 | -------------------------------------------------------------------------------- /config/testdata/missing_password.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user = abc 3 | -------------------------------------------------------------------------------- /config/testdata/missing_user.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | password = abc 3 | -------------------------------------------------------------------------------- /mysqld-mixin/.gitignore: -------------------------------------------------------------------------------- 1 | /alerts.yaml 2 | /rules.yaml 3 | dashboards_out 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Exporter for MySQL daemon. 2 | Copyright 2015 The Prometheus Authors 3 | -------------------------------------------------------------------------------- /config/testdata/child_client.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user = root 3 | password = abc 4 | [client.server1] 5 | user = root 6 | -------------------------------------------------------------------------------- /config/testdata/client.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user = root 3 | password = abc 4 | host = server2 5 | [client.server1] 6 | user = test 7 | password = foo 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test_exporter.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | host=localhost 3 | port=3306 4 | socket=/var/run/mysqld/mysqld.sock 5 | user=foo 6 | password=bar 7 | [client.server1] 8 | user = bar 9 | password = bar123 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, .circle/config.yml should also 3 | # be updated. 4 | version: 1.18 5 | repository: 6 | path: github.com/prometheus/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -p "${port}":"${port}" "${docker_image}" --config.my-cnf=test_exporter.cnf) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | paths: 5 | - "go.sum" 6 | - "go.mod" 7 | - "**.go" 8 | - "scripts/errcheck_excludes.txt" 9 | - ".github/workflows/golangci-lint.yml" 10 | - ".golangci.yml" 11 | pull_request: 12 | 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | - name: install Go 21 | uses: actions/setup-go@v2 22 | with: 23 | go-version: 1.18.x 24 | - name: Install snmp_exporter/generator dependencies 25 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 26 | if: github.repository == 'prometheus/snmp_exporter' 27 | - name: Lint 28 | uses: golangci/golangci-lint-action@v3.2.0 29 | with: 30 | version: v1.45.2 31 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | # Needs to be defined before including Makefile.common to auto-generate targets 15 | DOCKER_ARCHS ?= amd64 armv7 arm64 16 | 17 | all: vet 18 | 19 | include Makefile.common 20 | 21 | STATICCHECK_IGNORE = 22 | 23 | DOCKER_IMAGE_NAME ?= mysqld-exporter 24 | 25 | .PHONY: test-docker-single-exporter 26 | test-docker-single-exporter: 27 | @echo ">> testing docker image for single exporter" 28 | ./test_image.sh "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" 9104 29 | 30 | .PHONY: test-docker 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus/mysqld_exporter 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.0 7 | github.com/go-kit/log v0.2.1 8 | github.com/go-sql-driver/mysql v1.6.0 9 | github.com/google/uuid v1.3.0 10 | github.com/prometheus/client_golang v1.12.2 11 | github.com/prometheus/client_model v0.2.0 12 | github.com/prometheus/common v0.37.0 13 | github.com/prometheus/exporter-toolkit v0.7.1 14 | github.com/smartystreets/goconvey v1.7.2 15 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 16 | gopkg.in/ini.v1 v1.66.6 17 | ) 18 | 19 | require ( 20 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 21 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect 22 | github.com/beorn7/perks v1.0.1 // indirect 23 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 24 | github.com/go-logfmt/logfmt v0.5.1 // indirect 25 | github.com/golang/protobuf v1.5.2 // indirect 26 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect 27 | github.com/jpillora/backoff v1.0.0 // indirect 28 | github.com/jtolds/gls v4.20.0+incompatible // indirect 29 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 30 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 31 | github.com/pkg/errors v0.9.1 // indirect 32 | github.com/prometheus/procfs v0.7.3 // indirect 33 | github.com/smartystreets/assertions v1.2.0 // indirect 34 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect 35 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 36 | golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect 37 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect 38 | golang.org/x/text v0.3.7 // indirect 39 | google.golang.org/appengine v1.6.6 // indirect 40 | google.golang.org/protobuf v1.26.0 // indirect 41 | gopkg.in/yaml.v2 v2.4.0 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /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/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 | exporter := New( 37 | context.Background(), 38 | dsn, 39 | NewMetrics(), 40 | []Scraper{ 41 | ScrapeGlobalStatus{}, 42 | }, 43 | log.NewNopLogger(), 44 | ) 45 | 46 | convey.Convey("Metrics describing", t, func() { 47 | ch := make(chan *prometheus.Desc) 48 | go func() { 49 | exporter.Describe(ch) 50 | close(ch) 51 | }() 52 | 53 | for range ch { 54 | } 55 | }) 56 | 57 | convey.Convey("Metrics collection", t, func() { 58 | ch := make(chan prometheus.Metric) 59 | go func() { 60 | exporter.Collect(ch) 61 | close(ch) 62 | }() 63 | 64 | for m := range ch { 65 | got := readMetric(m) 66 | if got.labels[model.MetricNameLabel] == "mysql_up" { 67 | convey.So(got.value, convey.ShouldEqual, 1) 68 | } 69 | } 70 | }) 71 | } 72 | 73 | func TestGetMySQLVersion(t *testing.T) { 74 | if testing.Short() { 75 | t.Skip("-short is passed, skipping test") 76 | } 77 | 78 | logger := log.NewLogfmtLogger(os.Stderr) 79 | logger = level.NewFilter(logger, level.AllowDebug()) 80 | 81 | convey.Convey("Version parsing", t, func() { 82 | db, err := sql.Open("mysql", dsn) 83 | convey.So(err, convey.ShouldBeNil) 84 | defer db.Close() 85 | 86 | convey.So(getMySQLVersion(db, logger), convey.ShouldBeBetweenOrEqual, 5.6, 11.0) 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /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/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_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_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/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/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 | -------------------------------------------------------------------------------- /probe.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 main 15 | 16 | import ( 17 | "fmt" 18 | "net/http" 19 | 20 | "github.com/go-kit/log" 21 | "github.com/go-kit/log/level" 22 | "github.com/prometheus/client_golang/prometheus" 23 | "github.com/prometheus/client_golang/prometheus/promhttp" 24 | "github.com/prometheus/mysqld_exporter/collector" 25 | ) 26 | 27 | func handleProbe(metrics collector.Metrics, scrapers []collector.Scraper, logger log.Logger) http.HandlerFunc { 28 | return func(w http.ResponseWriter, r *http.Request) { 29 | var dsn, authModule string 30 | var err error 31 | 32 | ctx := r.Context() 33 | params := r.URL.Query() 34 | target := params.Get("target") 35 | if target == "" { 36 | http.Error(w, "target is required", http.StatusBadRequest) 37 | return 38 | } 39 | collectParams := r.URL.Query()["collect[]"] 40 | 41 | if authModule = params.Get("auth_module"); authModule == "" { 42 | authModule = "client" 43 | } 44 | 45 | cfg := c.GetConfig() 46 | cfgsection, ok := cfg.Sections[authModule] 47 | if !ok { 48 | level.Error(logger).Log("msg", fmt.Sprintf("Failed to parse section [%s] from config file", authModule), "err", err) 49 | http.Error(w, fmt.Sprintf("Error parsing config section [%s]", authModule), http.StatusBadRequest) 50 | } 51 | if dsn, err = cfgsection.FormDSN(target); err != nil { 52 | level.Error(logger).Log("msg", fmt.Sprintf("Failed to form dsn from section [%s]", authModule), "err", err) 53 | http.Error(w, fmt.Sprintf("Error forming dsn from config section [%s]", authModule), http.StatusBadRequest) 54 | } 55 | 56 | probeSuccessGauge := prometheus.NewGauge(prometheus.GaugeOpts{ 57 | Name: "probe_success", 58 | Help: "Displays whether or not the probe was a success", 59 | }) 60 | 61 | filteredScrapers := filterScrapers(scrapers, collectParams) 62 | 63 | registry := prometheus.NewRegistry() 64 | registry.MustRegister(probeSuccessGauge) 65 | registry.MustRegister(collector.New(ctx, dsn, metrics, filteredScrapers, logger)) 66 | 67 | if err != nil { 68 | probeSuccessGauge.Set(1) 69 | http.Error(w, err.Error(), http.StatusInternalServerError) 70 | return 71 | } 72 | 73 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 74 | h.ServeHTTP(w, r) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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_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/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/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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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_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/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/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/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/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_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 | const infoSchemaAutoIncrementQuery = ` 27 | SELECT table_schema, table_name, column_name, auto_increment, 28 | pow(2, case data_type 29 | when 'tinyint' then 7 30 | when 'smallint' then 15 31 | when 'mediumint' then 23 32 | when 'int' then 31 33 | when 'bigint' then 63 34 | end+(column_type like '% unsigned'))-1 as max_int 35 | FROM information_schema.tables t 36 | JOIN information_schema.columns c USING (table_schema,table_name) 37 | WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL 38 | ` 39 | 40 | // Metric descriptors. 41 | var ( 42 | globalInfoSchemaAutoIncrementDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column"), 44 | "The current value of an auto_increment column from information_schema.", 45 | []string{"schema", "table", "column"}, nil, 46 | ) 47 | globalInfoSchemaAutoIncrementMaxDesc = prometheus.NewDesc( 48 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column_max"), 49 | "The max value of an auto_increment column from information_schema.", 50 | []string{"schema", "table", "column"}, nil, 51 | ) 52 | ) 53 | 54 | // ScrapeAutoIncrementColumns collects auto_increment column information. 55 | type ScrapeAutoIncrementColumns struct{} 56 | 57 | // Name of the Scraper. Should be unique. 58 | func (ScrapeAutoIncrementColumns) Name() string { 59 | return "auto_increment.columns" 60 | } 61 | 62 | // Help describes the role of the Scraper. 63 | func (ScrapeAutoIncrementColumns) Help() string { 64 | return "Collect auto_increment columns and max values from information_schema" 65 | } 66 | 67 | // Version of MySQL from which scraper is available. 68 | func (ScrapeAutoIncrementColumns) Version() float64 { 69 | return 5.1 70 | } 71 | 72 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 73 | func (ScrapeAutoIncrementColumns) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 74 | autoIncrementRows, err := db.QueryContext(ctx, infoSchemaAutoIncrementQuery) 75 | if err != nil { 76 | return err 77 | } 78 | defer autoIncrementRows.Close() 79 | 80 | var ( 81 | schema, table, column string 82 | value, max float64 83 | ) 84 | 85 | for autoIncrementRows.Next() { 86 | if err := autoIncrementRows.Scan( 87 | &schema, &table, &column, &value, &max, 88 | ); err != nil { 89 | return err 90 | } 91 | ch <- prometheus.MustNewConstMetric( 92 | globalInfoSchemaAutoIncrementDesc, prometheus.GaugeValue, value, 93 | schema, table, column, 94 | ) 95 | ch <- prometheus.MustNewConstMetric( 96 | globalInfoSchemaAutoIncrementMaxDesc, prometheus.GaugeValue, max, 97 | schema, table, column, 98 | ) 99 | } 100 | return nil 101 | } 102 | 103 | // check interface 104 | var _ Scraper = ScrapeAutoIncrementColumns{} 105 | -------------------------------------------------------------------------------- /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/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/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/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" 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_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_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/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/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/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/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" 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_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/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/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/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/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/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/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_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/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_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_innodb_sys_tablespaces.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_sys_tablespaces`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "errors" 22 | "fmt" 23 | 24 | "github.com/go-kit/log" 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const innodbTablespacesTablenameQuery = ` 29 | SELECT 30 | table_name 31 | FROM information_schema.tables 32 | WHERE table_name = 'INNODB_SYS_TABLESPACES' 33 | OR table_name = 'INNODB_TABLESPACES' 34 | ` 35 | const innodbTablespacesQuery = ` 36 | SELECT 37 | SPACE, 38 | NAME, 39 | ifnull((SELECT column_name 40 | FROM information_schema.COLUMNS 41 | WHERE TABLE_SCHEMA = 'information_schema' 42 | AND TABLE_NAME = ` + "'%s'" + ` 43 | AND COLUMN_NAME = 'FILE_FORMAT' LIMIT 1), 'NONE') as FILE_FORMAT, 44 | ifnull(ROW_FORMAT, 'NONE') as ROW_FORMAT, 45 | ifnull(SPACE_TYPE, 'NONE') as SPACE_TYPE, 46 | FILE_SIZE, 47 | ALLOCATED_SIZE 48 | FROM information_schema.` + "`%s`" 49 | 50 | // Metric descriptors. 51 | var ( 52 | infoSchemaInnodbTablesspaceInfoDesc = prometheus.NewDesc( 53 | prometheus.BuildFQName(namespace, informationSchema, "innodb_tablespace_space_info"), 54 | "The Tablespace information and Space ID.", 55 | []string{"tablespace_name", "file_format", "row_format", "space_type"}, nil, 56 | ) 57 | infoSchemaInnodbTablesspaceFileSizeDesc = prometheus.NewDesc( 58 | prometheus.BuildFQName(namespace, informationSchema, "innodb_tablespace_file_size_bytes"), 59 | "The apparent size of the file, which represents the maximum size of the file, uncompressed.", 60 | []string{"tablespace_name"}, nil, 61 | ) 62 | infoSchemaInnodbTablesspaceAllocatedSizeDesc = prometheus.NewDesc( 63 | prometheus.BuildFQName(namespace, informationSchema, "innodb_tablespace_allocated_size_bytes"), 64 | "The actual size of the file, which is the amount of space allocated on disk.", 65 | []string{"tablespace_name"}, nil, 66 | ) 67 | ) 68 | 69 | // ScrapeInfoSchemaInnodbTablespaces collects from `information_schema.innodb_sys_tablespaces`. 70 | type ScrapeInfoSchemaInnodbTablespaces struct{} 71 | 72 | // Name of the Scraper. Should be unique. 73 | func (ScrapeInfoSchemaInnodbTablespaces) Name() string { 74 | return informationSchema + ".innodb_tablespaces" 75 | } 76 | 77 | // Help describes the role of the Scraper. 78 | func (ScrapeInfoSchemaInnodbTablespaces) Help() string { 79 | return "Collect metrics from information_schema.innodb_sys_tablespaces" 80 | } 81 | 82 | // Version of MySQL from which scraper is available. 83 | func (ScrapeInfoSchemaInnodbTablespaces) Version() float64 { 84 | return 5.7 85 | } 86 | 87 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 88 | func (ScrapeInfoSchemaInnodbTablespaces) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 89 | var tablespacesTablename string 90 | var query string 91 | err := db.QueryRowContext(ctx, innodbTablespacesTablenameQuery).Scan(&tablespacesTablename) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | switch tablespacesTablename { 97 | case "INNODB_SYS_TABLESPACES", "INNODB_TABLESPACES": 98 | query = fmt.Sprintf(innodbTablespacesQuery, tablespacesTablename, tablespacesTablename) 99 | default: 100 | return errors.New("Couldn't find INNODB_SYS_TABLESPACES or INNODB_TABLESPACES in information_schema.") 101 | } 102 | 103 | tablespacesRows, err := db.QueryContext(ctx, query) 104 | if err != nil { 105 | return err 106 | } 107 | defer tablespacesRows.Close() 108 | 109 | var ( 110 | tableSpace uint32 111 | tableName string 112 | fileFormat string 113 | rowFormat string 114 | spaceType string 115 | fileSize uint64 116 | allocatedSize uint64 117 | ) 118 | 119 | for tablespacesRows.Next() { 120 | err = tablespacesRows.Scan( 121 | &tableSpace, 122 | &tableName, 123 | &fileFormat, 124 | &rowFormat, 125 | &spaceType, 126 | &fileSize, 127 | &allocatedSize, 128 | ) 129 | if err != nil { 130 | return err 131 | } 132 | ch <- prometheus.MustNewConstMetric( 133 | infoSchemaInnodbTablesspaceInfoDesc, prometheus.GaugeValue, float64(tableSpace), 134 | tableName, fileFormat, rowFormat, spaceType, 135 | ) 136 | ch <- prometheus.MustNewConstMetric( 137 | infoSchemaInnodbTablesspaceFileSizeDesc, prometheus.GaugeValue, float64(fileSize), 138 | tableName, 139 | ) 140 | ch <- prometheus.MustNewConstMetric( 141 | infoSchemaInnodbTablesspaceAllocatedSizeDesc, prometheus.GaugeValue, float64(allocatedSize), 142 | tableName, 143 | ) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // check interface 150 | var _ Scraper = ScrapeInfoSchemaInnodbTablespaces{} 151 | -------------------------------------------------------------------------------- /collector/perf_schema_index_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_index_usage`. 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 perfIndexIOWaitsQuery = ` 27 | SELECT OBJECT_SCHEMA, OBJECT_NAME, ifnull(INDEX_NAME, 'NONE') as INDEX_NAME, 28 | COUNT_FETCH, COUNT_INSERT, COUNT_UPDATE, COUNT_DELETE, 29 | SUM_TIMER_FETCH, SUM_TIMER_INSERT, SUM_TIMER_UPDATE, SUM_TIMER_DELETE 30 | FROM performance_schema.table_io_waits_summary_by_index_usage 31 | WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema') 32 | ` 33 | 34 | // Metric descriptors. 35 | var ( 36 | performanceSchemaIndexWaitsDesc = prometheus.NewDesc( 37 | prometheus.BuildFQName(namespace, performanceSchema, "index_io_waits_total"), 38 | "The total number of index I/O wait events for each index and operation.", 39 | []string{"schema", "name", "index", "operation"}, nil, 40 | ) 41 | performanceSchemaIndexWaitsTimeDesc = prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, performanceSchema, "index_io_waits_seconds_total"), 43 | "The total time of index I/O wait events for each index and operation.", 44 | []string{"schema", "name", "index", "operation"}, nil, 45 | ) 46 | ) 47 | 48 | // ScrapePerfIndexIOWaits collects for `performance_schema.table_io_waits_summary_by_index_usage`. 49 | type ScrapePerfIndexIOWaits struct{} 50 | 51 | // Name of the Scraper. Should be unique. 52 | func (ScrapePerfIndexIOWaits) Name() string { 53 | return "perf_schema.indexiowaits" 54 | } 55 | 56 | // Help describes the role of the Scraper. 57 | func (ScrapePerfIndexIOWaits) Help() string { 58 | return "Collect metrics from performance_schema.table_io_waits_summary_by_index_usage" 59 | } 60 | 61 | // Version of MySQL from which scraper is available. 62 | func (ScrapePerfIndexIOWaits) Version() float64 { 63 | return 5.6 64 | } 65 | 66 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 67 | func (ScrapePerfIndexIOWaits) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 68 | perfSchemaIndexWaitsRows, err := db.QueryContext(ctx, perfIndexIOWaitsQuery) 69 | if err != nil { 70 | return err 71 | } 72 | defer perfSchemaIndexWaitsRows.Close() 73 | 74 | var ( 75 | objectSchema, objectName, indexName string 76 | countFetch, countInsert, countUpdate, countDelete uint64 77 | timeFetch, timeInsert, timeUpdate, timeDelete uint64 78 | ) 79 | 80 | for perfSchemaIndexWaitsRows.Next() { 81 | if err := perfSchemaIndexWaitsRows.Scan( 82 | &objectSchema, &objectName, &indexName, 83 | &countFetch, &countInsert, &countUpdate, &countDelete, 84 | &timeFetch, &timeInsert, &timeUpdate, &timeDelete, 85 | ); err != nil { 86 | return err 87 | } 88 | ch <- prometheus.MustNewConstMetric( 89 | performanceSchemaIndexWaitsDesc, prometheus.CounterValue, float64(countFetch), 90 | objectSchema, objectName, indexName, "fetch", 91 | ) 92 | // We only include the insert column when indexName is NONE. 93 | if indexName == "NONE" { 94 | ch <- prometheus.MustNewConstMetric( 95 | performanceSchemaIndexWaitsDesc, prometheus.CounterValue, float64(countInsert), 96 | objectSchema, objectName, indexName, "insert", 97 | ) 98 | } 99 | ch <- prometheus.MustNewConstMetric( 100 | performanceSchemaIndexWaitsDesc, prometheus.CounterValue, float64(countUpdate), 101 | objectSchema, objectName, indexName, "update", 102 | ) 103 | ch <- prometheus.MustNewConstMetric( 104 | performanceSchemaIndexWaitsDesc, prometheus.CounterValue, float64(countDelete), 105 | objectSchema, objectName, indexName, "delete", 106 | ) 107 | ch <- prometheus.MustNewConstMetric( 108 | performanceSchemaIndexWaitsTimeDesc, prometheus.CounterValue, float64(timeFetch)/picoSeconds, 109 | objectSchema, objectName, indexName, "fetch", 110 | ) 111 | // We only update write columns when indexName is NONE. 112 | if indexName == "NONE" { 113 | ch <- prometheus.MustNewConstMetric( 114 | performanceSchemaIndexWaitsTimeDesc, prometheus.CounterValue, float64(timeInsert)/picoSeconds, 115 | objectSchema, objectName, indexName, "insert", 116 | ) 117 | } 118 | ch <- prometheus.MustNewConstMetric( 119 | performanceSchemaIndexWaitsTimeDesc, prometheus.CounterValue, float64(timeUpdate)/picoSeconds, 120 | objectSchema, objectName, indexName, "update", 121 | ) 122 | ch <- prometheus.MustNewConstMetric( 123 | performanceSchemaIndexWaitsTimeDesc, prometheus.CounterValue, float64(timeDelete)/picoSeconds, 124 | objectSchema, objectName, indexName, "delete", 125 | ) 126 | } 127 | return nil 128 | } 129 | 130 | // check interface 131 | var _ Scraper = ScrapePerfIndexIOWaits{} 132 | -------------------------------------------------------------------------------- /collector/perf_schema_replication_group_members_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 | "github.com/DATA-DOG/go-sqlmock" 19 | "github.com/go-kit/log" 20 | "github.com/prometheus/client_golang/prometheus" 21 | dto "github.com/prometheus/client_model/go" 22 | "github.com/smartystreets/goconvey/convey" 23 | "testing" 24 | ) 25 | 26 | func TestScrapePerfReplicationGroupMembers(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 | columns := []string{ 34 | "CHANNEL_NAME", 35 | "MEMBER_ID", 36 | "MEMBER_HOST", 37 | "MEMBER_PORT", 38 | "MEMBER_STATE", 39 | "MEMBER_ROLE", 40 | "MEMBER_VERSION", 41 | } 42 | 43 | rows := sqlmock.NewRows(columns). 44 | AddRow("group_replication_applier", "uuid1", "hostname1", "3306", "ONLINE", "PRIMARY", "8.0.19"). 45 | AddRow("group_replication_applier", "uuid2", "hostname2", "3306", "ONLINE", "SECONDARY", "8.0.19"). 46 | AddRow("group_replication_applier", "uuid3", "hostname3", "3306", "ONLINE", "SECONDARY", "8.0.19") 47 | 48 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMembersQuery)).WillReturnRows(rows) 49 | 50 | ch := make(chan prometheus.Metric) 51 | go func() { 52 | if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 53 | t.Errorf("error calling function on test: %s", err) 54 | } 55 | close(ch) 56 | }() 57 | 58 | metricExpected := []MetricResult{ 59 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid1", "member_host": "hostname1", "member_port": "3306", 60 | "member_state": "ONLINE", "member_role": "PRIMARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 61 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid2", "member_host": "hostname2", "member_port": "3306", 62 | "member_state": "ONLINE", "member_role": "SECONDARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 63 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid3", "member_host": "hostname3", "member_port": "3306", 64 | "member_state": "ONLINE", "member_role": "SECONDARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 65 | } 66 | convey.Convey("Metrics comparison", t, func() { 67 | for _, expect := range metricExpected { 68 | got := readMetric(<-ch) 69 | convey.So(got, convey.ShouldResemble, expect) 70 | } 71 | }) 72 | 73 | // Ensure all SQL queries were executed. 74 | if err := mock.ExpectationsWereMet(); err != nil { 75 | t.Errorf("there were unfulfilled exceptions: %s", err) 76 | } 77 | } 78 | 79 | func TestScrapePerfReplicationGroupMembersMySQL57(t *testing.T) { 80 | db, mock, err := sqlmock.New() 81 | if err != nil { 82 | t.Fatalf("error opening a stub database connection: %s", err) 83 | } 84 | defer db.Close() 85 | 86 | columns := []string{ 87 | "CHANNEL_NAME", 88 | "MEMBER_ID", 89 | "MEMBER_HOST", 90 | "MEMBER_PORT", 91 | "MEMBER_STATE", 92 | } 93 | 94 | rows := sqlmock.NewRows(columns). 95 | AddRow("group_replication_applier", "uuid1", "hostname1", "3306", "ONLINE"). 96 | AddRow("group_replication_applier", "uuid2", "hostname2", "3306", "ONLINE"). 97 | AddRow("group_replication_applier", "uuid3", "hostname3", "3306", "ONLINE") 98 | 99 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMembersQuery)).WillReturnRows(rows) 100 | 101 | ch := make(chan prometheus.Metric) 102 | go func() { 103 | if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 104 | t.Errorf("error calling function on test: %s", err) 105 | } 106 | close(ch) 107 | }() 108 | 109 | metricExpected := []MetricResult{ 110 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid1", "member_host": "hostname1", "member_port": "3306", 111 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 112 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid2", "member_host": "hostname2", "member_port": "3306", 113 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 114 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid3", "member_host": "hostname3", "member_port": "3306", 115 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 116 | } 117 | convey.Convey("Metrics comparison", t, func() { 118 | for _, expect := range metricExpected { 119 | got := readMetric(<-ch) 120 | convey.So(got, convey.ShouldResemble, expect) 121 | } 122 | }) 123 | 124 | // Ensure all SQL queries were executed. 125 | if err := mock.ExpectationsWereMet(); err != nil { 126 | t.Errorf("there were unfulfilled exceptions: %s", err) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /collector/perf_schema_replication_group_member_stats.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 | "strconv" 20 | 21 | "github.com/go-kit/log" 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfReplicationGroupMemberStatsQuery = ` 26 | SELECT * FROM performance_schema.replication_group_member_stats WHERE MEMBER_ID=@@server_uuid 27 | ` 28 | 29 | var ( 30 | // The list of columns we are interesting in. 31 | // In MySQL 5.7 these are the 4 first columns available. In MySQL 8.x all 8. 32 | perfReplicationGroupMemberStats = map[string]struct { 33 | vtype prometheus.ValueType 34 | desc *prometheus.Desc 35 | }{ 36 | "COUNT_TRANSACTIONS_IN_QUEUE": {prometheus.GaugeValue, 37 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_in_queue"), 38 | "The number of transactions in the queue pending conflict detection checks.", nil, nil)}, 39 | "COUNT_TRANSACTIONS_CHECKED": {prometheus.CounterValue, 40 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_checked_total"), 41 | "The number of transactions that have been checked for conflicts.", nil, nil)}, 42 | "COUNT_CONFLICTS_DETECTED": {prometheus.CounterValue, 43 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "conflicts_detected_total"), 44 | "The number of transactions that have not passed the conflict detection check.", nil, nil)}, 45 | "COUNT_TRANSACTIONS_ROWS_VALIDATING": {prometheus.CounterValue, 46 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_rows_validating_total"), 47 | "Number of transaction rows which can be used for certification, but have not been garbage collected.", nil, nil)}, 48 | "COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE": {prometheus.GaugeValue, 49 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_remote_in_applier_queue"), 50 | "The number of transactions that this member has received from the replication group which are waiting to be applied.", nil, nil)}, 51 | "COUNT_TRANSACTIONS_REMOTE_APPLIED": {prometheus.CounterValue, 52 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_remote_applied_total"), 53 | "Number of transactions this member has received from the group and applied.", nil, nil)}, 54 | "COUNT_TRANSACTIONS_LOCAL_PROPOSED": {prometheus.CounterValue, 55 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_local_proposed_total"), 56 | "Number of transactions which originated on this member and were sent to the group.", nil, nil)}, 57 | "COUNT_TRANSACTIONS_LOCAL_ROLLBACK": {prometheus.CounterValue, 58 | prometheus.NewDesc(prometheus.BuildFQName(namespace, performanceSchema, "transactions_local_rollback_total"), 59 | "Number of transactions which originated on this member and were rolled back by the group.", nil, nil)}, 60 | } 61 | ) 62 | 63 | // ScrapePerfReplicationGroupMemberStats collects from `performance_schema.replication_group_member_stats`. 64 | type ScrapePerfReplicationGroupMemberStats struct{} 65 | 66 | // Name of the Scraper. Should be unique. 67 | func (ScrapePerfReplicationGroupMemberStats) Name() string { 68 | return performanceSchema + ".replication_group_member_stats" 69 | } 70 | 71 | // Help describes the role of the Scraper. 72 | func (ScrapePerfReplicationGroupMemberStats) Help() string { 73 | return "Collect metrics from performance_schema.replication_group_member_stats" 74 | } 75 | 76 | // Version of MySQL from which scraper is available. 77 | func (ScrapePerfReplicationGroupMemberStats) 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 (ScrapePerfReplicationGroupMemberStats) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error { 83 | rows, err := db.QueryContext(ctx, perfReplicationGroupMemberStatsQuery) 84 | if err != nil { 85 | return err 86 | } 87 | defer rows.Close() 88 | 89 | var columnNames []string 90 | if columnNames, err = rows.Columns(); err != nil { 91 | return err 92 | } 93 | 94 | var scanArgs = make([]interface{}, len(columnNames)) 95 | for i := range scanArgs { 96 | scanArgs[i] = &sql.RawBytes{} 97 | } 98 | 99 | for rows.Next() { 100 | if err := rows.Scan(scanArgs...); err != nil { 101 | return err 102 | } 103 | 104 | for i, columnName := range columnNames { 105 | if metric, ok := perfReplicationGroupMemberStats[columnName]; ok { 106 | value, err := strconv.ParseFloat(string(*scanArgs[i].(*sql.RawBytes)), 64) 107 | if err != nil { 108 | return err 109 | } 110 | ch <- prometheus.MustNewConstMetric(metric.desc, metric.vtype, value) 111 | } 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | // check interface 118 | var _ Scraper = ScrapePerfReplicationGroupMemberStats{} 119 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 config 15 | 16 | import ( 17 | "fmt" 18 | "os" 19 | "testing" 20 | 21 | "github.com/go-kit/log" 22 | 23 | "github.com/smartystreets/goconvey/convey" 24 | ) 25 | 26 | func TestValidateConfig(t *testing.T) { 27 | convey.Convey("Working config validation", t, func() { 28 | c := MySqlConfigHandler{ 29 | Config: &Config{}, 30 | } 31 | if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "", true, log.NewNopLogger()); err != nil { 32 | t.Error(err) 33 | } 34 | 35 | convey.Convey("Valid configuration", func() { 36 | cfg := c.GetConfig() 37 | convey.So(cfg.Sections, convey.ShouldContainKey, "client") 38 | convey.So(cfg.Sections, convey.ShouldContainKey, "client.server1") 39 | 40 | section, ok := cfg.Sections["client"] 41 | convey.So(ok, convey.ShouldBeTrue) 42 | convey.So(section.User, convey.ShouldEqual, "root") 43 | convey.So(section.Password, convey.ShouldEqual, "abc") 44 | 45 | childSection, ok := cfg.Sections["client.server1"] 46 | convey.So(ok, convey.ShouldBeTrue) 47 | convey.So(childSection.User, convey.ShouldEqual, "test") 48 | convey.So(childSection.Password, convey.ShouldEqual, "foo") 49 | 50 | }) 51 | 52 | convey.Convey("False on non-existent section", func() { 53 | cfg := c.GetConfig() 54 | _, ok := cfg.Sections["fakeclient"] 55 | convey.So(ok, convey.ShouldBeFalse) 56 | }) 57 | }) 58 | 59 | convey.Convey("Inherit from parent section", t, func() { 60 | c := MySqlConfigHandler{ 61 | Config: &Config{}, 62 | } 63 | if err := c.ReloadConfig("testdata/child_client.cnf", "localhost:3306", "", true, log.NewNopLogger()); err != nil { 64 | t.Error(err) 65 | } 66 | cfg := c.GetConfig() 67 | section, _ := cfg.Sections["client.server1"] 68 | convey.So(section.Password, convey.ShouldEqual, "abc") 69 | }) 70 | 71 | convey.Convey("Environment variable / CLI flags", t, func() { 72 | c := MySqlConfigHandler{ 73 | Config: &Config{}, 74 | } 75 | os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") 76 | if err := c.ReloadConfig("", "testhost:5000", "testuser", true, log.NewNopLogger()); err != nil { 77 | t.Error(err) 78 | } 79 | 80 | cfg := c.GetConfig() 81 | section := cfg.Sections["client"] 82 | convey.So(section.Host, convey.ShouldEqual, "testhost") 83 | convey.So(section.Port, convey.ShouldEqual, 5000) 84 | convey.So(section.User, convey.ShouldEqual, "testuser") 85 | convey.So(section.Password, convey.ShouldEqual, "supersecretpassword") 86 | }) 87 | 88 | convey.Convey("Environment variable / CLI flags error without port", t, func() { 89 | c := MySqlConfigHandler{ 90 | Config: &Config{}, 91 | } 92 | os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") 93 | err := c.ReloadConfig("", "testhost", "testuser", true, log.NewNopLogger()) 94 | convey.So( 95 | err, 96 | convey.ShouldBeError, 97 | ) 98 | }) 99 | 100 | convey.Convey("Config file precedence over environment variables", t, func() { 101 | c := MySqlConfigHandler{ 102 | Config: &Config{}, 103 | } 104 | os.Setenv("MYSQLD_EXPORTER_PASSWORD", "supersecretpassword") 105 | if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "fakeuser", true, log.NewNopLogger()); err != nil { 106 | t.Error(err) 107 | } 108 | 109 | cfg := c.GetConfig() 110 | section := cfg.Sections["client"] 111 | convey.So(section.User, convey.ShouldEqual, "root") 112 | convey.So(section.Password, convey.ShouldEqual, "abc") 113 | }) 114 | 115 | convey.Convey("Client without user", t, func() { 116 | c := MySqlConfigHandler{ 117 | Config: &Config{}, 118 | } 119 | os.Clearenv() 120 | err := c.ReloadConfig("testdata/missing_user.cnf", "localhost:3306", "", true, log.NewNopLogger()) 121 | convey.So( 122 | err, 123 | convey.ShouldResemble, 124 | fmt.Errorf("no configuration found"), 125 | ) 126 | }) 127 | 128 | convey.Convey("Client without password", t, func() { 129 | c := MySqlConfigHandler{ 130 | Config: &Config{}, 131 | } 132 | os.Clearenv() 133 | err := c.ReloadConfig("testdata/missing_password.cnf", "localhost:3306", "", true, log.NewNopLogger()) 134 | convey.So( 135 | err, 136 | convey.ShouldResemble, 137 | fmt.Errorf("no configuration found"), 138 | ) 139 | }) 140 | } 141 | 142 | func TestFormDSN(t *testing.T) { 143 | var ( 144 | c = MySqlConfigHandler{ 145 | Config: &Config{}, 146 | } 147 | err error 148 | dsn string 149 | ) 150 | 151 | convey.Convey("Host exporter dsn", t, func() { 152 | if err := c.ReloadConfig("testdata/client.cnf", "localhost:3306", "", true, log.NewNopLogger()); err != nil { 153 | t.Error(err) 154 | } 155 | convey.Convey("Default Client", func() { 156 | cfg := c.GetConfig() 157 | section, _ := cfg.Sections["client"] 158 | if dsn, err = section.FormDSN(""); err != nil { 159 | t.Error(err) 160 | } 161 | convey.So(dsn, convey.ShouldEqual, "root:abc@tcp(server2:3306)/") 162 | }) 163 | convey.Convey("Target specific with explicit port", func() { 164 | cfg := c.GetConfig() 165 | section, _ := cfg.Sections["client.server1"] 166 | if dsn, err = section.FormDSN("server1:5000"); err != nil { 167 | t.Error(err) 168 | } 169 | convey.So(dsn, convey.ShouldEqual, "test:foo@tcp(server1:5000)/") 170 | }) 171 | }) 172 | } 173 | -------------------------------------------------------------------------------- /collector/global_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 TestScrapeGlobalStatus(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{"Variable_name", "Value"} 35 | rows := sqlmock.NewRows(columns). 36 | AddRow("Com_alter_db", "1"). 37 | AddRow("Com_show_status", "2"). 38 | AddRow("Com_select", "3"). 39 | AddRow("Connection_errors_internal", "4"). 40 | AddRow("Handler_commit", "5"). 41 | AddRow("Innodb_buffer_pool_pages_data", "6"). 42 | AddRow("Innodb_buffer_pool_pages_flushed", "7"). 43 | AddRow("Innodb_buffer_pool_pages_dirty", "7"). 44 | AddRow("Innodb_buffer_pool_pages_free", "8"). 45 | AddRow("Innodb_buffer_pool_pages_misc", "9"). 46 | AddRow("Innodb_buffer_pool_pages_old", "10"). 47 | AddRow("Innodb_buffer_pool_pages_total", "11"). 48 | AddRow("Innodb_buffer_pool_pages_lru_flushed", "13"). 49 | AddRow("Innodb_buffer_pool_pages_made_not_young", "14"). 50 | AddRow("Innodb_buffer_pool_pages_made_young", "15"). 51 | AddRow("Innodb_rows_read", "8"). 52 | AddRow("Performance_schema_users_lost", "9"). 53 | AddRow("Slave_running", "OFF"). 54 | AddRow("Ssl_version", ""). 55 | AddRow("Uptime", "10"). 56 | AddRow("validate_password.dictionary_file_words_count", "11"). 57 | AddRow("wsrep_cluster_status", "Primary"). 58 | AddRow("wsrep_local_state_uuid", "6c06e583-686f-11e6-b9e3-8336ad58138c"). 59 | AddRow("wsrep_cluster_state_uuid", "6c06e583-686f-11e6-b9e3-8336ad58138c"). 60 | AddRow("wsrep_provider_version", "3.16(r5c765eb)"). 61 | AddRow("wsrep_evs_repl_latency", "0.000227664/0.00034135/0.000544298/6.03708e-05/212") 62 | mock.ExpectQuery(sanitizeQuery(globalStatusQuery)).WillReturnRows(rows) 63 | 64 | ch := make(chan prometheus.Metric) 65 | go func() { 66 | if err = (ScrapeGlobalStatus{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil { 67 | t.Errorf("error calling function on test: %s", err) 68 | } 69 | close(ch) 70 | }() 71 | 72 | counterExpected := []MetricResult{ 73 | {labels: labelMap{"command": "alter_db"}, value: 1, metricType: dto.MetricType_COUNTER}, 74 | {labels: labelMap{"command": "show_status"}, value: 2, metricType: dto.MetricType_COUNTER}, 75 | {labels: labelMap{"command": "select"}, value: 3, metricType: dto.MetricType_COUNTER}, 76 | {labels: labelMap{"error": "internal"}, value: 4, metricType: dto.MetricType_COUNTER}, 77 | {labels: labelMap{"handler": "commit"}, value: 5, metricType: dto.MetricType_COUNTER}, 78 | {labels: labelMap{"state": "data"}, value: 6, metricType: dto.MetricType_GAUGE}, 79 | {labels: labelMap{"operation": "flushed"}, value: 7, metricType: dto.MetricType_COUNTER}, 80 | {labels: labelMap{}, value: 7, metricType: dto.MetricType_GAUGE}, 81 | {labels: labelMap{"state": "free"}, value: 8, metricType: dto.MetricType_GAUGE}, 82 | {labels: labelMap{"state": "misc"}, value: 9, metricType: dto.MetricType_GAUGE}, 83 | {labels: labelMap{"state": "old"}, value: 10, metricType: dto.MetricType_GAUGE}, 84 | //{labels: labelMap{"state": "total_pages"}, value: 11, metricType: dto.MetricType_GAUGE}, 85 | {labels: labelMap{"operation": "lru_flushed"}, value: 13, metricType: dto.MetricType_COUNTER}, 86 | {labels: labelMap{"operation": "made_not_young"}, value: 14, metricType: dto.MetricType_COUNTER}, 87 | {labels: labelMap{"operation": "made_young"}, value: 15, metricType: dto.MetricType_COUNTER}, 88 | {labels: labelMap{"operation": "read"}, value: 8, metricType: dto.MetricType_COUNTER}, 89 | {labels: labelMap{"instrumentation": "users_lost"}, value: 9, metricType: dto.MetricType_COUNTER}, 90 | {labels: labelMap{}, value: 0, metricType: dto.MetricType_UNTYPED}, 91 | {labels: labelMap{}, value: 10, metricType: dto.MetricType_UNTYPED}, 92 | {labels: labelMap{}, value: 11, metricType: dto.MetricType_UNTYPED}, 93 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_UNTYPED}, 94 | {labels: labelMap{"wsrep_local_state_uuid": "6c06e583-686f-11e6-b9e3-8336ad58138c", "wsrep_cluster_state_uuid": "6c06e583-686f-11e6-b9e3-8336ad58138c", "wsrep_provider_version": "3.16(r5c765eb)"}, value: 1, metricType: dto.MetricType_GAUGE}, 95 | {labels: labelMap{}, value: 0.000227664, metricType: dto.MetricType_GAUGE}, 96 | {labels: labelMap{}, value: 0.00034135, metricType: dto.MetricType_GAUGE}, 97 | {labels: labelMap{}, value: 0.000544298, metricType: dto.MetricType_GAUGE}, 98 | {labels: labelMap{}, value: 6.03708e-05, metricType: dto.MetricType_GAUGE}, 99 | {labels: labelMap{}, value: 212, metricType: dto.MetricType_GAUGE}, 100 | } 101 | convey.Convey("Metrics comparison", t, func() { 102 | for _, expect := range counterExpected { 103 | got := readMetric(<-ch) 104 | convey.So(got, convey.ShouldResemble, expect) 105 | } 106 | }) 107 | 108 | // Ensure all SQL queries were executed 109 | if err := mock.ExpectationsWereMet(); err != nil { 110 | t.Errorf("there were unfulfilled exceptions: %s", err) 111 | } 112 | } 113 | --------------------------------------------------------------------------------