master
go 220 lines 8.77 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package redis
4
5 import (
6 "context"
7 "fmt"
8 "sort"
9 "strings"
10 "time"
11
12 "github.com/redis/go-redis/v9"
13
14 "github.com/netdata/netdata/go/plugins/pkg/funcapi"
15 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/strmutil"
16 )
17
18 const (
19 topQueriesMethodID = "top-queries"
20 redisMaxQueryTextLength = 4096
21 )
22
23 func topQueriesMethodConfig() funcapi.MethodConfig {
24 return funcapi.MethodConfig{
25 ID: topQueriesMethodID,
26 Name: "Top Queries",
27 UpdateEvery: 10,
28 Help: "Slow commands from Redis SLOWLOG. WARNING: Command arguments may contain unmasked literals (potential PII).",
29 RequireCloud: true,
30 RequiredParams: []funcapi.ParamConfig{funcapi.BuildSortParam(redisAllColumns)},
31 }
32 }
33
34 type redisColumn struct {
35 funcapi.ColumnMeta
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 redisColumn.
42 func (c redisColumn) IsSortOption() bool { return c.sortOpt }
43 func (c redisColumn) SortLabel() string { return c.sortLbl }
44 func (c redisColumn) IsDefaultSort() bool { return c.defaultSort }
45 func (c redisColumn) ColumnName() string { return c.Name }
46 func (c redisColumn) SortColumn() string { return "" }
47
48 func redisColumnSet(cols []redisColumn) funcapi.ColumnSet[redisColumn] {
49 return funcapi.Columns(cols, func(c redisColumn) funcapi.ColumnMeta { return c.ColumnMeta })
50 }
51
52 var redisAllColumns = []redisColumn{
53 {ColumnMeta: funcapi.ColumnMeta{Name: "id", Tooltip: "ID", Type: funcapi.FieldTypeInteger, Visible: false, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformNumber, UniqueKey: true, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryCount}, sortOpt: true, sortLbl: "Top queries by ID"},
54 {ColumnMeta: funcapi.ColumnMeta{Name: "timestamp", Tooltip: "Timestamp", Type: funcapi.FieldTypeTimestamp, Visible: true, Sortable: true, Filter: funcapi.FieldFilterRange, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformDatetime, Sort: funcapi.FieldSortDescending, Summary: funcapi.FieldSummaryMax}, sortOpt: true, sortLbl: "Top queries by Timestamp"},
55 {ColumnMeta: funcapi.ColumnMeta{Name: "command", Tooltip: "Command", Type: funcapi.FieldTypeString, Visible: true, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformText, Sticky: true, FullWidth: true, Wrap: true}},
56 {ColumnMeta: funcapi.ColumnMeta{Name: "command_name", Tooltip: "Command Name", Type: funcapi.FieldTypeString, Visible: true, Sortable: true, Filter: funcapi.FieldFilterMultiselect, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformText, Sort: funcapi.FieldSortAscending, GroupBy: &funcapi.GroupByOptions{IsDefault: true}}, sortOpt: true, sortLbl: "Top queries by Command Name"},
57 {ColumnMeta: funcapi.ColumnMeta{Name: "duration", Tooltip: "Duration", 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: "Duration", Title: "Execution Time", IsDefault: true}}, sortOpt: true, sortLbl: "Top queries by Duration", defaultSort: true},
58 {ColumnMeta: funcapi.ColumnMeta{Name: "client_addr", Tooltip: "Client Address", Type: funcapi.FieldTypeString, Visible: false, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformText, GroupBy: &funcapi.GroupByOptions{}}},
59 {ColumnMeta: funcapi.ColumnMeta{Name: "client_name", Tooltip: "Client Name", Type: funcapi.FieldTypeString, Visible: false, Sortable: false, Filter: funcapi.FieldFilterMultiselect, Visualization: funcapi.FieldVisualValue, Transform: funcapi.FieldTransformText, GroupBy: &funcapi.GroupByOptions{}}},
60 }
61
62 // Compile-time interface check.
63 var _ funcapi.MethodHandler = (*funcTopQueries)(nil)
64
65 // funcTopQueries handles the "top-queries" function for Redis.
66 type funcTopQueries struct {
67 router *funcRouter
68 }
69
70 func newFuncTopQueries(r *funcRouter) *funcTopQueries {
71 return &funcTopQueries{router: r}
72 }
73
74 // MethodParams implements funcapi.MethodHandler.
75 func (f *funcTopQueries) MethodParams(_ context.Context, method string) ([]funcapi.ParamConfig, error) {
76 if method != topQueriesMethodID {
77 return nil, fmt.Errorf("unknown method: %s", method)
78 }
79 if f.router.collector.Functions.TopQueries.Disabled {
80 return nil, fmt.Errorf("top-queries function disabled in configuration")
81 }
82 return []funcapi.ParamConfig{funcapi.BuildSortParam(redisAllColumns)}, nil
83 }
84
85 // Handle implements funcapi.MethodHandler.
86 func (f *funcTopQueries) Handle(ctx context.Context, method string, params funcapi.ResolvedParams) *funcapi.FunctionResponse {
87 if method != topQueriesMethodID {
88 return funcapi.NotFoundResponse(method)
89 }
90 if f.router.collector.Functions.TopQueries.Disabled {
91 return funcapi.UnavailableResponse("top-queries function has been disabled in configuration")
92 }
93
94 if f.router.collector.rdb == nil {
95 return funcapi.UnavailableResponse("collector is still initializing, please retry in a few seconds")
96 }
97
98 queryCtx, cancel := context.WithTimeout(ctx, f.router.collector.topQueriesTimeout())
99 defer cancel()
100 return f.collectTopQueries(queryCtx, params.Column("__sort"))
101 }
102
103 // Cleanup implements funcapi.MethodHandler.
104 func (f *funcTopQueries) Cleanup(ctx context.Context) {}
105
106 func (f *funcTopQueries) collectTopQueries(ctx context.Context, sortColumn string) *funcapi.FunctionResponse {
107 c := f.router.collector
108
109 limit := c.topQueriesLimit()
110
111 entries, err := c.rdb.SlowLogGet(ctx, -1).Result()
112 if err != nil {
113 if ctx.Err() == context.DeadlineExceeded {
114 return &funcapi.FunctionResponse{Status: 504, Message: "query timed out"}
115 }
116 return &funcapi.FunctionResponse{Status: 500, Message: fmt.Sprintf("slowlog query failed: %v", err)}
117 }
118
119 cs := redisColumnSet(redisAllColumns)
120 sortParam := funcapi.BuildSortParam(redisAllColumns)
121
122 if len(entries) == 0 {
123 return &funcapi.FunctionResponse{
124 Status: 200,
125 Message: "No slow commands found. SLOWLOG may be empty or disabled.",
126 Help: "Slow commands from Redis SLOWLOG. WARNING: Command arguments may contain unmasked literals (potential PII).",
127 Columns: cs.BuildColumns(),
128 Data: [][]any{},
129 DefaultSortColumn: "duration",
130 RequiredParams: []funcapi.ParamConfig{sortParam},
131 ChartingConfig: cs.BuildCharting(),
132 }
133 }
134
135 sortColumn = mapRedisSortColumn(sortColumn)
136 sortRedisSlowLogs(entries, sortColumn)
137
138 if len(entries) > limit {
139 entries = entries[:limit]
140 }
141
142 data := make([][]any, 0, len(entries))
143 for _, entry := range entries {
144 command := strings.Join(entry.Args, " ")
145 commandName := ""
146 if len(entry.Args) > 0 {
147 commandName = entry.Args[0]
148 }
149
150 row := make([]any, len(redisAllColumns))
151 for i, col := range redisAllColumns {
152 switch col.Name {
153 case "id":
154 row[i] = entry.ID
155 case "timestamp":
156 row[i] = entry.Time.Format(time.RFC3339Nano)
157 case "command":
158 row[i] = strmutil.TruncateText(command, redisMaxQueryTextLength)
159 case "command_name":
160 row[i] = commandName
161 case "duration":
162 row[i] = float64(entry.Duration) / float64(time.Millisecond)
163 case "client_addr":
164 row[i] = entry.ClientAddr
165 case "client_name":
166 row[i] = entry.ClientName
167 default:
168 row[i] = nil
169 }
170 }
171 data = append(data, row)
172 }
173
174 return &funcapi.FunctionResponse{
175 Status: 200,
176 Help: "Slow commands from Redis SLOWLOG. WARNING: Command arguments may contain unmasked literals (potential PII).",
177 Columns: cs.BuildColumns(),
178 Data: data,
179 DefaultSortColumn: "duration",
180 RequiredParams: []funcapi.ParamConfig{sortParam},
181 ChartingConfig: cs.BuildCharting(),
182 }
183 }
184
185 func mapRedisSortColumn(col string) string {
186 switch col {
187 case "duration", "timestamp", "id", "command_name":
188 return col
189 default:
190 return "duration"
191 }
192 }
193
194 func sortRedisSlowLogs(entries []redis.SlowLog, sortColumn string) {
195 switch sortColumn {
196 case "timestamp":
197 sort.Slice(entries, func(i, j int) bool {
198 return entries[i].Time.After(entries[j].Time)
199 })
200 case "id":
201 sort.Slice(entries, func(i, j int) bool {
202 return entries[i].ID > entries[j].ID
203 })
204 case "command_name":
205 sort.Slice(entries, func(i, j int) bool {
206 var a, b string
207 if len(entries[i].Args) > 0 {
208 a = entries[i].Args[0]
209 }
210 if len(entries[j].Args) > 0 {
211 b = entries[j].Args[0]
212 }
213 return a < b
214 })
215 default:
216 sort.Slice(entries, func(i, j int) bool {
217 return entries[i].Duration > entries[j].Duration
218 })
219 }
220 }