master
go 614 lines 16.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartengine
4
5 import (
6 "sort"
7 "testing"
8
9 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine/internal/program"
10 "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13 )
14
15 type mapLabelView map[string]string
16
17 func (m mapLabelView) Len() int { return len(m) }
18
19 func (m mapLabelView) Get(key string) (string, bool) {
20 value, ok := m[key]
21 return value, ok
22 }
23
24 func (m mapLabelView) Range(fn func(key, value string) bool) {
25 keys := make([]string, 0, len(m))
26 for key := range m {
27 keys = append(keys, key)
28 }
29 sort.Strings(keys)
30 for _, key := range keys {
31 if !fn(key, m[key]) {
32 return
33 }
34 }
35 }
36
37 func TestCompileScenarios(t *testing.T) {
38 tests := map[string]struct {
39 spec charttpl.Spec
40 rev uint64
41 wantErr bool
42 errLike string
43 assert func(t *testing.T, p *program.Program)
44 }{
45 "compiles dimension options under options block": {
46 spec: charttpl.Spec{
47 Version: charttpl.VersionV1,
48 Groups: []charttpl.Group{
49 {
50 Family: "Service",
51 Metrics: []string{"svc_requests_total"},
52 Charts: []charttpl.Chart{
53 {
54 Title: "Requests",
55 Context: "requests",
56 Units: "requests/s",
57 Dimensions: []charttpl.Dimension{
58 {
59 Selector: "svc_requests_total",
60 Name: "total",
61 Options: &charttpl.DimensionOptions{
62 Hidden: true,
63 Float: true,
64 Multiplier: -8,
65 Divisor: 1000,
66 },
67 },
68 },
69 },
70 },
71 },
72 },
73 },
74 assert: func(t *testing.T, p *program.Program) {
75 t.Helper()
76 charts := p.Charts()
77 require.Len(t, charts, 1)
78 require.Len(t, charts[0].Dimensions, 1)
79 assert.True(t, charts[0].Dimensions[0].Hidden)
80 assert.True(t, charts[0].Dimensions[0].Float)
81 assert.Equal(t, -8, charts[0].Dimensions[0].Multiplier)
82 assert.Equal(t, 1000, charts[0].Dimensions[0].Divisor)
83 },
84 },
85 "applies default lifecycle when omitted": {
86 spec: charttpl.Spec{
87 Version: charttpl.VersionV1,
88 Groups: []charttpl.Group{
89 {
90 Family: "Service",
91 Metrics: []string{"svc_requests_total"},
92 Charts: []charttpl.Chart{
93 {
94 Title: "Requests",
95 Context: "requests",
96 Units: "requests/s",
97 Dimensions: []charttpl.Dimension{
98 {Selector: "svc_requests_total", Name: "total"},
99 },
100 },
101 },
102 },
103 },
104 },
105 assert: func(t *testing.T, p *program.Program) {
106 t.Helper()
107 charts := p.Charts()
108 require.Len(t, charts, 1)
109 assert.Equal(t, 0, charts[0].Lifecycle.MaxInstances)
110 assert.Equal(t, 5, charts[0].Lifecycle.ExpireAfterCycles)
111 assert.Equal(t, 0, charts[0].Lifecycle.Dimensions.MaxDims)
112 assert.Equal(t, 0, charts[0].Lifecycle.Dimensions.ExpireAfterCycles)
113 },
114 },
115 "defaults chart priority when omitted": {
116 spec: charttpl.Spec{
117 Version: charttpl.VersionV1,
118 Groups: []charttpl.Group{
119 {
120 Family: "Service",
121 Metrics: []string{"svc_requests_total"},
122 Charts: []charttpl.Chart{
123 {
124 Title: "Requests",
125 Context: "requests",
126 Units: "requests/s",
127 Dimensions: []charttpl.Dimension{
128 {Selector: "svc_requests_total", Name: "total"},
129 },
130 },
131 },
132 },
133 },
134 },
135 assert: func(t *testing.T, p *program.Program) {
136 t.Helper()
137 charts := p.Charts()
138 require.Len(t, charts, 1)
139 assert.Equal(t, Priority, charts[0].Meta.Priority)
140 },
141 },
142 "preserves explicit chart priority": {
143 spec: charttpl.Spec{
144 Version: charttpl.VersionV1,
145 Groups: []charttpl.Group{
146 {
147 Family: "Service",
148 Metrics: []string{"svc_requests_total"},
149 Charts: []charttpl.Chart{
150 {
151 Title: "Requests",
152 Context: "requests",
153 Units: "requests/s",
154 Priority: Priority + 321,
155 Dimensions: []charttpl.Dimension{
156 {Selector: "svc_requests_total", Name: "total"},
157 },
158 },
159 },
160 },
161 },
162 },
163 assert: func(t *testing.T, p *program.Program) {
164 t.Helper()
165 charts := p.Charts()
166 require.Len(t, charts, 1)
167 assert.Equal(t, Priority+321, charts[0].Meta.Priority)
168 },
169 },
170 "does not apply chart_defaults inheritance during compile": {
171 spec: charttpl.Spec{
172 Version: charttpl.VersionV1,
173 Groups: []charttpl.Group{
174 {
175 Family: "Service",
176 Metrics: []string{"svc_requests_total"},
177 ChartDefaults: &charttpl.ChartDefaults{
178 LabelPromoted: []string{"cluster"},
179 Instances: &charttpl.Instances{
180 ByLabels: []string{"instance"},
181 },
182 },
183 Charts: []charttpl.Chart{
184 {
185 Title: "Requests",
186 Context: "requests",
187 Units: "requests/s",
188 Dimensions: []charttpl.Dimension{
189 {Selector: "svc_requests_total", Name: "total"},
190 },
191 },
192 },
193 },
194 },
195 },
196 assert: func(t *testing.T, p *program.Program) {
197 t.Helper()
198 charts := p.Charts()
199 require.Len(t, charts, 1)
200 assert.Empty(t, charts[0].Labels.PromoteKeys)
201 assert.Empty(t, charts[0].Identity.InstanceByLabels)
202 },
203 },
204 "keeps default chart expiry when lifecycle is present without expire_after_cycles": {
205 spec: charttpl.Spec{
206 Version: charttpl.VersionV1,
207 Groups: []charttpl.Group{
208 {
209 Family: "Service",
210 Metrics: []string{"svc_requests_total"},
211 Charts: []charttpl.Chart{
212 {
213 Title: "Requests",
214 Context: "requests",
215 Units: "requests/s",
216 Lifecycle: &charttpl.Lifecycle{
217 MaxInstances: 32,
218 Dimensions: &charttpl.DimensionLifecycle{
219 MaxDims: 16,
220 },
221 },
222 Dimensions: []charttpl.Dimension{
223 {Selector: "svc_requests_total", Name: "total"},
224 },
225 },
226 },
227 },
228 },
229 },
230 assert: func(t *testing.T, p *program.Program) {
231 t.Helper()
232 charts := p.Charts()
233 require.Len(t, charts, 1)
234 assert.Equal(t, 32, charts[0].Lifecycle.MaxInstances)
235 assert.Equal(t, 5, charts[0].Lifecycle.ExpireAfterCycles)
236 assert.Equal(t, 16, charts[0].Lifecycle.Dimensions.MaxDims)
237 assert.Equal(t, 0, charts[0].Lifecycle.Dimensions.ExpireAfterCycles)
238 },
239 },
240 "overrides default chart expiry when expire_after_cycles is set": {
241 spec: charttpl.Spec{
242 Version: charttpl.VersionV1,
243 Groups: []charttpl.Group{
244 {
245 Family: "Service",
246 Metrics: []string{"svc_requests_total"},
247 Charts: []charttpl.Chart{
248 {
249 Title: "Requests",
250 Context: "requests",
251 Units: "requests/s",
252 Lifecycle: &charttpl.Lifecycle{
253 ExpireAfterCycles: 9,
254 },
255 Dimensions: []charttpl.Dimension{
256 {Selector: "svc_requests_total", Name: "total"},
257 },
258 },
259 },
260 },
261 },
262 },
263 assert: func(t *testing.T, p *program.Program) {
264 t.Helper()
265 charts := p.Charts()
266 require.Len(t, charts, 1)
267 assert.Equal(t, 9, charts[0].Lifecycle.ExpireAfterCycles)
268 },
269 },
270 "compiles nested groups with inherited metric visibility and inferred algorithm": {
271 rev: 42,
272 spec: charttpl.Spec{
273 Version: charttpl.VersionV1,
274 ContextNamespace: "mysql",
275 Groups: []charttpl.Group{
276 {
277 Family: "Database",
278 ContextNamespace: "database",
279 Metrics: []string{"mysql_queries_total"},
280 Groups: []charttpl.Group{
281 {
282 Family: "Throughput",
283 Charts: []charttpl.Chart{
284 {
285 Title: "Queries",
286 Context: "queries_total",
287 Units: "queries/s",
288 Dimensions: []charttpl.Dimension{
289 {
290 Selector: "mysql_queries_total",
291 Name: "total",
292 },
293 },
294 },
295 },
296 },
297 },
298 },
299 },
300 },
301 assert: func(t *testing.T, p *program.Program) {
302 t.Helper()
303 assert.Equal(t, charttpl.VersionV1, p.Version())
304 assert.Equal(t, uint64(42), p.Revision())
305 assert.Equal(t, []string{"mysql_queries_total"}, p.MetricNames())
306
307 charts := p.Charts()
308 require.Len(t, charts, 1)
309 assert.Equal(t, "Database/Throughput", charts[0].Meta.Family)
310 assert.Equal(t, "mysql.database.queries_total", charts[0].Meta.Context)
311 assert.Equal(t, program.AlgorithmIncremental, charts[0].Meta.Algorithm)
312 assert.Equal(t, program.ChartTypeLine, charts[0].Meta.Type)
313 assert.True(t, charts[0].Identity.Static)
314
315 assert.True(t, charts[0].Dimensions[0].Selector.Matcher.Matches("mysql_queries_total", mapLabelView{}))
316 assert.False(t, charts[0].Dimensions[0].Selector.Matcher.Matches("mysql_queries", mapLabelView{}))
317 },
318 },
319 "infer histogram dimension name_from_label and parse instance selectors": {
320 rev: 7,
321 spec: charttpl.Spec{
322 Version: charttpl.VersionV1,
323 Groups: []charttpl.Group{
324 {
325 Family: "Latency",
326 Metrics: []string{"mysql_query_duration_seconds_bucket"},
327 Charts: []charttpl.Chart{
328 {
329 ID: "mysql_query_duration_seconds_bucket",
330 Title: "Query duration buckets",
331 Context: "query_duration_bucket",
332 Units: "observations",
333 Instances: &charttpl.Instances{
334 ByLabels: []string{"*", "!le"},
335 },
336 Dimensions: []charttpl.Dimension{
337 {
338 Selector: "mysql_query_duration_seconds_bucket",
339 },
340 },
341 },
342 },
343 },
344 },
345 },
346 assert: func(t *testing.T, p *program.Program) {
347 t.Helper()
348 charts := p.Charts()
349 require.Len(t, charts, 1)
350 assert.False(t, charts[0].Identity.Static)
351 require.Len(t, charts[0].Identity.InstanceByLabels, 2)
352 assert.True(t, charts[0].Identity.InstanceByLabels[0].IncludeAll)
353 assert.True(t, charts[0].Identity.InstanceByLabels[1].Exclude)
354 assert.Equal(t, "le", charts[0].Identity.InstanceByLabels[1].Key)
355
356 require.Len(t, charts[0].Dimensions, 1)
357 assert.Equal(t, "", charts[0].Dimensions[0].NameFromLabel)
358 assert.True(t, charts[0].Dimensions[0].InferNameFromSeriesMeta)
359 assert.True(t, charts[0].Dimensions[0].Dynamic)
360 assert.Equal(t, program.AlgorithmIncremental, charts[0].Meta.Algorithm)
361 },
362 },
363 "infer stateset dimension naming from runtime series metadata": {
364 rev: 8,
365 spec: charttpl.Spec{
366 Version: charttpl.VersionV1,
367 Groups: []charttpl.Group{
368 {
369 Family: "Replication",
370 Metrics: []string{"system_status"},
371 Charts: []charttpl.Chart{
372 {
373 Title: "Replication state",
374 Context: "replication_state",
375 Units: "state",
376 Dimensions: []charttpl.Dimension{
377 {
378 Selector: "system_status",
379 },
380 },
381 },
382 },
383 },
384 },
385 },
386 assert: func(t *testing.T, p *program.Program) {
387 t.Helper()
388 charts := p.Charts()
389 require.Len(t, charts, 1)
390 require.Len(t, charts[0].Dimensions, 1)
391 assert.Equal(t, "", charts[0].Dimensions[0].NameFromLabel)
392 assert.True(t, charts[0].Dimensions[0].InferNameFromSeriesMeta)
393 assert.Equal(t, program.AlgorithmAbsolute, charts[0].Meta.Algorithm)
394 },
395 },
396 "fails runtime inference for omitted naming on non-inferable selector": {
397 spec: charttpl.Spec{
398 Version: charttpl.VersionV1,
399 Groups: []charttpl.Group{
400 {
401 Family: "CPU",
402 Metrics: []string{"windows_cpu_usage"},
403 Charts: []charttpl.Chart{
404 {
405 Title: "CPU usage",
406 Context: "cpu_usage",
407 Units: "%",
408 Dimensions: []charttpl.Dimension{
409 {
410 Selector: "windows_cpu_usage",
411 },
412 },
413 },
414 },
415 },
416 },
417 },
418 wantErr: true,
419 errLike: "name inference requires inferable selector",
420 },
421 "compiles literal id with instances and label exclusion metadata": {
422 spec: charttpl.Spec{
423 Version: charttpl.VersionV1,
424 Groups: []charttpl.Group{
425 {
426 Family: "Disk",
427 Metrics: []string{"ceph_osd_space_bytes"},
428 Charts: []charttpl.Chart{
429 {
430 ID: "osd_space_usage",
431 Title: "OSD space usage",
432 Context: "osd_space_usage",
433 Units: "bytes",
434 LabelPromoted: []string{"cluster", "cluster", "host"},
435 Instances: &charttpl.Instances{
436 ByLabels: []string{"osd_uuid"},
437 },
438 Dimensions: []charttpl.Dimension{
439 {
440 Selector: `ceph_osd_space_bytes{state="used"}`,
441 NameFromLabel: "state",
442 },
443 },
444 },
445 },
446 },
447 },
448 },
449 assert: func(t *testing.T, p *program.Program) {
450 t.Helper()
451 charts := p.Charts()
452 require.Len(t, charts, 1)
453 require.Len(t, charts[0].Identity.InstanceByLabels, 1)
454 assert.Equal(t, "osd_uuid", charts[0].Identity.InstanceByLabels[0].Key)
455 assert.Equal(t, "Disk", charts[0].Meta.Family)
456 assert.Equal(t, "osd_space_usage", charts[0].Meta.Context)
457
458 assert.Equal(t, program.PromotionModeExplicitIntersection, charts[0].Labels.Mode)
459 assert.Equal(t, []string{"cluster", "host"}, charts[0].Labels.PromoteKeys)
460 assert.Equal(t, []string{"state"}, charts[0].Labels.Exclusions.SelectorConstrainedKeys)
461 assert.Equal(t, []string{"state"}, charts[0].Labels.Exclusions.DimensionKeyLabels)
462 },
463 },
464 "fails on mixed inferred metric kinds without explicit algorithm": {
465 spec: charttpl.Spec{
466 Version: charttpl.VersionV1,
467 Groups: []charttpl.Group{
468 {
469 Family: "Mixed",
470 Metrics: []string{
471 "requests_total",
472 "queue_depth",
473 },
474 Charts: []charttpl.Chart{
475 {
476 Title: "Mixed chart",
477 Context: "mixed",
478 Units: "value",
479 Dimensions: []charttpl.Dimension{
480 {Selector: "requests_total", Name: "req"},
481 {Selector: "queue_depth", Name: "depth"},
482 },
483 },
484 },
485 },
486 },
487 },
488 wantErr: true,
489 errLike: "algorithm inference is ambiguous",
490 },
491 "treats braces as literal characters in chart id": {
492 spec: charttpl.Spec{
493 Version: charttpl.VersionV1,
494 Groups: []charttpl.Group{
495 {
496 Family: "Disk",
497 Metrics: []string{"ceph_osd_space_bytes"},
498 Charts: []charttpl.Chart{
499 {
500 ID: "osd_{osd_uuid}_space_usage",
501 Title: "OSD space usage",
502 Context: "osd_space_usage",
503 Units: "bytes",
504 Dimensions: []charttpl.Dimension{
505 {Selector: "ceph_osd_space_bytes", Name: "used"},
506 },
507 },
508 },
509 },
510 },
511 },
512 assert: func(t *testing.T, p *program.Program) {
513 t.Helper()
514 charts := p.Charts()
515 require.Len(t, charts, 1)
516 assert.Equal(t, "osd_{osd_uuid}_space_usage", charts[0].Identity.IDTemplate.Raw)
517 },
518 },
519 "treats braces as literal characters in dimension name": {
520 spec: charttpl.Spec{
521 Version: charttpl.VersionV1,
522 Groups: []charttpl.Group{
523 {
524 Family: "Disk",
525 Metrics: []string{"ceph_osd_space_bytes"},
526 Charts: []charttpl.Chart{
527 {
528 ID: "osd_space_usage",
529 Title: "OSD space usage",
530 Context: "osd_space_usage",
531 Units: "bytes",
532 Dimensions: []charttpl.Dimension{
533 {Selector: "ceph_osd_space_bytes", Name: "{state}"},
534 },
535 },
536 },
537 },
538 },
539 },
540 assert: func(t *testing.T, p *program.Program) {
541 t.Helper()
542 charts := p.Charts()
543 require.Len(t, charts, 1)
544 require.Len(t, charts[0].Dimensions, 1)
545 assert.Equal(t, "{state}", charts[0].Dimensions[0].NameTemplate.Raw)
546 },
547 },
548 "fails on invalid selector syntax via compiler parse": {
549 spec: charttpl.Spec{
550 Version: charttpl.VersionV1,
551 Groups: []charttpl.Group{
552 {
553 Family: "Service",
554 Metrics: []string{"svc_requests_total"},
555 Charts: []charttpl.Chart{
556 {
557 Title: "Requests",
558 Context: "requests",
559 Units: "requests/s",
560 Dimensions: []charttpl.Dimension{
561 {Selector: `svc_requests_total{method="GET",}`},
562 },
563 },
564 },
565 },
566 },
567 },
568 wantErr: true,
569 errLike: "selector:",
570 },
571 "fails when selector metric is not visible in compile path": {
572 spec: charttpl.Spec{
573 Version: charttpl.VersionV1,
574 Groups: []charttpl.Group{
575 {
576 Family: "Service",
577 Metrics: []string{"svc_requests_total"},
578 Charts: []charttpl.Chart{
579 {
580 Title: "Errors",
581 Context: "errors",
582 Units: "errors/s",
583 Dimensions: []charttpl.Dimension{
584 {Selector: "svc_errors_total", Name: "total"},
585 },
586 },
587 },
588 },
589 },
590 },
591 wantErr: true,
592 errLike: "not visible in current group scope",
593 },
594 }
595
596 for name, tc := range tests {
597 t.Run(name, func(t *testing.T) {
598 compiled, err := Compile(&tc.spec, tc.rev)
599 if tc.wantErr {
600 require.Error(t, err)
601 if tc.errLike != "" {
602 assert.ErrorContains(t, err, tc.errLike)
603 }
604 return
605 }
606
607 require.NoError(t, err)
608 require.NotNil(t, compiled)
609 if tc.assert != nil {
610 tc.assert(t, compiled)
611 }
612 })
613 }
614 }