master
go 310 lines 15.9 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package postgres
4
5 import (
6 "context"
7 "database/sql"
8 "fmt"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/strmutil"
13 )
14
15 const (
16 runningQueriesMethodID = "running-queries"
17 runningQueriesMaxTextLength = 4096
18 )
19
20 func runningQueriesMethodConfig() funcapi.MethodConfig {
21 return funcapi.MethodConfig{
22 ID: runningQueriesMethodID,
23 Name: "Running Queries",
24 UpdateEvery: 10,
25 Help: "Currently executing queries from pg_stat_activity. WARNING: Query text may contain unmasked literals (potential PII).",
26 RequireCloud: true,
27 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(runningQueriesColumns)},
28 }
29 }
30
31 // runningQueriesColumn defines metadata for a pg_stat_activity column.
32 type runningQueriesColumn struct {
33 funcapi.ColumnMeta
34
35 // DBColumn is the database column expression
36 DBColumn string
37 // sortOpt indicates whether this column appears in the sort dropdown
38 sortOpt bool
39 // sortLbl is the label shown in the sort dropdown
40 sortLbl string
41 // defaultSort indicates whether this is the default sort column
42 defaultSort bool
43 // minVersion is the minimum PostgreSQL version (0 means all versions)
44 minVersion int
45 }
46
47 // funcapi.SortableColumn interface implementation.
48 func (c runningQueriesColumn) IsSortOption() bool { return c.sortOpt }
49 func (c runningQueriesColumn) SortLabel() string { return c.sortLbl }
50 func (c runningQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
51 func (c runningQueriesColumn) ColumnName() string { return c.Name }
52 func (c runningQueriesColumn) SortColumn() string { return "" }
53
54 // runningQueriesColumns defines ALL columns from pg_stat_activity.
55 // Order matters - this determines column display order. Visible columns first, then hidden.
56 var runningQueriesColumns = []runningQueriesColumn{
57 // === VISIBLE COLUMNS (most important first) ===
58
59 // Key metrics
60 {ColumnMeta: funcapi.ColumnMeta{Name: "durationMs", Tooltip: "Query duration in milliseconds (since query_start)", Type: funcapi.FieldTypeDuration, Units: "milliseconds", Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDuration, DecimalPoints: 2, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, DBColumn: "EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - query_start)) * 1000", sortOpt: true, sortLbl: "Query Duration", defaultSort: true},
61 {ColumnMeta: funcapi.ColumnMeta{Name: "query", Tooltip: "Query text (may be truncated at track_activity_query_size)", Type: funcapi.FieldTypeString, Visible: true, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sticky: true, FullWidth: true, Wrap: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "query"},
62
63 // Context: who/where
64 {ColumnMeta: funcapi.ColumnMeta{Name: "datname", Tooltip: "Name of the database", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "datname"},
65 {ColumnMeta: funcapi.ColumnMeta{Name: "usename", Tooltip: "Name of the user logged into this backend", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "usename"},
66 {ColumnMeta: funcapi.ColumnMeta{Name: "applicationName", Tooltip: "Name of the application connected to this backend", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "application_name"},
67 {ColumnMeta: funcapi.ColumnMeta{Name: "clientAddr", Tooltip: "IP address of the client (NULL for Unix socket or internal process)", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "client_addr::text"},
68
69 // Status
70 {ColumnMeta: funcapi.ColumnMeta{Name: "waitEvent", Tooltip: "Specific wait event name if backend is currently waiting", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "wait_event"},
71
72 // Process ID (useful for pg_terminate_backend)
73 {ColumnMeta: funcapi.ColumnMeta{Name: "pid", Tooltip: "Process ID of this backend", Type: funcapi.FieldTypeInteger, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, UniqueKey: true, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "pid"},
74
75 // === HIDDEN COLUMNS ===
76
77 // Additional status info
78 {ColumnMeta: funcapi.ColumnMeta{Name: "waitEventType", Tooltip: "Type of event the backend is waiting for (Activity, BufferPin, Client, Extension, IO, IPC, Lock, LWLock, Timeout)", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "wait_event_type"},
79 {ColumnMeta: funcapi.ColumnMeta{Name: "state", Tooltip: "Current state: active, idle, idle in transaction, idle in transaction (aborted), fastpath function call, disabled", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Visualization: funcapi.FieldVisualPill}, DBColumn: "state"},
80 {ColumnMeta: funcapi.ColumnMeta{Name: "backendType", Tooltip: "Type of backend: client backend, autovacuum worker, parallel worker, walsender, walreceiver, etc.", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "backend_type", minVersion: pgVersion10},
81
82 // Timestamps
83 {ColumnMeta: funcapi.ColumnMeta{Name: "queryStart", Tooltip: "Time when current/last query started", Type: funcapi.FieldTypeTimestamp, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDatetime, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, DBColumn: "query_start", sortOpt: true, sortLbl: "Query Start Time"},
84 {ColumnMeta: funcapi.ColumnMeta{Name: "xactStart", Tooltip: "Time when current transaction started (NULL if no transaction)", Type: funcapi.FieldTypeTimestamp, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDatetime, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, DBColumn: "xact_start"},
85 {ColumnMeta: funcapi.ColumnMeta{Name: "backendStart", Tooltip: "Time when this process/connection started", Type: funcapi.FieldTypeTimestamp, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDatetime, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, DBColumn: "backend_start"},
86 {ColumnMeta: funcapi.ColumnMeta{Name: "stateChange", Tooltip: "Time when state was last changed", Type: funcapi.FieldTypeTimestamp, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformDatetime, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, DBColumn: "state_change"},
87
88 // Query identification
89 {ColumnMeta: funcapi.ColumnMeta{Name: "queryId", Tooltip: "Query identifier (requires compute_query_id or extension)", Type: funcapi.FieldTypeString, Visible: false, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "query_id::text", minVersion: pgVersion14},
90
91 // Session IDs
92 {ColumnMeta: funcapi.ColumnMeta{Name: "leaderPid", Tooltip: "Process ID of parallel group leader (NULL if this is leader or not parallel)", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "leader_pid", minVersion: pgVersion13},
93 {ColumnMeta: funcapi.ColumnMeta{Name: "datid", Tooltip: "OID of the database", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: false, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "datid"},
94 {ColumnMeta: funcapi.ColumnMeta{Name: "usesysid", Tooltip: "OID of the user", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: false, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "usesysid"},
95
96 // Client details
97 {ColumnMeta: funcapi.ColumnMeta{Name: "clientHostname", Tooltip: "Hostname of the client via reverse DNS (only if log_hostname enabled)", Type: funcapi.FieldTypeString, Visible: false, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "client_hostname"},
98 {ColumnMeta: funcapi.ColumnMeta{Name: "clientPort", Tooltip: "TCP port of client (-1 for Unix socket, NULL for internal process)", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "client_port"},
99
100 // Transaction IDs
101 {ColumnMeta: funcapi.ColumnMeta{Name: "backendXid", Tooltip: "Top-level transaction identifier of this backend", Type: funcapi.FieldTypeString, Visible: false, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "backend_xid::text"},
102 {ColumnMeta: funcapi.ColumnMeta{Name: "backendXmin", Tooltip: "Backend's xmin horizon", Type: funcapi.FieldTypeString, Visible: false, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount}, DBColumn: "backend_xmin::text"},
103 }
104
105 // funcRunningQueries handles the running-queries function.
106 type funcRunningQueries struct {
107 router *funcRouter
108 }
109
110 func newFuncRunningQueries(r *funcRouter) *funcRunningQueries {
111 return &funcRunningQueries{router: r}
112 }
113
114 // Compile-time interface check.
115 var _ funcapi.MethodHandler = (*funcRunningQueries)(nil)
116
117 func (f *funcRunningQueries) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
118 return []funcapi.ParamConfig{funcapi.BuildSortParam(f.getColumnsForVersion())}, nil
119 }
120
121 func (f *funcRunningQueries) Cleanup(ctx context.Context) {}
122
123 func (f *funcRunningQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
124 if f.router.collector.db == nil {
125 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
126 }
127
128 cols := f.getColumnsForVersion()
129 sortColumn := f.resolveSortColumn(params.Column("__sort"), cols)
130 if sortColumn == "" {
131 return funcapi.InternalErrorResponse("no sortable columns available")
132 }
133
134 // Build the query
135 query := f.buildQuery(cols, sortColumn)
136
137 rows, err := f.router.collector.db.QueryContext(ctx, query)
138 if err != nil {
139 if ctx.Err() == context.DeadlineExceeded {
140 return funcapi.ErrorResponse(504, "query timed out")
141 }
142 return funcapi.InternalErrorResponse("running queries query failed: %v", err)
143 }
144 defer rows.Close()
145
146 data, err := f.scanRows(rows, cols)
147 if err != nil {
148 return funcapi.InternalErrorResponse("%s", err)
149 }
150
151 cs := f.columnSet(cols)
152 if len(data) == 0 {
153 return &funcapi.FunctionResponse{
154 Status: 200,
155 Message: "No active queries found.",
156 Help: "Currently executing queries from pg_stat_activity",
157 Columns: cs.BuildColumns(),
158 Data: [][]any{},
159 DefaultSortColumn: sortColumn,
160 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(cols)},
161 }
162 }
163
164 return &funcapi.FunctionResponse{
165 Status: 200,
166 Help: "Currently executing queries from pg_stat_activity. WARNING: Query text may contain unmasked literals (potential PII).",
167 Columns: cs.BuildColumns(),
168 Data: data,
169 DefaultSortColumn: sortColumn,
170 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(cols)},
171 }
172 }
173
174 func (f *funcRunningQueries) getColumnsForVersion() []runningQueriesColumn {
175 version := f.router.collector.pgVersion
176 if version == 0 {
177 version = pgVersion14 // Assume recent version if not detected
178 }
179
180 var cols []runningQueriesColumn
181 for _, col := range runningQueriesColumns {
182 if col.minVersion == 0 || version >= col.minVersion {
183 cols = append(cols, col)
184 }
185 }
186 return cols
187 }
188
189 func (f *funcRunningQueries) columnSet(cols []runningQueriesColumn) funcapi.ColumnSet[runningQueriesColumn] {
190 return funcapi.Columns(cols, func(c runningQueriesColumn) funcapi.ColumnMeta { return c.ColumnMeta })
191 }
192
193 func (f *funcRunningQueries) resolveSortColumn(requested string, cols []runningQueriesColumn) string {
194 for _, col := range cols {
195 if col.IsSortOption() && col.Name == requested {
196 return col.Name
197 }
198 }
199 // Return default sort column
200 for _, col := range cols {
201 if col.IsDefaultSort() {
202 return col.Name
203 }
204 }
205 // Fallback to first sortable column
206 for _, col := range cols {
207 if col.IsSortOption() {
208 return col.Name
209 }
210 }
211 return ""
212 }
213
214 func (f *funcRunningQueries) buildSelectClause(cols []runningQueriesColumn) string {
215 var parts []string
216 for _, col := range cols {
217 parts = append(parts, col.DBColumn)
218 }
219 return strings.Join(parts, ", ")
220 }
221
222 func (f *funcRunningQueries) buildQuery(cols []runningQueriesColumn, sortColumn string) string {
223 // Find the actual DB column for sorting
224 sortExpr := "query_start"
225 for _, col := range cols {
226 if col.Name == sortColumn {
227 sortExpr = col.DBColumn
228 break
229 }
230 }
231
232 return fmt.Sprintf(`
233 SELECT %s
234 FROM pg_stat_activity
235 WHERE state = 'active'
236 AND pid != pg_backend_pid()
237 AND query NOT LIKE '%%pg_stat_activity%%'
238 ORDER BY %s DESC NULLS LAST
239 LIMIT 500
240 `, f.buildSelectClause(cols), sortExpr)
241 }
242
243 func (f *funcRunningQueries) scanRows(rows *sql.Rows, cols []runningQueriesColumn) ([][]any, error) {
244 var result [][]any
245
246 for rows.Next() {
247 values := make([]any, len(cols))
248 valuePtrs := make([]any, len(cols))
249
250 for i := range values {
251 valuePtrs[i] = &values[i]
252 }
253
254 if err := rows.Scan(valuePtrs...); err != nil {
255 return nil, fmt.Errorf("scanning row: %w", err)
256 }
257
258 row := make([]any, len(cols))
259 for i, col := range cols {
260 row[i] = f.formatValue(values[i], col)
261 }
262 result = append(result, row)
263 }
264
265 if err := rows.Err(); err != nil {
266 return nil, fmt.Errorf("iterating rows: %w", err)
267 }
268
269 return result, nil
270 }
271
272 func (f *funcRunningQueries) formatValue(v any, col runningQueriesColumn) any {
273 if v == nil {
274 return nil
275 }
276
277 switch col.Type {
278 case funcapi.FieldTypeString:
279 s := fmt.Sprintf("%v", v)
280 // Truncate long strings (like query text)
281 if col.Name == "query" && len(s) > runningQueriesMaxTextLength {
282 s = strmutil.TruncateText(s, runningQueriesMaxTextLength)
283 }
284 return s
285 case funcapi.FieldTypeInteger:
286 switch val := v.(type) {
287 case int64:
288 return val
289 case int32:
290 return int64(val)
291 case int:
292 return int64(val)
293 default:
294 return v
295 }
296 case funcapi.FieldTypeDuration:
297 switch val := v.(type) {
298 case float64:
299 return val
300 case int64:
301 return float64(val)
302 default:
303 return v
304 }
305 case funcapi.FieldTypeTimestamp:
306 return v
307 default:
308 return v
309 }
310 }