master
go 248 lines 15.1 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package cockroachdb
4
5 import (
6 "context"
7 "database/sql"
8 "errors"
9 "fmt"
10 "strings"
11
12 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/strmutil"
14 )
15
16 const (
17 topQueriesMethodID = "top-queries"
18 topQueriesMaxTextLength = 4096
19 )
20
21 func topQueriesMethodConfig() funcapi.MethodConfig {
22 return funcapi.MethodConfig{
23 ID: topQueriesMethodID,
24 Name: "Top Queries",
25 UpdateEvery: 10,
26 Help: "Top SQL statements from crdb_internal.cluster_statement_statistics. WARNING: Query text may contain unmasked literals (potential PII).",
27 RequireCloud: true,
28 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(topQueriesColumns)},
29 }
30 }
31
32 // topQueriesColumn embeds funcapi.ColumnMeta and adds CockroachDB-specific fields.
33 type topQueriesColumn struct {
34 funcapi.ColumnMeta
35 SelectExpr string // SQL expression for SELECT clause
36 sortOpt bool // whether this column appears as a sort option
37 sortLbl string // label for sort option dropdown
38 defaultSort bool // default sort column
39 }
40
41 // funcapi.SortableColumn interface implementation for topQueriesColumn.
42 func (c topQueriesColumn) IsSortOption() bool { return c.sortOpt }
43 func (c topQueriesColumn) SortLabel() string { return c.sortLbl }
44 func (c topQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
45 func (c topQueriesColumn) ColumnName() string { return c.Name }
46 func (c topQueriesColumn) SortColumn() string { return "" }
47
48 var topQueriesColumns = []topQueriesColumn{
49 {ColumnMeta: funcapi.ColumnMeta{Name: "fingerprintId", Tooltip: "Fingerprint ID", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, UniqueKey: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.fingerprint_id::STRING"},
50 {ColumnMeta: funcapi.ColumnMeta{Name: "query", Tooltip: "Query", Type: funcapi.FieldTypeString, Visible: true, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sticky: true, FullWidth: true, Wrap: true}, SelectExpr: "s.metadata->>'query'"},
51 {ColumnMeta: funcapi.ColumnMeta{Name: "database", Tooltip: "Database", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, GroupBy: &funcapi.GroupByOptions{IsDefault: true}}, SelectExpr: "s.metadata->>'db'"},
52 {ColumnMeta: funcapi.ColumnMeta{Name: "application", Tooltip: "Application", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, GroupBy: &funcapi.GroupByOptions{}}, SelectExpr: "s.app_name"},
53 {ColumnMeta: funcapi.ColumnMeta{Name: "statementType", Tooltip: "Statement Type", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, GroupBy: &funcapi.GroupByOptions{}}, SelectExpr: "s.metadata->>'stmtTyp'"},
54 {ColumnMeta: funcapi.ColumnMeta{Name: "distributed", Tooltip: "Distributed", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.metadata->>'distsql'"},
55 {ColumnMeta: funcapi.ColumnMeta{Name: "fullScan", Tooltip: "Full Scan", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.metadata->>'fullScan'"},
56 {ColumnMeta: funcapi.ColumnMeta{Name: "implicitTxn", Tooltip: "Implicit Txn", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.metadata->>'implicitTxn'"},
57 {ColumnMeta: funcapi.ColumnMeta{Name: "vectorized", Tooltip: "Vectorized", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.metadata->>'vec'"},
58
59 {ColumnMeta: funcapi.ColumnMeta{Name: "executions", Tooltip: "Executions", Type: funcapi.FieldTypeInteger, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Calls", Title: "Executions", IsDefault: true}}, SelectExpr: "COALESCE((s.statistics->'statistics'->>'cnt')::INT8, 0)", sortOpt: true, sortLbl: "Top queries by Executions"},
60 {ColumnMeta: funcapi.ColumnMeta{Name: "totalTime", Tooltip: "Total Time", Type: funcapi.FieldTypeDuration, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformDuration, Units: "milliseconds", DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Time", Title: "Execution Time", IsDefault: true}}, SelectExpr: "COALESCE((s.statistics->'statistics'->'svcLat'->>'mean')::FLOAT8, 0) * COALESCE((s.statistics->'statistics'->>'cnt')::FLOAT8, 0) * 1000", sortOpt: true, defaultSort: true, sortLbl: "Top queries by Total Time"},
61 {ColumnMeta: funcapi.ColumnMeta{Name: "meanTime", Tooltip: "Mean Time", Type: funcapi.FieldTypeDuration, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformDuration, Units: "milliseconds", DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Chart: &funcapi.ChartOptions{Group: "Time", Title: "Execution Time"}}, SelectExpr: "COALESCE((s.statistics->'statistics'->'svcLat'->>'mean')::FLOAT8, 0) * 1000", sortOpt: true, sortLbl: "Top queries by Mean Time"},
62 {ColumnMeta: funcapi.ColumnMeta{Name: "runTime", Tooltip: "Run Time", Type: funcapi.FieldTypeDuration, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformDuration, Units: "milliseconds", DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Chart: &funcapi.ChartOptions{Group: "Time", Title: "Execution Time"}}, SelectExpr: "COALESCE((s.statistics->'statistics'->'runLat'->>'mean')::FLOAT8, 0) * 1000"},
63 {ColumnMeta: funcapi.ColumnMeta{Name: "planTime", Tooltip: "Plan Time", Type: funcapi.FieldTypeDuration, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformDuration, Units: "milliseconds", DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Chart: &funcapi.ChartOptions{Group: "Time", Title: "Execution Time"}}, SelectExpr: "COALESCE((s.statistics->'statistics'->'planLat'->>'mean')::FLOAT8, 0) * 1000"},
64 {ColumnMeta: funcapi.ColumnMeta{Name: "parseTime", Tooltip: "Parse Time", Type: funcapi.FieldTypeDuration, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualBar, Transform: funcapi.FieldTransformDuration, Units: "milliseconds", DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Chart: &funcapi.ChartOptions{Group: "Time", Title: "Execution Time"}}, SelectExpr: "COALESCE((s.statistics->'statistics'->'parseLat'->>'mean')::FLOAT8, 0) * 1000"},
65
66 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsRead", Tooltip: "Rows Read", Type: funcapi.FieldTypeInteger, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Rows", Title: "Rows"}}, SelectExpr: "CAST(ROUND(COALESCE((s.statistics->'statistics'->'rowsRead'->>'mean')::FLOAT8, 0) * COALESCE((s.statistics->'statistics'->>'cnt')::FLOAT8, 0)) AS INT8)", sortOpt: true, sortLbl: "Top queries by Rows Read"},
67 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsWritten", Tooltip: "Rows Written", Type: funcapi.FieldTypeInteger, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Rows", Title: "Rows"}}, SelectExpr: "CAST(ROUND(COALESCE((s.statistics->'statistics'->'rowsWritten'->>'mean')::FLOAT8, 0) * COALESCE((s.statistics->'statistics'->>'cnt')::FLOAT8, 0)) AS INT8)", sortOpt: true, sortLbl: "Top queries by Rows Written"},
68 {ColumnMeta: funcapi.ColumnMeta{Name: "rowsReturned", Tooltip: "Rows Returned", Type: funcapi.FieldTypeInteger, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Rows", Title: "Rows"}}, SelectExpr: "CAST(ROUND(COALESCE((s.statistics->'statistics'->'numRows'->>'mean')::FLOAT8, 0) * COALESCE((s.statistics->'statistics'->>'cnt')::FLOAT8, 0)) AS INT8)", sortOpt: true, sortLbl: "Top queries by Rows Returned"},
69 {ColumnMeta: funcapi.ColumnMeta{Name: "bytesRead", Tooltip: "Bytes Read", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Chart: &funcapi.ChartOptions{Group: "Bytes", Title: "Bytes"}}, SelectExpr: "CAST(ROUND(COALESCE((s.statistics->'statistics'->'bytesRead'->>'mean')::FLOAT8, 0) * COALESCE((s.statistics->'statistics'->>'cnt')::FLOAT8, 0)) AS INT8)", sortOpt: true, sortLbl: "Top queries by Bytes Read"},
70 {ColumnMeta: funcapi.ColumnMeta{Name: "maxRetries", Tooltip: "Max Retries", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Chart: &funcapi.ChartOptions{Group: "Retries", Title: "Retries"}}, SelectExpr: "COALESCE((s.statistics->'statistics'->>'maxRetries')::INT8, 0)"},
71 }
72
73 // funcTopQueries handles the top-queries function.
74 type funcTopQueries struct {
75 router *funcRouter
76 }
77
78 func newFuncTopQueries(r *funcRouter) *funcTopQueries {
79 return &funcTopQueries{router: r}
80 }
81
82 // Compile-time interface check.
83 var _ funcapi.MethodHandler = (*funcTopQueries)(nil)
84
85 func (f *funcTopQueries) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
86 if f.router.collector.Functions.TopQueries.Disabled {
87 return nil, fmt.Errorf("top-queries function disabled in configuration")
88 }
89 return []funcapi.ParamConfig{funcapi.BuildSortParam(topQueriesColumns)}, nil
90 }
91
92 func (f *funcTopQueries) Cleanup(ctx context.Context) {}
93
94 func (f *funcTopQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
95 if f.router.collector.Functions.TopQueries.Disabled {
96 return funcapi.UnavailableResponse("top-queries function has been disabled in configuration")
97 }
98 if err := f.router.ensureDB(ctx); err != nil {
99 status := 503
100 if errors.Is(err, errSQLDSNNotSet) {
101 status = 400
102 }
103 return funcapi.ErrorResponse(status, "%s", err)
104 }
105
106 sortColumn := f.resolveSortColumn(params.Column("__sort"))
107 limit := f.router.topQueriesLimit()
108
109 query := f.buildSQL(sortColumn)
110 queryCtx, cancel := context.WithTimeout(ctx, f.router.collector.topQueriesTimeout())
111 defer cancel()
112
113 rows, err := f.router.db.QueryContext(queryCtx, query, limit)
114 if err != nil {
115 if errors.Is(queryCtx.Err(), context.DeadlineExceeded) {
116 return funcapi.ErrorResponse(504, "query timed out")
117 }
118 return funcapi.InternalErrorResponse("query failed: %v", err)
119 }
120 defer rows.Close()
121
122 data, err := f.scanRows(rows)
123 if err != nil {
124 return funcapi.InternalErrorResponse("%s", err)
125 }
126
127 cs := f.columnSet()
128 return &funcapi.FunctionResponse{
129 Status: 200,
130 Help: "Top SQL statements from crdb_internal.cluster_statement_statistics. WARNING: Query text may contain unmasked literals (potential PII).",
131 Columns: cs.BuildColumns(),
132 Data: data,
133 DefaultSortColumn: sortColumn,
134 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(topQueriesColumns)},
135 ChartingConfig: cs.BuildCharting(),
136 }
137 }
138
139 func (f *funcTopQueries) columnSet() funcapi.ColumnSet[topQueriesColumn] {
140 return funcapi.Columns(topQueriesColumns, func(c topQueriesColumn) funcapi.ColumnMeta { return c.ColumnMeta })
141 }
142
143 func (f *funcTopQueries) resolveSortColumn(requested string) string {
144 if requested != "" {
145 for _, col := range topQueriesColumns {
146 if col.Name == requested && col.IsSortOption() {
147 return col.Name
148 }
149 }
150 }
151 for _, col := range topQueriesColumns {
152 if col.IsDefaultSort() && col.IsSortOption() {
153 return col.Name
154 }
155 }
156 for _, col := range topQueriesColumns {
157 if col.IsSortOption() {
158 return col.Name
159 }
160 }
161 if len(topQueriesColumns) > 0 {
162 return topQueriesColumns[0].Name
163 }
164 return ""
165 }
166
167 func (f *funcTopQueries) buildSQL(sortColumn string) string {
168 selectCols := make([]string, 0, len(topQueriesColumns))
169 for _, col := range topQueriesColumns {
170 selectCols = append(selectCols, fmt.Sprintf("%s AS %s", col.SelectExpr, col.Name))
171 }
172 return fmt.Sprintf(`
173 SELECT %s
174 FROM crdb_internal.cluster_statement_statistics AS s
175 WHERE s.metadata->>'query' IS NOT NULL
176 ORDER BY %s DESC NULLS LAST
177 LIMIT $1`, strings.Join(selectCols, ", "), sortColumn)
178 }
179
180 func (f *funcTopQueries) scanRows(rows *sql.Rows) ([][]any, error) {
181 cols := topQueriesColumns
182 data := make([][]any, 0, 500)
183
184 for rows.Next() {
185 values := make([]any, len(cols))
186 valuePtrs := make([]any, len(cols))
187
188 for i, col := range cols {
189 switch col.Type {
190 case funcapi.FieldTypeString:
191 var v sql.NullString
192 values[i] = &v
193 case funcapi.FieldTypeInteger:
194 var v sql.NullInt64
195 values[i] = &v
196 case funcapi.FieldTypeFloat, funcapi.FieldTypeDuration:
197 var v sql.NullFloat64
198 values[i] = &v
199 default:
200 var v any
201 values[i] = &v
202 }
203 valuePtrs[i] = values[i]
204 }
205
206 if err := rows.Scan(valuePtrs...); err != nil {
207 return nil, fmt.Errorf("row scan failed: %w", err)
208 }
209
210 row := make([]any, len(cols))
211 for i, col := range cols {
212 switch v := values[i].(type) {
213 case *sql.NullString:
214 if v.Valid {
215 s := v.String
216 if col.Name == "query" {
217 s = strmutil.TruncateText(s, topQueriesMaxTextLength)
218 }
219 row[i] = s
220 } else {
221 row[i] = ""
222 }
223 case *sql.NullInt64:
224 if v.Valid {
225 row[i] = v.Int64
226 } else {
227 row[i] = int64(0)
228 }
229 case *sql.NullFloat64:
230 if v.Valid {
231 row[i] = v.Float64
232 } else {
233 row[i] = float64(0)
234 }
235 default:
236 row[i] = nil
237 }
238 }
239
240 data = append(data, row)
241 }
242
243 if err := rows.Err(); err != nil {
244 return nil, fmt.Errorf("rows iteration error: %w", err)
245 }
246
247 return data, nil
248 }