master
go 235 lines 9.98 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 runningQueriesMethodID = "running-queries"
18 runningQueriesMaxTextLength = 4096
19 )
20
21 func runningQueriesMethodConfig() funcapi.MethodConfig {
22 return funcapi.MethodConfig{
23 ID: runningQueriesMethodID,
24 Name: "Running Queries",
25 UpdateEvery: 10,
26 Help: "Currently running SQL statements from SHOW CLUSTER STATEMENTS. WARNING: Query text may contain unmasked literals (potential PII).",
27 RequireCloud: true,
28 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(runningQueriesColumns)},
29 }
30 }
31
32 // runningQueriesColumn embeds funcapi.ColumnMeta and adds CockroachDB-specific fields.
33 type runningQueriesColumn 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 runningQueriesColumn.
42 func (c runningQueriesColumn) IsSortOption() bool { return c.sortOpt }
43 func (c runningQueriesColumn) SortLabel() string { return c.sortLbl }
44 func (c runningQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
45 func (c runningQueriesColumn) ColumnName() string { return c.Name }
46 func (c runningQueriesColumn) SortColumn() string { return "" }
47
48 var runningQueriesColumns = []runningQueriesColumn{
49 {ColumnMeta: funcapi.ColumnMeta{Name: "queryId", Tooltip: "Query ID", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, UniqueKey: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.query_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.query"},
51 {ColumnMeta: funcapi.ColumnMeta{Name: "user", Tooltip: "User", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.user_name"},
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}, SelectExpr: "s.application_name"},
53 {ColumnMeta: funcapi.ColumnMeta{Name: "clientAddress", Tooltip: "Client Address", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.client_address"},
54 {ColumnMeta: funcapi.ColumnMeta{Name: "nodeId", Tooltip: "Node ID", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.node_id::STRING"},
55 {ColumnMeta: funcapi.ColumnMeta{Name: "sessionId", Tooltip: "Session ID", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.session_id::STRING"},
56 {ColumnMeta: funcapi.ColumnMeta{Name: "phase", Tooltip: "Phase", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.phase"},
57 {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.distributed::STRING"},
58 {ColumnMeta: funcapi.ColumnMeta{Name: "startTime", Tooltip: "Start Time", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, SelectExpr: "TO_CHAR(s.start, 'YYYY-MM-DD\"T\"HH24:MI:SS.FF3')"},
59 {ColumnMeta: funcapi.ColumnMeta{Name: "elapsedMs", Tooltip: "Elapsed", 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.FieldSummaryMax}, SelectExpr: "EXTRACT(EPOCH FROM (clock_timestamp() - s.start)) * 1000", sortOpt: true, defaultSort: true, sortLbl: "Running queries by Elapsed Time"},
60 }
61
62 // funcRunningQueries handles the running-queries function.
63 type funcRunningQueries struct {
64 router *funcRouter
65 }
66
67 func newFuncRunningQueries(r *funcRouter) *funcRunningQueries {
68 return &funcRunningQueries{router: r}
69 }
70
71 // Compile-time interface check.
72 var _ funcapi.MethodHandler = (*funcRunningQueries)(nil)
73
74 func (f *funcRunningQueries) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
75 if f.router.collector.Functions.RunningQueries.Disabled {
76 return nil, fmt.Errorf("running-queries function disabled in configuration")
77 }
78 return []funcapi.ParamConfig{funcapi.BuildSortParam(runningQueriesColumns)}, nil
79 }
80
81 func (f *funcRunningQueries) Cleanup(ctx context.Context) {}
82
83 func (f *funcRunningQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
84 if f.router.collector.Functions.RunningQueries.Disabled {
85 return funcapi.UnavailableResponse("running-queries function has been disabled in configuration")
86 }
87 if err := f.router.ensureDB(ctx); err != nil {
88 status := 503
89 if errors.Is(err, errSQLDSNNotSet) {
90 status = 400
91 }
92 return funcapi.ErrorResponse(status, "%s", err)
93 }
94
95 sortColumn := f.resolveSortColumn(params.Column("__sort"))
96 limit := f.router.collector.runningQueriesLimit()
97
98 query := f.buildSQL(sortColumn)
99 queryCtx, cancel := context.WithTimeout(ctx, f.router.collector.runningQueriesTimeout())
100 defer cancel()
101
102 rows, err := f.router.db.QueryContext(queryCtx, query, limit)
103 if err != nil {
104 if errors.Is(queryCtx.Err(), context.DeadlineExceeded) {
105 return funcapi.ErrorResponse(504, "query timed out")
106 }
107 return funcapi.InternalErrorResponse("query failed: %v", err)
108 }
109 defer rows.Close()
110
111 data, err := f.scanRows(rows)
112 if err != nil {
113 return funcapi.InternalErrorResponse("%s", err)
114 }
115
116 cs := f.columnSet()
117 return &funcapi.FunctionResponse{
118 Status: 200,
119 Help: "Currently running SQL statements from SHOW CLUSTER STATEMENTS. WARNING: Query text may contain unmasked literals (potential PII).",
120 Columns: cs.BuildColumns(),
121 Data: data,
122 DefaultSortColumn: sortColumn,
123 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(runningQueriesColumns)},
124 }
125 }
126
127 func (f *funcRunningQueries) columnSet() funcapi.ColumnSet[runningQueriesColumn] {
128 return funcapi.Columns(runningQueriesColumns, func(c runningQueriesColumn) funcapi.ColumnMeta { return c.ColumnMeta })
129 }
130
131 func (f *funcRunningQueries) resolveSortColumn(requested string) string {
132 if requested != "" {
133 for _, col := range runningQueriesColumns {
134 if col.Name == requested && col.IsSortOption() {
135 return col.Name
136 }
137 }
138 }
139 for _, col := range runningQueriesColumns {
140 if col.IsDefaultSort() && col.IsSortOption() {
141 return col.Name
142 }
143 }
144 for _, col := range runningQueriesColumns {
145 if col.IsSortOption() {
146 return col.Name
147 }
148 }
149 if len(runningQueriesColumns) > 0 {
150 return runningQueriesColumns[0].Name
151 }
152 return ""
153 }
154
155 func (f *funcRunningQueries) buildSQL(sortColumn string) string {
156 selectCols := make([]string, 0, len(runningQueriesColumns))
157 for _, col := range runningQueriesColumns {
158 selectCols = append(selectCols, fmt.Sprintf("%s AS %s", col.SelectExpr, col.Name))
159 }
160 return fmt.Sprintf(`
161 SELECT %s
162 FROM [SHOW CLUSTER STATEMENTS] AS s
163 ORDER BY %s DESC NULLS LAST
164 LIMIT $1`, strings.Join(selectCols, ", "), sortColumn)
165 }
166
167 func (f *funcRunningQueries) scanRows(rows *sql.Rows) ([][]any, error) {
168 cols := runningQueriesColumns
169 data := make([][]any, 0, 500)
170
171 for rows.Next() {
172 values := make([]any, len(cols))
173 valuePtrs := make([]any, len(cols))
174
175 for i, col := range cols {
176 switch col.Type {
177 case funcapi.FieldTypeString:
178 var v sql.NullString
179 values[i] = &v
180 case funcapi.FieldTypeInteger:
181 var v sql.NullInt64
182 values[i] = &v
183 case funcapi.FieldTypeFloat, funcapi.FieldTypeDuration:
184 var v sql.NullFloat64
185 values[i] = &v
186 default:
187 var v any
188 values[i] = &v
189 }
190 valuePtrs[i] = values[i]
191 }
192
193 if err := rows.Scan(valuePtrs...); err != nil {
194 return nil, fmt.Errorf("row scan failed: %w", err)
195 }
196
197 row := make([]any, len(cols))
198 for i, col := range cols {
199 switch v := values[i].(type) {
200 case *sql.NullString:
201 if v.Valid {
202 s := v.String
203 if col.Name == "query" {
204 s = strmutil.TruncateText(s, runningQueriesMaxTextLength)
205 }
206 row[i] = s
207 } else {
208 row[i] = ""
209 }
210 case *sql.NullInt64:
211 if v.Valid {
212 row[i] = v.Int64
213 } else {
214 row[i] = int64(0)
215 }
216 case *sql.NullFloat64:
217 if v.Valid {
218 row[i] = v.Float64
219 } else {
220 row[i] = float64(0)
221 }
222 default:
223 row[i] = nil
224 }
225 }
226
227 data = append(data, row)
228 }
229
230 if err := rows.Err(); err != nil {
231 return nil, fmt.Errorf("rows iteration error: %w", err)
232 }
233
234 return data, nil
235 }