├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── container_description.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── .promu.yml ├── .yamllint ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── Makefile.common ├── NOTICE ├── README.md ├── SECURITY.md ├── VERSION ├── collector ├── binlog.go ├── binlog_test.go ├── collector.go ├── collector_test.go ├── engine_innodb.go ├── engine_innodb_test.go ├── engine_tokudb.go ├── engine_tokudb_test.go ├── exporter.go ├── exporter_test.go ├── global_status.go ├── global_status_test.go ├── global_variables.go ├── global_variables_test.go ├── heartbeat.go ├── heartbeat_test.go ├── info_schema.go ├── info_schema_auto_increment.go ├── info_schema_clientstats.go ├── info_schema_clientstats_test.go ├── info_schema_innodb_cmp.go ├── info_schema_innodb_cmp_test.go ├── info_schema_innodb_cmpmem.go ├── info_schema_innodb_cmpmem_test.go ├── info_schema_innodb_metrics.go ├── info_schema_innodb_metrics_test.go ├── info_schema_innodb_sys_tablespaces.go ├── info_schema_innodb_sys_tablespaces_test.go ├── info_schema_processlist.go ├── info_schema_processlist_test.go ├── info_schema_query_response_time.go ├── info_schema_query_response_time_test.go ├── info_schema_replica_host.go ├── info_schema_replica_host_test.go ├── info_schema_rocksdb_perf_context.go ├── info_schema_schemastats.go ├── info_schema_schemastats_test.go ├── info_schema_tables.go ├── info_schema_tablestats.go ├── info_schema_tablestats_test.go ├── info_schema_userstats.go ├── info_schema_userstats_test.go ├── instance.go ├── mysql_user.go ├── perf_schema.go ├── perf_schema_events_statements.go ├── perf_schema_events_statements_sum.go ├── perf_schema_events_statements_test.go ├── perf_schema_events_waits.go ├── perf_schema_file_events.go ├── perf_schema_file_instances.go ├── perf_schema_file_instances_test.go ├── perf_schema_index_io_waits.go ├── perf_schema_index_io_waits_test.go ├── perf_schema_memory_events.go ├── perf_schema_memory_events_test.go ├── perf_schema_replication_applier_status_by_worker.go ├── perf_schema_replication_applier_status_by_worker_test.go ├── perf_schema_replication_group_member_stats.go ├── perf_schema_replication_group_member_stats_test.go ├── perf_schema_replication_group_members.go ├── perf_schema_replication_group_members_test.go ├── perf_schema_table_io_waits.go ├── perf_schema_table_lock_waits.go ├── scraper.go ├── slave_hosts.go ├── slave_hosts_test.go ├── slave_status.go ├── slave_status_test.go ├── sys.go ├── sys_user_summary.go └── sys_user_summary_test.go ├── config ├── config.go ├── config_test.go └── testdata │ ├── child_client.cnf │ ├── client.cnf │ ├── client_custom_tls.cnf │ ├── missing_password.cnf │ └── missing_user.cnf ├── go.mod ├── go.sum ├── mysqld-mixin ├── .gitignore ├── Makefile ├── README.md ├── alerts │ ├── galera.yaml │ └── general.yaml ├── dashboards │ └── mysql-overview.json ├── mixin.libsonnet └── rules │ └── rules.yaml ├── mysqld_exporter.go ├── mysqld_exporter_test.go ├── probe.go ├── test_exporter.cnf └── test_image.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1 3 | orbs: 4 | prometheus: prometheus/prometheus@0.17.1 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.24 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.24 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 | mixin: 37 | executor: golang 38 | steps: 39 | - checkout 40 | - run: go install github.com/monitoring-mixins/mixtool/cmd/mixtool@latest 41 | - run: go install github.com/google/go-jsonnet/cmd/jsonnetfmt@latest 42 | - run: make -C mysqld-mixin lint build 43 | workflows: 44 | version: 2 45 | mysqld_exporter: 46 | jobs: 47 | - test: 48 | filters: 49 | tags: 50 | only: /.*/ 51 | - integration: 52 | matrix: 53 | parameters: 54 | mysql_image: 55 | - percona:5.6 56 | - mysql:5.7.33 57 | - mysql:8.0 58 | - mysql:8.4 59 | - mariadb:10.5 60 | - mariadb:10.6 61 | - mariadb:10.11 62 | - mariadb:11.4 63 | - prometheus/build: 64 | name: build 65 | parallelism: 3 66 | promu_opts: "-p linux/amd64 -p windows/amd64 -p linux/arm64 -p darwin/amd64 -p darwin/arm64 -p linux/386" 67 | filters: 68 | tags: 69 | ignore: /^v.*/ 70 | branches: 71 | ignore: /^(main|release-.*|.*build-all.*)$/ 72 | - prometheus/build: 73 | name: build_all 74 | parallelism: 12 75 | filters: 76 | branches: 77 | only: /^(main|release-.*|.*build-all.*)$/ 78 | tags: 79 | only: /^v.*/ 80 | - mixin: 81 | filters: 82 | tags: 83 | only: /.*/ 84 | - prometheus/publish_main: 85 | context: org-context 86 | requires: 87 | - test 88 | - build_all 89 | filters: 90 | branches: 91 | only: main 92 | - prometheus/publish_release: 93 | context: org-context 94 | requires: 95 | - test 96 | - build_all 97 | filters: 98 | tags: 99 | only: /^v.*/ 100 | branches: 101 | ignore: /.*/ 102 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | ### Host operating system: output of `uname -a` 13 | 14 | ### mysqld_exporter version: output of `mysqld_exporter --version` 15 | 16 | 17 | ### MySQL server version 18 | 19 | ### mysqld_exporter command line flags 20 | 21 | 22 | ### What did you do that produced an error? 23 | 24 | ### What did you expect to see? 25 | 26 | ### What did you see instead? 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/container_description.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Push README to Docker Hub 3 | on: 4 | push: 5 | paths: 6 | - "README.md" 7 | - "README-containers.md" 8 | - ".github/workflows/container_description.yml" 9 | branches: [ main, master ] 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | PushDockerHubReadme: 16 | runs-on: ubuntu-latest 17 | name: Push README to Docker Hub 18 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 19 | steps: 20 | - name: git checkout 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Set docker hub repo name 23 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 24 | - name: Push README to Dockerhub 25 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 26 | env: 27 | DOCKER_USER: ${{ secrets.DOCKER_HUB_LOGIN }} 28 | DOCKER_PASS: ${{ secrets.DOCKER_HUB_PASSWORD }} 29 | with: 30 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 31 | provider: dockerhub 32 | short_description: ${{ env.DOCKER_REPO_NAME }} 33 | # Empty string results in README-containers.md being pushed if it 34 | # exists. Otherwise, README.md is pushed. 35 | readme_file: '' 36 | 37 | PushQuayIoReadme: 38 | runs-on: ubuntu-latest 39 | name: Push README to quay.io 40 | if: github.repository_owner == 'prometheus' || github.repository_owner == 'prometheus-community' # Don't run this workflow on forks. 41 | steps: 42 | - name: git checkout 43 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 44 | - name: Set quay.io org name 45 | run: echo "DOCKER_REPO=$(echo quay.io/${GITHUB_REPOSITORY_OWNER} | tr -d '-')" >> $GITHUB_ENV 46 | - name: Set quay.io repo name 47 | run: echo "DOCKER_REPO_NAME=$(make docker-repo-name)" >> $GITHUB_ENV 48 | - name: Push README to quay.io 49 | uses: christian-korneck/update-container-description-action@d36005551adeaba9698d8d67a296bd16fa91f8e8 # v1 50 | env: 51 | DOCKER_APIKEY: ${{ secrets.QUAY_IO_API_TOKEN }} 52 | with: 53 | destination_container_repo: ${{ env.DOCKER_REPO_NAME }} 54 | provider: quay 55 | # Empty string results in README-containers.md being pushed if it 56 | # exists. Otherwise, README.md is pushed. 57 | readme_file: '' 58 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This action is synced from https://github.com/prometheus/prometheus 3 | name: golangci-lint 4 | on: 5 | push: 6 | paths: 7 | - "go.sum" 8 | - "go.mod" 9 | - "**.go" 10 | - "scripts/errcheck_excludes.txt" 11 | - ".github/workflows/golangci-lint.yml" 12 | - ".golangci.yml" 13 | pull_request: 14 | 15 | permissions: # added using https://github.com/step-security/secure-repo 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | permissions: 21 | contents: read # for actions/checkout to fetch code 22 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 23 | name: lint 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | - name: Install Go 29 | uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 30 | with: 31 | go-version: 1.24.x 32 | - name: Install snmp_exporter/generator dependencies 33 | run: sudo apt-get update && sudo apt-get -y install libsnmp-dev 34 | if: github.repository == 'prometheus/snmp_exporter' 35 | - name: Lint 36 | uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 37 | with: 38 | args: --verbose 39 | version: v2.1.5 40 | -------------------------------------------------------------------------------- /.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 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - misspell 6 | - sloglint 7 | - staticcheck 8 | exclusions: 9 | generated: lax 10 | presets: 11 | - comments 12 | - common-false-positives 13 | - legacy 14 | - std-error-handling 15 | paths: 16 | - third_party$ 17 | - builtin$ 18 | - examples$ 19 | formatters: 20 | exclusions: 21 | generated: lax 22 | paths: 23 | - third_party$ 24 | - builtin$ 25 | - examples$ 26 | -------------------------------------------------------------------------------- /.promu.yml: -------------------------------------------------------------------------------- 1 | go: 2 | # Whenever the Go version is updated here, .circle/config.yml should also 3 | # be updated. 4 | version: 1.24 5 | repository: 6 | path: github.com/prometheus/mysqld_exporter 7 | build: 8 | ldflags: | 9 | -X github.com/prometheus/common/version.Version={{.Version}} 10 | -X github.com/prometheus/common/version.Revision={{.Revision}} 11 | -X github.com/prometheus/common/version.Branch={{.Branch}} 12 | -X github.com/prometheus/common/version.BuildUser={{user}}@{{host}} 13 | -X github.com/prometheus/common/version.BuildDate={{date "20060102-15:04:05"}} 14 | tarball: 15 | files: 16 | - LICENSE 17 | - NOTICE 18 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | ignore: | 4 | **/node_modules 5 | 6 | rules: 7 | braces: 8 | max-spaces-inside: 1 9 | level: error 10 | brackets: 11 | max-spaces-inside: 1 12 | level: error 13 | commas: disable 14 | comments: disable 15 | comments-indentation: disable 16 | document-start: disable 17 | indentation: 18 | spaces: consistent 19 | indent-sequences: consistent 20 | key-duplicates: 21 | ignore: | 22 | config/testdata/section_key_dup.bad.yml 23 | line-length: disable 24 | truthy: 25 | check-keys: false 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Prometheus Community Code of Conduct 2 | 3 | Prometheus follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Prometheus uses GitHub to manage reviews of pull requests. 4 | 5 | * If you have a trivial fix or improvement, go ahead and create a pull request, 6 | addressing (with `@...`) the maintainer of this repository (see 7 | [MAINTAINERS.md](MAINTAINERS.md)) in the description of the pull request. 8 | 9 | * If you plan to do something more involved, first discuss your ideas 10 | on our [mailing list](https://groups.google.com/forum/?fromgroups#!forum/prometheus-developers). 11 | This will avoid unnecessary work and surely give you and us a good deal 12 | of inspiration. 13 | 14 | * Relevant coding style guidelines are the [Go Code Review 15 | Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments) 16 | and the _Formatting and style_ section of Peter Bourgon's [Go: Best 17 | Practices for Production 18 | Environments](http://peter.bourgon.org/go-in-production/#formatting-and-style). 19 | 20 | 21 | ## Local setup 22 | 23 | The easiest way to make a local development setup is to use Docker Compose. 24 | 25 | ``` 26 | docker-compose up 27 | make 28 | make test 29 | ``` 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ARCH="amd64" 2 | ARG OS="linux" 3 | FROM quay.io/prometheus/busybox-${OS}-${ARCH}:latest 4 | LABEL maintainer="The Prometheus Authors " 5 | 6 | ARG ARCH="amd64" 7 | ARG OS="linux" 8 | COPY .build/${OS}-${ARCH}/mysqld_exporter /bin/mysqld_exporter 9 | 10 | EXPOSE 9104 11 | USER nobody 12 | ENTRYPOINT [ "/bin/mysqld_exporter" ] 13 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | * Ben Kochie 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2015 The Prometheus Authors 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # 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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Exporter for MySQL daemon. 2 | Copyright 2015 The Prometheus Authors 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | The Prometheus security policy, including how to report vulnerabilities, can be 4 | found here: 5 | 6 | 7 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.17.2 2 | -------------------------------------------------------------------------------- /collector/binlog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW BINARY LOGS` 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "log/slog" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const ( 29 | // Subsystem. 30 | binlog = "binlog" 31 | // Queries. 32 | logbinQuery = `SELECT @@log_bin` 33 | binlogQuery = `SHOW BINARY LOGS` 34 | ) 35 | 36 | // Metric descriptors. 37 | var ( 38 | binlogSizeDesc = prometheus.NewDesc( 39 | prometheus.BuildFQName(namespace, binlog, "size_bytes"), 40 | "Combined size of all registered binlog files.", 41 | []string{}, nil, 42 | ) 43 | binlogFilesDesc = prometheus.NewDesc( 44 | prometheus.BuildFQName(namespace, binlog, "files"), 45 | "Number of registered binlog files.", 46 | []string{}, nil, 47 | ) 48 | binlogFileNumberDesc = prometheus.NewDesc( 49 | prometheus.BuildFQName(namespace, binlog, "file_number"), 50 | "The last binlog file number.", 51 | []string{}, nil, 52 | ) 53 | ) 54 | 55 | // ScrapeBinlogSize collects from `SHOW BINARY LOGS`. 56 | type ScrapeBinlogSize struct{} 57 | 58 | // Name of the Scraper. Should be unique. 59 | func (ScrapeBinlogSize) Name() string { 60 | return "binlog_size" 61 | } 62 | 63 | // Help describes the role of the Scraper. 64 | func (ScrapeBinlogSize) Help() string { 65 | return "Collect the current size of all registered binlog files" 66 | } 67 | 68 | // Version of MySQL from which scraper is available. 69 | func (ScrapeBinlogSize) Version() float64 { 70 | return 5.1 71 | } 72 | 73 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 74 | func (ScrapeBinlogSize) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 75 | var logBin uint8 76 | db := instance.getDB() 77 | err := db.QueryRowContext(ctx, logbinQuery).Scan(&logBin) 78 | if err != nil { 79 | return err 80 | } 81 | // If log_bin is OFF, do not run SHOW BINARY LOGS which explicitly produces MySQL error 82 | if logBin == 0 { 83 | return nil 84 | } 85 | 86 | masterLogRows, err := db.QueryContext(ctx, binlogQuery) 87 | if err != nil { 88 | return err 89 | } 90 | defer masterLogRows.Close() 91 | 92 | var ( 93 | size uint64 94 | count uint64 95 | filename string 96 | filesize uint64 97 | encrypted string 98 | ) 99 | size = 0 100 | count = 0 101 | 102 | columns, err := masterLogRows.Columns() 103 | if err != nil { 104 | return err 105 | } 106 | columnCount := len(columns) 107 | 108 | for masterLogRows.Next() { 109 | switch columnCount { 110 | case 2: 111 | if err := masterLogRows.Scan(&filename, &filesize); err != nil { 112 | return nil 113 | } 114 | case 3: 115 | if err := masterLogRows.Scan(&filename, &filesize, &encrypted); err != nil { 116 | return nil 117 | } 118 | default: 119 | return fmt.Errorf("invalid number of columns: %q", columnCount) 120 | } 121 | 122 | size += filesize 123 | count++ 124 | } 125 | 126 | ch <- prometheus.MustNewConstMetric( 127 | binlogSizeDesc, prometheus.GaugeValue, float64(size), 128 | ) 129 | ch <- prometheus.MustNewConstMetric( 130 | binlogFilesDesc, prometheus.GaugeValue, float64(count), 131 | ) 132 | // The last row contains the last binlog file number. 133 | value, _ := strconv.ParseFloat(strings.Split(filename, ".")[1], 64) 134 | ch <- prometheus.MustNewConstMetric( 135 | binlogFileNumberDesc, prometheus.GaugeValue, value, 136 | ) 137 | 138 | return nil 139 | } 140 | 141 | // check interface 142 | var _ Scraper = ScrapeBinlogSize{} 143 | -------------------------------------------------------------------------------- /collector/binlog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 35 | 36 | mock.ExpectQuery(logbinQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) 37 | 38 | columns := []string{"Log_name", "File_size"} 39 | rows := sqlmock.NewRows(columns). 40 | AddRow("centos6-bin.000001", "1813"). 41 | AddRow("centos6-bin.000002", "120"). 42 | AddRow("centos6-bin.000444", "573009") 43 | mock.ExpectQuery(sanitizeQuery(binlogQuery)).WillReturnRows(rows) 44 | 45 | ch := make(chan prometheus.Metric) 46 | go func() { 47 | if err = (ScrapeBinlogSize{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 48 | t.Errorf("error calling function on test: %s", err) 49 | } 50 | close(ch) 51 | }() 52 | 53 | counterExpected := []MetricResult{ 54 | {labels: labelMap{}, value: 574942, metricType: dto.MetricType_GAUGE}, 55 | {labels: labelMap{}, value: 3, metricType: dto.MetricType_GAUGE}, 56 | {labels: labelMap{}, value: 444, metricType: dto.MetricType_GAUGE}, 57 | } 58 | convey.Convey("Metrics comparison", t, func() { 59 | for _, expect := range counterExpected { 60 | got := readMetric(<-ch) 61 | convey.So(got, convey.ShouldResemble, expect) 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/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 _2 15:04:05 2006 MST", string(data)); err == nil { 63 | return float64(ts.Unix()), true 64 | } 65 | if ts, err := time.Parse(time.DateTime, string(data)); err == nil { 66 | return float64(ts.Unix()), true 67 | } 68 | if logNum := logRE.Find(data); logNum != nil { 69 | value, err := strconv.ParseFloat(string(logNum), 64) 70 | return value, err == nil 71 | } 72 | value, err := strconv.ParseFloat(string(data), 64) 73 | return value, err == nil 74 | } 75 | 76 | func parsePrivilege(data sql.RawBytes) (float64, bool) { 77 | if bytes.Equal(data, []byte("Y")) { 78 | return 1, true 79 | } 80 | if bytes.Equal(data, []byte("N")) { 81 | return 0, true 82 | } 83 | return -1, false 84 | } 85 | -------------------------------------------------------------------------------- /collector/collector_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.Summary != nil { 45 | return MetricResult{labels: labels, value: pb.GetSummary().GetSampleSum(), metricType: dto.MetricType_SUMMARY} 46 | } 47 | if pb.Untyped != nil { 48 | return MetricResult{labels: labels, value: pb.GetUntyped().GetValue(), metricType: dto.MetricType_UNTYPED} 49 | } 50 | panic("Unsupported metric type") 51 | } 52 | 53 | func sanitizeQuery(q string) string { 54 | q = strings.Join(strings.Fields(q), " ") 55 | q = strings.ReplaceAll(q, "(", "\\(") 56 | q = strings.ReplaceAll(q, ")", "\\)") 57 | q = strings.ReplaceAll(q, "*", "\\*") 58 | return q 59 | } 60 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const ( 29 | // Subsystem. 30 | innodb = "engine_innodb" 31 | // Query. 32 | engineInnodbStatusQuery = `SHOW ENGINE INNODB STATUS` 33 | ) 34 | 35 | // ScrapeEngineInnodbStatus scrapes from `SHOW ENGINE INNODB STATUS`. 36 | type ScrapeEngineInnodbStatus struct{} 37 | 38 | // Name of the Scraper. Should be unique. 39 | func (ScrapeEngineInnodbStatus) Name() string { 40 | return "engine_innodb_status" 41 | } 42 | 43 | // Help describes the role of the Scraper. 44 | func (ScrapeEngineInnodbStatus) Help() string { 45 | return "Collect from SHOW ENGINE INNODB STATUS" 46 | } 47 | 48 | // Version of MySQL from which scraper is available. 49 | func (ScrapeEngineInnodbStatus) Version() float64 { 50 | return 5.1 51 | } 52 | 53 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 54 | func (ScrapeEngineInnodbStatus) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 55 | db := instance.getDB() 56 | rows, err := db.QueryContext(ctx, engineInnodbStatusQuery) 57 | if err != nil { 58 | return err 59 | } 60 | defer rows.Close() 61 | 62 | var typeCol, nameCol, statusCol string 63 | // First row should contain the necessary info. If many rows returned then it's unknown case. 64 | if rows.Next() { 65 | if err := rows.Scan(&typeCol, &nameCol, &statusCol); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | // 0 queries inside InnoDB, 0 queries in queue 71 | // 0 read views open inside InnoDB 72 | rQueries, _ := regexp.Compile(`(\d+) queries inside InnoDB, (\d+) queries in queue`) 73 | rViews, _ := regexp.Compile(`(\d+) read views open inside InnoDB`) 74 | 75 | for _, line := range strings.Split(statusCol, "\n") { 76 | if data := rQueries.FindStringSubmatch(line); data != nil { 77 | value, _ := strconv.ParseFloat(data[1], 64) 78 | ch <- prometheus.MustNewConstMetric( 79 | newDesc(innodb, "queries_inside_innodb", "Queries inside InnoDB."), 80 | prometheus.GaugeValue, 81 | value, 82 | ) 83 | value, _ = strconv.ParseFloat(data[2], 64) 84 | ch <- prometheus.MustNewConstMetric( 85 | newDesc(innodb, "queries_in_queue", "Queries in queue."), 86 | prometheus.GaugeValue, 87 | value, 88 | ) 89 | } else if data := rViews.FindStringSubmatch(line); data != nil { 90 | value, _ := strconv.ParseFloat(data[1], 64) 91 | ch <- prometheus.MustNewConstMetric( 92 | newDesc(innodb, "read_views_open_inside_innodb", "Read views open inside InnoDB."), 93 | prometheus.GaugeValue, 94 | value, 95 | ) 96 | } 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // check interface 103 | var _ Scraper = ScrapeEngineInnodbStatus{} 104 | -------------------------------------------------------------------------------- /collector/engine_tokudb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `SHOW ENGINE TOKUDB STATUS`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "log/slog" 22 | "strings" 23 | 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, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 54 | db := instance.getDB() 55 | tokudbRows, err := db.QueryContext(ctx, engineTokudbStatusQuery) 56 | if err != nil { 57 | return err 58 | } 59 | defer tokudbRows.Close() 60 | 61 | var temp, key string 62 | var val sql.RawBytes 63 | 64 | for tokudbRows.Next() { 65 | if err := tokudbRows.Scan(&temp, &key, &val); err != nil { 66 | return err 67 | } 68 | key = strings.ToLower(key) 69 | if floatVal, ok := parseStatus(val); ok { 70 | ch <- prometheus.MustNewConstMetric( 71 | newDesc(tokudb, sanitizeTokudbMetric(key), "Generic metric from SHOW ENGINE TOKUDB STATUS."), 72 | prometheus.UntypedValue, 73 | floatVal, 74 | ) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func sanitizeTokudbMetric(metricName string) string { 81 | replacements := map[string]string{ 82 | ">": "", 83 | ",": "", 84 | ":": "", 85 | "(": "", 86 | ")": "", 87 | " ": "_", 88 | "-": "_", 89 | "+": "and", 90 | "/": "and", 91 | } 92 | for r := range replacements { 93 | metricName = strings.ReplaceAll(metricName, r, replacements[r]) 94 | } 95 | return metricName 96 | } 97 | 98 | // check interface 99 | var _ Scraper = ScrapeEngineTokudbStatus{} 100 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 50 | 51 | columns := []string{"Type", "Name", "Status"} 52 | rows := sqlmock.NewRows(columns). 53 | AddRow("TokuDB", "indexer: number of calls to indexer->build() succeeded", "1"). 54 | AddRow("TokuDB", "ft: promotion: stopped anyway, after locking the child", "45316247"). 55 | AddRow("TokuDB", "memory: mallocator version", "3.3.1-0-g9ef9d9e8c271cdf14f664b871a8f98c827714784"). 56 | AddRow("TokuDB", "filesystem: most recent disk full", "Thu Jan 1 00:00:00 1970"). 57 | AddRow("TokuDB", "locktree: time spent ending the STO early (seconds)", "9115.904484") 58 | 59 | mock.ExpectQuery(sanitizeQuery(engineTokudbStatusQuery)).WillReturnRows(rows) 60 | 61 | ch := make(chan prometheus.Metric) 62 | go func() { 63 | if err = (ScrapeEngineTokudbStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 64 | t.Errorf("error calling function on test: %s", err) 65 | } 66 | close(ch) 67 | }() 68 | 69 | metricsExpected := []MetricResult{ 70 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_UNTYPED}, 71 | {labels: labelMap{}, value: 45316247, metricType: dto.MetricType_UNTYPED}, 72 | {labels: labelMap{}, value: 9115.904484, metricType: dto.MetricType_UNTYPED}, 73 | } 74 | convey.Convey("Metrics comparison", t, func() { 75 | for _, expect := range metricsExpected { 76 | got := readMetric(<-ch) 77 | convey.So(got, convey.ShouldResemble, expect) 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/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 | "testing" 19 | 20 | "github.com/prometheus/client_golang/prometheus" 21 | "github.com/prometheus/common/model" 22 | "github.com/prometheus/common/promslog" 23 | "github.com/smartystreets/goconvey/convey" 24 | ) 25 | 26 | const dsn = "root@/mysql" 27 | 28 | func TestExporter(t *testing.T) { 29 | if testing.Short() { 30 | t.Skip("-short is passed, skipping test") 31 | } 32 | 33 | exporter := New( 34 | context.Background(), 35 | dsn, 36 | []Scraper{ 37 | ScrapeGlobalStatus{}, 38 | }, 39 | promslog.NewNopLogger(), 40 | ) 41 | 42 | convey.Convey("Metrics describing", t, func() { 43 | ch := make(chan *prometheus.Desc) 44 | go func() { 45 | exporter.Describe(ch) 46 | close(ch) 47 | }() 48 | 49 | for range ch { 50 | } 51 | }) 52 | 53 | convey.Convey("Metrics collection", t, func() { 54 | ch := make(chan prometheus.Metric) 55 | go func() { 56 | exporter.Collect(ch) 57 | close(ch) 58 | }() 59 | 60 | for m := range ch { 61 | got := readMetric(m) 62 | if got.labels[model.MetricNameLabel] == "mysql_up" { 63 | convey.So(got.value, convey.ShouldEqual, 1) 64 | } 65 | } 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | columns := []string{"Variable_name", "Value"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("Com_alter_db", "1"). 38 | AddRow("Com_show_status", "2"). 39 | AddRow("Com_select", "3"). 40 | AddRow("Connection_errors_internal", "4"). 41 | AddRow("Handler_commit", "5"). 42 | AddRow("Innodb_buffer_pool_pages_data", "6"). 43 | AddRow("Innodb_buffer_pool_pages_flushed", "7"). 44 | AddRow("Innodb_buffer_pool_pages_dirty", "7"). 45 | AddRow("Innodb_buffer_pool_pages_free", "8"). 46 | AddRow("Innodb_buffer_pool_pages_misc", "9"). 47 | AddRow("Innodb_buffer_pool_pages_old", "10"). 48 | AddRow("Innodb_buffer_pool_pages_total", "11"). 49 | AddRow("Innodb_buffer_pool_pages_lru_flushed", "13"). 50 | AddRow("Innodb_buffer_pool_pages_made_not_young", "14"). 51 | AddRow("Innodb_buffer_pool_pages_made_young", "15"). 52 | AddRow("Innodb_rows_read", "8"). 53 | AddRow("Performance_schema_users_lost", "9"). 54 | AddRow("Slave_running", "OFF"). 55 | AddRow("Ssl_version", ""). 56 | AddRow("Uptime", "10"). 57 | AddRow("validate_password.dictionary_file_words_count", "11"). 58 | AddRow("wsrep_cluster_status", "Primary"). 59 | AddRow("wsrep_local_state_uuid", "6c06e583-686f-11e6-b9e3-8336ad58138c"). 60 | AddRow("wsrep_cluster_state_uuid", "6c06e583-686f-11e6-b9e3-8336ad58138c"). 61 | AddRow("wsrep_provider_version", "3.16(r5c765eb)"). 62 | AddRow("wsrep_evs_repl_latency", "0.000227664/0.00034135/0.000544298/6.03708e-05/212") 63 | mock.ExpectQuery(sanitizeQuery(globalStatusQuery)).WillReturnRows(rows) 64 | 65 | ch := make(chan prometheus.Metric) 66 | go func() { 67 | if err = (ScrapeGlobalStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 68 | t.Errorf("error calling function on test: %s", err) 69 | } 70 | close(ch) 71 | }() 72 | 73 | counterExpected := []MetricResult{ 74 | {labels: labelMap{"command": "alter_db"}, value: 1, metricType: dto.MetricType_COUNTER}, 75 | {labels: labelMap{"command": "show_status"}, value: 2, metricType: dto.MetricType_COUNTER}, 76 | {labels: labelMap{"command": "select"}, value: 3, metricType: dto.MetricType_COUNTER}, 77 | {labels: labelMap{"error": "internal"}, value: 4, metricType: dto.MetricType_COUNTER}, 78 | {labels: labelMap{"handler": "commit"}, value: 5, metricType: dto.MetricType_COUNTER}, 79 | {labels: labelMap{"state": "data"}, value: 6, metricType: dto.MetricType_GAUGE}, 80 | {labels: labelMap{"operation": "flushed"}, value: 7, metricType: dto.MetricType_COUNTER}, 81 | {labels: labelMap{}, value: 7, metricType: dto.MetricType_GAUGE}, 82 | {labels: labelMap{"state": "free"}, value: 8, metricType: dto.MetricType_GAUGE}, 83 | {labels: labelMap{"state": "misc"}, value: 9, metricType: dto.MetricType_GAUGE}, 84 | {labels: labelMap{"state": "old"}, value: 10, metricType: dto.MetricType_GAUGE}, 85 | //{labels: labelMap{"state": "total_pages"}, value: 11, metricType: dto.MetricType_GAUGE}, 86 | {labels: labelMap{"operation": "lru_flushed"}, value: 13, metricType: dto.MetricType_COUNTER}, 87 | {labels: labelMap{"operation": "made_not_young"}, value: 14, metricType: dto.MetricType_COUNTER}, 88 | {labels: labelMap{"operation": "made_young"}, value: 15, metricType: dto.MetricType_COUNTER}, 89 | {labels: labelMap{"operation": "read"}, value: 8, metricType: dto.MetricType_COUNTER}, 90 | {labels: labelMap{"instrumentation": "users_lost"}, value: 9, metricType: dto.MetricType_COUNTER}, 91 | {labels: labelMap{}, value: 0, metricType: dto.MetricType_UNTYPED}, 92 | {labels: labelMap{}, value: 10, metricType: dto.MetricType_UNTYPED}, 93 | {labels: labelMap{}, value: 11, metricType: dto.MetricType_UNTYPED}, 94 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_UNTYPED}, 95 | {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}, 96 | {labels: labelMap{}, value: 0.000227664, metricType: dto.MetricType_GAUGE}, 97 | {labels: labelMap{}, value: 0.00034135, metricType: dto.MetricType_GAUGE}, 98 | {labels: labelMap{}, value: 0.000544298, metricType: dto.MetricType_GAUGE}, 99 | {labels: labelMap{}, value: 6.03708e-05, metricType: dto.MetricType_GAUGE}, 100 | {labels: labelMap{}, value: 212, metricType: dto.MetricType_GAUGE}, 101 | } 102 | convey.Convey("Metrics comparison", t, func() { 103 | for _, expect := range counterExpected { 104 | got := readMetric(<-ch) 105 | convey.So(got, convey.ShouldResemble, expect) 106 | } 107 | }) 108 | 109 | // Ensure all SQL queries were executed 110 | if err := mock.ExpectationsWereMet(); err != nil { 111 | t.Errorf("there were unfulfilled exceptions: %s", err) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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 | "log/slog" 23 | "strconv" 24 | 25 | "github.com/alecthomas/kingpin/v2" 26 | "github.com/prometheus/client_golang/prometheus" 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 | // 73 | // ts varchar(26) NOT NULL, 74 | // server_id int unsigned NOT NULL PRIMARY KEY, 75 | // 76 | // ); 77 | type ScrapeHeartbeat struct{} 78 | 79 | // Name of the Scraper. Should be unique. 80 | func (ScrapeHeartbeat) Name() string { 81 | return "heartbeat" 82 | } 83 | 84 | // Help describes the role of the Scraper. 85 | func (ScrapeHeartbeat) Help() string { 86 | return "Collect from heartbeat" 87 | } 88 | 89 | // Version of MySQL from which scraper is available. 90 | func (ScrapeHeartbeat) Version() float64 { 91 | return 5.1 92 | } 93 | 94 | // nowExpr returns a current timestamp expression. 95 | func nowExpr() string { 96 | if *collectHeartbeatUtc { 97 | return "UTC_TIMESTAMP(6)" 98 | } 99 | return "NOW(6)" 100 | } 101 | 102 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 103 | func (ScrapeHeartbeat) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 104 | db := instance.getDB() 105 | query := fmt.Sprintf(heartbeatQuery, nowExpr(), *collectHeartbeatDatabase, *collectHeartbeatTable) 106 | heartbeatRows, err := db.QueryContext(ctx, query) 107 | if err != nil { 108 | return err 109 | } 110 | defer heartbeatRows.Close() 111 | 112 | var ( 113 | now, ts sql.RawBytes 114 | serverId int 115 | ) 116 | 117 | for heartbeatRows.Next() { 118 | if err := heartbeatRows.Scan(&ts, &now, &serverId); err != nil { 119 | return err 120 | } 121 | 122 | tsFloatVal, err := strconv.ParseFloat(string(ts), 64) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | nowFloatVal, err := strconv.ParseFloat(string(now), 64) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | serverId := strconv.Itoa(serverId) 133 | 134 | ch <- prometheus.MustNewConstMetric( 135 | HeartbeatNowDesc, 136 | prometheus.GaugeValue, 137 | nowFloatVal, 138 | serverId, 139 | ) 140 | ch <- prometheus.MustNewConstMetric( 141 | HeartbeatStoredDesc, 142 | prometheus.GaugeValue, 143 | tsFloatVal, 144 | serverId, 145 | ) 146 | } 147 | 148 | return nil 149 | } 150 | 151 | // check interface 152 | var _ Scraper = ScrapeHeartbeat{} 153 | -------------------------------------------------------------------------------- /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/alecthomas/kingpin/v2" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/smartystreets/goconvey/convey" 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 | inst := &instance{db: db} 69 | 70 | rows := sqlmock.NewRows(tt.Columns). 71 | AddRow("1487597613.001320", "1487598113.448042", 1) 72 | mock.ExpectQuery(sanitizeQuery(tt.Query)).WillReturnRows(rows) 73 | 74 | ch := make(chan prometheus.Metric) 75 | go func() { 76 | if err = (ScrapeHeartbeat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 77 | t.Errorf("error calling function on test: %s", err) 78 | } 79 | close(ch) 80 | }() 81 | 82 | counterExpected := []MetricResult{ 83 | {labels: labelMap{"server_id": "1"}, value: 1487598113.448042, metricType: dto.MetricType_GAUGE}, 84 | {labels: labelMap{"server_id": "1"}, value: 1487597613.00132, metricType: dto.MetricType_GAUGE}, 85 | } 86 | convey.Convey("Metrics comparison", t, func() { 87 | for _, expect := range counterExpected { 88 | got := readMetric(<-ch) 89 | convey.So(got, convey.ShouldResemble, expect) 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 | } 99 | } 100 | -------------------------------------------------------------------------------- /collector/info_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | // Subsystem. 17 | const informationSchema = "info_schema" 18 | -------------------------------------------------------------------------------- /collector/info_schema_auto_increment.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape auto_increment column information. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const infoSchemaAutoIncrementQuery = ` 26 | SELECT c.table_schema, c.table_name, column_name, auto_increment, 27 | pow(2, case data_type 28 | when 'tinyint' then 7 29 | when 'smallint' then 15 30 | when 'mediumint' then 23 31 | when 'int' then 31 32 | when 'bigint' then 63 33 | end+(column_type like '% unsigned'))-1 as max_int 34 | FROM information_schema.columns c 35 | STRAIGHT_JOIN information_schema.tables t ON (BINARY c.table_schema=t.table_schema AND BINARY c.table_name=t.table_name) 36 | WHERE c.extra = 'auto_increment' AND t.auto_increment IS NOT NULL 37 | ` 38 | 39 | // Metric descriptors. 40 | var ( 41 | globalInfoSchemaAutoIncrementDesc = prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column"), 43 | "The current value of an auto_increment column from information_schema.", 44 | []string{"schema", "table", "column"}, nil, 45 | ) 46 | globalInfoSchemaAutoIncrementMaxDesc = prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, informationSchema, "auto_increment_column_max"), 48 | "The max value of an auto_increment column from information_schema.", 49 | []string{"schema", "table", "column"}, nil, 50 | ) 51 | ) 52 | 53 | // ScrapeAutoIncrementColumns collects auto_increment column information. 54 | type ScrapeAutoIncrementColumns struct{} 55 | 56 | // Name of the Scraper. Should be unique. 57 | func (ScrapeAutoIncrementColumns) Name() string { 58 | return "auto_increment.columns" 59 | } 60 | 61 | // Help describes the role of the Scraper. 62 | func (ScrapeAutoIncrementColumns) Help() string { 63 | return "Collect auto_increment columns and max values from information_schema" 64 | } 65 | 66 | // Version of MySQL from which scraper is available. 67 | func (ScrapeAutoIncrementColumns) 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 (ScrapeAutoIncrementColumns) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 73 | db := instance.getDB() 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/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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 36 | AddRow("userstat", "ON")) 37 | 38 | 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"} 39 | rows := sqlmock.NewRows(columns). 40 | 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) 41 | mock.ExpectQuery(sanitizeQuery(clientStatQuery)).WillReturnRows(rows) 42 | 43 | ch := make(chan prometheus.Metric) 44 | go func() { 45 | if err = (ScrapeClientStat{}).Scrape(context.Background(), inst, ch, promslog.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{"client": "localhost"}, value: 1002, metricType: dto.MetricType_COUNTER}, 53 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_GAUGE}, 54 | {labels: labelMap{"client": "localhost"}, value: 127027, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"client": "localhost"}, value: 286, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"client": "localhost"}, value: 245, metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"client": "localhost"}, value: float64(2565104853), metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"client": "localhost"}, value: 21090856, metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"client": "localhost"}, value: float64(2380108042), metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"client": "localhost"}, value: 767691, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"client": "localhost"}, value: 1764, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"client": "localhost"}, value: 8778, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"client": "localhost"}, value: 1210741, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"client": "localhost"}, value: 1764, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"client": "localhost"}, value: 1214416, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"client": "localhost"}, value: 293, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"client": "localhost"}, value: 2430888, 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 | {labels: labelMap{"client": "localhost"}, value: 0, metricType: dto.MetricType_COUNTER}, 74 | } 75 | convey.Convey("Metrics comparison", t, func() { 76 | for _, expect := range expected { 77 | got := readMetric(<-ch) 78 | convey.So(expect, convey.ShouldResemble, got) 79 | } 80 | }) 81 | 82 | // Ensure all SQL queries were executed 83 | if err := mock.ExpectationsWereMet(); err != nil { 84 | t.Errorf("there were unfulfilled exceptions: %s", err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const innodbCmpQuery = ` 26 | SELECT 27 | page_size, compress_ops, compress_ops_ok, compress_time, uncompress_ops, uncompress_time 28 | FROM information_schema.innodb_cmp 29 | ` 30 | 31 | // Metric descriptors. 32 | var ( 33 | infoSchemaInnodbCmpCompressOps = prometheus.NewDesc( 34 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_ops_total"), 35 | "Number of times a B-tree page of the size PAGE_SIZE has been compressed.", 36 | []string{"page_size"}, nil, 37 | ) 38 | infoSchemaInnodbCmpCompressOpsOk = prometheus.NewDesc( 39 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_ops_ok_total"), 40 | "Number of times a B-tree page of the size PAGE_SIZE has been successfully compressed.", 41 | []string{"page_size"}, nil, 42 | ) 43 | infoSchemaInnodbCmpCompressTime = prometheus.NewDesc( 44 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_compress_time_seconds_total"), 45 | "Total time in seconds spent in attempts to compress B-tree pages.", 46 | []string{"page_size"}, nil, 47 | ) 48 | infoSchemaInnodbCmpUncompressOps = prometheus.NewDesc( 49 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_uncompress_ops_total"), 50 | "Number of times a B-tree page of the size PAGE_SIZE has been uncompressed.", 51 | []string{"page_size"}, nil, 52 | ) 53 | infoSchemaInnodbCmpUncompressTime = prometheus.NewDesc( 54 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmp_uncompress_time_seconds_total"), 55 | "Total time in seconds spent in uncompressing B-tree pages.", 56 | []string{"page_size"}, nil, 57 | ) 58 | ) 59 | 60 | // ScrapeInnodbCmp collects from `information_schema.innodb_cmp`. 61 | type ScrapeInnodbCmp struct{} 62 | 63 | // Name of the Scraper. Should be unique. 64 | func (ScrapeInnodbCmp) Name() string { 65 | return informationSchema + ".innodb_cmp" 66 | } 67 | 68 | // Help describes the role of the Scraper. 69 | func (ScrapeInnodbCmp) Help() string { 70 | return "Collect metrics from information_schema.innodb_cmp" 71 | } 72 | 73 | // Version of MySQL from which scraper is available. 74 | func (ScrapeInnodbCmp) Version() float64 { 75 | return 5.5 76 | } 77 | 78 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 79 | func (ScrapeInnodbCmp) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 80 | db := instance.getDB() 81 | informationSchemaInnodbCmpRows, err := db.QueryContext(ctx, innodbCmpQuery) 82 | if err != nil { 83 | return err 84 | } 85 | defer informationSchemaInnodbCmpRows.Close() 86 | 87 | var ( 88 | page_size string 89 | compress_ops, compress_ops_ok, compress_time, uncompress_ops, uncompress_time float64 90 | ) 91 | 92 | for informationSchemaInnodbCmpRows.Next() { 93 | if err := informationSchemaInnodbCmpRows.Scan( 94 | &page_size, &compress_ops, &compress_ops_ok, &compress_time, &uncompress_ops, &uncompress_time, 95 | ); err != nil { 96 | return err 97 | } 98 | 99 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressOps, prometheus.CounterValue, compress_ops, page_size) 100 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressOpsOk, prometheus.CounterValue, compress_ops_ok, page_size) 101 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpCompressTime, prometheus.CounterValue, compress_time, page_size) 102 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpUncompressOps, prometheus.CounterValue, uncompress_ops, page_size) 103 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpUncompressTime, prometheus.CounterValue, uncompress_time, page_size) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | // check interface 110 | var _ Scraper = ScrapeInnodbCmp{} 111 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | columns := []string{"page_size", "compress_ops", "compress_ops_ok", "compress_time", "uncompress_ops", "uncompress_time"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("1024", 10, 20, 30, 40, 50) 38 | mock.ExpectQuery(sanitizeQuery(innodbCmpQuery)).WillReturnRows(rows) 39 | 40 | ch := make(chan prometheus.Metric) 41 | go func() { 42 | if err = (ScrapeInnodbCmp{}).Scrape(context.Background(), inst, ch, promslog.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{"page_size": "1024"}, value: 10, metricType: dto.MetricType_COUNTER}, 50 | {labels: labelMap{"page_size": "1024"}, value: 20, metricType: dto.MetricType_COUNTER}, 51 | {labels: labelMap{"page_size": "1024"}, value: 30, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"page_size": "1024"}, value: 40, metricType: dto.MetricType_COUNTER}, 53 | {labels: labelMap{"page_size": "1024"}, value: 50, metricType: dto.MetricType_COUNTER}, 54 | } 55 | convey.Convey("Metrics comparison", t, func() { 56 | for _, expect := range expected { 57 | got := readMetric(<-ch) 58 | convey.So(expect, convey.ShouldResemble, got) 59 | } 60 | }) 61 | 62 | // Ensure all SQL queries were executed 63 | if err := mock.ExpectationsWereMet(); err != nil { 64 | t.Errorf("there were unfulfilled exceptions: %s", err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const innodbCmpMemQuery = ` 26 | SELECT 27 | page_size, buffer_pool_instance, pages_used, pages_free, relocation_ops, relocation_time 28 | FROM information_schema.innodb_cmpmem 29 | ` 30 | 31 | // Metric descriptors. 32 | var ( 33 | infoSchemaInnodbCmpMemPagesRead = prometheus.NewDesc( 34 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_pages_used_total"), 35 | "Number of blocks of the size PAGE_SIZE that are currently in use.", 36 | []string{"page_size", "buffer_pool"}, nil, 37 | ) 38 | infoSchemaInnodbCmpMemPagesFree = prometheus.NewDesc( 39 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_pages_free_total"), 40 | "Number of blocks of the size PAGE_SIZE that are currently available for allocation.", 41 | []string{"page_size", "buffer_pool"}, nil, 42 | ) 43 | infoSchemaInnodbCmpMemRelocationOps = prometheus.NewDesc( 44 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_relocation_ops_total"), 45 | "Number of times a block of the size PAGE_SIZE has been relocated.", 46 | []string{"page_size", "buffer_pool"}, nil, 47 | ) 48 | infoSchemaInnodbCmpMemRelocationTime = prometheus.NewDesc( 49 | prometheus.BuildFQName(namespace, informationSchema, "innodb_cmpmem_relocation_time_seconds_total"), 50 | "Total time in seconds spent in relocating blocks.", 51 | []string{"page_size", "buffer_pool"}, nil, 52 | ) 53 | ) 54 | 55 | // ScrapeInnodbCmp collects from `information_schema.innodb_cmp`. 56 | type ScrapeInnodbCmpMem struct{} 57 | 58 | // Name of the Scraper. Should be unique. 59 | func (ScrapeInnodbCmpMem) Name() string { 60 | return informationSchema + ".innodb_cmpmem" 61 | } 62 | 63 | // Help describes the role of the Scraper. 64 | func (ScrapeInnodbCmpMem) Help() string { 65 | return "Collect metrics from information_schema.innodb_cmpmem" 66 | } 67 | 68 | // Version of MySQL from which scraper is available. 69 | func (ScrapeInnodbCmpMem) Version() float64 { 70 | return 5.5 71 | } 72 | 73 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 74 | func (ScrapeInnodbCmpMem) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 75 | db := instance.getDB() 76 | informationSchemaInnodbCmpMemRows, err := db.QueryContext(ctx, innodbCmpMemQuery) 77 | if err != nil { 78 | return err 79 | } 80 | defer informationSchemaInnodbCmpMemRows.Close() 81 | 82 | var ( 83 | page_size, buffer_pool string 84 | pages_used, pages_free, relocation_ops, relocation_time float64 85 | ) 86 | 87 | for informationSchemaInnodbCmpMemRows.Next() { 88 | if err := informationSchemaInnodbCmpMemRows.Scan( 89 | &page_size, &buffer_pool, &pages_used, &pages_free, &relocation_ops, &relocation_time, 90 | ); err != nil { 91 | return err 92 | } 93 | 94 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesRead, prometheus.CounterValue, pages_used, page_size, buffer_pool) 95 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemPagesFree, prometheus.CounterValue, pages_free, page_size, buffer_pool) 96 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationOps, prometheus.CounterValue, relocation_ops, page_size, buffer_pool) 97 | ch <- prometheus.MustNewConstMetric(infoSchemaInnodbCmpMemRelocationTime, prometheus.CounterValue, (relocation_time / 1000), page_size, buffer_pool) 98 | } 99 | return nil 100 | } 101 | 102 | // check interface 103 | var _ Scraper = ScrapeInnodbCmpMem{} 104 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_cmpmem_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | columns := []string{"page_size", "buffer_pool", "pages_used", "pages_free", "relocation_ops", "relocation_time"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("1024", "0", 30, 40, 50, 6000) 38 | mock.ExpectQuery(sanitizeQuery(innodbCmpMemQuery)).WillReturnRows(rows) 39 | 40 | ch := make(chan prometheus.Metric) 41 | go func() { 42 | if err = (ScrapeInnodbCmpMem{}).Scrape(context.Background(), inst, ch, promslog.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{"page_size": "1024", "buffer_pool": "0"}, value: 30, metricType: dto.MetricType_COUNTER}, 50 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 40, metricType: dto.MetricType_COUNTER}, 51 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 50, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"page_size": "1024", "buffer_pool": "0"}, value: 6, metricType: dto.MetricType_COUNTER}, 53 | } 54 | convey.Convey("Metrics comparison", t, func() { 55 | for _, expect := range expected { 56 | got := readMetric(<-ch) 57 | convey.So(expect, convey.ShouldResemble, got) 58 | } 59 | }) 60 | 61 | // Ensure all SQL queries were executed 62 | if err := mock.ExpectationsWereMet(); err != nil { 63 | t.Errorf("there were unfulfilled exceptions: %s", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /collector/info_schema_innodb_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/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 35 | 36 | enabledColumnName := []string{"COLUMN_NAME"} 37 | rows := sqlmock.NewRows(enabledColumnName). 38 | AddRow("STATUS") 39 | mock.ExpectQuery(sanitizeQuery(infoSchemaInnodbMetricsEnabledColumnQuery)).WillReturnRows(rows) 40 | 41 | columns := []string{"name", "subsystem", "type", "comment", "count"} 42 | rows = sqlmock.NewRows(columns). 43 | AddRow("lock_timeouts", "lock", "counter", "Number of lock timeouts", 0). 44 | AddRow("buffer_pool_reads", "buffer", "status_counter", "Number of reads directly from disk (innodb_buffer_pool_reads)", 1). 45 | AddRow("buffer_pool_size", "server", "value", "Server buffer pool size (all buffer pools) in bytes", 2). 46 | AddRow("buffer_page_read_system_page", "buffer_page_io", "counter", "Number of System Pages read", 3). 47 | AddRow("buffer_page_written_undo_log", "buffer_page_io", "counter", "Number of Undo Log Pages written", 4). 48 | AddRow("buffer_pool_pages_dirty", "buffer", "gauge", "Number of dirt buffer pool pages", 5). 49 | AddRow("buffer_pool_pages_data", "buffer", "gauge", "Number of data buffer pool pages", 6). 50 | AddRow("buffer_pool_pages_total", "buffer", "gauge", "Number of total buffer pool pages", 7). 51 | AddRow("NOPE", "buffer_page_io", "counter", "An invalid buffer_page_io metric", 999) 52 | query := fmt.Sprintf(infoSchemaInnodbMetricsQuery, "status", "enabled") 53 | mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) 54 | 55 | ch := make(chan prometheus.Metric) 56 | go func() { 57 | if err = (ScrapeInnodbMetrics{}).Scrape(context.Background(), inst, ch, promslog.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{}, value: 0, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{}, value: 1, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{}, value: 2, metricType: dto.MetricType_GAUGE}, 67 | {labels: labelMap{"type": "system_page"}, value: 3, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"type": "undo_log"}, value: 4, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{}, value: 5, metricType: dto.MetricType_GAUGE}, 70 | {labels: labelMap{"state": "data"}, value: 6, metricType: dto.MetricType_GAUGE}, 71 | } 72 | convey.Convey("Metrics comparison", t, func() { 73 | for _, expect := range metricExpected { 74 | got := readMetric(<-ch) 75 | convey.So(got, convey.ShouldResemble, expect) 76 | } 77 | }) 78 | 79 | // Ensure all SQL queries were executed 80 | if err := mock.ExpectationsWereMet(); err != nil { 81 | t.Errorf("there were unfulfilled exceptions: %s", err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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/alecthomas/kingpin/v2" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/smartystreets/goconvey/convey" 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 | inst := &instance{db: db} 44 | 45 | query := fmt.Sprintf(infoSchemaProcesslistQuery, 0) 46 | columns := []string{"user", "host", "command", "state", "processes", "seconds"} 47 | rows := sqlmock.NewRows(columns). 48 | AddRow("manager", "10.0.7.234", "Sleep", "", 10, 87). 49 | AddRow("feedback", "10.0.7.154", "Sleep", "", 8, 842). 50 | AddRow("root", "10.0.7.253", "Sleep", "", 1, 20). 51 | AddRow("feedback", "10.0.7.179", "Sleep", "", 2, 14). 52 | AddRow("system user", "", "Connect", "waiting for handler commit", 1, 7271248). 53 | AddRow("manager", "10.0.7.234", "Sleep", "", 4, 62). 54 | AddRow("system user", "", "Query", "Slave has read all relay log; waiting for more updates", 1, 7271248). 55 | AddRow("event_scheduler", "localhost", "Daemon", "Waiting on empty queue", 1, 7271248) 56 | mock.ExpectQuery(sanitizeQuery(query)).WillReturnRows(rows) 57 | 58 | ch := make(chan prometheus.Metric) 59 | go func() { 60 | if err = (ScrapeProcesslist{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 61 | t.Errorf("error calling function on test: %s", err) 62 | } 63 | close(ch) 64 | }() 65 | 66 | expected := []MetricResult{ 67 | {labels: labelMap{"command": "connect", "state": "waiting_for_handler_commit"}, value: 1, metricType: dto.MetricType_GAUGE}, 68 | {labels: labelMap{"command": "connect", "state": "waiting_for_handler_commit"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 69 | {labels: labelMap{"command": "daemon", "state": "waiting_on_empty_queue"}, value: 1, metricType: dto.MetricType_GAUGE}, 70 | {labels: labelMap{"command": "daemon", "state": "waiting_on_empty_queue"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 71 | {labels: labelMap{"command": "query", "state": "slave_has_read_all_relay_log_waiting_for_more_updates"}, value: 1, metricType: dto.MetricType_GAUGE}, 72 | {labels: labelMap{"command": "query", "state": "slave_has_read_all_relay_log_waiting_for_more_updates"}, value: 7271248, metricType: dto.MetricType_GAUGE}, 73 | {labels: labelMap{"command": "sleep", "state": "unknown"}, value: 25, metricType: dto.MetricType_GAUGE}, 74 | {labels: labelMap{"command": "sleep", "state": "unknown"}, value: 1025, metricType: dto.MetricType_GAUGE}, 75 | {labels: labelMap{"client_host": "10.0.7.154"}, value: 8, metricType: dto.MetricType_GAUGE}, 76 | {labels: labelMap{"client_host": "10.0.7.179"}, value: 2, metricType: dto.MetricType_GAUGE}, 77 | {labels: labelMap{"client_host": "10.0.7.234"}, value: 14, metricType: dto.MetricType_GAUGE}, 78 | {labels: labelMap{"client_host": "10.0.7.253"}, value: 1, metricType: dto.MetricType_GAUGE}, 79 | {labels: labelMap{"client_host": "localhost"}, value: 1, metricType: dto.MetricType_GAUGE}, 80 | {labels: labelMap{"client_host": "unknown"}, value: 2, metricType: dto.MetricType_GAUGE}, 81 | {labels: labelMap{"mysql_user": "event_scheduler"}, value: 1, metricType: dto.MetricType_GAUGE}, 82 | {labels: labelMap{"mysql_user": "feedback"}, value: 10, metricType: dto.MetricType_GAUGE}, 83 | {labels: labelMap{"mysql_user": "manager"}, value: 14, metricType: dto.MetricType_GAUGE}, 84 | {labels: labelMap{"mysql_user": "root"}, value: 1, metricType: dto.MetricType_GAUGE}, 85 | {labels: labelMap{"mysql_user": "system user"}, value: 2, metricType: dto.MetricType_GAUGE}, 86 | } 87 | convey.Convey("Metrics comparison", t, func() { 88 | for _, expect := range expected { 89 | got := readMetric(<-ch) 90 | convey.So(expect, convey.ShouldResemble, got) 91 | } 92 | }) 93 | 94 | // Ensure all SQL queries were executed 95 | if err := mock.ExpectationsWereMet(); err != nil { 96 | t.Errorf("there were unfulfilled exceptions: %s", err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const queryResponseCheckQuery = `SELECT @@query_response_time_stats` 28 | 29 | var ( 30 | // Use uppercase for table names, otherwise read/write split will return the same results as total 31 | // due to the bug. 32 | queryResponseTimeQueries = [3]string{ 33 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME", 34 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME_READ", 35 | "SELECT TIME, COUNT, TOTAL FROM INFORMATION_SCHEMA.QUERY_RESPONSE_TIME_WRITE", 36 | } 37 | 38 | infoSchemaQueryResponseTimeCountDescs = [3]*prometheus.Desc{ 39 | prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "query_response_time_seconds"), 41 | "The number of all queries by duration they took to execute.", 42 | []string{}, nil, 43 | ), 44 | prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "read_query_response_time_seconds"), 46 | "The number of read queries by duration they took to execute.", 47 | []string{}, nil, 48 | ), 49 | prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "write_query_response_time_seconds"), 51 | "The number of write queries by duration they took to execute.", 52 | []string{}, nil, 53 | ), 54 | } 55 | ) 56 | 57 | func processQueryResponseTimeTable(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, query string, i int) error { 58 | db := instance.getDB() 59 | queryDistributionRows, err := db.QueryContext(ctx, query) 60 | if err != nil { 61 | return err 62 | } 63 | defer queryDistributionRows.Close() 64 | 65 | var ( 66 | length string 67 | count uint64 68 | total string 69 | histogramCnt uint64 70 | histogramSum float64 71 | countBuckets = map[float64]uint64{} 72 | ) 73 | 74 | for queryDistributionRows.Next() { 75 | err = queryDistributionRows.Scan( 76 | &length, 77 | &count, 78 | &total, 79 | ) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | length, _ := strconv.ParseFloat(strings.TrimSpace(length), 64) 85 | total, _ := strconv.ParseFloat(strings.TrimSpace(total), 64) 86 | histogramCnt += count 87 | histogramSum += total 88 | // Special case for "TOO LONG" row where we take into account the count field which is the only available 89 | // and do not add it as a part of histogram or metric 90 | if length == 0 { 91 | continue 92 | } 93 | countBuckets[length] = histogramCnt 94 | } 95 | // Create histogram with query counts 96 | ch <- prometheus.MustNewConstHistogram( 97 | infoSchemaQueryResponseTimeCountDescs[i], histogramCnt, histogramSum, countBuckets, 98 | ) 99 | return nil 100 | } 101 | 102 | // ScrapeQueryResponseTime collects from `information_schema.query_response_time`. 103 | type ScrapeQueryResponseTime struct{} 104 | 105 | // Name of the Scraper. Should be unique. 106 | func (ScrapeQueryResponseTime) Name() string { 107 | return "info_schema.query_response_time" 108 | } 109 | 110 | // Help describes the role of the Scraper. 111 | func (ScrapeQueryResponseTime) Help() string { 112 | return "Collect query response time distribution if query_response_time_stats is ON." 113 | } 114 | 115 | // Version of MySQL from which scraper is available. 116 | func (ScrapeQueryResponseTime) Version() float64 { 117 | return 5.5 118 | } 119 | 120 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 121 | func (ScrapeQueryResponseTime) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 122 | var queryStats uint8 123 | db := instance.getDB() 124 | err := db.QueryRowContext(ctx, queryResponseCheckQuery).Scan(&queryStats) 125 | if err != nil { 126 | logger.Debug("Query response time distribution is not available.") 127 | return nil 128 | } 129 | if queryStats == 0 { 130 | logger.Debug("MySQL variable is OFF.", "var", "query_response_time_stats") 131 | return nil 132 | } 133 | 134 | for i, query := range queryResponseTimeQueries { 135 | err := processQueryResponseTimeTable(ctx, instance, ch, query, i) 136 | // The first query should not fail if query_response_time_stats is ON, 137 | // unlike the other two when the read/write tables exist only with Percona Server 5.6/5.7. 138 | if i == 0 && err != nil { 139 | return err 140 | } 141 | } 142 | return nil 143 | } 144 | 145 | // check interface 146 | var _ Scraper = ScrapeQueryResponseTime{} 147 | -------------------------------------------------------------------------------- /collector/info_schema_query_response_time_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | mock.ExpectQuery(queryResponseCheckQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(1)) 36 | 37 | rows := sqlmock.NewRows([]string{"TIME", "COUNT", "TOTAL"}). 38 | AddRow(0.000001, 124, 0.000000). 39 | AddRow(0.000010, 179, 0.000797). 40 | AddRow(0.000100, 2859, 0.107321). 41 | AddRow(0.001000, 1085, 0.335395). 42 | AddRow(0.010000, 269, 0.522264). 43 | AddRow(0.100000, 11, 0.344209). 44 | AddRow(1.000000, 1, 0.267369). 45 | AddRow(10.000000, 0, 0.000000). 46 | AddRow(100.000000, 0, 0.000000). 47 | AddRow(1000.000000, 0, 0.000000). 48 | AddRow(10000.000000, 0, 0.000000). 49 | AddRow(100000.000000, 0, 0.000000). 50 | AddRow(1000000.000000, 0, 0.000000). 51 | AddRow("TOO LONG", 0, "TOO LONG") 52 | mock.ExpectQuery(sanitizeQuery(queryResponseTimeQueries[0])).WillReturnRows(rows) 53 | 54 | ch := make(chan prometheus.Metric) 55 | go func() { 56 | if err = (ScrapeQueryResponseTime{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 57 | t.Errorf("error calling function on test: %s", err) 58 | } 59 | close(ch) 60 | }() 61 | 62 | // Test histogram 63 | expectCounts := map[float64]uint64{ 64 | 1e-06: 124, 65 | 1e-05: 303, 66 | 0.0001: 3162, 67 | 0.001: 4247, 68 | 0.01: 4516, 69 | 0.1: 4527, 70 | 1: 4528, 71 | 10: 4528, 72 | 100: 4528, 73 | 1000: 4528, 74 | 10000: 4528, 75 | 100000: 4528, 76 | 1e+06: 4528, 77 | } 78 | expectHistogram := prometheus.MustNewConstHistogram(infoSchemaQueryResponseTimeCountDescs[0], 79 | 4528, 1.5773549999999998, expectCounts) 80 | expectPb := &dto.Metric{} 81 | expectHistogram.Write(expectPb) 82 | 83 | gotPb := &dto.Metric{} 84 | gotHistogram := <-ch // read the last item from channel 85 | gotHistogram.Write(gotPb) 86 | convey.Convey("Histogram comparison", t, func() { 87 | convey.So(expectPb.Histogram, convey.ShouldResemble, gotPb.Histogram) 88 | }) 89 | 90 | // Ensure all SQL queries were executed 91 | if err := mock.ExpectationsWereMet(); err != nil { 92 | t.Errorf("there were unfulfilled exceptions: %s", err) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | MySQL "github.com/go-sql-driver/mysql" 23 | "github.com/prometheus/client_golang/prometheus" 24 | ) 25 | 26 | const replicaHostQuery = ` 27 | SELECT SERVER_ID 28 | , if(SESSION_ID='MASTER_SESSION_ID','writer','reader') AS ROLE 29 | , CPU 30 | , MASTER_SLAVE_LATENCY_IN_MICROSECONDS 31 | , REPLICA_LAG_IN_MILLISECONDS 32 | , LOG_STREAM_SPEED_IN_KiB_PER_SECOND 33 | , CURRENT_REPLAY_LATENCY_IN_MICROSECONDS 34 | FROM information_schema.replica_host_status 35 | ` 36 | 37 | // Metric descriptors. 38 | var ( 39 | infoSchemaReplicaHostCpuDesc = prometheus.NewDesc( 40 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_cpu_percent"), 41 | "The CPU usage as a percentage.", 42 | []string{"server_id", "role"}, nil, 43 | ) 44 | infoSchemaReplicaHostReplicaLatencyDesc = prometheus.NewDesc( 45 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_replica_latency_seconds"), 46 | "The source-replica latency in seconds.", 47 | []string{"server_id", "role"}, nil, 48 | ) 49 | infoSchemaReplicaHostLagDesc = prometheus.NewDesc( 50 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_lag_seconds"), 51 | "The replica lag in seconds.", 52 | []string{"server_id", "role"}, nil, 53 | ) 54 | infoSchemaReplicaHostLogStreamSpeedDesc = prometheus.NewDesc( 55 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_log_stream_speed"), 56 | "The log stream speed in kilobytes per second.", 57 | []string{"server_id", "role"}, nil, 58 | ) 59 | infoSchemaReplicaHostReplayLatencyDesc = prometheus.NewDesc( 60 | prometheus.BuildFQName(namespace, informationSchema, "replica_host_replay_latency_seconds"), 61 | "The current replay latency in seconds.", 62 | []string{"server_id", "role"}, nil, 63 | ) 64 | ) 65 | 66 | // ScrapeReplicaHost collects from `information_schema.replica_host_status`. 67 | type ScrapeReplicaHost struct{} 68 | 69 | // Name of the Scraper. Should be unique. 70 | func (ScrapeReplicaHost) Name() string { 71 | return "info_schema.replica_host" 72 | } 73 | 74 | // Help describes the role of the Scraper. 75 | func (ScrapeReplicaHost) Help() string { 76 | return "Collect metrics from information_schema.replica_host_status" 77 | } 78 | 79 | // Version of MySQL from which scraper is available. 80 | func (ScrapeReplicaHost) Version() float64 { 81 | return 5.6 82 | } 83 | 84 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 85 | func (ScrapeReplicaHost) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 86 | db := instance.getDB() 87 | replicaHostRows, err := db.QueryContext(ctx, replicaHostQuery) 88 | if err != nil { 89 | if mysqlErr, ok := err.(*MySQL.MySQLError); ok { // Now the error number is accessible directly 90 | // Check for error 1109: Unknown table 91 | if mysqlErr.Number == 1109 { 92 | logger.Debug("information_schema.replica_host_status is not available.") 93 | return nil 94 | } 95 | } 96 | return err 97 | } 98 | defer replicaHostRows.Close() 99 | 100 | var ( 101 | serverId string 102 | role string 103 | cpu float64 104 | replicaLatency uint64 105 | replicaLag float64 106 | logStreamSpeed float64 107 | replayLatency uint64 108 | ) 109 | for replicaHostRows.Next() { 110 | if err := replicaHostRows.Scan( 111 | &serverId, 112 | &role, 113 | &cpu, 114 | &replicaLatency, 115 | &replicaLag, 116 | &logStreamSpeed, 117 | &replayLatency, 118 | ); err != nil { 119 | return err 120 | } 121 | ch <- prometheus.MustNewConstMetric( 122 | infoSchemaReplicaHostCpuDesc, prometheus.GaugeValue, cpu, 123 | serverId, role, 124 | ) 125 | ch <- prometheus.MustNewConstMetric( 126 | infoSchemaReplicaHostReplicaLatencyDesc, prometheus.GaugeValue, float64(replicaLatency)*0.000001, 127 | serverId, role, 128 | ) 129 | ch <- prometheus.MustNewConstMetric( 130 | infoSchemaReplicaHostLagDesc, prometheus.GaugeValue, replicaLag*0.001, 131 | serverId, role, 132 | ) 133 | ch <- prometheus.MustNewConstMetric( 134 | infoSchemaReplicaHostLogStreamSpeedDesc, prometheus.GaugeValue, logStreamSpeed, 135 | serverId, role, 136 | ) 137 | ch <- prometheus.MustNewConstMetric( 138 | infoSchemaReplicaHostReplayLatencyDesc, prometheus.GaugeValue, float64(replayLatency)*0.000001, 139 | serverId, role, 140 | ) 141 | } 142 | return nil 143 | } 144 | 145 | // check interface 146 | var _ Scraper = ScrapeReplicaHost{} 147 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | 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"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("dbtools-cluster-us-west-2c", "reader", 1.2531328201293945, 250000, 20.069000244140625, 2.0368164549078225, 500000). 38 | AddRow("dbtools-cluster-writer", "writer", 1.9607843160629272, 250000, 0, 2.0368164549078225, 0) 39 | mock.ExpectQuery(sanitizeQuery(replicaHostQuery)).WillReturnRows(rows) 40 | 41 | ch := make(chan prometheus.Metric) 42 | go func() { 43 | if err = (ScrapeReplicaHost{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 44 | t.Errorf("error calling function on test: %s", err) 45 | } 46 | close(ch) 47 | }() 48 | 49 | expected := []MetricResult{ 50 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 1.2531328201293945, metricType: dto.MetricType_GAUGE}, 51 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.25, metricType: dto.MetricType_GAUGE}, 52 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.020069000244140625, metricType: dto.MetricType_GAUGE}, 53 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 2.0368164549078225, metricType: dto.MetricType_GAUGE}, 54 | {labels: labelMap{"server_id": "dbtools-cluster-us-west-2c", "role": "reader"}, value: 0.5, metricType: dto.MetricType_GAUGE}, 55 | 56 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 1.9607843160629272, metricType: dto.MetricType_GAUGE}, 57 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.25, metricType: dto.MetricType_GAUGE}, 58 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.0, metricType: dto.MetricType_GAUGE}, 59 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 2.0368164549078225, metricType: dto.MetricType_GAUGE}, 60 | {labels: labelMap{"server_id": "dbtools-cluster-writer", "role": "writer"}, value: 0.0, metricType: dto.MetricType_GAUGE}, 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_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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const schemaStatQuery = ` 26 | SELECT 27 | TABLE_SCHEMA, 28 | SUM(ROWS_READ) AS ROWS_READ, 29 | SUM(ROWS_CHANGED) AS ROWS_CHANGED, 30 | SUM(ROWS_CHANGED_X_INDEXES) AS ROWS_CHANGED_X_INDEXES 31 | FROM information_schema.TABLE_STATISTICS 32 | GROUP BY TABLE_SCHEMA; 33 | ` 34 | 35 | // Metric descriptors. 36 | var ( 37 | infoSchemaStatsRowsReadDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_read_total"), 39 | "The number of rows read from the schema.", 40 | []string{"schema"}, nil, 41 | ) 42 | infoSchemaStatsRowsChangedDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_changed_total"), 44 | "The number of rows changed in the schema.", 45 | []string{"schema"}, nil, 46 | ) 47 | infoSchemaStatsRowsChangedXIndexesDesc = prometheus.NewDesc( 48 | prometheus.BuildFQName(namespace, informationSchema, "schema_statistics_rows_changed_x_indexes_total"), 49 | "The number of rows changed in the schema, multiplied by the number of indexes changed.", 50 | []string{"schema"}, nil, 51 | ) 52 | ) 53 | 54 | // ScrapeSchemaStat collects from `information_schema.table_statistics` grouped by schema. 55 | type ScrapeSchemaStat struct{} 56 | 57 | // Name of the Scraper. Should be unique. 58 | func (ScrapeSchemaStat) Name() string { 59 | return "info_schema.schemastats" 60 | } 61 | 62 | // Help describes the role of the Scraper. 63 | func (ScrapeSchemaStat) Help() string { 64 | return "If running with userstat=1, set to true to collect schema statistics" 65 | } 66 | 67 | // Version of MySQL from which scraper is available. 68 | func (ScrapeSchemaStat) 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 (ScrapeSchemaStat) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 74 | var varName, varVal string 75 | 76 | db := instance.getDB() 77 | err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) 78 | if err != nil { 79 | logger.Debug("Detailed schema stats are not available.") 80 | return nil 81 | } 82 | if varVal == "OFF" { 83 | logger.Debug("MySQL variable is OFF.", "var", varName) 84 | return nil 85 | } 86 | 87 | informationSchemaTableStatisticsRows, err := db.QueryContext(ctx, schemaStatQuery) 88 | if err != nil { 89 | return err 90 | } 91 | defer informationSchemaTableStatisticsRows.Close() 92 | 93 | var ( 94 | tableSchema string 95 | rowsRead uint64 96 | rowsChanged uint64 97 | rowsChangedXIndexes uint64 98 | ) 99 | 100 | for informationSchemaTableStatisticsRows.Next() { 101 | err = informationSchemaTableStatisticsRows.Scan( 102 | &tableSchema, 103 | &rowsRead, 104 | &rowsChanged, 105 | &rowsChangedXIndexes, 106 | ) 107 | 108 | if err != nil { 109 | return err 110 | } 111 | ch <- prometheus.MustNewConstMetric( 112 | infoSchemaStatsRowsReadDesc, prometheus.CounterValue, float64(rowsRead), 113 | tableSchema, 114 | ) 115 | ch <- prometheus.MustNewConstMetric( 116 | infoSchemaStatsRowsChangedDesc, prometheus.CounterValue, float64(rowsChanged), 117 | tableSchema, 118 | ) 119 | ch <- prometheus.MustNewConstMetric( 120 | infoSchemaStatsRowsChangedXIndexesDesc, prometheus.CounterValue, float64(rowsChangedXIndexes), 121 | tableSchema, 122 | ) 123 | } 124 | return nil 125 | } 126 | 127 | // check interface 128 | var _ Scraper = ScrapeSchemaStat{} 129 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 33 | 34 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 35 | AddRow("userstat", "ON")) 36 | 37 | columns := []string{"TABLE_SCHEMA", "ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES"} 38 | rows := sqlmock.NewRows(columns). 39 | AddRow("mysql", 238, 0, 8). 40 | AddRow("default", 99, 1, 0) 41 | mock.ExpectQuery(sanitizeQuery(schemaStatQuery)).WillReturnRows(rows) 42 | 43 | ch := make(chan prometheus.Metric) 44 | go func() { 45 | if err = (ScrapeSchemaStat{}).Scrape(context.Background(), inst, ch, promslog.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"}, value: 238}, 53 | {labels: labelMap{"schema": "mysql"}, value: 0}, 54 | {labels: labelMap{"schema": "mysql"}, value: 8}, 55 | {labels: labelMap{"schema": "default"}, value: 99}, 56 | {labels: labelMap{"schema": "default"}, value: 1}, 57 | {labels: labelMap{"schema": "default"}, value: 0}, 58 | } 59 | convey.Convey("Metrics comparison", t, func() { 60 | for _, expect := range expected { 61 | got := readMetric(<-ch) 62 | convey.So(expect, convey.ShouldResemble, got) 63 | } 64 | }) 65 | 66 | // Ensure all SQL queries were executed 67 | if err := mock.ExpectationsWereMet(); err != nil { 68 | t.Errorf("there were unfulfilled exceptions: %s", err) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const tableStatQuery = ` 26 | SELECT 27 | TABLE_SCHEMA, 28 | TABLE_NAME, 29 | ROWS_READ, 30 | ROWS_CHANGED, 31 | ROWS_CHANGED_X_INDEXES 32 | FROM information_schema.table_statistics 33 | ` 34 | 35 | // Metric descriptors. 36 | var ( 37 | infoSchemaTableStatsRowsReadDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_read_total"), 39 | "The number of rows read from the table.", 40 | []string{"schema", "table"}, nil, 41 | ) 42 | infoSchemaTableStatsRowsChangedDesc = prometheus.NewDesc( 43 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_changed_total"), 44 | "The number of rows changed in the table.", 45 | []string{"schema", "table"}, nil, 46 | ) 47 | infoSchemaTableStatsRowsChangedXIndexesDesc = prometheus.NewDesc( 48 | prometheus.BuildFQName(namespace, informationSchema, "table_statistics_rows_changed_x_indexes_total"), 49 | "The number of rows changed in the table, multiplied by the number of indexes changed.", 50 | []string{"schema", "table"}, nil, 51 | ) 52 | ) 53 | 54 | // ScrapeTableStat collects from `information_schema.table_statistics`. 55 | type ScrapeTableStat struct{} 56 | 57 | // Name of the Scraper. Should be unique. 58 | func (ScrapeTableStat) Name() string { 59 | return "info_schema.tablestats" 60 | } 61 | 62 | // Help describes the role of the Scraper. 63 | func (ScrapeTableStat) Help() string { 64 | return "If running with userstat=1, set to true to collect table statistics" 65 | } 66 | 67 | // Version of MySQL from which scraper is available. 68 | func (ScrapeTableStat) 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 (ScrapeTableStat) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 74 | var varName, varVal string 75 | db := instance.getDB() 76 | err := db.QueryRowContext(ctx, userstatCheckQuery).Scan(&varName, &varVal) 77 | if err != nil { 78 | logger.Debug("Detailed table stats are not available.") 79 | return nil 80 | } 81 | if varVal == "OFF" { 82 | logger.Debug("MySQL variable is OFF.", "var", varName) 83 | return nil 84 | } 85 | 86 | informationSchemaTableStatisticsRows, err := db.QueryContext(ctx, tableStatQuery) 87 | if err != nil { 88 | return err 89 | } 90 | defer informationSchemaTableStatisticsRows.Close() 91 | 92 | var ( 93 | tableSchema string 94 | tableName string 95 | rowsRead uint64 96 | rowsChanged uint64 97 | rowsChangedXIndexes uint64 98 | ) 99 | 100 | for informationSchemaTableStatisticsRows.Next() { 101 | err = informationSchemaTableStatisticsRows.Scan( 102 | &tableSchema, 103 | &tableName, 104 | &rowsRead, 105 | &rowsChanged, 106 | &rowsChangedXIndexes, 107 | ) 108 | if err != nil { 109 | return err 110 | } 111 | ch <- prometheus.MustNewConstMetric( 112 | infoSchemaTableStatsRowsReadDesc, prometheus.CounterValue, float64(rowsRead), 113 | tableSchema, tableName, 114 | ) 115 | ch <- prometheus.MustNewConstMetric( 116 | infoSchemaTableStatsRowsChangedDesc, prometheus.CounterValue, float64(rowsChanged), 117 | tableSchema, tableName, 118 | ) 119 | ch <- prometheus.MustNewConstMetric( 120 | infoSchemaTableStatsRowsChangedXIndexesDesc, prometheus.CounterValue, float64(rowsChangedXIndexes), 121 | tableSchema, tableName, 122 | ) 123 | } 124 | return nil 125 | } 126 | 127 | // check interface 128 | var _ Scraper = ScrapeTableStat{} 129 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 33 | 34 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 35 | AddRow("userstat", "ON")) 36 | 37 | columns := []string{"TABLE_SCHEMA", "TABLE_NAME", "ROWS_READ", "ROWS_CHANGED", "ROWS_CHANGED_X_INDEXES"} 38 | rows := sqlmock.NewRows(columns). 39 | AddRow("mysql", "db", 238, 0, 8). 40 | AddRow("mysql", "proxies_priv", 99, 1, 0). 41 | AddRow("mysql", "user", 1064, 2, 5) 42 | mock.ExpectQuery(sanitizeQuery(tableStatQuery)).WillReturnRows(rows) 43 | 44 | ch := make(chan prometheus.Metric) 45 | go func() { 46 | if err = (ScrapeTableStat{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 47 | t.Errorf("error calling function on test: %s", err) 48 | } 49 | close(ch) 50 | }() 51 | 52 | expected := []MetricResult{ 53 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 238}, 54 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 0}, 55 | {labels: labelMap{"schema": "mysql", "table": "db"}, value: 8}, 56 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 99}, 57 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 1}, 58 | {labels: labelMap{"schema": "mysql", "table": "proxies_priv"}, value: 0}, 59 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 1064}, 60 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 2}, 61 | {labels: labelMap{"schema": "mysql", "table": "user"}, value: 5}, 62 | } 63 | convey.Convey("Metrics comparison", t, func() { 64 | for _, expect := range expected { 65 | got := readMetric(<-ch) 66 | convey.So(expect, convey.ShouldResemble, got) 67 | } 68 | }) 69 | 70 | // Ensure all SQL queries were executed 71 | if err := mock.ExpectationsWereMet(); err != nil { 72 | t.Errorf("there were unfulfilled exceptions: %s", err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | mock.ExpectQuery(sanitizeQuery(userstatCheckQuery)).WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 36 | AddRow("userstat", "ON")) 37 | 38 | 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"} 39 | rows := sqlmock.NewRows(columns). 40 | 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) 41 | mock.ExpectQuery(sanitizeQuery(userStatQuery)).WillReturnRows(rows) 42 | 43 | ch := make(chan prometheus.Metric) 44 | go func() { 45 | if err = (ScrapeUserStat{}).Scrape(context.Background(), inst, ch, promslog.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{"user": "user_test"}, value: 1002, metricType: dto.MetricType_COUNTER}, 53 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_GAUGE}, 54 | {labels: labelMap{"user": "user_test"}, value: 127027, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"user": "user_test"}, value: 286, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"user": "user_test"}, value: 245, metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"user": "user_test"}, value: float64(2565104853), metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"user": "user_test"}, value: 21090856, metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"user": "user_test"}, value: float64(2380108042), metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"user": "user_test"}, value: 767691, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"user": "user_test"}, value: 1764, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"user": "user_test"}, value: 8778, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"user": "user_test"}, value: 1210741, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"user": "user_test"}, value: 1764, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"user": "user_test"}, value: 1214416, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"user": "user_test"}, value: 293, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"user": "user_test"}, value: 2430888, 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 | {labels: labelMap{"user": "user_test"}, value: 0, metricType: dto.MetricType_COUNTER}, 74 | } 75 | convey.Convey("Metrics comparison", t, func() { 76 | for _, expect := range expected { 77 | got := readMetric(<-ch) 78 | convey.So(expect, convey.ShouldResemble, got) 79 | } 80 | }) 81 | 82 | // Ensure all SQL queries were executed 83 | if err := mock.ExpectationsWereMet(); err != nil { 84 | t.Errorf("there were unfulfilled exceptions: %s", err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /collector/instance.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "database/sql" 18 | "fmt" 19 | "regexp" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/blang/semver/v4" 24 | ) 25 | 26 | const ( 27 | FlavorMySQL = "mysql" 28 | FlavorMariaDB = "mariadb" 29 | ) 30 | 31 | type instance struct { 32 | db *sql.DB 33 | flavor string 34 | version semver.Version 35 | versionMajorMinor float64 36 | } 37 | 38 | func newInstance(dsn string) (*instance, error) { 39 | i := &instance{} 40 | db, err := sql.Open("mysql", dsn) 41 | if err != nil { 42 | return nil, err 43 | } 44 | db.SetMaxOpenConns(1) 45 | db.SetMaxIdleConns(1) 46 | i.db = db 47 | 48 | version, versionString, err := queryVersion(db) 49 | if err != nil { 50 | db.Close() 51 | return nil, err 52 | } 53 | 54 | i.version = version 55 | 56 | versionMajorMinor, err := strconv.ParseFloat(fmt.Sprintf("%d.%d", i.version.Major, i.version.Minor), 64) 57 | if err != nil { 58 | db.Close() 59 | return nil, err 60 | } 61 | 62 | i.versionMajorMinor = versionMajorMinor 63 | 64 | if strings.Contains(strings.ToLower(versionString), "mariadb") { 65 | i.flavor = FlavorMariaDB 66 | } else { 67 | i.flavor = FlavorMySQL 68 | } 69 | 70 | return i, nil 71 | } 72 | 73 | func (i *instance) getDB() *sql.DB { 74 | return i.db 75 | } 76 | 77 | func (i *instance) Close() error { 78 | return i.db.Close() 79 | } 80 | 81 | // Ping checks connection availability and possibly invalidates the connection if it fails. 82 | func (i *instance) Ping() error { 83 | if err := i.db.Ping(); err != nil { 84 | if cerr := i.Close(); cerr != nil { 85 | return err 86 | } 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | // The result of SELECT version() is something like: 93 | // for MariaDB: "10.5.17-MariaDB-1:10.5.17+maria~ubu2004-log" 94 | // for MySQL: "8.0.36-28.1" 95 | var versionRegex = regexp.MustCompile(`^((\d+)(\.\d+)(\.\d+))`) 96 | 97 | func queryVersion(db *sql.DB) (semver.Version, string, error) { 98 | var version string 99 | err := db.QueryRow("SELECT @@version;").Scan(&version) 100 | if err != nil { 101 | return semver.Version{}, version, err 102 | } 103 | 104 | matches := versionRegex.FindStringSubmatch(version) 105 | if len(matches) > 1 { 106 | parsedVersion, err := semver.ParseTolerant(matches[1]) 107 | if err != nil { 108 | return semver.Version{}, version, fmt.Errorf("could not parse version from %q", matches[1]) 109 | } 110 | return parsedVersion, version, nil 111 | } 112 | 113 | return semver.Version{}, version, fmt.Errorf("could not parse version from %q", version) 114 | } 115 | -------------------------------------------------------------------------------- /collector/perf_schema.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | // Subsystem. 17 | const performanceSchema = "perf_schema" 18 | -------------------------------------------------------------------------------- /collector/perf_schema_events_waits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.events_waits_summary_global_by_event_name`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfEventsWaitsQuery = ` 26 | SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT 27 | FROM performance_schema.events_waits_summary_global_by_event_name 28 | ` 29 | 30 | // Metric descriptors. 31 | var ( 32 | performanceSchemaEventsWaitsDesc = prometheus.NewDesc( 33 | prometheus.BuildFQName(namespace, performanceSchema, "events_waits_total"), 34 | "The total events waits by event name.", 35 | []string{"event_name"}, nil, 36 | ) 37 | performanceSchemaEventsWaitsTimeDesc = prometheus.NewDesc( 38 | prometheus.BuildFQName(namespace, performanceSchema, "events_waits_seconds_total"), 39 | "The total seconds of events waits by event name.", 40 | []string{"event_name"}, nil, 41 | ) 42 | ) 43 | 44 | // ScrapePerfEventsWaits collects from `performance_schema.events_waits_summary_global_by_event_name`. 45 | type ScrapePerfEventsWaits struct{} 46 | 47 | // Name of the Scraper. Should be unique. 48 | func (ScrapePerfEventsWaits) Name() string { 49 | return "perf_schema.eventswaits" 50 | } 51 | 52 | // Help describes the role of the Scraper. 53 | func (ScrapePerfEventsWaits) Help() string { 54 | return "Collect metrics from performance_schema.events_waits_summary_global_by_event_name" 55 | } 56 | 57 | // Version of MySQL from which scraper is available. 58 | func (ScrapePerfEventsWaits) Version() float64 { 59 | return 5.5 60 | } 61 | 62 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 63 | func (ScrapePerfEventsWaits) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 64 | db := instance.getDB() 65 | // Timers here are returned in picoseconds. 66 | perfSchemaEventsWaitsRows, err := db.QueryContext(ctx, perfEventsWaitsQuery) 67 | if err != nil { 68 | return err 69 | } 70 | defer perfSchemaEventsWaitsRows.Close() 71 | 72 | var ( 73 | eventName string 74 | count, time uint64 75 | ) 76 | 77 | for perfSchemaEventsWaitsRows.Next() { 78 | if err := perfSchemaEventsWaitsRows.Scan( 79 | &eventName, &count, &time, 80 | ); err != nil { 81 | return err 82 | } 83 | ch <- prometheus.MustNewConstMetric( 84 | performanceSchemaEventsWaitsDesc, prometheus.CounterValue, float64(count), 85 | eventName, 86 | ) 87 | ch <- prometheus.MustNewConstMetric( 88 | performanceSchemaEventsWaitsTimeDesc, prometheus.CounterValue, float64(time)/picoSeconds, 89 | eventName, 90 | ) 91 | } 92 | return nil 93 | } 94 | 95 | // check interface 96 | var _ Scraper = ScrapePerfEventsWaits{} 97 | -------------------------------------------------------------------------------- /collector/perf_schema_file_events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.file_summary_by_event_name`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfFileEventsQuery = ` 26 | SELECT 27 | EVENT_NAME, 28 | COUNT_READ, SUM_TIMER_READ, SUM_NUMBER_OF_BYTES_READ, 29 | COUNT_WRITE, SUM_TIMER_WRITE, SUM_NUMBER_OF_BYTES_WRITE, 30 | COUNT_MISC, SUM_TIMER_MISC 31 | FROM performance_schema.file_summary_by_event_name 32 | ` 33 | 34 | // Metric descriptors. 35 | var ( 36 | performanceSchemaFileEventsDesc = prometheus.NewDesc( 37 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_total"), 38 | "The total file events by event name/mode.", 39 | []string{"event_name", "mode"}, nil, 40 | ) 41 | performanceSchemaFileEventsTimeDesc = prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_seconds_total"), 43 | "The total seconds of file events by event name/mode.", 44 | []string{"event_name", "mode"}, nil, 45 | ) 46 | performanceSchemaFileEventsBytesDesc = prometheus.NewDesc( 47 | prometheus.BuildFQName(namespace, performanceSchema, "file_events_bytes_total"), 48 | "The total bytes of file events by event name/mode.", 49 | []string{"event_name", "mode"}, nil, 50 | ) 51 | ) 52 | 53 | // ScrapePerfFileEvents collects from `performance_schema.file_summary_by_event_name`. 54 | type ScrapePerfFileEvents struct{} 55 | 56 | // Name of the Scraper. Should be unique. 57 | func (ScrapePerfFileEvents) Name() string { 58 | return "perf_schema.file_events" 59 | } 60 | 61 | // Help describes the role of the Scraper. 62 | func (ScrapePerfFileEvents) Help() string { 63 | return "Collect metrics from performance_schema.file_summary_by_event_name" 64 | } 65 | 66 | // Version of MySQL from which scraper is available. 67 | func (ScrapePerfFileEvents) Version() float64 { 68 | return 5.6 69 | } 70 | 71 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 72 | func (ScrapePerfFileEvents) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 73 | db := instance.getDB() 74 | // Timers here are returned in picoseconds. 75 | perfSchemaFileEventsRows, err := db.QueryContext(ctx, perfFileEventsQuery) 76 | if err != nil { 77 | return err 78 | } 79 | defer perfSchemaFileEventsRows.Close() 80 | 81 | var ( 82 | eventName string 83 | countRead, timeRead, bytesRead uint64 84 | countWrite, timeWrite, bytesWrite uint64 85 | countMisc, timeMisc uint64 86 | ) 87 | for perfSchemaFileEventsRows.Next() { 88 | if err := perfSchemaFileEventsRows.Scan( 89 | &eventName, 90 | &countRead, &timeRead, &bytesRead, 91 | &countWrite, &timeWrite, &bytesWrite, 92 | &countMisc, &timeMisc, 93 | ); err != nil { 94 | return err 95 | } 96 | ch <- prometheus.MustNewConstMetric( 97 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countRead), 98 | eventName, "read", 99 | ) 100 | ch <- prometheus.MustNewConstMetric( 101 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeRead)/picoSeconds, 102 | eventName, "read", 103 | ) 104 | ch <- prometheus.MustNewConstMetric( 105 | performanceSchemaFileEventsBytesDesc, prometheus.CounterValue, float64(bytesRead), 106 | eventName, "read", 107 | ) 108 | ch <- prometheus.MustNewConstMetric( 109 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countWrite), 110 | eventName, "write", 111 | ) 112 | ch <- prometheus.MustNewConstMetric( 113 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeWrite)/picoSeconds, 114 | eventName, "write", 115 | ) 116 | ch <- prometheus.MustNewConstMetric( 117 | performanceSchemaFileEventsBytesDesc, prometheus.CounterValue, float64(bytesWrite), 118 | eventName, "write", 119 | ) 120 | ch <- prometheus.MustNewConstMetric( 121 | performanceSchemaFileEventsDesc, prometheus.CounterValue, float64(countMisc), 122 | eventName, "misc", 123 | ) 124 | ch <- prometheus.MustNewConstMetric( 125 | performanceSchemaFileEventsTimeDesc, prometheus.CounterValue, float64(timeMisc)/picoSeconds, 126 | eventName, "misc", 127 | ) 128 | } 129 | return nil 130 | } 131 | 132 | // check interface 133 | var _ Scraper = ScrapePerfFileEvents{} 134 | -------------------------------------------------------------------------------- /collector/perf_schema_file_instances.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Scrape `performance_schema.file_summary_by_instance`. 15 | 16 | package collector 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | "strings" 22 | 23 | "github.com/alecthomas/kingpin/v2" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const perfFileInstancesQuery = ` 28 | SELECT 29 | FILE_NAME, EVENT_NAME, 30 | COUNT_READ, COUNT_WRITE, 31 | SUM_NUMBER_OF_BYTES_READ, SUM_NUMBER_OF_BYTES_WRITE 32 | FROM performance_schema.file_summary_by_instance 33 | where FILE_NAME REGEXP ? 34 | ` 35 | 36 | // Tunable flags. 37 | var ( 38 | performanceSchemaFileInstancesFilter = kingpin.Flag( 39 | "collect.perf_schema.file_instances.filter", 40 | "RegEx file_name filter for performance_schema.file_summary_by_instance", 41 | ).Default(".*").String() 42 | 43 | performanceSchemaFileInstancesRemovePrefix = kingpin.Flag( 44 | "collect.perf_schema.file_instances.remove_prefix", 45 | "Remove path prefix in performance_schema.file_summary_by_instance", 46 | ).Default("/var/lib/mysql/").String() 47 | ) 48 | 49 | // Metric descriptors. 50 | var ( 51 | performanceSchemaFileInstancesBytesDesc = prometheus.NewDesc( 52 | prometheus.BuildFQName(namespace, performanceSchema, "file_instances_bytes"), 53 | "The number of bytes processed by file read/write operations.", 54 | []string{"file_name", "event_name", "mode"}, nil, 55 | ) 56 | performanceSchemaFileInstancesCountDesc = prometheus.NewDesc( 57 | prometheus.BuildFQName(namespace, performanceSchema, "file_instances_total"), 58 | "The total number of file read/write operations.", 59 | []string{"file_name", "event_name", "mode"}, nil, 60 | ) 61 | ) 62 | 63 | // ScrapePerfFileInstances collects from `performance_schema.file_summary_by_instance`. 64 | type ScrapePerfFileInstances struct{} 65 | 66 | // Name of the Scraper. Should be unique. 67 | func (ScrapePerfFileInstances) Name() string { 68 | return "perf_schema.file_instances" 69 | } 70 | 71 | // Help describes the role of the Scraper. 72 | func (ScrapePerfFileInstances) Help() string { 73 | return "Collect metrics from performance_schema.file_summary_by_instance" 74 | } 75 | 76 | // Version of MySQL from which scraper is available. 77 | func (ScrapePerfFileInstances) Version() float64 { 78 | return 5.5 79 | } 80 | 81 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 82 | func (ScrapePerfFileInstances) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 83 | db := instance.getDB() 84 | // Timers here are returned in picoseconds. 85 | perfSchemaFileInstancesRows, err := db.QueryContext(ctx, perfFileInstancesQuery, *performanceSchemaFileInstancesFilter) 86 | if err != nil { 87 | return err 88 | } 89 | defer perfSchemaFileInstancesRows.Close() 90 | 91 | var ( 92 | fileName, eventName string 93 | countRead, countWrite uint64 94 | sumBytesRead, sumBytesWritten uint64 95 | ) 96 | 97 | for perfSchemaFileInstancesRows.Next() { 98 | if err := perfSchemaFileInstancesRows.Scan( 99 | &fileName, &eventName, 100 | &countRead, &countWrite, 101 | &sumBytesRead, &sumBytesWritten, 102 | ); err != nil { 103 | return err 104 | } 105 | 106 | fileName = strings.TrimPrefix(fileName, *performanceSchemaFileInstancesRemovePrefix) 107 | ch <- prometheus.MustNewConstMetric( 108 | performanceSchemaFileInstancesCountDesc, prometheus.CounterValue, float64(countRead), 109 | fileName, eventName, "read", 110 | ) 111 | ch <- prometheus.MustNewConstMetric( 112 | performanceSchemaFileInstancesCountDesc, prometheus.CounterValue, float64(countWrite), 113 | fileName, eventName, "write", 114 | ) 115 | ch <- prometheus.MustNewConstMetric( 116 | performanceSchemaFileInstancesBytesDesc, prometheus.CounterValue, float64(sumBytesRead), 117 | fileName, eventName, "read", 118 | ) 119 | ch <- prometheus.MustNewConstMetric( 120 | performanceSchemaFileInstancesBytesDesc, prometheus.CounterValue, float64(sumBytesWritten), 121 | fileName, eventName, "write", 122 | ) 123 | 124 | } 125 | return nil 126 | } 127 | 128 | // check interface 129 | var _ Scraper = ScrapePerfFileInstances{} 130 | -------------------------------------------------------------------------------- /collector/perf_schema_file_instances_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/alecthomas/kingpin/v2" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/smartystreets/goconvey/convey" 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 | inst := &instance{db: db} 41 | 42 | columns := []string{"FILE_NAME", "EVENT_NAME", "COUNT_READ", "COUNT_WRITE", "SUM_NUMBER_OF_BYTES_READ", "SUM_NUMBER_OF_BYTES_WRITE"} 43 | 44 | rows := sqlmock.NewRows(columns). 45 | AddRow("/var/lib/mysql/db1/file", "event1", "3", "4", "725", "128"). 46 | AddRow("/var/lib/mysql/db2/file", "event2", "23", "12", "3123", "967"). 47 | AddRow("db3/file", "event3", "45", "32", "1337", "326") 48 | mock.ExpectQuery(sanitizeQuery(perfFileInstancesQuery)).WillReturnRows(rows) 49 | 50 | ch := make(chan prometheus.Metric) 51 | go func() { 52 | if err = (ScrapePerfFileInstances{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 53 | panic(fmt.Sprintf("error calling function on test: %s", err)) 54 | } 55 | close(ch) 56 | }() 57 | 58 | metricExpected := []MetricResult{ 59 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "read"}, value: 3, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "write"}, value: 4, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "read"}, value: 725, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"file_name": "db1/file", "event_name": "event1", "mode": "write"}, value: 128, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "read"}, value: 23, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "write"}, value: 12, metricType: dto.MetricType_COUNTER}, 65 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "read"}, value: 3123, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"file_name": "db2/file", "event_name": "event2", "mode": "write"}, value: 967, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "read"}, value: 45, metricType: dto.MetricType_COUNTER}, 68 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "write"}, value: 32, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "read"}, value: 1337, metricType: dto.MetricType_COUNTER}, 70 | {labels: labelMap{"file_name": "db3/file", "event_name": "event3", "mode": "write"}, value: 326, metricType: dto.MetricType_COUNTER}, 71 | } 72 | convey.Convey("Metrics comparison", t, func() { 73 | for _, expect := range metricExpected { 74 | got := readMetric(<-ch) 75 | convey.So(got, convey.ShouldResemble, expect) 76 | } 77 | }) 78 | 79 | // Ensure all SQL queries were executed 80 | if err := mock.ExpectationsWereMet(); err != nil { 81 | t.Errorf("there were unfulfilled exceptions: %s", err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfIndexIOWaitsQuery = ` 26 | SELECT OBJECT_SCHEMA, OBJECT_NAME, ifnull(INDEX_NAME, 'NONE') as INDEX_NAME, 27 | COUNT_FETCH, COUNT_INSERT, COUNT_UPDATE, COUNT_DELETE, 28 | SUM_TIMER_FETCH, SUM_TIMER_INSERT, SUM_TIMER_UPDATE, SUM_TIMER_DELETE 29 | FROM performance_schema.table_io_waits_summary_by_index_usage 30 | WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema') 31 | ` 32 | 33 | // Metric descriptors. 34 | var ( 35 | performanceSchemaIndexWaitsDesc = prometheus.NewDesc( 36 | prometheus.BuildFQName(namespace, performanceSchema, "index_io_waits_total"), 37 | "The total number of index I/O wait events for each index and operation.", 38 | []string{"schema", "name", "index", "operation"}, nil, 39 | ) 40 | performanceSchemaIndexWaitsTimeDesc = prometheus.NewDesc( 41 | prometheus.BuildFQName(namespace, performanceSchema, "index_io_waits_seconds_total"), 42 | "The total time of index I/O wait events for each index and operation.", 43 | []string{"schema", "name", "index", "operation"}, nil, 44 | ) 45 | ) 46 | 47 | // ScrapePerfIndexIOWaits collects for `performance_schema.table_io_waits_summary_by_index_usage`. 48 | type ScrapePerfIndexIOWaits struct{} 49 | 50 | // Name of the Scraper. Should be unique. 51 | func (ScrapePerfIndexIOWaits) Name() string { 52 | return "perf_schema.indexiowaits" 53 | } 54 | 55 | // Help describes the role of the Scraper. 56 | func (ScrapePerfIndexIOWaits) Help() string { 57 | return "Collect metrics from performance_schema.table_io_waits_summary_by_index_usage" 58 | } 59 | 60 | // Version of MySQL from which scraper is available. 61 | func (ScrapePerfIndexIOWaits) Version() float64 { 62 | return 5.6 63 | } 64 | 65 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 66 | func (ScrapePerfIndexIOWaits) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 67 | db := instance.getDB() 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_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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | 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"} 36 | rows := sqlmock.NewRows(columns). 37 | // Note, timers are in picoseconds. 38 | AddRow("database", "table", "index", "10", "11", "12", "13", "14000000000000", "15000000000000", "16000000000000", "17000000000000"). 39 | AddRow("database", "table", "NONE", "20", "21", "22", "23", "24000000000000", "25000000000000", "26000000000000", "27000000000000") 40 | mock.ExpectQuery(sanitizeQuery(perfIndexIOWaitsQuery)).WillReturnRows(rows) 41 | 42 | ch := make(chan prometheus.Metric) 43 | go func() { 44 | if err = (ScrapePerfIndexIOWaits{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 45 | t.Errorf("error calling function on test: %s", err) 46 | } 47 | close(ch) 48 | }() 49 | 50 | metricExpected := []MetricResult{ 51 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "fetch"}, value: 10, metricType: dto.MetricType_COUNTER}, 52 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "update"}, value: 12, metricType: dto.MetricType_COUNTER}, 53 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "delete"}, value: 13, metricType: dto.MetricType_COUNTER}, 54 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "fetch"}, value: 14, metricType: dto.MetricType_COUNTER}, 55 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "update"}, value: 16, metricType: dto.MetricType_COUNTER}, 56 | {labels: labelMap{"schema": "database", "name": "table", "index": "index", "operation": "delete"}, value: 17, metricType: dto.MetricType_COUNTER}, 57 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "fetch"}, value: 20, metricType: dto.MetricType_COUNTER}, 58 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "insert"}, value: 21, metricType: dto.MetricType_COUNTER}, 59 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "update"}, value: 22, metricType: dto.MetricType_COUNTER}, 60 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "delete"}, value: 23, metricType: dto.MetricType_COUNTER}, 61 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "fetch"}, value: 24, metricType: dto.MetricType_COUNTER}, 62 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "insert"}, value: 25, metricType: dto.MetricType_COUNTER}, 63 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "update"}, value: 26, metricType: dto.MetricType_COUNTER}, 64 | {labels: labelMap{"schema": "database", "name": "table", "index": "NONE", "operation": "delete"}, value: 27, metricType: dto.MetricType_COUNTER}, 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 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | "strings" 22 | 23 | "github.com/alecthomas/kingpin/v2" 24 | "github.com/prometheus/client_golang/prometheus" 25 | ) 26 | 27 | const perfMemoryEventsQuery = ` 28 | SELECT 29 | EVENT_NAME, SUM_NUMBER_OF_BYTES_ALLOC, SUM_NUMBER_OF_BYTES_FREE, 30 | CURRENT_NUMBER_OF_BYTES_USED 31 | FROM performance_schema.memory_summary_global_by_event_name 32 | where COUNT_ALLOC > 0; 33 | ` 34 | 35 | // Tunable flags. 36 | var ( 37 | performanceSchemaMemoryEventsRemovePrefix = kingpin.Flag( 38 | "collect.perf_schema.memory_events.remove_prefix", 39 | "Remove instrument prefix in performance_schema.memory_summary_global_by_event_name", 40 | ).Default("memory/").String() 41 | ) 42 | 43 | // Metric descriptors. 44 | var ( 45 | performanceSchemaMemoryBytesAllocDesc = prometheus.NewDesc( 46 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_alloc_bytes_total"), 47 | "The total number of bytes allocated by events.", 48 | []string{"event_name"}, nil, 49 | ) 50 | performanceSchemaMemoryBytesFreeDesc = prometheus.NewDesc( 51 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_free_bytes_total"), 52 | "The total number of bytes freed by events.", 53 | []string{"event_name"}, nil, 54 | ) 55 | perforanceSchemaMemoryUsedBytesDesc = prometheus.NewDesc( 56 | prometheus.BuildFQName(namespace, performanceSchema, "memory_events_used_bytes"), 57 | "The number of bytes currently allocated by events.", 58 | []string{"event_name"}, nil, 59 | ) 60 | ) 61 | 62 | // ScrapePerfMemoryEvents collects from `performance_schema.memory_summary_global_by_event_name`. 63 | type ScrapePerfMemoryEvents struct{} 64 | 65 | // Name of the Scraper. Should be unique. 66 | func (ScrapePerfMemoryEvents) Name() string { 67 | return "perf_schema.memory_events" 68 | } 69 | 70 | // Help describes the role of the Scraper. 71 | func (ScrapePerfMemoryEvents) Help() string { 72 | return "Collect metrics from performance_schema.memory_summary_global_by_event_name" 73 | } 74 | 75 | // Version of MySQL from which scraper is available. 76 | func (ScrapePerfMemoryEvents) Version() float64 { 77 | return 5.7 78 | } 79 | 80 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 81 | func (ScrapePerfMemoryEvents) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 82 | db := instance.getDB() 83 | perfSchemaMemoryEventsRows, err := db.QueryContext(ctx, perfMemoryEventsQuery) 84 | if err != nil { 85 | return err 86 | } 87 | defer perfSchemaMemoryEventsRows.Close() 88 | 89 | var ( 90 | eventName string 91 | bytesAlloc uint64 92 | bytesFree uint64 93 | currentBytes int64 94 | ) 95 | 96 | for perfSchemaMemoryEventsRows.Next() { 97 | if err := perfSchemaMemoryEventsRows.Scan( 98 | &eventName, &bytesAlloc, &bytesFree, ¤tBytes, 99 | ); err != nil { 100 | return err 101 | } 102 | 103 | eventName := strings.TrimPrefix(eventName, *performanceSchemaMemoryEventsRemovePrefix) 104 | ch <- prometheus.MustNewConstMetric( 105 | performanceSchemaMemoryBytesAllocDesc, prometheus.CounterValue, float64(bytesAlloc), eventName, 106 | ) 107 | ch <- prometheus.MustNewConstMetric( 108 | performanceSchemaMemoryBytesFreeDesc, prometheus.CounterValue, float64(bytesFree), eventName, 109 | ) 110 | ch <- prometheus.MustNewConstMetric( 111 | perforanceSchemaMemoryUsedBytesDesc, prometheus.GaugeValue, float64(currentBytes), eventName, 112 | ) 113 | } 114 | return nil 115 | } 116 | 117 | // check interface 118 | var _ Scraper = ScrapePerfMemoryEvents{} 119 | -------------------------------------------------------------------------------- /collector/perf_schema_memory_events_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The Prometheus Authors 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package collector 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/DATA-DOG/go-sqlmock" 22 | "github.com/alecthomas/kingpin/v2" 23 | "github.com/prometheus/client_golang/prometheus" 24 | dto "github.com/prometheus/client_model/go" 25 | "github.com/prometheus/common/promslog" 26 | "github.com/smartystreets/goconvey/convey" 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 | inst := &instance{db: db} 41 | 42 | columns := []string{ 43 | "EVENT_NAME", 44 | "SUM_NUMBER_OF_BYTES_ALLOC", 45 | "SUM_NUMBER_OF_BYTES_FREE", 46 | "CURRENT_NUMBER_OF_BYTES_USED", 47 | } 48 | 49 | rows := sqlmock.NewRows(columns). 50 | AddRow("memory/innodb/event1", "1001", "500", "501"). 51 | AddRow("memory/performance_schema/event1", "6000", "7", "-83904"). 52 | AddRow("memory/innodb/event2", "2002", "1000", "1002"). 53 | AddRow("memory/sql/event1", "30", "4", "26") 54 | mock.ExpectQuery(sanitizeQuery(perfMemoryEventsQuery)).WillReturnRows(rows) 55 | 56 | ch := make(chan prometheus.Metric) 57 | go func() { 58 | if err = (ScrapePerfMemoryEvents{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 59 | panic(fmt.Sprintf("error calling function on test: %s", err)) 60 | } 61 | close(ch) 62 | }() 63 | 64 | metricExpected := []MetricResult{ 65 | {labels: labelMap{"event_name": "innodb/event1"}, value: 1001, metricType: dto.MetricType_COUNTER}, 66 | {labels: labelMap{"event_name": "innodb/event1"}, value: 500, metricType: dto.MetricType_COUNTER}, 67 | {labels: labelMap{"event_name": "innodb/event1"}, value: 501, metricType: dto.MetricType_GAUGE}, 68 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: 6000, metricType: dto.MetricType_COUNTER}, 69 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: 7, metricType: dto.MetricType_COUNTER}, 70 | {labels: labelMap{"event_name": "performance_schema/event1"}, value: -83904, metricType: dto.MetricType_GAUGE}, 71 | {labels: labelMap{"event_name": "innodb/event2"}, value: 2002, metricType: dto.MetricType_COUNTER}, 72 | {labels: labelMap{"event_name": "innodb/event2"}, value: 1000, metricType: dto.MetricType_COUNTER}, 73 | {labels: labelMap{"event_name": "innodb/event2"}, value: 1002, metricType: dto.MetricType_GAUGE}, 74 | {labels: labelMap{"event_name": "sql/event1"}, value: 30, metricType: dto.MetricType_COUNTER}, 75 | {labels: labelMap{"event_name": "sql/event1"}, value: 4, metricType: dto.MetricType_COUNTER}, 76 | {labels: labelMap{"event_name": "sql/event1"}, value: 26, metricType: dto.MetricType_GAUGE}, 77 | } 78 | convey.Convey("Metrics comparison", t, func() { 79 | for _, expect := range metricExpected { 80 | got := readMetric(<-ch) 81 | convey.So(got, convey.ShouldResemble, expect) 82 | } 83 | }) 84 | 85 | // Ensure all SQL queries were executed 86 | if err := mock.ExpectationsWereMet(); err != nil { 87 | t.Errorf("there were unfulfilled exceptions: %s", err) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 23 | dto "github.com/prometheus/client_model/go" 24 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 35 | 36 | columns := []string{ 37 | "CHANNEL_NAME", 38 | "WORKER_ID", 39 | "LAST_APPLIED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP", 40 | "LAST_APPLIED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP", 41 | "LAST_APPLIED_TRANSACTION_START_APPLY_TIMESTAMP", 42 | "LAST_APPLIED_TRANSACTION_END_APPLY_TIMESTAMP", 43 | "APPLYING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP", 44 | "APPLYING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP", 45 | "APPLYING_TRANSACTION_START_APPLY_TIMESTAMP", 46 | } 47 | 48 | timeZero := "0000-00-00 00:00:00.000000" 49 | 50 | stubTime := time.Date(2019, 3, 14, 0, 0, 0, int(time.Millisecond), time.UTC) 51 | rows := sqlmock.NewRows(columns). 52 | AddRow("dummy_0", "0", timeZero, timeZero, timeZero, timeZero, timeZero, timeZero, timeZero). 53 | 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)) 54 | mock.ExpectQuery(sanitizeQuery(perfReplicationApplierStatsByWorkerQuery)).WillReturnRows(rows) 55 | 56 | ch := make(chan prometheus.Metric) 57 | go func() { 58 | if err = (ScrapePerfReplicationApplierStatsByWorker{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 59 | t.Errorf("error calling function on test: %s", err) 60 | } 61 | close(ch) 62 | }() 63 | 64 | metricExpected := []MetricResult{ 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_0", "member_id": "0"}, value: 0, metricType: dto.MetricType_GAUGE}, 72 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521600001e+9, metricType: dto.MetricType_GAUGE}, 73 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521660001e+9, metricType: dto.MetricType_GAUGE}, 74 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521720001e+9, metricType: dto.MetricType_GAUGE}, 75 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521780001e+9, metricType: dto.MetricType_GAUGE}, 76 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521840001e+9, metricType: dto.MetricType_GAUGE}, 77 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521900001e+9, metricType: dto.MetricType_GAUGE}, 78 | {labels: labelMap{"channel_name": "dummy_1", "member_id": "1"}, value: 1.552521960001e+9, metricType: dto.MetricType_GAUGE}, 79 | } 80 | convey.Convey("Metrics comparison", t, func() { 81 | for _, expect := range metricExpected { 82 | got := readMetric(<-ch) 83 | convey.So(got, convey.ShouldResemble, expect) 84 | } 85 | }) 86 | 87 | // Ensure all SQL queries were executed 88 | if err := mock.ExpectationsWereMet(); err != nil { 89 | t.Errorf("there were unfulfilled exceptions: %s", err) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /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 | "log/slog" 20 | "strconv" 21 | 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, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 83 | db := instance.getDB() 84 | rows, err := db.QueryContext(ctx, perfReplicationGroupMemberStatsQuery) 85 | if err != nil { 86 | return err 87 | } 88 | defer rows.Close() 89 | 90 | var columnNames []string 91 | if columnNames, err = rows.Columns(); err != nil { 92 | return err 93 | } 94 | 95 | var scanArgs = make([]interface{}, len(columnNames)) 96 | for i := range scanArgs { 97 | scanArgs[i] = &sql.RawBytes{} 98 | } 99 | 100 | for rows.Next() { 101 | if err := rows.Scan(scanArgs...); err != nil { 102 | return err 103 | } 104 | 105 | for i, columnName := range columnNames { 106 | if metric, ok := perfReplicationGroupMemberStats[columnName]; ok { 107 | value, err := strconv.ParseFloat(string(*scanArgs[i].(*sql.RawBytes)), 64) 108 | if err != nil { 109 | return err 110 | } 111 | ch <- prometheus.MustNewConstMetric(metric.desc, metric.vtype, value) 112 | } 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | // check interface 119 | var _ Scraper = ScrapePerfReplicationGroupMemberStats{} 120 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | columns := []string{ 36 | "CHANNEL_NAME", 37 | "VIEW_ID", 38 | "MEMBER_ID", 39 | "COUNT_TRANSACTIONS_IN_QUEUE", 40 | "COUNT_TRANSACTIONS_CHECKED", 41 | "COUNT_CONFLICTS_DETECTED", 42 | "COUNT_TRANSACTIONS_ROWS_VALIDATING", 43 | "TRANSACTIONS_COMMITTED_ALL_MEMBERS", 44 | "LAST_CONFLICT_FREE_TRANSACTION", 45 | "COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE", 46 | "COUNT_TRANSACTIONS_REMOTE_APPLIED", 47 | "COUNT_TRANSACTIONS_LOCAL_PROPOSED", 48 | "COUNT_TRANSACTIONS_LOCAL_ROLLBACK", 49 | } 50 | rows := sqlmock.NewRows(columns). 51 | AddRow( 52 | "group_replication_applier", 53 | "15813535259046852:43", 54 | "e14c4f71-025f-11ea-b800-0620049edbec", 55 | float64(0), 56 | float64(7389775), 57 | float64(1), 58 | float64(48), 59 | "0515b3c2-f59f-11e9-881b-0620049edbec:1-15270987,\n8f782839-34f7-11e7-a774-060ac4f023ae:4-39:2387-161606", 60 | "0515b3c2-f59f-11e9-881b-0620049edbec:15271011", 61 | float64(2), 62 | float64(22), 63 | float64(7389759), 64 | float64(7), 65 | ) 66 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMemberStatsQuery)).WillReturnRows(rows) 67 | 68 | ch := make(chan prometheus.Metric) 69 | go func() { 70 | if err = (ScrapePerfReplicationGroupMemberStats{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 71 | t.Errorf("error calling function on test: %s", err) 72 | } 73 | close(ch) 74 | }() 75 | 76 | expected := []MetricResult{ 77 | {labels: labelMap{}, value: 0, metricType: dto.MetricType_GAUGE}, 78 | {labels: labelMap{}, value: float64(7389775), metricType: dto.MetricType_COUNTER}, 79 | {labels: labelMap{}, value: float64(1), metricType: dto.MetricType_COUNTER}, 80 | {labels: labelMap{}, value: float64(48), metricType: dto.MetricType_COUNTER}, 81 | {labels: labelMap{}, value: 2, metricType: dto.MetricType_GAUGE}, 82 | {labels: labelMap{}, value: float64(22), metricType: dto.MetricType_COUNTER}, 83 | {labels: labelMap{}, value: float64(7389759), metricType: dto.MetricType_COUNTER}, 84 | {labels: labelMap{}, value: float64(7), metricType: dto.MetricType_COUNTER}, 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_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 | "log/slog" 20 | "strings" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfReplicationGroupMembersQuery = ` 26 | SELECT * FROM performance_schema.replication_group_members 27 | ` 28 | 29 | // ScrapeReplicationGroupMembers collects from `performance_schema.replication_group_members`. 30 | type ScrapePerfReplicationGroupMembers struct{} 31 | 32 | // Name of the Scraper. Should be unique. 33 | func (ScrapePerfReplicationGroupMembers) Name() string { 34 | return performanceSchema + ".replication_group_members" 35 | } 36 | 37 | // Help describes the role of the Scraper. 38 | func (ScrapePerfReplicationGroupMembers) Help() string { 39 | return "Collect metrics from performance_schema.replication_group_members" 40 | } 41 | 42 | // Version of MySQL from which scraper is available. 43 | func (ScrapePerfReplicationGroupMembers) Version() float64 { 44 | return 5.7 45 | } 46 | 47 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 48 | func (ScrapePerfReplicationGroupMembers) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 49 | db := instance.getDB() 50 | perfReplicationGroupMembersRows, err := db.QueryContext(ctx, perfReplicationGroupMembersQuery) 51 | if err != nil { 52 | return err 53 | } 54 | defer perfReplicationGroupMembersRows.Close() 55 | 56 | var columnNames []string 57 | if columnNames, err = perfReplicationGroupMembersRows.Columns(); err != nil { 58 | return err 59 | } 60 | 61 | var scanArgs = make([]interface{}, len(columnNames)) 62 | for i := range scanArgs { 63 | scanArgs[i] = &sql.RawBytes{} 64 | } 65 | 66 | for perfReplicationGroupMembersRows.Next() { 67 | if err := perfReplicationGroupMembersRows.Scan(scanArgs...); err != nil { 68 | return err 69 | } 70 | 71 | var labelNames = make([]string, len(columnNames)) 72 | var values = make([]string, len(columnNames)) 73 | for i, columnName := range columnNames { 74 | labelNames[i] = strings.ToLower(columnName) 75 | values[i] = string(*scanArgs[i].(*sql.RawBytes)) 76 | } 77 | 78 | var performanceSchemaReplicationGroupMembersMemberDesc = prometheus.NewDesc( 79 | prometheus.BuildFQName(namespace, performanceSchema, "replication_group_member_info"), 80 | "Information about the replication group member: "+ 81 | "channel_name, member_id, member_host, member_port, member_state. "+ 82 | "(member_role and member_version where available)", 83 | labelNames, nil, 84 | ) 85 | 86 | ch <- prometheus.MustNewConstMetric(performanceSchemaReplicationGroupMembersMemberDesc, 87 | prometheus.GaugeValue, 1, values...) 88 | } 89 | return nil 90 | 91 | } 92 | 93 | // check interface 94 | var _ Scraper = ScrapePerfReplicationGroupMembers{} 95 | -------------------------------------------------------------------------------- /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 | "testing" 19 | 20 | "github.com/DATA-DOG/go-sqlmock" 21 | "github.com/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 24 | "github.com/smartystreets/goconvey/convey" 25 | ) 26 | 27 | func TestScrapePerfReplicationGroupMembers(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 | inst := &instance{db: db} 34 | 35 | columns := []string{ 36 | "CHANNEL_NAME", 37 | "MEMBER_ID", 38 | "MEMBER_HOST", 39 | "MEMBER_PORT", 40 | "MEMBER_STATE", 41 | "MEMBER_ROLE", 42 | "MEMBER_VERSION", 43 | } 44 | 45 | rows := sqlmock.NewRows(columns). 46 | AddRow("group_replication_applier", "uuid1", "hostname1", "3306", "ONLINE", "PRIMARY", "8.0.19"). 47 | AddRow("group_replication_applier", "uuid2", "hostname2", "3306", "ONLINE", "SECONDARY", "8.0.19"). 48 | AddRow("group_replication_applier", "uuid3", "hostname3", "3306", "ONLINE", "SECONDARY", "8.0.19") 49 | 50 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMembersQuery)).WillReturnRows(rows) 51 | 52 | ch := make(chan prometheus.Metric) 53 | go func() { 54 | if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 55 | t.Errorf("error calling function on test: %s", err) 56 | } 57 | close(ch) 58 | }() 59 | 60 | metricExpected := []MetricResult{ 61 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid1", "member_host": "hostname1", "member_port": "3306", 62 | "member_state": "ONLINE", "member_role": "PRIMARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 63 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid2", "member_host": "hostname2", "member_port": "3306", 64 | "member_state": "ONLINE", "member_role": "SECONDARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 65 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid3", "member_host": "hostname3", "member_port": "3306", 66 | "member_state": "ONLINE", "member_role": "SECONDARY", "member_version": "8.0.19"}, value: 1, metricType: dto.MetricType_GAUGE}, 67 | } 68 | convey.Convey("Metrics comparison", t, func() { 69 | for _, expect := range metricExpected { 70 | got := readMetric(<-ch) 71 | convey.So(got, convey.ShouldResemble, expect) 72 | } 73 | }) 74 | 75 | // Ensure all SQL queries were executed. 76 | if err := mock.ExpectationsWereMet(); err != nil { 77 | t.Errorf("there were unfulfilled exceptions: %s", err) 78 | } 79 | } 80 | 81 | func TestScrapePerfReplicationGroupMembersMySQL57(t *testing.T) { 82 | db, mock, err := sqlmock.New() 83 | if err != nil { 84 | t.Fatalf("error opening a stub database connection: %s", err) 85 | } 86 | defer db.Close() 87 | inst := &instance{db: db} 88 | 89 | columns := []string{ 90 | "CHANNEL_NAME", 91 | "MEMBER_ID", 92 | "MEMBER_HOST", 93 | "MEMBER_PORT", 94 | "MEMBER_STATE", 95 | } 96 | 97 | rows := sqlmock.NewRows(columns). 98 | AddRow("group_replication_applier", "uuid1", "hostname1", "3306", "ONLINE"). 99 | AddRow("group_replication_applier", "uuid2", "hostname2", "3306", "ONLINE"). 100 | AddRow("group_replication_applier", "uuid3", "hostname3", "3306", "ONLINE") 101 | 102 | mock.ExpectQuery(sanitizeQuery(perfReplicationGroupMembersQuery)).WillReturnRows(rows) 103 | 104 | ch := make(chan prometheus.Metric) 105 | go func() { 106 | if err = (ScrapePerfReplicationGroupMembers{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 107 | t.Errorf("error calling function on test: %s", err) 108 | } 109 | close(ch) 110 | }() 111 | 112 | metricExpected := []MetricResult{ 113 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid1", "member_host": "hostname1", "member_port": "3306", 114 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 115 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid2", "member_host": "hostname2", "member_port": "3306", 116 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 117 | {labels: labelMap{"channel_name": "group_replication_applier", "member_id": "uuid3", "member_host": "hostname3", "member_port": "3306", 118 | "member_state": "ONLINE"}, value: 1, metricType: dto.MetricType_GAUGE}, 119 | } 120 | convey.Convey("Metrics comparison", t, func() { 121 | for _, expect := range metricExpected { 122 | got := readMetric(<-ch) 123 | convey.So(got, convey.ShouldResemble, expect) 124 | } 125 | }) 126 | 127 | // Ensure all SQL queries were executed. 128 | if err := mock.ExpectationsWereMet(); err != nil { 129 | t.Errorf("there were unfulfilled exceptions: %s", err) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /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 | "log/slog" 21 | 22 | "github.com/prometheus/client_golang/prometheus" 23 | ) 24 | 25 | const perfTableIOWaitsQuery = ` 26 | SELECT 27 | OBJECT_SCHEMA, OBJECT_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_table 31 | WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema') 32 | ` 33 | 34 | // Metric descriptors. 35 | var ( 36 | performanceSchemaTableWaitsDesc = prometheus.NewDesc( 37 | prometheus.BuildFQName(namespace, performanceSchema, "table_io_waits_total"), 38 | "The total number of table I/O wait events for each table and operation.", 39 | []string{"schema", "name", "operation"}, nil, 40 | ) 41 | performanceSchemaTableWaitsTimeDesc = prometheus.NewDesc( 42 | prometheus.BuildFQName(namespace, performanceSchema, "table_io_waits_seconds_total"), 43 | "The total time of table I/O wait events for each table and operation.", 44 | []string{"schema", "name", "operation"}, nil, 45 | ) 46 | ) 47 | 48 | // ScrapePerfTableIOWaits collects from `performance_schema.table_io_waits_summary_by_table`. 49 | type ScrapePerfTableIOWaits struct{} 50 | 51 | // Name of the Scraper. Should be unique. 52 | func (ScrapePerfTableIOWaits) Name() string { 53 | return "perf_schema.tableiowaits" 54 | } 55 | 56 | // Help describes the role of the Scraper. 57 | func (ScrapePerfTableIOWaits) Help() string { 58 | return "Collect metrics from performance_schema.table_io_waits_summary_by_table" 59 | } 60 | 61 | // Version of MySQL from which scraper is available. 62 | func (ScrapePerfTableIOWaits) 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 (ScrapePerfTableIOWaits) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 68 | db := instance.getDB() 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/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 | "log/slog" 19 | 20 | _ "github.com/go-sql-driver/mysql" 21 | "github.com/prometheus/client_golang/prometheus" 22 | ) 23 | 24 | // Scraper is minimal interface that let's you add new prometheus metrics to mysqld_exporter. 25 | type Scraper interface { 26 | // Name of the Scraper. Should be unique. 27 | Name() string 28 | 29 | // Help describes the role of the Scraper. 30 | // Example: "Collect from SHOW ENGINE INNODB STATUS" 31 | Help() string 32 | 33 | // Version of MySQL from which scraper is available. 34 | Version() float64 35 | 36 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 37 | Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error 38 | } 39 | -------------------------------------------------------------------------------- /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 | "log/slog" 22 | 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 | showReplicasQuery = "SHOW REPLICAS" 36 | ) 37 | 38 | // Metric descriptors. 39 | var ( 40 | SlaveHostsInfo = prometheus.NewDesc( 41 | prometheus.BuildFQName(namespace, heartbeat, "mysql_slave_hosts_info"), 42 | "Information about running slaves", 43 | []string{"server_id", "slave_host", "port", "master_id", "slave_uuid"}, nil, 44 | ) 45 | ) 46 | 47 | // ScrapeSlaveHosts scrapes metrics about the replicating slaves. 48 | type ScrapeSlaveHosts struct{} 49 | 50 | // Name of the Scraper. Should be unique. 51 | func (ScrapeSlaveHosts) Name() string { 52 | return slavehosts 53 | } 54 | 55 | // Help describes the role of the Scraper. 56 | func (ScrapeSlaveHosts) Help() string { 57 | return "Scrape information from 'SHOW SLAVE HOSTS'" 58 | } 59 | 60 | // Version of MySQL from which scraper is available. 61 | func (ScrapeSlaveHosts) Version() float64 { 62 | return 5.1 63 | } 64 | 65 | // Scrape collects data from database connection and sends it over channel as prometheus metric. 66 | func (ScrapeSlaveHosts) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 67 | var ( 68 | slaveHostsRows *sql.Rows 69 | err error 70 | ) 71 | db := instance.getDB() 72 | // Try the both syntax for MySQL 8.0 and MySQL 8.4 73 | if slaveHostsRows, err = db.QueryContext(ctx, slaveHostsQuery); err != nil { 74 | if slaveHostsRows, err = db.QueryContext(ctx, showReplicasQuery); err != nil { 75 | return err 76 | } 77 | } 78 | defer slaveHostsRows.Close() 79 | 80 | // fields of row 81 | var serverId string 82 | var host string 83 | var port string 84 | var rrrOrMasterId string 85 | var slaveUuidOrMasterId string 86 | 87 | // Depends on the version of MySQL being scraped 88 | var masterId string 89 | var slaveUuid string 90 | 91 | columnNames, err := slaveHostsRows.Columns() 92 | if err != nil { 93 | return err 94 | } 95 | 96 | for slaveHostsRows.Next() { 97 | // Newer versions of mysql have the following 98 | // Server_id, Host, Port, Master_id, Slave_UUID 99 | // Older versions of mysql have the following 100 | // Server_id, Host, Port, Rpl_recovery_rank, Master_id 101 | // MySQL 5.5 and MariaDB 10.5 have the following 102 | // Server_id, Host, Port, Master_id 103 | if len(columnNames) == 5 { 104 | err = slaveHostsRows.Scan(&serverId, &host, &port, &rrrOrMasterId, &slaveUuidOrMasterId) 105 | } else { 106 | err = slaveHostsRows.Scan(&serverId, &host, &port, &rrrOrMasterId) 107 | } 108 | if err != nil { 109 | return err 110 | } 111 | 112 | // if a Slave_UUID or Rpl_recovery_rank field is present 113 | if len(columnNames) == 5 { 114 | // Check to see if slaveUuidOrMasterId resembles a UUID or not 115 | // to find out if we are using an old version of MySQL 116 | if _, err = uuid.Parse(slaveUuidOrMasterId); err != nil { 117 | // We are running an older version of MySQL with no slave UUID 118 | slaveUuid = "" 119 | masterId = slaveUuidOrMasterId 120 | } else { 121 | // We are running a more recent version of MySQL 122 | slaveUuid = slaveUuidOrMasterId 123 | masterId = rrrOrMasterId 124 | } 125 | } else { 126 | slaveUuid = "" 127 | masterId = rrrOrMasterId 128 | } 129 | 130 | ch <- prometheus.MustNewConstMetric( 131 | SlaveHostsInfo, 132 | prometheus.GaugeValue, 133 | 1, 134 | serverId, 135 | host, 136 | port, 137 | masterId, 138 | slaveUuid, 139 | ) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | // check interface 146 | var _ Scraper = ScrapeSlaveHosts{} 147 | -------------------------------------------------------------------------------- /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 | "log/slog" 23 | "strings" 24 | 25 | "github.com/prometheus/client_golang/prometheus" 26 | ) 27 | 28 | const ( 29 | // Subsystem. 30 | slaveStatus = "slave_status" 31 | ) 32 | 33 | var slaveStatusQueries = [3]string{"SHOW ALL SLAVES STATUS", "SHOW SLAVE STATUS", "SHOW REPLICA 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, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error { 73 | var ( 74 | slaveStatusRows *sql.Rows 75 | err error 76 | ) 77 | db := instance.getDB() 78 | // Try the both syntax for MySQL/Percona and MariaDB 79 | for _, query := range slaveStatusQueries { 80 | slaveStatusRows, err = db.QueryContext(ctx, query) 81 | if err != nil { // MySQL/Percona 82 | // Leverage lock-free SHOW SLAVE STATUS by guessing the right suffix 83 | for _, suffix := range slaveStatusQuerySuffixes { 84 | slaveStatusRows, err = db.QueryContext(ctx, fmt.Sprint(query, suffix)) 85 | if err == nil { 86 | break 87 | } 88 | } 89 | } else { // MariaDB 90 | break 91 | } 92 | } 93 | if err != nil { 94 | return err 95 | } 96 | defer slaveStatusRows.Close() 97 | 98 | slaveCols, err := slaveStatusRows.Columns() 99 | if err != nil { 100 | return err 101 | } 102 | 103 | for slaveStatusRows.Next() { 104 | // As the number of columns varies with mysqld versions, 105 | // and sql.Scan requires []interface{}, we need to create a 106 | // slice of pointers to the elements of slaveData. 107 | scanArgs := make([]interface{}, len(slaveCols)) 108 | for i := range scanArgs { 109 | scanArgs[i] = &sql.RawBytes{} 110 | } 111 | 112 | if err := slaveStatusRows.Scan(scanArgs...); err != nil { 113 | return err 114 | } 115 | 116 | masterUUID := columnValue(scanArgs, slaveCols, "Master_UUID") 117 | if masterUUID == "" { 118 | masterUUID = columnValue(scanArgs, slaveCols, "Source_UUID") 119 | } 120 | masterHost := columnValue(scanArgs, slaveCols, "Master_Host") 121 | if masterHost == "" { 122 | masterHost = columnValue(scanArgs, slaveCols, "Source_Host") 123 | } 124 | channelName := columnValue(scanArgs, slaveCols, "Channel_Name") // MySQL & Percona 125 | connectionName := columnValue(scanArgs, slaveCols, "Connection_name") // MariaDB 126 | 127 | for i, col := range slaveCols { 128 | if value, ok := parseStatus(*scanArgs[i].(*sql.RawBytes)); ok { // Silently skip unparsable values. 129 | ch <- prometheus.MustNewConstMetric( 130 | prometheus.NewDesc( 131 | prometheus.BuildFQName(namespace, slaveStatus, strings.ToLower(col)), 132 | "Generic metric from SHOW SLAVE STATUS.", 133 | []string{"master_host", "master_uuid", "channel_name", "connection_name"}, 134 | nil, 135 | ), 136 | prometheus.UntypedValue, 137 | value, 138 | masterHost, masterUUID, channelName, connectionName, 139 | ) 140 | } 141 | } 142 | } 143 | return nil 144 | } 145 | 146 | // check interface 147 | var _ Scraper = ScrapeSlaveStatus{} 148 | -------------------------------------------------------------------------------- /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/prometheus/client_golang/prometheus" 22 | dto "github.com/prometheus/client_model/go" 23 | "github.com/prometheus/common/promslog" 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 | inst := &instance{db: db} 34 | 35 | columns := []string{"Master_Host", "Read_Master_Log_Pos", "Slave_IO_Running", "Slave_SQL_Running", "Seconds_Behind_Master"} 36 | rows := sqlmock.NewRows(columns). 37 | AddRow("127.0.0.1", "1", "Connecting", "Yes", "2") 38 | mock.ExpectQuery(sanitizeQuery("SHOW SLAVE STATUS")).WillReturnRows(rows) 39 | 40 | ch := make(chan prometheus.Metric) 41 | go func() { 42 | if err = (ScrapeSlaveStatus{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 43 | t.Errorf("error calling function on test: %s", err) 44 | } 45 | close(ch) 46 | }() 47 | 48 | counterExpected := []MetricResult{ 49 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 1, metricType: dto.MetricType_UNTYPED}, 50 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 0, metricType: dto.MetricType_UNTYPED}, 51 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 1, metricType: dto.MetricType_UNTYPED}, 52 | {labels: labelMap{"channel_name": "", "connection_name": "", "master_host": "127.0.0.1", "master_uuid": ""}, value: 2, metricType: dto.MetricType_UNTYPED}, 53 | } 54 | convey.Convey("Metrics comparison", t, func() { 55 | for _, expect := range counterExpected { 56 | got := readMetric(<-ch) 57 | convey.So(got, convey.ShouldResemble, expect) 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/sys.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 collector 15 | 16 | const sysSchema = "sys" 17 | -------------------------------------------------------------------------------- /collector/sys_user_summary_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 collector 15 | 16 | import ( 17 | "context" 18 | "database/sql/driver" 19 | "regexp" 20 | "strconv" 21 | "testing" 22 | 23 | "github.com/DATA-DOG/go-sqlmock" 24 | "github.com/prometheus/client_golang/prometheus" 25 | dto "github.com/prometheus/client_model/go" 26 | "github.com/prometheus/common/promslog" 27 | "github.com/smartystreets/goconvey/convey" 28 | ) 29 | 30 | func TestScrapeSysUserSummary(t *testing.T) { 31 | 32 | db, mock, err := sqlmock.New() 33 | if err != nil { 34 | t.Fatalf("error opening a stub database connection: %s", err) 35 | } 36 | defer db.Close() 37 | inst := &instance{db: db} 38 | 39 | columns := []string{ 40 | "user", 41 | "statemets", 42 | "statement_latency", 43 | "table_scans", 44 | "file_ios", 45 | "file_io_latency", 46 | "current_connections", 47 | "total_connections", 48 | "unique_hosts", 49 | "current_memory", 50 | "total_memory_allocated", 51 | } 52 | rows := sqlmock.NewRows(columns) 53 | queryResults := [][]driver.Value{ 54 | { 55 | "user1", 56 | "110", 57 | "120", 58 | "140", 59 | "150", 60 | "160", 61 | "170", 62 | "180", 63 | "190", 64 | "110", 65 | "111", 66 | }, 67 | { 68 | "user2", 69 | "210", 70 | "220", 71 | "240", 72 | "250", 73 | "260", 74 | "270", 75 | "280", 76 | "290", 77 | "210", 78 | "211", 79 | }, 80 | } 81 | expectedMetrics := []MetricResult{} 82 | // Register the query results with mock SQL driver and assemble expected metric results list 83 | for _, row := range queryResults { 84 | rows.AddRow(row...) 85 | user := row[0] 86 | for i, metricsValue := range row { 87 | if i == 0 { 88 | continue 89 | } 90 | metricType := dto.MetricType_COUNTER 91 | // Current Connections and Current Memory are gauges 92 | if i == 6 || i == 9 { 93 | metricType = dto.MetricType_GAUGE 94 | } 95 | value, err := strconv.ParseFloat(metricsValue.(string), 64) 96 | if err != nil { 97 | t.Errorf("Failed to parse result value as float64: %+v", err) 98 | } 99 | // Statement latency & IO latency are latencies in picoseconds, convert them to seconds 100 | if i == 2 || i == 5 { 101 | value = value / picoSeconds 102 | } 103 | expectedMetrics = append(expectedMetrics, MetricResult{ 104 | labels: labelMap{"user": user.(string)}, 105 | value: value, 106 | metricType: metricType, 107 | }) 108 | } 109 | } 110 | 111 | mock.ExpectQuery(sanitizeQuery(regexp.QuoteMeta(sysUserSummaryQuery))).WillReturnRows(rows) 112 | 113 | ch := make(chan prometheus.Metric) 114 | 115 | go func() { 116 | if err = (ScrapeSysUserSummary{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil { 117 | t.Errorf("error calling function on test: %s", err) 118 | } 119 | close(ch) 120 | }() 121 | 122 | // Ensure metrics look OK 123 | convey.Convey("Metrics comparison", t, func() { 124 | for _, expect := range expectedMetrics { 125 | got := readMetric(<-ch) 126 | convey.So(expect, convey.ShouldResemble, got) 127 | } 128 | }) 129 | 130 | // Ensure all SQL queries were executed 131 | if err := mock.ExpectationsWereMet(); err != nil { 132 | t.Errorf("there were unfulfilled exceptions: %s", err) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/testdata/client_custom_tls.cnf: -------------------------------------------------------------------------------- 1 | [client_tls_true] 2 | host = server2 3 | port = 3306 4 | user = usr 5 | password = pwd 6 | tls=true 7 | [client_tls_preferred] 8 | host = server3 9 | port = 3306 10 | user = usr 11 | password = pwd 12 | tls=preferred 13 | [client_tls_skip_verify] 14 | host = server3 15 | port = 3306 16 | user = usr 17 | password = pwd 18 | tls=skip-verify 19 | -------------------------------------------------------------------------------- /config/testdata/missing_password.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | user = abc 3 | -------------------------------------------------------------------------------- /config/testdata/missing_user.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | password = abc 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prometheus/mysqld_exporter 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.2 7 | github.com/alecthomas/kingpin/v2 v2.4.0 8 | github.com/blang/semver/v4 v4.0.0 9 | github.com/go-sql-driver/mysql v1.9.2 10 | github.com/google/go-cmp v0.7.0 11 | github.com/google/uuid v1.6.0 12 | github.com/prometheus/client_golang v1.22.0 13 | github.com/prometheus/client_model v0.6.2 14 | github.com/prometheus/common v0.64.0 15 | github.com/prometheus/exporter-toolkit v0.13.2 16 | github.com/smartystreets/goconvey v1.8.1 17 | gopkg.in/ini.v1 v1.67.0 18 | ) 19 | 20 | require ( 21 | filippo.io/edwards25519 v1.1.0 // indirect 22 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 23 | github.com/beorn7/perks v1.0.1 // indirect 24 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 25 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 26 | github.com/gopherjs/gopherjs v1.17.2 // indirect 27 | github.com/jpillora/backoff v1.0.0 // indirect 28 | github.com/jtolds/gls v4.20.0+incompatible // indirect 29 | github.com/mdlayher/socket v0.4.1 // indirect 30 | github.com/mdlayher/vsock v1.2.1 // indirect 31 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 32 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect 33 | github.com/prometheus/procfs v0.15.1 // indirect 34 | github.com/smarty/assertions v1.15.0 // indirect 35 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 36 | golang.org/x/crypto v0.38.0 // indirect 37 | golang.org/x/net v0.40.0 // indirect 38 | golang.org/x/oauth2 v0.30.0 // indirect 39 | golang.org/x/sync v0.14.0 // indirect 40 | golang.org/x/sys v0.33.0 // indirect 41 | golang.org/x/text v0.25.0 // indirect 42 | google.golang.org/protobuf v1.36.6 // indirect 43 | gopkg.in/yaml.v2 v2.4.0 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /mysqld-mixin/.gitignore: -------------------------------------------------------------------------------- 1 | /alerts.yaml 2 | /rules.yaml 3 | dashboards_out 4 | -------------------------------------------------------------------------------- /mysqld-mixin/Makefile: -------------------------------------------------------------------------------- 1 | JSONNET_FMT := jsonnetfmt -n 2 --max-blank-lines 2 --string-style s --comment-style s 2 | 3 | default: build 4 | 5 | all: fmt lint build clean 6 | 7 | fmt: 8 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 9 | xargs -n 1 -- $(JSONNET_FMT) -i 10 | 11 | lint: 12 | find . -name 'vendor' -prune -o -name '*.libsonnet' -print -o -name '*.jsonnet' -print | \ 13 | while read f; do \ 14 | $(JSONNET_FMT) "$$f" | diff -u "$$f" -; \ 15 | done 16 | 17 | mixtool lint mixin.libsonnet 18 | 19 | build: 20 | mixtool generate all mixin.libsonnet 21 | 22 | clean: 23 | rm -rf dashboards_out alerts.yaml rules.yaml 24 | -------------------------------------------------------------------------------- /mysqld-mixin/README.md: -------------------------------------------------------------------------------- 1 | # MySQLd Mixin 2 | 3 | The MySQLd Mixin is a set of configurable, reusable, and extensible alerts and 4 | dashboards based on the metrics exported by the MySQLd Exporter. The mixin creates 5 | recording and alerting rules for Prometheus and suitable dashboard descriptions 6 | for Grafana. 7 | 8 | To use them, you need to have `mixtool` and `jsonnetfmt` installed. If you 9 | have a working Go development environment, it's easiest to run the following: 10 | ```bash 11 | $ go get github.com/monitoring-mixins/mixtool/cmd/mixtool 12 | $ go get github.com/google/go-jsonnet/cmd/jsonnetfmt 13 | ``` 14 | 15 | You can then build the Prometheus rules files `alerts.yaml` and 16 | `rules.yaml` and a directory `dashboard_out` with the JSON dashboard files 17 | for Grafana: 18 | ```bash 19 | $ make build 20 | ``` 21 | 22 | For more advanced uses of mixins, see 23 | https://github.com/monitoring-mixins/docs. 24 | -------------------------------------------------------------------------------- /mysqld-mixin/alerts/galera.yaml: -------------------------------------------------------------------------------- 1 | ### 2 | # Sample prometheus rules/alerts for mysqld. 3 | # 4 | # NOTE: Please review these carefully as thresholds and behavior may not meet 5 | # your SLOs or labels. 6 | # 7 | ### 8 | 9 | groups: 10 | - name: GaleraAlerts 11 | rules: 12 | - alert: MySQLGaleraNotReady 13 | expr: mysql_global_status_wsrep_ready != 1 14 | for: 5m 15 | labels: 16 | severity: warning 17 | annotations: 18 | description: '{{$labels.job}} on {{$labels.instance}} is not ready.' 19 | summary: Galera cluster node not ready. 20 | - alert: MySQLGaleraOutOfSync 21 | expr: (mysql_global_status_wsrep_local_state != 4 and mysql_global_variables_wsrep_desync 22 | == 0) 23 | for: 5m 24 | labels: 25 | severity: warning 26 | annotations: 27 | description: '{{$labels.job}} on {{$labels.instance}} is not in sync ({{$value}} 28 | != 4).' 29 | summary: Galera cluster node out of sync. 30 | - alert: MySQLGaleraDonorFallingBehind 31 | expr: (mysql_global_status_wsrep_local_state == 2 and mysql_global_status_wsrep_local_recv_queue 32 | > 100) 33 | for: 5m 34 | labels: 35 | severity: warning 36 | annotations: 37 | description: '{{$labels.job}} on {{$labels.instance}} is a donor (hotbackup) 38 | and is falling behind (queue size {{$value}}).' 39 | summary: XtraDB cluster donor node falling behind. 40 | - alert: MySQLReplicationNotRunning 41 | expr: mysql_slave_status_slave_io_running == 0 or mysql_slave_status_slave_sql_running 42 | == 0 43 | for: 2m 44 | labels: 45 | severity: critical 46 | annotations: 47 | description: "Replication on {{$labels.instance}} (IO or SQL) has been down for more than 2 minutes." 48 | summary: Replication is not running. 49 | - alert: MySQLReplicationLag 50 | expr: (instance:mysql_slave_lag_seconds > 30) and on(instance) (predict_linear(instance:mysql_slave_lag_seconds[5m], 51 | 60 * 2) > 0) 52 | for: 1m 53 | labels: 54 | severity: critical 55 | annotations: 56 | description: "Replication on {{$labels.instance}} has fallen behind and is not recovering." 57 | summary: MySQL slave replication is lagging. 58 | - alert: MySQLHeartbeatLag 59 | expr: (instance:mysql_heartbeat_lag_seconds > 30) and on(instance) (predict_linear(instance:mysql_heartbeat_lag_seconds[5m], 60 | 60 * 2) > 0) 61 | for: 1m 62 | labels: 63 | severity: critical 64 | annotations: 65 | description: "The heartbeat is lagging on {{$labels.instance}} and is not recovering." 66 | summary: MySQL heartbeat is lagging. 67 | - alert: MySQLInnoDBLogWaits 68 | expr: rate(mysql_global_status_innodb_log_waits[15m]) > 10 69 | labels: 70 | severity: warning 71 | annotations: 72 | description: The innodb logs are waiting for disk at a rate of {{$value}} / 73 | second 74 | summary: MySQL innodb log writes stalling. 75 | -------------------------------------------------------------------------------- /mysqld-mixin/alerts/general.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: MySQLdAlerts 3 | rules: 4 | - alert: MySQLDown 5 | expr: mysql_up != 1 6 | for: 5m 7 | labels: 8 | severity: critical 9 | annotations: 10 | description: 'MySQL {{$labels.job}} on {{$labels.instance}} is not up.' 11 | summary: MySQL not up. 12 | -------------------------------------------------------------------------------- /mysqld-mixin/mixin.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | grafanaDashboards: { 3 | 'mysql-overview.json': (import 'dashboards/mysql-overview.json'), 4 | }, 5 | 6 | // Helper function to ensure that we don't override other rules, by forcing 7 | // the patching of the groups list, and not the overall rules object. 8 | local importRules(rules) = { 9 | groups+: std.native('parseYaml')(rules)[0].groups, 10 | }, 11 | 12 | prometheusRules+: importRules(importstr 'rules/rules.yaml'), 13 | 14 | prometheusAlerts+: 15 | importRules(importstr 'alerts/general.yaml') + 16 | importRules(importstr 'alerts/galera.yaml'), 17 | } 18 | -------------------------------------------------------------------------------- /mysqld-mixin/rules/rules.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: mysqld_rules 3 | rules: 4 | 5 | # Record slave lag seconds for pre-computed timeseries that takes 6 | # `mysql_slave_status_sql_delay` into account 7 | - record: instance:mysql_slave_lag_seconds 8 | expr: mysql_slave_status_seconds_behind_master - mysql_slave_status_sql_delay 9 | 10 | # Record slave lag via heartbeat method 11 | - record: instance:mysql_heartbeat_lag_seconds 12 | expr: mysql_heartbeat_now_timestamp_seconds - mysql_heartbeat_stored_timestamp_seconds 13 | 14 | - record: job:mysql_transactions:rate5m 15 | expr: sum without (command) (rate(mysql_global_status_commands_total{command=~"(commit|rollback)"}[5m])) 16 | -------------------------------------------------------------------------------- /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 | "context" 18 | "fmt" 19 | "log/slog" 20 | "net/http" 21 | "time" 22 | 23 | "github.com/prometheus/client_golang/prometheus" 24 | "github.com/prometheus/client_golang/prometheus/promhttp" 25 | "github.com/prometheus/mysqld_exporter/collector" 26 | ) 27 | 28 | func handleProbe(scrapers []collector.Scraper, logger *slog.Logger) http.HandlerFunc { 29 | return func(w http.ResponseWriter, r *http.Request) { 30 | ctx := r.Context() 31 | params := r.URL.Query() 32 | target := params.Get("target") 33 | if target == "" { 34 | http.Error(w, "target is required", http.StatusBadRequest) 35 | return 36 | } 37 | collectParams := r.URL.Query()["collect[]"] 38 | 39 | authModule := params.Get("auth_module") 40 | if authModule == "" { 41 | authModule = "client" 42 | } 43 | 44 | cfg := c.GetConfig() 45 | cfgsection, ok := cfg.Sections[authModule] 46 | if !ok { 47 | logger.Error(fmt.Sprintf("Could not find section [%s] from config file", authModule)) 48 | http.Error(w, fmt.Sprintf("Could not find config section [%s]", authModule), http.StatusBadRequest) 49 | return 50 | } 51 | dsn, err := cfgsection.FormDSN(target) 52 | if err != nil { 53 | logger.Error(fmt.Sprintf("Failed to form dsn from section [%s]", authModule), "err", err) 54 | http.Error(w, fmt.Sprintf("Error forming dsn from config section [%s]", authModule), http.StatusBadRequest) 55 | return 56 | } 57 | 58 | // If a timeout is configured via the Prometheus header, add it to the context. 59 | timeoutSeconds, err := getScrapeTimeoutSeconds(r, *timeoutOffset) 60 | if err != nil { 61 | logger.Error("Error getting timeout from Prometheus header", "err", err) 62 | } 63 | if timeoutSeconds > 0 { 64 | // Create new timeout context with request context as parent. 65 | var cancel context.CancelFunc 66 | ctx, cancel = context.WithTimeout(ctx, time.Duration(timeoutSeconds*float64(time.Second))) 67 | defer cancel() 68 | // Overwrite request with timeout context. 69 | r = r.WithContext(ctx) 70 | } 71 | 72 | filteredScrapers := filterScrapers(scrapers, collectParams) 73 | 74 | registry := prometheus.NewRegistry() 75 | registry.MustRegister(collector.New(ctx, dsn, filteredScrapers, logger)) 76 | 77 | h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) 78 | h.ServeHTTP(w, r) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------