Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
2 changes: 1 addition & 1 deletion collector/exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
func (e *Exporter) scrape(ctx context.Context, ch chan<- prometheus.Metric) float64 {
var err error
scrapeTime := time.Now()
instance, err := newInstance(e.dsn)
instance, err := newInstance(ctx, e.dsn)
if err != nil {
e.logger.Error("Error opening connection to database", "err", err)
return 0.0
Expand Down
13 changes: 6 additions & 7 deletions collector/exporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package collector

import (
"context"
"testing"

"github.com/prometheus/client_golang/prometheus"
Expand All @@ -31,7 +30,7 @@ func TestExporter(t *testing.T) {
}

exporter := New(
context.Background(),
t.Context(),
dsn,
[]Scraper{
ScrapeGlobalStatus{},
Expand Down Expand Up @@ -70,7 +69,7 @@ func TestExporterDSN(t *testing.T) {
convey.Convey("DSN with special characters in password (w/o table)", t, func() {
dsn := "test:UfY9s73Gx`~!?@#$%^&*(){}[]<>|/:;,.-_+=@tcp(localhost:3306)/"
exporter := New(
context.Background(),
t.Context(),
dsn,
[]Scraper{
ScrapeGlobalStatus{},
Expand All @@ -83,7 +82,7 @@ func TestExporterDSN(t *testing.T) {
convey.Convey("DSN with special characters in password (with table)", t, func() {
dsn := "test:UfY9s73Gx`~!?@#$%^&*(){}[]<>|/:;,.-_+=@tcp(localhost:3306)/mysql"
exporter := New(
context.Background(),
t.Context(),
dsn,
[]Scraper{
ScrapeGlobalStatus{},
Expand All @@ -96,7 +95,7 @@ func TestExporterDSN(t *testing.T) {
convey.Convey("DSN with special characters in password, with tls", t, func() {
dsn := "test:UfY9s73Gx`~!?@#$%^&*(){}[]<>|/:;,.-_+=@tcp(localhost:3306)/?tls=true"
exporter := New(
context.Background(),
t.Context(),
dsn,
[]Scraper{
ScrapeGlobalStatus{},
Expand All @@ -109,7 +108,7 @@ func TestExporterDSN(t *testing.T) {
convey.Convey("DSN with special characters in password, no tls", t, func() {
dsn := "test:UfY9s73Gx`~!?@#$%^&*(){}[]<>|/:;,.-_+=@tcp(localhost:3306)/test?tls=skip-verify"
exporter := New(
context.Background(),
t.Context(),
dsn,
[]Scraper{
ScrapeGlobalStatus{},
Expand All @@ -126,7 +125,7 @@ func TestGetMySQLVersion(t *testing.T) {
}

convey.Convey("Version parsing", t, func() {
instance, err := newInstance(dsn)
instance, err := newInstance(t.Context(), dsn)
Comment thread
4nte marked this conversation as resolved.
convey.So(err, convey.ShouldBeNil)

convey.So(instance.versionMajorMinor, convey.ShouldBeBetweenOrEqual, 5.7, 11.4)
Expand Down
42 changes: 31 additions & 11 deletions collector/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package collector

import (
"context"
"database/sql"
"fmt"
"regexp"
Expand All @@ -25,9 +26,10 @@ import (
)

const (
FlavorMySQL = "mysql"
FlavorMariaDB = "mariadb"
versionQuery = "SELECT @@version;"
FlavorMySQL = "mysql"
FlavorMariaDB = "mariadb"
versionQuery = "SELECT @@version;"
performanceSchemaQuery = "SELECT @@performance_schema;"
)

var (
Expand All @@ -46,13 +48,14 @@ var (
)

type instance struct {
db *sql.DB
flavor string
version semver.Version
versionMajorMinor float64
db *sql.DB
flavor string
version semver.Version
versionMajorMinor float64
isPerformanceSchemaEnabled bool
}

func newInstance(dsn string) (*instance, error) {
func newInstance(ctx context.Context, dsn string) (*instance, error) {
i := &instance{}
db, err := sql.Open("mysql", dsn)
if err != nil {
Expand All @@ -64,7 +67,7 @@ func newInstance(dsn string) (*instance, error) {

i.db = db

version, versionString, err := queryVersion(db)
version, versionString, err := queryVersion(ctx, db)
if err != nil {
db.Close()
return nil, err
Expand All @@ -86,9 +89,26 @@ func newInstance(dsn string) (*instance, error) {
i.flavor = FlavorMySQL
}

isPerformanceSchemaEnabled, err := queryPerformanceSchemaEnabled(ctx, db)
if err != nil {
db.Close()
return nil, err
}
i.isPerformanceSchemaEnabled = isPerformanceSchemaEnabled

return i, nil
}

// queryPerformanceSchemaEnabled reports whether performance_schema is enabled in the server
func queryPerformanceSchemaEnabled(ctx context.Context, db *sql.DB) (bool, error) {
var enabled uint8
err := db.QueryRowContext(ctx, performanceSchemaQuery).Scan(&enabled)
if err != nil {
return false, fmt.Errorf("failed to query performance_schema status: %w", err)
}
return enabled == 1, nil
}

// getDB returns the database connection for the instance.
func (i *instance) getDB() *sql.DB {
return i.db
Expand All @@ -115,9 +135,9 @@ func (i *instance) Ping() error {
// for MySQL: "8.0.36-28.1"
var versionRegex = regexp.MustCompile(`^((\d+)(\.\d+)(\.\d+))`)

func queryVersion(db *sql.DB) (semver.Version, string, error) {
func queryVersion(ctx context.Context, db *sql.DB) (semver.Version, string, error) {
var version string
err := db.QueryRow(versionQuery).Scan(&version)
err := db.QueryRowContext(ctx, versionQuery).Scan(&version)
if err != nil {
return semver.Version{}, version, err
}
Expand Down
12 changes: 6 additions & 6 deletions collector/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,32 @@ func TestGetMySQLVersion_Percona(t *testing.T) {
var semVer semver.Version
var err error
mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow(""))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeError(err)
convey.So(semVer.String(), convey.ShouldEqual, "0.0.0")

mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("something"))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeNil(err)
convey.So(semVer.String(), convey.ShouldEqual, "0.0.0")

mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("10.1.17-MariaDB"))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeNil(err)
convey.So(semVer.String(), convey.ShouldEqual, "10.1.17")

mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.7.13-6-log"))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeNil(err)
convey.So(semVer.String(), convey.ShouldEqual, "5.7.13")

mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.6.30-76.3-56-log"))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeNil(err)
convey.So(semVer.String(), convey.ShouldEqual, "5.6.30")

mock.ExpectQuery(versionQuery).WillReturnRows(sqlmock.NewRows([]string{""}).AddRow("5.5.51-38.1"))
semVer, _, err = queryVersion(db)
semVer, _, err = queryVersion(t.Context(), db)
convey.ShouldBeNil(err)
convey.So(semVer.String(), convey.ShouldEqual, "5.5.51")
})
Expand Down
32 changes: 27 additions & 5 deletions collector/percona_info_schema_process_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,31 @@ import (
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
)

const (
processlistInfoSchema = "information_schema"
processlistPerfSchema = "performance_schema"
)

const pInfoSchemaProcesslistQuery = `
SELECT COALESCE(command,''),COALESCE(state,''),count(*),sum(time)
FROM information_schema.processlist
FROM %s.processlist
WHERE ID != connection_id()
AND TIME >= %d
GROUP BY command,state
ORDER BY null
`

// MySQL version boundaries for querying perf schema
var (
v8_0_22 = semver.MustParse("8.0.22")
v5_7_39 = semver.MustParse("5.7.39")
v8_0_0 = semver.MustParse("8.0.0")
)

// Tunable flags.
var (
pProcesslistMinTime = kingpin.Flag(
Expand Down Expand Up @@ -185,10 +198,19 @@ func (PScrapeProcesslist) Version() float64 {

// Scrape collects data from database connection and sends it over channel as prometheus metric.
func (PScrapeProcesslist) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error {
processQuery := fmt.Sprintf(
pInfoSchemaProcesslistQuery,
*pProcesslistMinTime,
)
// Prefer querying performance_schema.processlist instead of information_schema.processlist to avoid negative perf consequences
// Supported by Percona Server/MySQL >=5.7.39 and >=8.0.22
usePerfSchema := instance.flavor == FlavorMySQL &&
instance.isPerformanceSchemaEnabled &&
(instance.version.GTE(v8_0_22) ||
(instance.version.GTE(v5_7_39) && instance.version.LT(v8_0_0)))
Comment thread
maxkondr marked this conversation as resolved.

schema := processlistInfoSchema
if usePerfSchema {
schema = processlistPerfSchema
}

processQuery := fmt.Sprintf(pInfoSchemaProcesslistQuery, schema, *pProcesslistMinTime)
db := instance.getDB()
processlistRows, err := db.QueryContext(ctx, processQuery)
if err != nil {
Expand Down
86 changes: 86 additions & 0 deletions collector/percona_info_schema_process_list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2018 The Prometheus Authors, 2023 Percona LLC
// 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 (
"fmt"
"testing"

"github.com/DATA-DOG/go-sqlmock"
"github.com/alecthomas/kingpin/v2"
"github.com/blang/semver/v4"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/promslog"
)

func TestPScrapeProcesslistQuerySelection(t *testing.T) {
if _, err := kingpin.CommandLine.Parse([]string{}); err != nil {
t.Fatal(err)
}

cases := []struct {
name string
flavor string
version semver.Version
perfSchemaEnabled bool
expectedSchema string
}{
{"MySQL 8.0.22 + PS on -> perf_schema", FlavorMySQL, semver.MustParse("8.0.22"), true, processlistPerfSchema},
{"MySQL 8.0.30 + PS on -> perf_schema", FlavorMySQL, semver.MustParse("8.0.30"), true, processlistPerfSchema},
{"MySQL 5.7.39 + PS on -> perf_schema", FlavorMySQL, semver.MustParse("5.7.39"), true, processlistPerfSchema},
{"MySQL 8.0.22 + PS off -> info_schema", FlavorMySQL, semver.MustParse("8.0.22"), false, processlistInfoSchema},
{"MySQL 5.7.39 + PS off -> info_schema", FlavorMySQL, semver.MustParse("5.7.39"), false, processlistInfoSchema},
{"MySQL 8.0.21 -> info_schema", FlavorMySQL, semver.MustParse("8.0.21"), true, processlistInfoSchema},
{"MySQL 5.7.38 -> info_schema", FlavorMySQL, semver.MustParse("5.7.38"), true, processlistInfoSchema},
{"MySQL 8.0.0 -> info_schema", FlavorMySQL, semver.MustParse("8.0.0"), true, processlistInfoSchema},
{"MySQL 5.6.50 -> info_schema", FlavorMySQL, semver.MustParse("5.6.50"), true, processlistInfoSchema},
{"MariaDB 10.11.0 -> info_schema", FlavorMariaDB, semver.MustParse("10.11.0"), true, processlistInfoSchema},
}

for _, tc := range cases {
t.Run(tc.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,
flavor: tc.flavor,
version: tc.version,
isPerformanceSchemaEnabled: tc.perfSchemaEnabled,
}

expectedSQL := fmt.Sprintf(pInfoSchemaProcesslistQuery, tc.expectedSchema, 0)
columns := []string{"command", "state", "count", "time"}
mock.ExpectQuery(sanitizeQuery(expectedSQL)).
WillReturnRows(sqlmock.NewRows(columns))

ch := make(chan prometheus.Metric)
go func() {
if err := (PScrapeProcesslist{}).Scrape(t.Context(), inst, ch, promslog.NewNopLogger()); err != nil {
t.Errorf("error calling Scrape: %s", err)
}
close(ch)
}()
for range ch {
}

if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
})
}
}
4 changes: 2 additions & 2 deletions percona/tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ prepare-base-exporter:
tar -xf assets/mysqld_exporter_percona.tar.xz -C assets/

start-mysql-db:
docker-compose -f assets/mysql-compose.yml up -d --force-recreate --renew-anon-volumes --remove-orphans
docker compose -f assets/mysql-compose.yml up -d --force-recreate --renew-anon-volumes --remove-orphans

stop-mysql-db:
docker-compose -f assets/mysql-compose.yml down
docker compose -f assets/mysql-compose.yml down

prepare-env-from-repo: prepare-exporter-from-repo prepare-base-exporter start-mysql-db
1 change: 0 additions & 1 deletion percona/tests/assets/test.exporter-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
--exporter.max-idle-conns=3
--exporter.max-open-conns=3
--exporter.conn-max-lifetime=55s
--exporter.global-conn-pool
--collect.info_schema.innodb_tablespaces
--collect.auto_increment.columns
--collect.info_schema.tables
Expand Down
2 changes: 1 addition & 1 deletion percona/tests/env_prepare_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func extractExporter(gzipStream io.Reader, fileName string) {
case tar.TypeDir:
continue
case tar.TypeReg:
if strings.HasSuffix(header.Name, "postgres_exporter") {
if strings.HasSuffix(header.Name, "mysqld_exporter") {
outFile, err := os.Create(fileName)
if err != nil {
log.Fatalf("ExtractTarGz: Create() failed: %s", err.Error())
Expand Down
Loading