master
go 578 lines 29.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package mysqlfunc
4
5 import (
6 "context"
7 "fmt"
8 "strings"
9 "sync"
10
11 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/sqlquery"
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/strmutil"
14 )
15
16 const (
17 topQueriesMethodID = "top-queries"
18 topQueriesMaxTextLength = 4096
19 topQueriesParamSort = "__sort"
20 )
21
22 func topQueriesMethodConfig() funcapi.MethodConfig {
23 return funcapi.MethodConfig{
24 ID: topQueriesMethodID,
25 Name: "Top Queries",
26 UpdateEvery: 10,
27 Help: "Top SQL queries from performance_schema",
28 RequireCloud: true,
29 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(topQueriesColumns)},
30 }
31 }
32
33 // topQueriesColumn defines a column for MySQL top-queries function.
34 // Embeds funcapi.ColumnMeta for UI display and adds collector-specific fields.
35 type topQueriesColumn struct {
36 funcapi.ColumnMeta
37
38 // Data access
39 DBColumn string // Column name in database (e.g., "SUM_TIMER_WAIT")
40 IsPicoseconds bool // Needs picoseconds to milliseconds conversion
41
42 // Sort parameter metadata
43 sortOpt bool // Show in sort dropdown
44 sortLbl string // Label for sort option
45 defaultSort bool // Is this the default sort option
46 }
47
48 // funcapi.SortableColumn interface implementation for topQueriesColumn.
49 func (c topQueriesColumn) IsSortOption() bool { return c.sortOpt }
50 func (c topQueriesColumn) SortLabel() string { return c.sortLbl }
51 func (c topQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
52 func (c topQueriesColumn) ColumnName() string { return c.Name }
53 func (c topQueriesColumn) SortColumn() string { return "" }
54
55 // topQueriesColumns defines ALL possible columns from events_statements_summary_by_digest
56 // Columns that don't exist in certain MySQL/MariaDB versions will be filtered at runtime
57 var topQueriesColumns = []topQueriesColumn{
58 // Identity columns - always available
59 {ColumnMeta: funcapi.ColumnMeta{Name: "digest", Tooltip: "Digest", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, UniqueKey: true, Sortable: true}, DBColumn: "DIGEST"},
60 {ColumnMeta: funcapi.ColumnMeta{Name: "query", Tooltip: "Query", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sticky: true, FullWidth: true, Sortable: true}, DBColumn: "DIGEST_TEXT"},
61 {ColumnMeta: funcapi.ColumnMeta{Name: "schema", Tooltip: "Schema", Type: funcapi.FieldTypeString, Visible: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, DBColumn: "SCHEMA_NAME"},
62
63 // Execution counts
64 {ColumnMeta: funcapi.ColumnMeta{Name: "calls", Tooltip: "Calls", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "COUNT_STAR", sortOpt: true, sortLbl: "Top queries by Number of Calls"},
65
66 // Timer metrics (picoseconds -> milliseconds)
67 {ColumnMeta: funcapi.ColumnMeta{Name: "totalTime", Tooltip: "Total Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_TIMER_WAIT", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by Total Execution Time", defaultSort: true},
68 {ColumnMeta: funcapi.ColumnMeta{Name: "minTime", Tooltip: "Min Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "MIN_TIMER_WAIT", IsPicoseconds: true},
69 {ColumnMeta: funcapi.ColumnMeta{Name: "avgTime", Tooltip: "Avg Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "AVG_TIMER_WAIT", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by Average Execution Time"},
70 {ColumnMeta: funcapi.ColumnMeta{Name: "maxTime", Tooltip: "Max Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "MAX_TIMER_WAIT", IsPicoseconds: true},
71
72 // Lock time (picoseconds -> milliseconds)
73 {ColumnMeta: funcapi.ColumnMeta{Name: "lockTime", Tooltip: "Lock Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_LOCK_TIME", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by Lock Time"},
74
75 // Error and warning counts
76 {ColumnMeta: funcapi.ColumnMeta{Name: "errors", Tooltip: "Errors", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_ERRORS", sortOpt: true, sortLbl: "Top queries by Errors"},
77 {ColumnMeta: funcapi.ColumnMeta{Name: "warnings", Tooltip: "Warnings", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_WARNINGS", sortOpt: true, sortLbl: "Top queries by Warnings"},
78
79 // Row operations
80 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsAffected", Tooltip: "Rows Affected", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_ROWS_AFFECTED", sortOpt: true, sortLbl: "Top queries by Rows Affected"},
81 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsSent", Tooltip: "Rows Sent", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_ROWS_SENT", sortOpt: true, sortLbl: "Top queries by Rows Sent"},
82 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsExamined", Tooltip: "Rows Examined", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_ROWS_EXAMINED", sortOpt: true, sortLbl: "Top queries by Rows Examined"},
83
84 // Temp table usage
85 {ColumnMeta: funcapi.ColumnMeta{Name: "tmpDiskTables", Tooltip: "Temp Disk Tables", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_CREATED_TMP_DISK_TABLES", sortOpt: true, sortLbl: "Top queries by Temp Disk Tables"},
86 {ColumnMeta: funcapi.ColumnMeta{Name: "tmpTables", Tooltip: "Temp Tables", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_CREATED_TMP_TABLES", sortOpt: true, sortLbl: "Top queries by Temp Tables"},
87
88 // Join operations
89 {ColumnMeta: funcapi.ColumnMeta{Name: "fullJoin", Tooltip: "Full Joins", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SELECT_FULL_JOIN", sortOpt: true, sortLbl: "Top queries by Full Joins"},
90 {ColumnMeta: funcapi.ColumnMeta{Name: "fullRangeJoin", Tooltip: "Full Range Joins", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SELECT_FULL_RANGE_JOIN"},
91 {ColumnMeta: funcapi.ColumnMeta{Name: "selectRange", Tooltip: "Select Range", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SELECT_RANGE"},
92 {ColumnMeta: funcapi.ColumnMeta{Name: "selectRangeCheck", Tooltip: "Select Range Check", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SELECT_RANGE_CHECK"},
93 {ColumnMeta: funcapi.ColumnMeta{Name: "selectScan", Tooltip: "Select Scan", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SELECT_SCAN", sortOpt: true, sortLbl: "Top queries by Table Scans"},
94
95 // Sort operations
96 {ColumnMeta: funcapi.ColumnMeta{Name: "sortMergePasses", Tooltip: "Sort Merge Passes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SORT_MERGE_PASSES"},
97 {ColumnMeta: funcapi.ColumnMeta{Name: "sortRange", Tooltip: "Sort Range", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SORT_RANGE"},
98 {ColumnMeta: funcapi.ColumnMeta{Name: "sortRows", Tooltip: "Sort Rows", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SORT_ROWS", sortOpt: true, sortLbl: "Top queries by Rows Sorted"},
99 {ColumnMeta: funcapi.ColumnMeta{Name: "sortScan", Tooltip: "Sort Scan", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_SORT_SCAN"},
100
101 // Index usage
102 {ColumnMeta: funcapi.ColumnMeta{Name: "noIndexUsed", Tooltip: "No Index Used", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_NO_INDEX_USED", sortOpt: true, sortLbl: "Top queries by No Index Used"},
103 {ColumnMeta: funcapi.ColumnMeta{Name: "noGoodIndexUsed", Tooltip: "No Good Index Used", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_NO_GOOD_INDEX_USED"},
104
105 // Timestamp columns
106 {ColumnMeta: funcapi.ColumnMeta{Name: "firstSeen", Tooltip: "First Seen", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, DBColumn: "FIRST_SEEN"},
107 {ColumnMeta: funcapi.ColumnMeta{Name: "lastSeen", Tooltip: "Last Seen", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, DBColumn: "LAST_SEEN"},
108
109 // MySQL 8.0+ quantile columns
110 {ColumnMeta: funcapi.ColumnMeta{Name: "p95Time", Tooltip: "P95 Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "QUANTILE_95", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by 95th Percentile Time"},
111 {ColumnMeta: funcapi.ColumnMeta{Name: "p99Time", Tooltip: "P99 Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "QUANTILE_99", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by 99th Percentile Time"},
112 {ColumnMeta: funcapi.ColumnMeta{Name: "p999Time", Tooltip: "P99.9 Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "QUANTILE_999", IsPicoseconds: true},
113
114 // MySQL 8.0+ sample query
115 {ColumnMeta: funcapi.ColumnMeta{Name: "sampleQuery", Tooltip: "Sample Query", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, FullWidth: true, Sortable: true}, DBColumn: "QUERY_SAMPLE_TEXT"},
116 {ColumnMeta: funcapi.ColumnMeta{Name: "sampleSeen", Tooltip: "Sample Seen", Type: funcapi.FieldTypeString, Visible: false, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sortable: true}, DBColumn: "QUERY_SAMPLE_SEEN"},
117 {ColumnMeta: funcapi.ColumnMeta{Name: "sampleTime", Tooltip: "Sample Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: false, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "QUERY_SAMPLE_TIMER_WAIT", IsPicoseconds: true},
118
119 // MySQL 8.0.28+ CPU time
120 {ColumnMeta: funcapi.ColumnMeta{Name: "cpuTime", Tooltip: "CPU Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "SUM_CPU_TIME", IsPicoseconds: true, sortOpt: true, sortLbl: "Top queries by CPU Time"},
121
122 // MySQL 8.0.31+ memory columns
123 {ColumnMeta: funcapi.ColumnMeta{Name: "maxControlledMemory", Tooltip: "Max Controlled Memory", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "MAX_CONTROLLED_MEMORY", sortOpt: true, sortLbl: "Top queries by Max Controlled Memory"},
124 {ColumnMeta: funcapi.ColumnMeta{Name: "maxTotalMemory", Tooltip: "Max Total Memory", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange, Sortable: true}, DBColumn: "MAX_TOTAL_MEMORY", sortOpt: true, sortLbl: "Top queries by Max Total Memory"},
125 }
126
127 type topQueriesChartGroup struct {
128 key string
129 title string
130 columns []string
131 defaultChart bool
132 }
133
134 var topQueriesChartGroups = []topQueriesChartGroup{
135 {key: "Calls", title: "Number of Calls", columns: []string{"calls"}, defaultChart: true},
136 {key: "Time", title: "Execution Time", columns: []string{"totalTime", "avgTime", "minTime", "maxTime"}, defaultChart: true},
137 {key: "Percentiles", title: "Execution Time Percentiles", columns: []string{"p95Time", "p99Time", "p999Time"}},
138 {key: "LockTime", title: "Lock Time", columns: []string{"lockTime"}},
139 {key: "Errors", title: "Errors & Warnings", columns: []string{"errors", "warnings"}},
140 {key: "Rows", title: "Rows", columns: []string{"rowsSent", "rowsExamined", "rowsAffected"}},
141 {key: "TempTables", title: "Temp Tables", columns: []string{"tmpDiskTables", "tmpTables"}},
142 {key: "Joins", title: "Join Operations", columns: []string{"fullJoin", "fullRangeJoin", "selectRange", "selectRangeCheck", "selectScan"}},
143 {key: "Sort", title: "Sort Operations", columns: []string{"sortMergePasses", "sortRange", "sortRows", "sortScan"}},
144 {key: "Index", title: "Index Usage", columns: []string{"noIndexUsed", "noGoodIndexUsed"}},
145 {key: "CPU", title: "CPU Time", columns: []string{"cpuTime"}},
146 {key: "Memory", title: "Memory", columns: []string{"maxControlledMemory", "maxTotalMemory"}},
147 {key: "Sample", title: "Sample Time", columns: []string{"sampleTime"}},
148 }
149
150 var topQueriesLabelColumns = map[string]bool{
151 "schema": true,
152 }
153
154 const topQueriesPrimaryLabel = "schema"
155
156 // topQueriesRowScanner interface for testing
157 type topQueriesRowScanner interface {
158 Next() bool
159 Scan(dest ...any) error
160 Err() error
161 }
162
163 // funcTopQueries implements funcapi.MethodHandler for MySQL top-queries.
164 // All function-related logic is encapsulated here, keeping Collector focused on metrics collection.
165 type funcTopQueries struct {
166 router *router
167
168 stmtSummaryCols map[string]bool
169 stmtSummaryColsMu sync.RWMutex
170 }
171
172 func newFuncTopQueries(r *router) *funcTopQueries {
173 return &funcTopQueries{router: r}
174 }
175
176 // Compile-time interface check.
177 var _ funcapi.MethodHandler = (*funcTopQueries)(nil)
178
179 // MethodParams implements funcapi.MethodHandler.
180 func (f *funcTopQueries) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
181 if f.router.cfg.topQueriesDisabled() {
182 return nil, fmt.Errorf("top-queries function disabled in configuration")
183 }
184 if _, err := f.router.deps.DB(); err != nil {
185 return nil, fmt.Errorf("collector is still initializing")
186 }
187 switch method {
188 case topQueriesMethodID:
189 return f.methodParams(ctx)
190 default:
191 return nil, fmt.Errorf("unknown method: %s", method)
192 }
193 }
194
195 // Handle implements funcapi.MethodHandler.
196 func (f *funcTopQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
197 if f.router.cfg.topQueriesDisabled() {
198 return funcapi.UnavailableResponse("top-queries function has been disabled in configuration")
199 }
200 if _, err := f.router.deps.DB(); err != nil {
201 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
202 }
203
204 switch method {
205 case topQueriesMethodID:
206 queryCtx, cancel := context.WithTimeout(ctx, f.router.cfg.topQueriesTimeout())
207 defer cancel()
208 return f.collectData(queryCtx, params.Column(topQueriesParamSort))
209 default:
210 return funcapi.NotFoundResponse(method)
211 }
212 }
213
214 // Cleanup implements funcapi.MethodHandler.
215 func (f *funcTopQueries) Cleanup(ctx context.Context) {}
216
217 func (f *funcTopQueries) methodParams(ctx context.Context) ([]funcapi.ParamConfig, error) {
218 available, err := isPerformanceSchemaEnabled(ctx, f.router.deps)
219 if err != nil {
220 return nil, err
221 }
222 if !available {
223 return nil, fmt.Errorf("performance_schema is not enabled")
224 }
225
226 availableCols, err := f.detectStatementsColumns(ctx)
227 if err != nil {
228 return nil, err
229 }
230 cols := f.buildAvailableColumns(availableCols)
231 if len(cols) == 0 {
232 return nil, fmt.Errorf("no columns available in events_statements_summary_by_digest")
233 }
234
235 sortParam := f.buildSortParam(cols)
236 return []funcapi.ParamConfig{sortParam}, nil
237 }
238
239 // collectData queries performance_schema for top queries using dynamic columns
240 func (f *funcTopQueries) collectData(ctx context.Context, sortColumn string) *funcapi.FunctionResponse {
241 if resp := f.checkPerformanceSchema(ctx); resp != nil {
242 return resp
243 }
244
245 cols, availableCols, resp := f.detectAndFilterColumns(ctx)
246 if resp != nil {
247 return resp
248 }
249
250 data, resp := f.executeTopQueriesSQL(ctx, cols, availableCols, sortColumn, f.router.cfg.topQueriesLimit())
251 if resp != nil {
252 return resp
253 }
254
255 data, cols = f.enrichWithErrorAttribution(ctx, data, cols)
256 return f.buildTopQueriesResponse(data, cols)
257 }
258
259 func (f *funcTopQueries) checkPerformanceSchema(ctx context.Context) *funcapi.FunctionResponse {
260 available, err := isPerformanceSchemaEnabled(ctx, f.router.deps)
261 if err != nil {
262 return &funcapi.FunctionResponse{
263 Status: 500,
264 Message: fmt.Sprintf("failed to check performance_schema availability: %v", err),
265 }
266 }
267 if !available {
268 return &funcapi.FunctionResponse{
269 Status: 503,
270 Message: "performance_schema is not enabled",
271 }
272 }
273 return nil
274 }
275
276 func (f *funcTopQueries) detectAndFilterColumns(ctx context.Context) ([]topQueriesColumn, map[string]bool, *funcapi.FunctionResponse) {
277 availableCols, err := f.detectStatementsColumns(ctx)
278 if err != nil {
279 return nil, nil, &funcapi.FunctionResponse{
280 Status: 500,
281 Message: fmt.Sprintf("failed to detect available columns: %v", err),
282 }
283 }
284
285 cols := f.buildAvailableColumns(availableCols)
286 if len(cols) == 0 {
287 return nil, nil, &funcapi.FunctionResponse{
288 Status: 500,
289 Message: "no columns available in events_statements_summary_by_digest",
290 }
291 }
292 return cols, availableCols, nil
293 }
294
295 func (f *funcTopQueries) executeTopQueriesSQL(ctx context.Context, cols []topQueriesColumn, availableCols map[string]bool, sortColumn string, limit int) ([][]any, *funcapi.FunctionResponse) {
296 dbSortColumn := f.mapAndValidateSortColumn(sortColumn, availableCols)
297 query := f.buildDynamicSQL(cols, dbSortColumn, limit)
298
299 db, err := f.router.deps.DB()
300 if err != nil {
301 return nil, &funcapi.FunctionResponse{Status: 503, Message: "collector database is unavailable"}
302 }
303 rows, err := db.QueryContext(ctx, query)
304 if err != nil {
305 if ctx.Err() == context.DeadlineExceeded {
306 return nil, &funcapi.FunctionResponse{Status: 504, Message: "query timed out"}
307 }
308 return nil, &funcapi.FunctionResponse{Status: 500, Message: fmt.Sprintf("query failed: %v", err)}
309 }
310 defer rows.Close()
311
312 data, err := f.scanDynamicRows(rows, cols)
313 if err != nil {
314 return nil, &funcapi.FunctionResponse{Status: 500, Message: err.Error()}
315 }
316 return data, nil
317 }
318
319 func (f *funcTopQueries) enrichWithErrorAttribution(ctx context.Context, data [][]any, cols []topQueriesColumn) ([][]any, []topQueriesColumn) {
320 errorCols := mysqlErrorAttributionColumns()
321 errorStatus := mysqlErrorAttrNotSupported
322 errorDetails := map[string]mysqlErrorRow{}
323
324 digestIdx := -1
325 for i, col := range cols {
326 if col.Name == "digest" {
327 digestIdx = i
328 break
329 }
330 }
331
332 if digestIdx >= 0 {
333 digests := make([]string, 0, len(data))
334 seen := make(map[string]bool)
335 for _, row := range data {
336 if digestIdx >= len(row) {
337 continue
338 }
339 digest, ok := row[digestIdx].(string)
340 if !ok || digest == "" || seen[digest] {
341 continue
342 }
343 seen[digest] = true
344 digests = append(digests, digest)
345 }
346 if len(digests) > 0 {
347 errorStatus, errorDetails = collectMySQLErrorDetailsForDigests(ctx, f.router.deps, f.router.cfg, f.router.log, digests)
348 } else {
349 errorStatus = mysqlErrorAttrNoData
350 }
351 }
352
353 if len(errorCols) == 0 {
354 return data, cols
355 }
356
357 for i := range data {
358 status := errorStatus
359 var errRow mysqlErrorRow
360 var errNo any
361 if digestIdx >= 0 && digestIdx < len(data[i]) {
362 if digest, ok := data[i][digestIdx].(string); ok && digest != "" {
363 if row, ok := errorDetails[digest]; ok {
364 status = mysqlErrorAttrEnabled
365 errRow = row
366 if errRow.ErrorNumber != nil {
367 errNo = *errRow.ErrorNumber
368 }
369 } else if status == mysqlErrorAttrEnabled {
370 status = mysqlErrorAttrNoData
371 }
372 }
373 }
374
375 data[i] = append(data[i],
376 status,
377 errNo,
378 nullableString(errRow.SQLState),
379 nullableString(errRow.Message),
380 )
381 }
382
383 return data, append(cols, errorCols...)
384 }
385
386 func (f *funcTopQueries) buildTopQueriesResponse(data [][]any, cols []topQueriesColumn) *funcapi.FunctionResponse {
387 sortParam := f.buildSortParam(cols)
388 sortOptions := sortParam.Options
389
390 defaultSort := ""
391 for _, col := range cols {
392 if col.IsDefaultSort() && col.IsSortOption() {
393 defaultSort = col.Name
394 break
395 }
396 }
397 if defaultSort == "" && len(sortOptions) > 0 {
398 defaultSort = sortOptions[0].ID
399 }
400
401 annotatedCols := f.decorateColumns(cols)
402 cs := f.columnSet(annotatedCols)
403
404 return &funcapi.FunctionResponse{
405 Status: 200,
406 Help: "Top SQL queries from performance_schema.events_statements_summary_by_digest",
407 Columns: cs.BuildColumns(),
408 Data: data,
409 DefaultSortColumn: defaultSort,
410 RequiredParams: []funcapi.ParamConfig{sortParam},
411 ChartingConfig: cs.BuildCharting(),
412 }
413 }
414
415 func (f *funcTopQueries) detectStatementsColumns(ctx context.Context) (map[string]bool, error) {
416 f.stmtSummaryColsMu.RLock()
417 if f.stmtSummaryCols != nil {
418 cols := f.stmtSummaryCols
419 f.stmtSummaryColsMu.RUnlock()
420 return cols, nil
421 }
422 f.stmtSummaryColsMu.RUnlock()
423
424 f.stmtSummaryColsMu.Lock()
425 defer f.stmtSummaryColsMu.Unlock()
426
427 if f.stmtSummaryCols != nil {
428 return f.stmtSummaryCols, nil
429 }
430
431 db, err := f.router.deps.DB()
432 if err != nil {
433 return nil, err
434 }
435 cols, err := sqlquery.FetchTableColumns(
436 ctx,
437 db,
438 "performance_schema",
439 "events_statements_summary_by_digest",
440 sqlquery.PlaceholderQuestion,
441 nil,
442 )
443 if err != nil {
444 return nil, fmt.Errorf("failed to query column information: %w", err)
445 }
446
447 f.stmtSummaryCols = cols
448 return cols, nil
449 }
450
451 func (f *funcTopQueries) columnSet(cols []topQueriesColumn) funcapi.ColumnSet[topQueriesColumn] {
452 return funcapi.Columns(cols, func(c topQueriesColumn) funcapi.ColumnMeta { return c.ColumnMeta })
453 }
454
455 // buildAvailableColumns filters columns based on what's available in the database
456 func (f *funcTopQueries) buildAvailableColumns(availableCols map[string]bool) []topQueriesColumn {
457 var cols []topQueriesColumn
458 for _, col := range topQueriesColumns {
459 if availableCols[col.DBColumn] {
460 cols = append(cols, col)
461 }
462 }
463 return cols
464 }
465
466 // mapAndValidateSortColumn validates sort key and returns the ID to use
467 func (f *funcTopQueries) mapAndValidateSortColumn(sortKey string, availableCols map[string]bool) string {
468 // Find the column by ID or DBColumn
469 for _, col := range topQueriesColumns {
470 if (col.Name == sortKey || col.DBColumn == sortKey) && availableCols[col.DBColumn] {
471 return col.Name
472 }
473 }
474 // Default to totalTime if available
475 if availableCols["SUM_TIMER_WAIT"] {
476 return "totalTime"
477 }
478 return "calls" // Ultimate fallback
479 }
480
481 // buildDynamicSQL builds the SQL query with only available columns
482 func (f *funcTopQueries) buildDynamicSQL(cols []topQueriesColumn, sortColumn string, limit int) string {
483 var selectParts []string
484 for _, col := range cols {
485 // Use backticks to handle reserved keywords
486 if col.IsPicoseconds {
487 // Convert picoseconds to milliseconds (divide by 10^9)
488 selectParts = append(selectParts, fmt.Sprintf("%s/1000000000 AS `%s`", col.DBColumn, col.Name))
489 } else if col.DBColumn == "SCHEMA_NAME" {
490 selectParts = append(selectParts, fmt.Sprintf("IFNULL(%s, '') AS `%s`", col.DBColumn, col.Name))
491 } else {
492 selectParts = append(selectParts, fmt.Sprintf("%s AS `%s`", col.DBColumn, col.Name))
493 }
494 }
495
496 return fmt.Sprintf(`
497 SELECT %s
498 FROM performance_schema.events_statements_summary_by_digest
499 WHERE DIGEST IS NOT NULL
500 ORDER BY `+"`%s`"+` DESC
501 LIMIT %d
502 `, strings.Join(selectParts, ", "), sortColumn, limit)
503 }
504
505 // scanDynamicRows scans rows dynamically based on column types
506 func (f *funcTopQueries) scanDynamicRows(rows topQueriesRowScanner, cols []topQueriesColumn) ([][]any, error) {
507 specs := make([]sqlquery.ScanColumnSpec, len(cols))
508 for i, col := range cols {
509 specs[i] = topQueriesScanSpec(col)
510 }
511
512 data, err := sqlquery.ScanTypedRows(rows, specs)
513 if err != nil {
514 return nil, fmt.Errorf("row scan failed: %w", err)
515 }
516 if err := rows.Err(); err != nil {
517 return nil, fmt.Errorf("rows iteration error: %w", err)
518 }
519 return data, nil
520 }
521
522 func topQueriesScanSpec(col topQueriesColumn) sqlquery.ScanColumnSpec {
523 spec := sqlquery.ScanColumnSpec{}
524 switch col.Type {
525 case funcapi.FieldTypeString:
526 spec.Type = sqlquery.ScanValueString
527 if col.Name == "query" || col.Name == "sampleQuery" {
528 spec.Transform = func(v any) any {
529 s, _ := v.(string)
530 return strmutil.TruncateText(s, topQueriesMaxTextLength)
531 }
532 }
533 case funcapi.FieldTypeInteger:
534 spec.Type = sqlquery.ScanValueInteger
535 case funcapi.FieldTypeDuration:
536 spec.Type = sqlquery.ScanValueFloat
537 default:
538 spec.Type = sqlquery.ScanValueDiscard
539 }
540 return spec
541 }
542
543 func (f *funcTopQueries) buildSortParam(cols []topQueriesColumn) funcapi.ParamConfig {
544 return funcapi.BuildSortParam(cols)
545 }
546
547 func (f *funcTopQueries) decorateColumns(cols []topQueriesColumn) []topQueriesColumn {
548 out := make([]topQueriesColumn, len(cols))
549 index := make(map[string]int, len(cols))
550 for i, col := range cols {
551 out[i] = col
552 index[col.Name] = i
553 }
554
555 for i := range out {
556 if topQueriesLabelColumns[out[i].Name] {
557 out[i].GroupBy = &funcapi.GroupByOptions{
558 IsDefault: out[i].Name == topQueriesPrimaryLabel,
559 }
560 }
561 }
562
563 for _, group := range topQueriesChartGroups {
564 for _, key := range group.columns {
565 idx, ok := index[key]
566 if !ok {
567 continue
568 }
569 out[idx].Chart = &funcapi.ChartOptions{
570 Group: group.key,
571 Title: group.title,
572 IsDefault: group.defaultChart,
573 }
574 }
575 }
576
577 return out
578 }