master
go 430 lines 13.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package sql
4
5 import (
6 "errors"
7 "fmt"
8 "regexp"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/pkg/confopt"
12 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/cloudauth"
13 )
14
15 type Config struct {
16 Name string `yaml:"name,omitempty" json:"name,omitempty"`
17 UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
18 AutoDetectionRetry int `yaml:"autodetection_retry,omitempty" json:"autodetection_retry"`
19
20 Driver string `yaml:"driver" json:"driver"`
21 DSN string `yaml:"dsn" json:"dsn"`
22 Timeout confopt.Duration `yaml:"timeout" json:"timeout"`
23 CloudAuth cloudauth.Config `yaml:"cloud_auth" json:"cloud_auth"`
24
25 StaticLabels map[string]string `yaml:"static_labels,omitempty" json:"static_labels"`
26 Queries []ConfigQueryDef `yaml:"queries,omitempty" json:"queries"`
27 Metrics []ConfigMetricBlock `yaml:"metrics,omitempty" json:"metrics"`
28 Functions []ConfigFunction `yaml:"functions,omitempty" json:"functions,omitempty"`
29 FunctionOnly bool `yaml:"function_only,omitempty" json:"function_only,omitempty"`
30 }
31
32 const (
33 defaultFunctionLimit = 100
34 maxFunctionLimit = 10000
35 )
36
37 type ConfigFunction struct {
38 ID string `yaml:"id" json:"id"`
39 Name string `yaml:"name,omitempty" json:"name,omitempty"`
40 Description string `yaml:"description,omitempty" json:"description,omitempty"`
41 Query string `yaml:"query" json:"query"`
42 Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty"`
43 Limit int `yaml:"limit,omitempty" json:"limit,omitempty"`
44 DefaultSort string `yaml:"default_sort,omitempty" json:"default_sort,omitempty"`
45 DefaultSortDesc *bool `yaml:"default_sort_desc,omitempty" json:"default_sort_desc,omitempty"`
46 Columns map[string]ConfigFuncColumn `yaml:"columns,omitempty" json:"columns,omitempty"`
47 }
48
49 type ConfigFuncColumn struct {
50 Type string `yaml:"type,omitempty" json:"type,omitempty"`
51 Units string `yaml:"units,omitempty" json:"units,omitempty"`
52 Tooltip string `yaml:"tooltip,omitempty" json:"tooltip,omitempty"`
53 Visible *bool `yaml:"visible,omitempty" json:"visible,omitempty"`
54 Sortable *bool `yaml:"sortable,omitempty" json:"sortable,omitempty"`
55 }
56
57 func (f *ConfigFunction) derivedName() string {
58 if f.Name != "" {
59 return f.Name
60 }
61 return deriveNameFromID(f.ID)
62 }
63
64 func deriveNameFromID(id string) string {
65 words := strings.Split(strings.ReplaceAll(id, "_", "-"), "-")
66 for i, w := range words {
67 if len(w) > 0 {
68 words[i] = strings.ToUpper(w[:1]) + w[1:]
69 }
70 }
71 return strings.Join(words, " ")
72 }
73
74 type (
75 ConfigQueryDef struct {
76 ID string `yaml:"id" json:"id"`
77 Query string `yaml:"query" json:"query"`
78 }
79
80 ConfigMetricBlock struct {
81 ID string `yaml:"id" json:"id"`
82 QueryRef string `yaml:"query_ref,omitempty" json:"query_ref"`
83 Query string `yaml:"query,omitempty" json:"query"`
84 Mode string `yaml:"mode" json:"mode"` // "columns" | "kv"
85 KVMode *ConfigKVMode `yaml:"kv_mode,omitempty" json:"kv_mode"`
86 LabelsFromRow []ConfigLabelFromRow `yaml:"labels_from_row,omitempty" json:"labels_from_row"`
87 Charts []ConfigChartConfig `yaml:"charts,omitempty" json:"charts"`
88 }
89 ConfigKVMode struct {
90 NameCol string `yaml:"name_col,omitempty" json:"name_col"`
91 ValueCol string `yaml:"value_col,omitempty" json:"value_col"`
92 }
93 ConfigLabelFromRow struct {
94 Source string `yaml:"source" json:"source"`
95 Name string `yaml:"name" json:"name"`
96 //ValueMap map[string]string `yaml:"value_map" json:"value_map"`
97 }
98 )
99
100 type (
101 ConfigChartConfig struct {
102 Title string `yaml:"title" json:"title"`
103 Context string `yaml:"context" json:"context"`
104 Family string `yaml:"family" json:"family"`
105 Type string `yaml:"type" json:"type"`
106 Units string `yaml:"units" json:"units"`
107 Algorithm string `yaml:"algorithm" json:"algorithm"`
108 Dims []ConfigDimConfig `yaml:"dims" json:"dims"`
109 }
110 ConfigDimConfig struct {
111 Name string `yaml:"name" json:"name"`
112 Source string `yaml:"source" json:"source"`
113 StatusWhen *ConfigStatusWhen `yaml:"status_when,omitempty" json:"status_when,omitempty"`
114 }
115 ConfigStatusWhen struct {
116 Equals string `yaml:"equals,omitempty" json:"equals,omitempty"`
117 In []string `yaml:"in,omitempty" json:"in,omitempty"`
118 Match string `yaml:"match,omitempty" json:"match,omitempty"`
119
120 re *regexp.Regexp
121 }
122 )
123
124 func (c *Collector) validateConfig() error {
125 var errs []error
126
127 if c.Driver == "" {
128 errs = append(errs, errors.New("driver required"))
129 } else if !supportedDrivers[c.Driver] {
130 errs = append(errs, fmt.Errorf("unsupported driver %q", c.Driver))
131 }
132 if c.DSN == "" {
133 errs = append(errs, errors.New("dsn required"))
134 }
135 if err := c.CloudAuth.Validate(); err != nil {
136 errs = append(errs, err)
137 }
138 if c.CloudAuth.IsEnabled() {
139 switch c.Driver {
140 case "pgx", "sqlserver", "azuresql":
141 default:
142 errs = append(errs, fmt.Errorf("cloud_auth.provider %q is supported only for drivers %q, %q and %q",
143 c.CloudAuth.ProviderName(),
144 "pgx", "sqlserver", "azuresql"))
145 }
146 }
147
148 if c.FunctionOnly {
149 if len(c.Metrics) > 0 {
150 errs = append(errs, errors.New("function_only is set but metrics are defined"))
151 }
152 if len(c.Functions) == 0 {
153 errs = append(errs, errors.New("function_only is set but no functions defined"))
154 }
155 } else {
156 if len(c.Metrics) == 0 {
157 errs = append(errs, errors.New("metrics required (or set function_only: true)"))
158 }
159 }
160
161 queryIdx := map[string]bool{}
162 for i := range c.Queries {
163 errs = append(errs, c.Queries[i].validate(i, queryIdx)...)
164 }
165
166 for i := range c.Metrics {
167 errs = append(errs, c.Metrics[i].validate(i, queryIdx)...)
168 }
169
170 funcIdx := map[string]bool{}
171 for i := range c.Functions {
172 errs = append(errs, c.Functions[i].validate(i, funcIdx)...)
173 }
174
175 return errors.Join(errs...)
176 }
177
178 func (f *ConfigFunction) validate(idx int, seen map[string]bool) []error {
179 var errs []error
180 fidx := idx + 1
181
182 if f.ID == "" {
183 errs = append(errs, fmt.Errorf("functions[%d] missing id", fidx))
184 }
185 if f.Query == "" {
186 errs = append(errs, fmt.Errorf("functions[%d] missing query", fidx))
187 }
188
189 if f.ID != "" {
190 if strings.Contains(f.ID, ":") {
191 errs = append(errs, fmt.Errorf("functions[%d] id %q cannot contain ':'", fidx, f.ID))
192 }
193 if _, dup := seen[f.ID]; dup {
194 errs = append(errs, fmt.Errorf("functions[%d] duplicate id %q", fidx, f.ID))
195 }
196 seen[f.ID] = true
197 }
198
199 if f.Limit < 0 {
200 errs = append(errs, fmt.Errorf("functions[%d] limit cannot be negative", fidx))
201 }
202 if f.Limit > maxFunctionLimit {
203 errs = append(errs, fmt.Errorf("functions[%d] limit exceeds maximum (%d)", fidx, maxFunctionLimit))
204 }
205 if f.Timeout.Duration() < 0 {
206 errs = append(errs, fmt.Errorf("functions[%d] timeout cannot be negative", fidx))
207 }
208
209 validTypes := map[string]bool{
210 "string": true, "integer": true, "float": true,
211 "boolean": true, "duration": true, "timestamp": true,
212 }
213 for colName, col := range f.Columns {
214 if col.Type != "" && !validTypes[col.Type] {
215 errs = append(errs, fmt.Errorf("functions[%d] column %q invalid type %q", fidx, colName, col.Type))
216 }
217 }
218
219 return errs
220 }
221
222 // ---- Per-struct validation helpers ----
223
224 func (q *ConfigQueryDef) validate(idx int, seen map[string]bool) []error {
225 var errs []error
226 qidx := idx + 1
227
228 if q.ID == "" {
229 errs = append(errs, fmt.Errorf("queries[%d] missing id", qidx))
230 }
231 if q.Query == "" {
232 errs = append(errs, fmt.Errorf("queries[%d] missing query", qidx))
233 }
234
235 if q.ID != "" {
236 if _, dup := seen[q.ID]; dup {
237 errs = append(errs, fmt.Errorf("queries[%d] duplicate id %q", qidx, q.ID))
238 }
239 seen[q.ID] = true
240 }
241
242 return errs
243 }
244
245 func (m *ConfigMetricBlock) validate(idx int, queryIdx map[string]bool) []error {
246 var errs []error
247 midx := idx + 1
248
249 if m.ID == "" {
250 errs = append(errs, fmt.Errorf("metrics[%d] missing id", midx))
251 }
252
253 hasRef := strings.TrimSpace(m.QueryRef) != ""
254 hasInline := strings.TrimSpace(m.Query) != ""
255 switch {
256 case hasRef && hasInline:
257 errs = append(errs, fmt.Errorf("metrics[%d] must set exactly one of query_ref or query, not both", midx))
258 case !hasRef && !hasInline:
259 errs = append(errs, fmt.Errorf("metrics[%d] must set exactly one of query_ref or query", midx))
260 case hasRef:
261 if _, ok := queryIdx[m.QueryRef]; !ok {
262 errs = append(errs, fmt.Errorf("metrics[%d] query_ref %q not found in queries", midx, m.QueryRef))
263 }
264 }
265
266 mode := strings.ToLower(strings.TrimSpace(m.Mode))
267 switch mode {
268 case "kv":
269 if m.KVMode == nil {
270 errs = append(errs, fmt.Errorf("metrics[%d] kv mode requires kv_mode to be defined", midx))
271 } else {
272 if strings.TrimSpace(m.KVMode.NameCol) == "" {
273 errs = append(errs, fmt.Errorf("metrics[%d] kv_mode.name_col is required in kv mode", midx))
274 }
275 if strings.TrimSpace(m.KVMode.ValueCol) == "" {
276 errs = append(errs, fmt.Errorf("metrics[%d] kv_mode.value_col is required in kv mode", midx))
277 }
278 if strings.EqualFold(m.KVMode.NameCol, m.KVMode.ValueCol) {
279 errs = append(errs, fmt.Errorf("metrics[%d] kv_mode.name_col must differ from kv_mode.value_col", midx))
280 }
281 }
282 case "columns", "":
283 default:
284 errs = append(errs, fmt.Errorf("metrics[%d] invalid mode %q (expected: columns|kv)", midx, m.Mode))
285 }
286
287 labelCols := map[string]bool{}
288 for j := range m.LabelsFromRow {
289 errs = append(errs, m.LabelsFromRow[j].validate(midx, j, labelCols)...)
290 }
291
292 if len(m.Charts) == 0 {
293 errs = append(errs, fmt.Errorf("metrics[%d].%s missing charts", midx, m.ID))
294 }
295
296 for k := range m.Charts {
297 errs = append(errs, m.Charts[k].validate(idx, m.ID, k, mode, labelCols, m.KVMode)...)
298 }
299
300 return errs
301 }
302
303 func (lf *ConfigLabelFromRow) validate(metricIdx, lfIdx int, labelCols map[string]bool) []error {
304 var errs []error
305 midx := metricIdx + 1
306 lidx := lfIdx + 1
307
308 if lf.Source == "" {
309 errs = append(errs, fmt.Errorf("metrics[%d].labels_from_row[%d] missing source", midx, lidx))
310 }
311 if lf.Name == "" {
312 errs = append(errs, fmt.Errorf("metrics[%d].labels_from_row[%d] missing name", midx, lidx))
313 }
314 if lf.Source != "" {
315 labelCols[strings.ToLower(lf.Source)] = true
316 }
317
318 return errs
319 }
320
321 func (ch *ConfigChartConfig) validate(metricIdx int, metricID string, chartIdx int, mode string, labelCols map[string]bool, kv *ConfigKVMode) []error {
322 var errs []error
323 midx := metricIdx + 1
324 cidx := chartIdx + 1
325
326 if strings.TrimSpace(ch.Context) == "" {
327 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d] missing context", midx, cidx))
328 }
329 if strings.TrimSpace(ch.Title) == "" {
330 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d] missing title", midx, cidx))
331 }
332 if strings.TrimSpace(ch.Family) == "" {
333 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d] missing family", midx, cidx))
334 }
335 if strings.TrimSpace(ch.Units) == "" {
336 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d] missing units", midx, cidx))
337 }
338
339 seenDims := map[string]bool{}
340 for d := range ch.Dims {
341 errs = append(errs, ch.Dims[d].validate(metricIdx, metricID, chartIdx, d, mode, labelCols, kv, seenDims)...)
342 }
343
344 return errs
345 }
346
347 func (dm *ConfigDimConfig) validate(
348 metricIdx int,
349 metricID string,
350 chartIdx int,
351 dimIdx int,
352 mode string,
353 labelCols map[string]bool,
354 kv *ConfigKVMode,
355 seenDims map[string]bool,
356 ) []error {
357 var errs []error
358 midx := metricIdx + 1
359 cidx := chartIdx + 1
360 didx := dimIdx + 1
361
362 if strings.TrimSpace(dm.Name) == "" {
363 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d] missing name", midx, cidx, didx))
364 }
365 if strings.TrimSpace(dm.Source) == "" {
366 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d] missing source", midx, cidx, didx))
367 }
368
369 // unique dim names (case-insensitive)
370 if dm.Name != "" {
371 key := strings.ToLower(dm.Name)
372 if _, ok := seenDims[key]; ok {
373 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d] duplicate dim name %q", midx, cidx, dm.Name))
374 } else {
375 seenDims[key] = true
376 }
377 }
378
379 if dm.StatusWhen != nil {
380 errs = append(errs, dm.StatusWhen.validate(metricIdx, metricID, chartIdx, dimIdx)...)
381 }
382
383 switch mode {
384 case "columns":
385 if _, clash := labelCols[strings.ToLower(dm.Source)]; clash {
386 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d] source %q conflicts with labels_from_row source", midx, cidx, didx, dm.Source))
387 }
388 case "kv":
389 // In kv mode, dim.source must be a KEY name (not a column).
390 if kv != nil && strings.EqualFold(dm.Source, kv.ValueCol) {
391 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d] source %q equals kv_mode.value_col; dim.source must be a KEY name", midx, cidx, didx, dm.Source))
392 }
393 }
394
395 return errs
396 }
397
398 func (sw *ConfigStatusWhen) validate(metricIdx int, metricID string, chartIdx, dimIdx int) []error {
399 var errs []error
400 midx := metricIdx + 1
401 cidx := chartIdx + 1
402 didx := dimIdx + 1
403
404 count := 0
405 if sw.Equals != "" {
406 count++
407 }
408 if len(sw.In) > 0 {
409 count++
410 }
411 if strings.TrimSpace(sw.Match) != "" {
412 re, err := regexp.Compile(sw.Match)
413 if err != nil {
414 errs = append(errs, fmt.Errorf("invalid regex in status_when.match for metric %q: %w", metricID, err))
415 } else {
416 // store compiled regex for later use
417 sw.re = re
418 }
419 count++
420 }
421
422 if count == 0 {
423 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d].status_when must have exactly one of equals|in|match", midx, cidx, didx))
424 }
425 if count > 1 {
426 errs = append(errs, fmt.Errorf("metrics[%d].charts[%d].dims[%d].status_when must not set multiple selectors", midx, cidx, didx))
427 }
428
429 return errs
430 }