Skip to content

Add a gtid transaction counter based on the @@gtid_executed global variable #965

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ collect.engine_innodb_status | 5.1 | C
collect.engine_tokudb_status | 5.6 | Collect from SHOW ENGINE TOKUDB STATUS.
collect.global_status | 5.1 | Collect from SHOW GLOBAL STATUS (Enabled by default)
collect.global_variables | 5.1 | Collect from SHOW GLOBAL VARIABLES (Enabled by default)
collect.gtid_transactions | 5.6 | Collect a transaction count from @gtid_executed.
collect.heartbeat | 5.1 | Collect from [heartbeat](#heartbeat).
collect.heartbeat.database | 5.1 | Database from where to collect heartbeat data. (default: heartbeat)
collect.heartbeat.table | 5.1 | Table from where to collect heartbeat data. (default: heartbeat)
Expand Down
87 changes: 87 additions & 0 deletions collector/mysql_gtid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Scrape a transaction counter from the gtid_executed

package collector

import (
"context"
"log/slog"

"github.com/prometheus/client_golang/prometheus"
"github.com/sjmudd/mysqlgtid"
)

const (
// transactions is the Metric subsystem we use.
prometheusSubsystem = "gtid"
prometheusName = "transactions"
// gtidTransactionCountQuery is the query used to fetch gtid_executed.
// With this value we can convert it to an incremental transaction counter.
gtidTransactionCountQuery = "SELECT @@gtid_executed"
)

var (
// Metric descriptors.
GtidTransactionCounterDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, prometheusSubsystem, prometheusName),
"Number of GTID transactions",
[]string{}, nil,
)
)

// ScrapeGtidExecuted scrapes transaction count from @@gtid_executed.
type ScrapeGtidExecuted struct{}

// Name of the Scraper. Should be unique.
func (ScrapeGtidExecuted) Name() string {
return "gtid_transactions"
}

// Help describes the role of the Scraper.
func (ScrapeGtidExecuted) Help() string {
return "Number of GTID transactions"
}

// Version of MySQL from which scraper is available.
func (ScrapeGtidExecuted) Version() float64 {
return 5.6
}

// Scrape collects data from database connection and sends it over channel as prometheus metric.
func (ScrapeGtidExecuted) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error {
var gtidExecuted string

db := instance.getDB()
if err := db.QueryRowContext(ctx, gtidTransactionCountQuery).Scan(&gtidExecuted); err != nil {
return err
}

// convert into a counter
gtidExecutedTransactionCounterIntVal, err := mysqlgtid.TransactionCount(gtidExecuted)
if err != nil {
return err
}

ch <- prometheus.MustNewConstMetric(
GtidTransactionCounterDesc,
prometheus.CounterValue,
float64(gtidExecutedTransactionCounterIntVal),
)

return nil
}

// check interface
var _ Scraper = ScrapeGtidExecuted{}
90 changes: 90 additions & 0 deletions collector/mysql_gtid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package collector

import (
"context"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/promslog"
"github.com/smartystreets/goconvey/convey"
)

// TestScrapeGtidExecuted tests ScrapeGtidExecuted behaviour
func TestScrapeGtidExecuted(t *testing.T) {

tests := []struct {
name string
gtidSet string
expected float64
}{
{"empty_set", "", 0},
{"single_uuid_and_range", `uuid1:1-1000`, 1000},
{"multiple_uuid_single_range", `uuid1:1-1000,
uuid1:1001-2000`, 2000},
{"single_uuid_with_ranges", `uuid1:1-1000,2001-4000`, 3000},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("error opening a stub database connection: %s", err)
}
defer db.Close()

inst := &instance{db: db}

columns := []string{"@@gtid_executed"}
rows := sqlmock.NewRows(columns).
AddRow(test.gtidSet)
mock.ExpectQuery(gtidTransactionCountQuery).
WithArgs().
WillReturnRows(rows)

ch := make(chan prometheus.Metric)
go func() {
if err = (ScrapeGtidExecuted{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil {
t.Errorf("error calling function on test: %s", err)
}
close(ch)
}()

counterExpected := []MetricResult{
{
labels: labelMap{},
value: test.expected,
metricType: dto.MetricType_COUNTER,
},
}

convey.Convey("Metrics comparison", t, func() {
for _, expect := range counterExpected {
got := readMetric(<-ch)
convey.So(got, convey.ShouldResemble, expect)
}
})

// Ensure all SQL queries were executed
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})

}

}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/prometheus/mysqld_exporter

go 1.23.0
go 1.23.3

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
Expand All @@ -13,6 +13,7 @@ require (
github.com/prometheus/client_model v0.6.2
github.com/prometheus/common v0.65.0
github.com/prometheus/exporter-toolkit v0.13.2
github.com/sjmudd/mysqlgtid v0.1.0
github.com/smartystreets/goconvey v1.8.1
gopkg.in/ini.v1 v1.67.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sjmudd/mysqlgtid v0.1.0 h1:L/jycKaCUcO0vGBT1UuUuuZR6XmN/3SQpbO7IarPVu0=
github.com/sjmudd/mysqlgtid v0.1.0/go.mod h1:+guZnxmozw/hUgPZXSSryyRL2o3pBNC6aoVh1QlNg0E=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
Expand Down
1 change: 1 addition & 0 deletions mysqld_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ var scrapers = map[collector.Scraper]bool{
collector.ScrapeSlaveHosts{}: false,
collector.ScrapeReplicaHost{}: false,
collector.ScrapeRocksDBPerfContext{}: false,
collector.ScrapeGtidExecuted{}: false,
}

func filterScrapers(scrapers []collector.Scraper, collectParams []string) []collector.Scraper {
Expand Down