master
go 236 lines 9.99 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package yugabytedb
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 pg_stat_activity. 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 YugabyteDB-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 var runningQueriesColumns = []runningQueriesColumn{
42 {ColumnMeta: funcapi.ColumnMeta{Name: "pid", Tooltip: "PID", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, UniqueKey: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.pid::text"},
43 {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"},
44 {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}, SelectExpr: "s.datname"},
45 {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.usename"},
46 {ColumnMeta: funcapi.ColumnMeta{Name: "state", Tooltip: "State", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.state"},
47 {ColumnMeta: funcapi.ColumnMeta{Name: "waitEventType", Tooltip: "Wait Event Type", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.wait_event_type"},
48 {ColumnMeta: funcapi.ColumnMeta{Name: "waitEvent", Tooltip: "Wait Event", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.wait_event"},
49 {ColumnMeta: funcapi.ColumnMeta{Name: "application", Tooltip: "Application", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, SelectExpr: "s.application_name"},
50 {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_addr::text"},
51 {ColumnMeta: funcapi.ColumnMeta{Name: "queryStart", Tooltip: "Query Start", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, SelectExpr: "TO_CHAR(s.query_start, 'YYYY-MM-DD\"T\"HH24:MI:SS.FF3')"},
52 {ColumnMeta: funcapi.ColumnMeta{Name: "elapsedMs", Tooltip: "Elapsed", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, SelectExpr: "CASE WHEN s.query_start IS NULL THEN 0 ELSE EXTRACT(EPOCH FROM (clock_timestamp() - s.query_start)) * 1000 END", sortOpt: true, defaultSort: true, sortLbl: "Running queries by Elapsed Time"},
53 }
54
55 // funcapi.SortableColumn interface implementation for runningQueriesColumn.
56 func (c runningQueriesColumn) IsSortOption() bool { return c.sortOpt }
57 func (c runningQueriesColumn) SortLabel() string { return c.sortLbl }
58 func (c runningQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
59 func (c runningQueriesColumn) ColumnName() string { return c.Name }
60 func (c runningQueriesColumn) SortColumn() string { return "" }
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 pg_stat_activity. 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 pg_stat_activity s
163 WHERE s.state IS DISTINCT FROM 'idle'
164 ORDER BY %s DESC NULLS LAST
165 LIMIT $1`, strings.Join(selectCols, ", "), sortColumn)
166 }
167
168 func (f *funcRunningQueries) scanRows(rows *sql.Rows) ([][]any, error) {
169 cols := runningQueriesColumns
170 data := make([][]any, 0, 500)
171
172 for rows.Next() {
173 values := make([]any, len(cols))
174 valuePtrs := make([]any, len(cols))
175
176 for i, col := range cols {
177 switch col.Type {
178 case funcapi.FieldTypeString:
179 var v sql.NullString
180 values[i] = &v
181 case funcapi.FieldTypeInteger:
182 var v sql.NullInt64
183 values[i] = &v
184 case funcapi.FieldTypeFloat, funcapi.FieldTypeDuration:
185 var v sql.NullFloat64
186 values[i] = &v
187 default:
188 var v any
189 values[i] = &v
190 }
191 valuePtrs[i] = values[i]
192 }
193
194 if err := rows.Scan(valuePtrs...); err != nil {
195 return nil, fmt.Errorf("row scan failed: %w", err)
196 }
197
198 row := make([]any, len(cols))
199 for i, col := range cols {
200 switch v := values[i].(type) {
201 case *sql.NullString:
202 if v.Valid {
203 s := v.String
204 if col.Name == "query" {
205 s = strmutil.TruncateText(s, runningQueriesMaxTextLength)
206 }
207 row[i] = s
208 } else {
209 row[i] = ""
210 }
211 case *sql.NullInt64:
212 if v.Valid {
213 row[i] = v.Int64
214 } else {
215 row[i] = int64(0)
216 }
217 case *sql.NullFloat64:
218 if v.Valid {
219 row[i] = v.Float64
220 } else {
221 row[i] = float64(0)
222 }
223 default:
224 row[i] = nil
225 }
226 }
227
228 data = append(data, row)
229 }
230
231 if err := rows.Err(); err != nil {
232 return nil, fmt.Errorf("rows iteration error: %w", err)
233 }
234
235 return data, nil
236 }