Skip to content

Commit f2955ea

Browse files
committed
use two separate queries and add tests
1 parent 22c7a26 commit f2955ea

File tree

3 files changed

+231
-23
lines changed

3 files changed

+231
-23
lines changed

collector/collector_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ func readMetric(m prometheus.Metric) MetricResult {
4141
if pb.Counter != nil {
4242
return MetricResult{labels: labels, value: pb.GetCounter().GetValue(), metricType: dto.MetricType_COUNTER}
4343
}
44+
if pb.Summary != nil {
45+
return MetricResult{labels: labels, value: pb.GetSummary().GetSampleSum(), metricType: dto.MetricType_SUMMARY}
46+
}
4447
if pb.Untyped != nil {
4548
return MetricResult{labels: labels, value: pb.GetUntyped().GetValue(), metricType: dto.MetricType_UNTYPED}
4649
}

collector/perf_schema_events_statements.go

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package collector
1717

1818
import (
1919
"context"
20-
"fmt"
2120
"log/slog"
2221

2322
"github.com/alecthomas/kingpin/v2"
@@ -45,7 +44,6 @@ const perfEventsStatementsQuery = `
4544
QUANTILE_95,
4645
QUANTILE_99,
4746
QUANTILE_999
48-
%s
4947
FROM (
5048
SELECT *
5149
FROM performance_schema.events_statements_summary_by_digest
@@ -72,7 +70,60 @@ const perfEventsStatementsQuery = `
7270
Q.QUANTILE_95,
7371
Q.QUANTILE_99,
7472
Q.QUANTILE_999
75-
%s
73+
ORDER BY SUM_TIMER_WAIT DESC
74+
LIMIT %d
75+
`
76+
77+
const perfEventsStatementsQueryMySQL = `
78+
SELECT
79+
ifnull(SCHEMA_NAME, 'NONE') as SCHEMA_NAME,
80+
DIGEST,
81+
LEFT(DIGEST_TEXT, %d) as DIGEST_TEXT,
82+
COUNT_STAR,
83+
SUM_TIMER_WAIT,
84+
SUM_LOCK_TIME,
85+
SUM_CPU_TIME,
86+
SUM_ERRORS,
87+
SUM_WARNINGS,
88+
SUM_ROWS_AFFECTED,
89+
SUM_ROWS_SENT,
90+
SUM_ROWS_EXAMINED,
91+
SUM_CREATED_TMP_DISK_TABLES,
92+
SUM_CREATED_TMP_TABLES,
93+
SUM_SORT_MERGE_PASSES,
94+
SUM_SORT_ROWS,
95+
SUM_NO_INDEX_USED,
96+
QUANTILE_95,
97+
QUANTILE_99,
98+
QUANTILE_999
99+
FROM (
100+
SELECT *
101+
FROM performance_schema.events_statements_summary_by_digest
102+
WHERE SCHEMA_NAME NOT IN ('mysql', 'performance_schema', 'information_schema')
103+
AND LAST_SEEN > DATE_SUB(NOW(), INTERVAL %d SECOND)
104+
ORDER BY LAST_SEEN DESC
105+
)Q
106+
GROUP BY
107+
Q.SCHEMA_NAME,
108+
Q.DIGEST,
109+
Q.DIGEST_TEXT,
110+
Q.COUNT_STAR,
111+
Q.SUM_TIMER_WAIT,
112+
Q.SUM_LOCK_TIME,
113+
Q.SUM_CPU_TIME,
114+
Q.SUM_ERRORS,
115+
Q.SUM_WARNINGS,
116+
Q.SUM_ROWS_AFFECTED,
117+
Q.SUM_ROWS_SENT,
118+
Q.SUM_ROWS_EXAMINED,
119+
Q.SUM_CREATED_TMP_DISK_TABLES,
120+
Q.SUM_CREATED_TMP_TABLES,
121+
Q.SUM_SORT_MERGE_PASSES,
122+
Q.SUM_SORT_ROWS,
123+
Q.SUM_NO_INDEX_USED,
124+
Q.QUANTILE_95,
125+
Q.QUANTILE_99,
126+
Q.QUANTILE_999
76127
ORDER BY SUM_TIMER_WAIT DESC
77128
LIMIT %d
78129
`
@@ -192,23 +243,12 @@ func (ScrapePerfEventsStatements) Version() float64 {
192243

193244
// Scrape collects data from database connection and sends it over channel as prometheus metric.
194245
func (ScrapePerfEventsStatements) Scrape(ctx context.Context, instance *instance, ch chan<- prometheus.Metric, logger *slog.Logger) error {
195-
additionalColumns := ""
196-
additionalGroupBy := ""
197-
useAdditionalColumns := false
198-
if instance.flavor == FlavorMySQL && instance.version.GTE(semver.MustParse("8.0.28")) {
199-
additionalColumns = ", SUM_LOCK_TIME, SUM_CPU_TIME"
200-
additionalGroupBy = ", Q.SUM_LOCK_TIME, Q.SUM_CPU_TIME"
201-
useAdditionalColumns = true
202-
}
246+
mysqlVersion8028 := instance.flavor == FlavorMySQL && instance.version.GTE(semver.MustParse("8.0.28"))
203247

204-
perfQuery := fmt.Sprintf(
205-
perfEventsStatementsQuery,
206-
*perfEventsStatementsDigestTextLimit,
207-
additionalColumns,
208-
*perfEventsStatementsTimeLimit,
209-
additionalGroupBy,
210-
*perfEventsStatementsLimit,
211-
)
248+
perfQuery := perfEventsStatementsQuery
249+
if mysqlVersion8028 {
250+
perfQuery = perfEventsStatementsQueryMySQL
251+
}
212252

213253
db := instance.getDB()
214254
// Timers here are returned in picoseconds.
@@ -220,20 +260,19 @@ func (ScrapePerfEventsStatements) Scrape(ctx context.Context, instance *instance
220260

221261
var (
222262
schemaName, digest, digestText string
223-
count, queryTime uint64
263+
count, queryTime, lockTime, cpuTime uint64
224264
errors, warnings uint64
225265
rowsAffected, rowsSent, rowsExamined uint64
226266
tmpTables, tmpDiskTables uint64
227267
sortMergePasses, sortRows uint64
228268
noIndexUsed uint64
229269
quantile95, quantile99, quantile999 uint64
230-
lockTime, cpuTime uint64
231270
)
232271
for perfSchemaEventsStatementsRows.Next() {
233272
var err error
234-
if useAdditionalColumns {
273+
if mysqlVersion8028 {
235274
err = perfSchemaEventsStatementsRows.Scan(
236-
&schemaName, &digest, &digestText, &count, &queryTime, &errors, &warnings, &rowsAffected, &rowsSent, &rowsExamined, &tmpDiskTables, &tmpTables, &sortMergePasses, &sortRows, &noIndexUsed, &quantile95, &quantile99, &quantile999, &lockTime, &cpuTime,
275+
&schemaName, &digest, &digestText, &count, &queryTime, &lockTime, &cpuTime, &errors, &warnings, &rowsAffected, &rowsSent, &rowsExamined, &tmpDiskTables, &tmpTables, &sortMergePasses, &sortRows, &noIndexUsed, &quantile95, &quantile99, &quantile999,
237276
)
238277
} else {
239278
err = perfSchemaEventsStatementsRows.Scan(
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Copyright 2025 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/blang/semver/v4"
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 TestScrapePerfEventsStatements(t *testing.T) {
29+
db, mock, err := sqlmock.New()
30+
if err != nil {
31+
t.Fatalf("error opening a stub database connection: %s", err)
32+
}
33+
defer db.Close()
34+
35+
columns := []string{
36+
"SCHEMA_NAME", "DIGEST", "DIGEST_TEXT",
37+
"COUNT_STAR", "SUM_TIMER_WAIT", "SUM_ERRORS", "SUM_WARNINGS",
38+
"SUM_ROWS_AFFECTED", "SUM_ROWS_SENT", "SUM_ROWS_EXAMINED",
39+
"SUM_CREATED_TMP_DISK_TABLES", "SUM_CREATED_TMP_TABLES", "SUM_SORT_MERGE_PASSES",
40+
"SUM_SORT_ROWS", "SUM_NO_INDEX_USED",
41+
"QUANTILE_95", "QUANTILE_99", "QUANTILE_999",
42+
}
43+
44+
rows := sqlmock.NewRows(columns).
45+
AddRow(
46+
"db1", "digest1", "SELECT * FROM test",
47+
100, 1000, 1, 2,
48+
50, 100, 150,
49+
1, 2, 3,
50+
100, 1,
51+
100, 150, 200)
52+
53+
mock.ExpectQuery(sanitizeQuery(perfEventsStatementsQuery)).WillReturnRows(rows)
54+
55+
ch := make(chan prometheus.Metric)
56+
go func() {
57+
if err = (ScrapePerfEventsStatements{}).Scrape(context.Background(), &instance{db: db}, ch, promslog.NewNopLogger()); err != nil {
58+
t.Errorf("error calling function on test: %s", err)
59+
}
60+
close(ch)
61+
}()
62+
63+
expected := []MetricResult{
64+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
65+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1000 / picoSeconds, metricType: dto.MetricType_COUNTER},
66+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 0, metricType: dto.MetricType_COUNTER},
67+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 0, metricType: dto.MetricType_COUNTER},
68+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
69+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 2, metricType: dto.MetricType_COUNTER},
70+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 50, metricType: dto.MetricType_COUNTER},
71+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
72+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 150, metricType: dto.MetricType_COUNTER},
73+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 2, metricType: dto.MetricType_COUNTER},
74+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
75+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 3, metricType: dto.MetricType_COUNTER},
76+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
77+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
78+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1000 / picoSeconds, metricType: dto.MetricType_SUMMARY},
79+
}
80+
81+
convey.Convey("Metrics comparison", t, func() {
82+
for _, expect := range expected {
83+
got := readMetric(<-ch)
84+
convey.So(expect, convey.ShouldResemble, got)
85+
}
86+
})
87+
88+
if err := mock.ExpectationsWereMet(); err != nil {
89+
t.Errorf("there were unfulfilled expectations: %s", err)
90+
}
91+
}
92+
93+
func TestScrapePerfEventsStatementsMySQL8028(t *testing.T) {
94+
db, mock, err := sqlmock.New()
95+
if err != nil {
96+
t.Fatalf("error opening a stub database connection: %s", err)
97+
}
98+
defer db.Close()
99+
100+
inst := &instance{
101+
db: db,
102+
flavor: FlavorMySQL,
103+
version: semver.MustParse("8.0.28"),
104+
}
105+
106+
columns := []string{
107+
"SCHEMA_NAME", "DIGEST", "DIGEST_TEXT",
108+
"COUNT_STAR", "SUM_TIMER_WAIT",
109+
"SUM_LOCK_TIME", "SUM_CPU_TIME",
110+
"SUM_ERRORS", "SUM_WARNINGS",
111+
"SUM_ROWS_AFFECTED", "SUM_ROWS_SENT", "SUM_ROWS_EXAMINED",
112+
"SUM_CREATED_TMP_DISK_TABLES", "SUM_CREATED_TMP_TABLES", "SUM_SORT_MERGE_PASSES",
113+
"SUM_SORT_ROWS", "SUM_NO_INDEX_USED",
114+
"QUANTILE_95", "QUANTILE_99", "QUANTILE_999",
115+
}
116+
117+
rows := sqlmock.NewRows(columns).
118+
AddRow(
119+
"db1", "digest1", "SELECT * FROM test",
120+
100, 1000,
121+
30, 50,
122+
1, 2,
123+
50, 100, 150,
124+
1, 2, 3,
125+
100, 1,
126+
100, 150, 200)
127+
128+
mock.ExpectQuery(sanitizeQuery(perfEventsStatementsQueryMySQL)).WillReturnRows(rows)
129+
130+
ch := make(chan prometheus.Metric)
131+
go func() {
132+
if err = (ScrapePerfEventsStatements{}).Scrape(context.Background(), inst, ch, promslog.NewNopLogger()); err != nil {
133+
t.Errorf("error calling function on test: %s", err)
134+
}
135+
close(ch)
136+
}()
137+
138+
expected := []MetricResult{
139+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
140+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1000 / picoSeconds, metricType: dto.MetricType_COUNTER},
141+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 30 / picoSeconds, metricType: dto.MetricType_COUNTER},
142+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 50 / picoSeconds, metricType: dto.MetricType_COUNTER},
143+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
144+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 2, metricType: dto.MetricType_COUNTER},
145+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 50, metricType: dto.MetricType_COUNTER},
146+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
147+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 150, metricType: dto.MetricType_COUNTER},
148+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 2, metricType: dto.MetricType_COUNTER},
149+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
150+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 3, metricType: dto.MetricType_COUNTER},
151+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 100, metricType: dto.MetricType_COUNTER},
152+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1, metricType: dto.MetricType_COUNTER},
153+
{labels: labelMap{"schema": "db1", "digest": "digest1", "digest_text": "SELECT * FROM test"}, value: 1000 / picoSeconds, metricType: dto.MetricType_SUMMARY},
154+
}
155+
156+
convey.Convey("MySQL 8.0.28+ metrics comparison", t, func() {
157+
for _, expect := range expected {
158+
got := readMetric(<-ch)
159+
convey.So(expect, convey.ShouldResemble, got)
160+
}
161+
})
162+
163+
if err := mock.ExpectationsWereMet(); err != nil {
164+
t.Errorf("there were unfulfilled expectations: %s", err)
165+
}
166+
}

0 commit comments

Comments
 (0)