master
go 716 lines 39.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package mssql
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/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 Query Store. WARNING: Query text may contain unmasked literals (potential PII).",
28 RequireCloud: true,
29 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(topQueriesColumns)},
30 }
31 }
32
33 // topQueriesColumn embeds funcapi.ColumnMeta and adds MSSQL-specific fields.
34 type topQueriesColumn struct {
35 funcapi.ColumnMeta
36 DBColumn string // Column name in sys.query_store_runtime_stats
37 IsMicroseconds bool // Needs microseconds to milliseconds conversion
38 sortOpt bool // Show in sort dropdown
39 sortLbl string // Label for sort option
40 defaultSort bool // Is this the default sort option
41 IsIdentity bool // Is this an identity column (query_hash, query_text, etc.)
42 NeedsAvg bool // Needs weighted average calculation (avg_* columns)
43 }
44
45 // topQueriesColumns defines ALL possible columns from Query Store.
46 // Columns that don't exist in certain SQL Server versions will be filtered at runtime.
47 var topQueriesColumns = []topQueriesColumn{
48 // Identity columns - always available
49 {ColumnMeta: funcapi.ColumnMeta{Name: "queryHash", Tooltip: "Query Hash", Type: funcapi.FieldTypeString, Visible: false, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, UniqueKey: true}, DBColumn: "query_hash", IsIdentity: true},
50 {ColumnMeta: funcapi.ColumnMeta{Name: "query", Tooltip: "Query", Type: funcapi.FieldTypeString, Visible: true, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect, Sticky: true, FullWidth: true}, DBColumn: "query_sql_text", IsIdentity: true},
51 {ColumnMeta: funcapi.ColumnMeta{Name: "database", Tooltip: "Database", Type: funcapi.FieldTypeString, Visible: true, Transform: funcapi.FieldTransformNone, Sort: funcapi.FieldSortAscending, Summary: funcapi.FieldSummaryCount, Filter: funcapi.FieldFilterMultiselect}, DBColumn: "database_name", IsIdentity: true},
52
53 // Execution count - always available
54 {ColumnMeta: funcapi.ColumnMeta{Name: "calls", Tooltip: "Calls", Type: funcapi.FieldTypeInteger, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange}, DBColumn: "count_executions", sortOpt: true, sortLbl: "Top queries by Number of Calls"},
55
56 // Duration metrics (microseconds -> milliseconds) - SQL 2016+
57 {ColumnMeta: funcapi.ColumnMeta{Name: "totalTime", Tooltip: "Total Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: true, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummarySum, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_duration", IsMicroseconds: true, sortOpt: true, sortLbl: "Top queries by Total Execution Time", defaultSort: true},
58 {ColumnMeta: funcapi.ColumnMeta{Name: "avgTime", Tooltip: "Avg Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: true, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_duration", IsMicroseconds: true, NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Execution Time"},
59 {ColumnMeta: funcapi.ColumnMeta{Name: "lastTime", Tooltip: "Last Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_duration", IsMicroseconds: true},
60 {ColumnMeta: funcapi.ColumnMeta{Name: "minTime", Tooltip: "Min Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_duration", IsMicroseconds: true},
61 {ColumnMeta: funcapi.ColumnMeta{Name: "maxTime", Tooltip: "Max Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_duration", IsMicroseconds: true},
62 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevTime", Tooltip: "StdDev Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_duration", IsMicroseconds: true},
63
64 // CPU time metrics (microseconds -> milliseconds)
65 {ColumnMeta: funcapi.ColumnMeta{Name: "avgCpu", Tooltip: "Avg CPU", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: true, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_cpu_time", IsMicroseconds: true, NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average CPU Time"},
66 {ColumnMeta: funcapi.ColumnMeta{Name: "lastCpu", Tooltip: "Last CPU", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_cpu_time", IsMicroseconds: true},
67 {ColumnMeta: funcapi.ColumnMeta{Name: "minCpu", Tooltip: "Min CPU", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_cpu_time", IsMicroseconds: true},
68 {ColumnMeta: funcapi.ColumnMeta{Name: "maxCpu", Tooltip: "Max CPU", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_cpu_time", IsMicroseconds: true},
69 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevCpu", Tooltip: "StdDev CPU", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_cpu_time", IsMicroseconds: true},
70
71 // Logical I/O reads
72 {ColumnMeta: funcapi.ColumnMeta{Name: "avgReads", Tooltip: "Avg Logical Reads", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_logical_io_reads", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Logical Reads"},
73 {ColumnMeta: funcapi.ColumnMeta{Name: "lastReads", Tooltip: "Last Logical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_logical_io_reads"},
74 {ColumnMeta: funcapi.ColumnMeta{Name: "minReads", Tooltip: "Min Logical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_logical_io_reads"},
75 {ColumnMeta: funcapi.ColumnMeta{Name: "maxReads", Tooltip: "Max Logical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_logical_io_reads"},
76 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevReads", Tooltip: "StdDev Logical Reads", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_logical_io_reads"},
77
78 // Logical I/O writes
79 {ColumnMeta: funcapi.ColumnMeta{Name: "avgWrites", Tooltip: "Avg Logical Writes", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_logical_io_writes", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Logical Writes"},
80 {ColumnMeta: funcapi.ColumnMeta{Name: "lastWrites", Tooltip: "Last Logical Writes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_logical_io_writes"},
81 {ColumnMeta: funcapi.ColumnMeta{Name: "minWrites", Tooltip: "Min Logical Writes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_logical_io_writes"},
82 {ColumnMeta: funcapi.ColumnMeta{Name: "maxWrites", Tooltip: "Max Logical Writes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_logical_io_writes"},
83 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevWrites", Tooltip: "StdDev Logical Writes", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_logical_io_writes"},
84
85 // Physical I/O reads
86 {ColumnMeta: funcapi.ColumnMeta{Name: "avgPhysReads", Tooltip: "Avg Physical Reads", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_physical_io_reads", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Physical Reads"},
87 {ColumnMeta: funcapi.ColumnMeta{Name: "lastPhysReads", Tooltip: "Last Physical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_physical_io_reads"},
88 {ColumnMeta: funcapi.ColumnMeta{Name: "minPhysReads", Tooltip: "Min Physical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_physical_io_reads"},
89 {ColumnMeta: funcapi.ColumnMeta{Name: "maxPhysReads", Tooltip: "Max Physical Reads", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_physical_io_reads"},
90 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevPhysReads", Tooltip: "StdDev Physical Reads", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_physical_io_reads"},
91
92 // CLR time (microseconds -> milliseconds)
93 {ColumnMeta: funcapi.ColumnMeta{Name: "avgClr", Tooltip: "Avg CLR Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_clr_time", IsMicroseconds: true, NeedsAvg: true},
94 {ColumnMeta: funcapi.ColumnMeta{Name: "lastClr", Tooltip: "Last CLR Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_clr_time", IsMicroseconds: true},
95 {ColumnMeta: funcapi.ColumnMeta{Name: "minClr", Tooltip: "Min CLR Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_clr_time", IsMicroseconds: true},
96 {ColumnMeta: funcapi.ColumnMeta{Name: "maxClr", Tooltip: "Max CLR Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_clr_time", IsMicroseconds: true},
97 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevClr", Tooltip: "StdDev CLR Time", Type: funcapi.FieldTypeDuration, Units: "milliseconds", DecimalPoints: 2, Visible: false, Transform: funcapi.FieldTransformDuration, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_clr_time", IsMicroseconds: true},
98
99 // DOP (degree of parallelism)
100 {ColumnMeta: funcapi.ColumnMeta{Name: "avgDop", Tooltip: "Avg DOP", Type: funcapi.FieldTypeFloat, DecimalPoints: 1, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_dop", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Parallelism"},
101 {ColumnMeta: funcapi.ColumnMeta{Name: "lastDop", Tooltip: "Last DOP", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_dop"},
102 {ColumnMeta: funcapi.ColumnMeta{Name: "minDop", Tooltip: "Min DOP", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_dop"},
103 {ColumnMeta: funcapi.ColumnMeta{Name: "maxDop", Tooltip: "Max DOP", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_dop"},
104 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevDop", Tooltip: "StdDev DOP", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_dop"},
105
106 // Memory grant (8KB pages)
107 {ColumnMeta: funcapi.ColumnMeta{Name: "avgMemory", Tooltip: "Avg Memory (8KB pages)", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_query_max_used_memory", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Memory Grant"},
108 {ColumnMeta: funcapi.ColumnMeta{Name: "lastMemory", Tooltip: "Last Memory (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_query_max_used_memory"},
109 {ColumnMeta: funcapi.ColumnMeta{Name: "minMemory", Tooltip: "Min Memory (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_query_max_used_memory"},
110 {ColumnMeta: funcapi.ColumnMeta{Name: "maxMemory", Tooltip: "Max Memory (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_query_max_used_memory"},
111 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevMemory", Tooltip: "StdDev Memory", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_query_max_used_memory"},
112
113 // Row count
114 {ColumnMeta: funcapi.ColumnMeta{Name: "avgRows", Tooltip: "Avg Rows", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_rowcount", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Row Count"},
115 {ColumnMeta: funcapi.ColumnMeta{Name: "lastRows", Tooltip: "Last Rows", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_rowcount"},
116 {ColumnMeta: funcapi.ColumnMeta{Name: "minRows", Tooltip: "Min Rows", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_rowcount"},
117 {ColumnMeta: funcapi.ColumnMeta{Name: "maxRows", Tooltip: "Max Rows", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_rowcount"},
118 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevRows", Tooltip: "StdDev Rows", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_rowcount"},
119
120 // SQL Server 2017+ log bytes
121 {ColumnMeta: funcapi.ColumnMeta{Name: "avgLogBytes", Tooltip: "Avg Log Bytes", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_log_bytes_used", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average Log Bytes"},
122 {ColumnMeta: funcapi.ColumnMeta{Name: "lastLogBytes", Tooltip: "Last Log Bytes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_log_bytes_used"},
123 {ColumnMeta: funcapi.ColumnMeta{Name: "minLogBytes", Tooltip: "Min Log Bytes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_log_bytes_used"},
124 {ColumnMeta: funcapi.ColumnMeta{Name: "maxLogBytes", Tooltip: "Max Log Bytes", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_log_bytes_used"},
125 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevLogBytes", Tooltip: "StdDev Log Bytes", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_log_bytes_used"},
126
127 // SQL Server 2017+ tempdb space
128 {ColumnMeta: funcapi.ColumnMeta{Name: "avgTempdb", Tooltip: "Avg TempDB (8KB pages)", Type: funcapi.FieldTypeFloat, DecimalPoints: 0, Visible: true, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMean, Filter: funcapi.FieldFilterRange}, DBColumn: "avg_tempdb_space_used", NeedsAvg: true, sortOpt: true, sortLbl: "Top queries by Average TempDB Usage"},
129 {ColumnMeta: funcapi.ColumnMeta{Name: "lastTempdb", Tooltip: "Last TempDB (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "last_tempdb_space_used"},
130 {ColumnMeta: funcapi.ColumnMeta{Name: "minTempdb", Tooltip: "Min TempDB (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMin, Filter: funcapi.FieldFilterRange}, DBColumn: "min_tempdb_space_used"},
131 {ColumnMeta: funcapi.ColumnMeta{Name: "maxTempdb", Tooltip: "Max TempDB (8KB pages)", Type: funcapi.FieldTypeInteger, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "max_tempdb_space_used"},
132 {ColumnMeta: funcapi.ColumnMeta{Name: "stdevTempdb", Tooltip: "StdDev TempDB", Type: funcapi.FieldTypeFloat, Visible: false, Transform: funcapi.FieldTransformNumber, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax, Filter: funcapi.FieldFilterRange}, DBColumn: "stdev_tempdb_space_used"},
133 }
134
135 // funcapi.SortableColumn interface implementation for topQueriesColumn.
136 func (c topQueriesColumn) IsSortOption() bool { return c.sortOpt }
137 func (c topQueriesColumn) SortLabel() string { return c.sortLbl }
138 func (c topQueriesColumn) IsDefaultSort() bool { return c.defaultSort }
139 func (c topQueriesColumn) ColumnName() string { return c.Name }
140 func (c topQueriesColumn) SortColumn() string { return "" }
141
142 type topQueriesChartGroupDef struct {
143 key string
144 title string
145 columns []string
146 defaultChart bool
147 }
148
149 var topQueriesChartGroupDefs = []topQueriesChartGroupDef{
150 {key: "Calls", title: "Number of Calls", columns: []string{"calls"}, defaultChart: true},
151 {key: "Time", title: "Execution Time", columns: []string{"totalTime", "avgTime", "lastTime", "minTime", "maxTime", "stdevTime"}, defaultChart: true},
152 {key: "CPU", title: "CPU Time", columns: []string{"avgCpu", "lastCpu", "minCpu", "maxCpu", "stdevCpu"}},
153 {key: "LogicalIO", title: "Logical I/O", columns: []string{"avgReads", "lastReads", "minReads", "maxReads", "stdevReads", "avgWrites", "lastWrites", "minWrites", "maxWrites", "stdevWrites"}},
154 {key: "PhysicalIO", title: "Physical Reads", columns: []string{"avgPhysReads", "lastPhysReads", "minPhysReads", "maxPhysReads", "stdevPhysReads"}},
155 {key: "CLR", title: "CLR Time", columns: []string{"avgClr", "lastClr", "minClr", "maxClr", "stdevClr"}},
156 {key: "DOP", title: "Parallelism", columns: []string{"avgDop", "lastDop", "minDop", "maxDop", "stdevDop"}},
157 {key: "Memory", title: "Memory Grant", columns: []string{"avgMemory", "lastMemory", "minMemory", "maxMemory", "stdevMemory"}},
158 {key: "Rows", title: "Rows", columns: []string{"avgRows", "lastRows", "minRows", "maxRows", "stdevRows"}},
159 {key: "LogBytes", title: "Log Bytes", columns: []string{"avgLogBytes", "lastLogBytes", "minLogBytes", "maxLogBytes", "stdevLogBytes"}},
160 {key: "TempDB", title: "TempDB Usage", columns: []string{"avgTempdb", "lastTempdb", "minTempdb", "maxTempdb", "stdevTempdb"}},
161 }
162
163 var topQueriesLabelColumnIDs = map[string]bool{
164 "database": true,
165 }
166
167 const topQueriesPrimaryLabelID = "database"
168
169 // topQueriesRowScanner interface for testing.
170 type topQueriesRowScanner interface {
171 Next() bool
172 Scan(dest ...any) error
173 Err() error
174 }
175
176 // funcTopQueries implements funcapi.MethodHandler for MSSQL top-queries.
177 // All function-related logic is encapsulated here, keeping Collector focused on metrics collection.
178 type funcTopQueries struct {
179 router *funcRouter
180 }
181
182 func newFuncTopQueries(r *funcRouter) *funcTopQueries {
183 return &funcTopQueries{router: r}
184 }
185
186 // Compile-time interface check.
187 var _ funcapi.MethodHandler = (*funcTopQueries)(nil)
188
189 // MethodParams implements funcapi.MethodHandler.
190 func (f *funcTopQueries) MethodParams(ctx context.Context, method string) ([]funcapi.ParamConfig, error) {
191 if f.router.collector.db == nil {
192 return nil, fmt.Errorf("collector is still initializing")
193 }
194 switch method {
195 case topQueriesMethodID:
196 return f.methodParams(ctx)
197 default:
198 return nil, fmt.Errorf("unknown method: %s", method)
199 }
200 }
201
202 // Handle implements funcapi.MethodHandler.
203 func (f *funcTopQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
204 if f.router.collector.db == nil {
205 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
206 }
207 switch method {
208 case topQueriesMethodID:
209 queryCtx, cancel := context.WithTimeout(ctx, f.router.collector.topQueriesTimeout())
210 defer cancel()
211 return f.collectData(queryCtx, params.Column(topQueriesParamSort))
212 default:
213 return funcapi.NotFoundResponse(method)
214 }
215 }
216
217 // Cleanup implements funcapi.MethodHandler.
218 func (f *funcTopQueries) Cleanup(ctx context.Context) {}
219
220 func (f *funcTopQueries) methodParams(ctx context.Context) ([]funcapi.ParamConfig, error) {
221 if f.router.collector.Functions.TopQueries.Disabled {
222 return nil, fmt.Errorf("top-queries function disabled in configuration")
223 }
224
225 availableCols, err := f.detectQueryStoreColumns(ctx)
226 if err != nil {
227 return nil, err
228 }
229
230 cols := f.buildAvailableColumns(availableCols)
231 if len(cols) == 0 {
232 return nil, fmt.Errorf("no columns available in Query Store")
233 }
234
235 sortParam, _ := f.buildSortParam(cols)
236 return []funcapi.ParamConfig{sortParam}, nil
237 }
238
239 func (f *funcTopQueries) collectData(ctx context.Context, sortColumn string) *funcapi.FunctionResponse {
240 if f.router.collector.Functions.TopQueries.Disabled {
241 return funcapi.UnavailableResponse("top-queries function has been disabled in configuration")
242 }
243
244 availableCols, err := f.detectQueryStoreColumns(ctx)
245 if err != nil {
246 return &funcapi.FunctionResponse{
247 Status: 500,
248 Message: fmt.Sprintf("failed to detect available columns: %v", err),
249 }
250 }
251
252 cols := f.buildAvailableColumns(availableCols)
253 if len(cols) == 0 {
254 return &funcapi.FunctionResponse{
255 Status: 500,
256 Message: "no columns available in Query Store",
257 }
258 }
259
260 validatedSortColumn := f.mapAndValidateSortColumn(sortColumn, cols)
261
262 timeWindowDays := f.router.collector.topQueriesTimeWindowDays()
263 limit := f.router.collector.topQueriesLimit()
264 query := f.buildDynamicSQL(cols, validatedSortColumn, timeWindowDays, limit)
265
266 rows, err := f.router.collector.db.QueryContext(ctx, query)
267 if err != nil {
268 if ctx.Err() == context.DeadlineExceeded {
269 return &funcapi.FunctionResponse{Status: 504, Message: "query timed out"}
270 }
271 colIDs := make([]string, len(cols))
272 for i, col := range cols {
273 colIDs[i] = col.Name
274 }
275 return &funcapi.FunctionResponse{
276 Status: 500,
277 Message: fmt.Sprintf("query failed: %v (sort: %s, detected cols: %v)", err, validatedSortColumn, colIDs),
278 }
279 }
280 defer rows.Close()
281
282 data, err := f.scanDynamicRows(rows, cols)
283 if err != nil {
284 return &funcapi.FunctionResponse{Status: 500, Message: err.Error()}
285 }
286
287 errorStatus, errorDetails := f.router.collector.collectMSSQLErrorDetails(ctx)
288 planOpsByDB := f.router.collector.collectMSSQLPlanOps(ctx, data, cols)
289 extraCols := append(mssqlErrorAttributionColumns(), mssqlPlanAttributionColumns()...)
290
291 queryIdx := -1
292 queryHashIdx := -1
293 dbIdx := -1
294 for i, col := range cols {
295 switch col.Name {
296 case "query":
297 queryIdx = i
298 case "queryHash":
299 queryHashIdx = i
300 case "database":
301 dbIdx = i
302 }
303 }
304
305 if len(extraCols) > 0 {
306 for i := range data {
307 status := errorStatus
308 var errRow mssqlErrorRow
309 if errorStatus == mssqlErrorAttrEnabled {
310 found := false
311 if queryHashIdx >= 0 && queryHashIdx < len(data[i]) {
312 queryHash := rowString(data[i][queryHashIdx])
313 if queryHash != "" {
314 if row, ok := errorDetails[queryHash]; ok {
315 status = mssqlErrorAttrEnabled
316 errRow = row
317 found = true
318 }
319 }
320 }
321 if !found && queryIdx >= 0 && queryIdx < len(data[i]) {
322 queryText := normalizeSQLText(rowString(data[i][queryIdx]))
323 if queryText != "" {
324 if row, ok := errorDetails[queryText]; ok {
325 status = mssqlErrorAttrEnabled
326 errRow = row
327 found = true
328 }
329 }
330 }
331 if !found {
332 status = mssqlErrorAttrNoData
333 }
334 }
335
336 var hashMatch, mergeJoin, nestedLoops, sorts any
337 if dbIdx >= 0 && dbIdx < len(data[i]) && queryHashIdx >= 0 && queryHashIdx < len(data[i]) {
338 dbName := rowString(data[i][dbIdx])
339 queryHash := rowString(data[i][queryHashIdx])
340 if dbName != "" && queryHash != "" {
341 if opsByHash, ok := planOpsByDB[dbName]; ok {
342 if ops, ok := opsByHash[queryHash]; ok {
343 hashMatch = ops.HashMatch
344 mergeJoin = ops.MergeJoin
345 nestedLoops = ops.NestedLoops
346 sorts = ops.Sorts
347 }
348 }
349 }
350 }
351
352 var errNo any
353 if errRow.ErrorNumber != nil {
354 errNo = *errRow.ErrorNumber
355 }
356 var errState any
357 if errRow.ErrorState != nil {
358 errState = *errRow.ErrorState
359 }
360 data[i] = append(data[i],
361 status,
362 errNo,
363 errState,
364 nullableString(rowString(errRow.Message)),
365 hashMatch,
366 mergeJoin,
367 nestedLoops,
368 sorts,
369 )
370 }
371 cols = append(cols, extraCols...)
372 }
373
374 sortParam, sortOptions := f.buildSortParam(cols)
375
376 defaultSort := ""
377 for _, col := range cols {
378 if col.IsDefaultSort() && col.IsSortOption() {
379 defaultSort = col.Name
380 break
381 }
382 }
383 if defaultSort == "" && len(sortOptions) > 0 {
384 defaultSort = sortOptions[0].ID
385 }
386
387 annotatedCols := f.decorateColumns(cols)
388 cs := f.columnSet(annotatedCols)
389
390 return &funcapi.FunctionResponse{
391 Status: 200,
392 Help: "Top SQL queries from Query Store. WARNING: Query text may contain unmasked literals (potential PII).",
393 Columns: cs.BuildColumns(),
394 Data: data,
395 DefaultSortColumn: defaultSort,
396 RequiredParams: []funcapi.ParamConfig{sortParam},
397 ChartingConfig: cs.BuildCharting(),
398 }
399 }
400
401 func (f *funcTopQueries) columnSet(cols []topQueriesColumn) funcapi.ColumnSet[topQueriesColumn] {
402 return funcapi.Columns(cols, func(c topQueriesColumn) funcapi.ColumnMeta { return c.ColumnMeta })
403 }
404
405 func (f *funcTopQueries) detectQueryStoreColumns(ctx context.Context) (map[string]bool, error) {
406 // Fast path: return cached result
407 f.router.collector.queryStoreColsMu.RLock()
408 if f.router.collector.queryStoreCols != nil {
409 cols := f.router.collector.queryStoreCols
410 f.router.collector.queryStoreColsMu.RUnlock()
411 return cols, nil
412 }
413 f.router.collector.queryStoreColsMu.RUnlock()
414
415 // Slow path: query and cache
416 f.router.collector.queryStoreColsMu.Lock()
417 defer f.router.collector.queryStoreColsMu.Unlock()
418
419 // Double-check after acquiring write lock
420 if f.router.collector.queryStoreCols != nil {
421 return f.router.collector.queryStoreCols, nil
422 }
423
424 // Find any database with Query Store enabled (excluding system databases)
425 var sampleDB string
426 err := f.router.collector.db.QueryRowContext(ctx, `
427 SELECT TOP 1 name
428 FROM sys.databases
429 WHERE is_query_store_on = 1
430 AND name NOT IN ('master', 'tempdb', 'model', 'msdb')
431 `).Scan(&sampleDB)
432 if err != nil {
433 if err == sql.ErrNoRows {
434 return nil, fmt.Errorf("no databases have Query Store enabled")
435 }
436 return nil, fmt.Errorf("failed to find database with Query Store: %w", err)
437 }
438
439 // Use dynamic SQL to get column metadata from that database's Query Store view
440 query := fmt.Sprintf(`SELECT TOP 0 * FROM [%s].sys.query_store_runtime_stats`, sampleDB)
441 rows, err := f.router.collector.db.QueryContext(ctx, query)
442 if err != nil {
443 return nil, fmt.Errorf("failed to query Query Store columns from %s: %w", sampleDB, err)
444 }
445 defer rows.Close()
446
447 columnNames, err := rows.Columns()
448 if err != nil {
449 return nil, fmt.Errorf("failed to get column names: %w", err)
450 }
451
452 cols := make(map[string]bool)
453 for _, colName := range columnNames {
454 cols[strings.ToLower(colName)] = true
455 }
456
457 if len(cols) == 0 {
458 return nil, fmt.Errorf("no columns found in sys.query_store_runtime_stats")
459 }
460
461 // Add identity columns that are always available (from other Query Store views)
462 cols["query_hash"] = true
463 cols["query_sql_text"] = true
464 cols["database_name"] = true
465
466 f.router.collector.queryStoreCols = cols
467
468 return cols, nil
469 }
470
471 func (f *funcTopQueries) buildAvailableColumns(availableCols map[string]bool) []topQueriesColumn {
472 var cols []topQueriesColumn
473 seen := make(map[string]bool)
474
475 for _, col := range topQueriesColumns {
476 if seen[col.Name] {
477 continue
478 }
479 if col.IsIdentity {
480 cols = append(cols, col)
481 seen[col.Name] = true
482 continue
483 }
484 if availableCols[col.DBColumn] {
485 cols = append(cols, col)
486 seen[col.Name] = true
487 }
488 }
489 return cols
490 }
491
492 func (f *funcTopQueries) mapAndValidateSortColumn(sortKey string, cols []topQueriesColumn) string {
493 for _, col := range cols {
494 if col.Name == sortKey && col.IsSortOption() {
495 return col.Name
496 }
497 }
498
499 for _, col := range cols {
500 if col.IsSortOption() {
501 return col.Name
502 }
503 }
504
505 for _, col := range cols {
506 if !col.IsIdentity {
507 return col.Name
508 }
509 }
510
511 if len(cols) > 0 {
512 return cols[0].Name
513 }
514
515 return ""
516 }
517
518 func (f *funcTopQueries) buildSelectExpressions(cols []topQueriesColumn, dbNameExpr string) []string {
519 var selectParts []string
520
521 for _, col := range cols {
522 var expr string
523 switch {
524 case col.IsIdentity:
525 switch col.Name {
526 case "queryHash":
527 expr = fmt.Sprintf("CONVERT(VARCHAR(64), q.query_hash, 1) AS [%s]", col.Name)
528 case "query":
529 expr = fmt.Sprintf("qt.query_sql_text AS [%s]", col.Name)
530 case "database":
531 expr = fmt.Sprintf("%s AS [%s]", dbNameExpr, col.Name)
532 }
533 case col.Name == "calls":
534 expr = fmt.Sprintf("SUM(rs.count_executions) AS [%s]", col.Name)
535 case col.Name == "totalTime":
536 expr = fmt.Sprintf("SUM(rs.avg_duration * rs.count_executions) / 1000.0 AS [%s]", col.Name)
537 case col.NeedsAvg && col.IsMicroseconds:
538 expr = fmt.Sprintf("CASE WHEN SUM(rs.count_executions) > 0 THEN SUM(rs.%s * rs.count_executions) / SUM(rs.count_executions) / 1000.0 ELSE 0 END AS [%s]", col.DBColumn, col.Name)
539 case col.NeedsAvg:
540 expr = fmt.Sprintf("CASE WHEN SUM(rs.count_executions) > 0 THEN SUM(rs.%s * rs.count_executions) / SUM(rs.count_executions) ELSE 0 END AS [%s]", col.DBColumn, col.Name)
541 case col.IsMicroseconds:
542 aggFunc := "MAX"
543 if strings.HasPrefix(col.DBColumn, "min_") {
544 aggFunc = "MIN"
545 }
546 expr = fmt.Sprintf("%s(rs.%s) / 1000.0 AS [%s]", aggFunc, col.DBColumn, col.Name)
547 default:
548 aggFunc := "MAX"
549 if strings.HasPrefix(col.DBColumn, "min_") {
550 aggFunc = "MIN"
551 }
552 if strings.HasPrefix(col.DBColumn, "stdev_") {
553 aggFunc = "MAX"
554 }
555 expr = fmt.Sprintf("%s(rs.%s) AS [%s]", aggFunc, col.DBColumn, col.Name)
556 }
557 if expr != "" {
558 selectParts = append(selectParts, expr)
559 }
560 }
561 return selectParts
562 }
563
564 func (f *funcTopQueries) buildDynamicSQL(cols []topQueriesColumn, sortColumn string, timeWindowDays int, limit int) string {
565 selectParts := f.buildSelectExpressions(cols, "''' + name + N'''")
566 selectExpr := strings.Join(selectParts, ",\n ")
567
568 timeFilter := ""
569 if timeWindowDays > 0 {
570 timeFilter = fmt.Sprintf("WHERE rsi.start_time >= DATEADD(day, -%d, GETUTCDATE())", timeWindowDays)
571 }
572
573 orderByExpr := sortColumn
574 if orderByExpr == "" {
575 for _, col := range cols {
576 if !col.IsIdentity {
577 orderByExpr = col.Name
578 break
579 }
580 }
581 if orderByExpr == "" && len(cols) > 0 {
582 orderByExpr = cols[0].Name
583 }
584 }
585
586 return fmt.Sprintf(`
587 DECLARE @sql NVARCHAR(MAX) = N'';
588
589 SELECT @sql = @sql +
590 CASE WHEN @sql = N'' THEN N'' ELSE N' UNION ALL ' END +
591 N'SELECT
592 %s
593 FROM ' + QUOTENAME(name) + N'.sys.query_store_query q
594 INNER JOIN ' + QUOTENAME(name) + N'.sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
595 INNER JOIN ' + QUOTENAME(name) + N'.sys.query_store_plan p ON q.query_id = p.query_id
596 INNER JOIN ' + QUOTENAME(name) + N'.sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
597 INNER JOIN ' + QUOTENAME(name) + N'.sys.query_store_runtime_stats_interval rsi ON rs.runtime_stats_interval_id = rsi.runtime_stats_interval_id
598 %s
599 GROUP BY q.query_hash, qt.query_sql_text'
600 FROM sys.databases
601 WHERE is_query_store_on = 1
602 AND name NOT IN ('master', 'tempdb', 'model', 'msdb');
603
604 IF @sql = N''
605 BEGIN
606 RAISERROR('No databases have Query Store enabled', 16, 1);
607 RETURN;
608 END
609
610 SET @sql = N'SELECT TOP %d * FROM (' + @sql + N') AS combined ORDER BY [%s] DESC';
611 EXEC sp_executesql @sql;
612 `, selectExpr, timeFilter, limit, orderByExpr)
613 }
614
615 func (f *funcTopQueries) scanDynamicRows(rows topQueriesRowScanner, cols []topQueriesColumn) ([][]any, error) {
616 specs := make([]sqlquery.ScanColumnSpec, len(cols))
617 for i, col := range cols {
618 specs[i] = mssqlTopQueriesScanSpec(col)
619 }
620
621 data, err := sqlquery.ScanTypedRows(rows, specs)
622 if err != nil {
623 return nil, fmt.Errorf("row scan failed: %w", err)
624 }
625 if err := rows.Err(); err != nil {
626 return nil, fmt.Errorf("rows iteration error: %w", err)
627 }
628 return data, nil
629 }
630
631 func mssqlTopQueriesScanSpec(col topQueriesColumn) sqlquery.ScanColumnSpec {
632 spec := sqlquery.ScanColumnSpec{}
633 switch col.Type {
634 case funcapi.FieldTypeString:
635 spec.Type = sqlquery.ScanValueString
636 if col.Name == "query" {
637 spec.Transform = func(v any) any {
638 s, _ := v.(string)
639 return strmutil.TruncateText(s, topQueriesMaxTextLength)
640 }
641 }
642 case funcapi.FieldTypeInteger:
643 spec.Type = sqlquery.ScanValueInteger
644 case funcapi.FieldTypeFloat, funcapi.FieldTypeDuration:
645 spec.Type = sqlquery.ScanValueFloat
646 default:
647 spec.Type = sqlquery.ScanValueDiscard
648 }
649 return spec
650 }
651
652 func (f *funcTopQueries) buildSortParam(cols []topQueriesColumn) (funcapi.ParamConfig, []funcapi.ParamOption) {
653 sortOptions := buildTopQueriesSortOptions(cols)
654 sortParam := funcapi.ParamConfig{
655 ID: topQueriesParamSort,
656 Name: "Filter By",
657 Help: "Select the primary sort column",
658 Selection: funcapi.ParamSelect,
659 Options: sortOptions,
660 UniqueView: true,
661 }
662 return sortParam, sortOptions
663 }
664
665 func (f *funcTopQueries) decorateColumns(cols []topQueriesColumn) []topQueriesColumn {
666 out := make([]topQueriesColumn, len(cols))
667 index := make(map[string]int, len(cols))
668 for i, col := range cols {
669 out[i] = col
670 index[col.Name] = i
671 }
672
673 for i := range out {
674 if topQueriesLabelColumnIDs[out[i].Name] {
675 out[i].GroupBy = &funcapi.GroupByOptions{
676 IsDefault: out[i].Name == topQueriesPrimaryLabelID,
677 }
678 }
679 }
680
681 for _, group := range topQueriesChartGroupDefs {
682 for _, key := range group.columns {
683 idx, ok := index[key]
684 if !ok {
685 continue
686 }
687 out[idx].Chart = &funcapi.ChartOptions{
688 Group: group.key,
689 Title: group.title,
690 IsDefault: group.defaultChart,
691 }
692 }
693 }
694
695 return out
696 }
697
698 // buildTopQueriesSortOptions builds sort options for method registration (before handler exists).
699 func buildTopQueriesSortOptions(cols []topQueriesColumn) []funcapi.ParamOption {
700 var sortOptions []funcapi.ParamOption
701 sortDir := funcapi.FieldSortDescending
702 seen := make(map[string]bool)
703 for _, col := range cols {
704 if col.IsSortOption() && !seen[col.Name] {
705 seen[col.Name] = true
706 sortOptions = append(sortOptions, funcapi.ParamOption{
707 ID: col.Name,
708 Column: col.Name,
709 Name: col.SortLabel(),
710 Default: col.IsDefaultSort(),
711 Sort: &sortDir,
712 })
713 }
714 }
715 return sortOptions
716 }