master
go 448 lines 12.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/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "github.com/netdata/netdata/go/plugins/pkg/metrix"
14 )
15
16 func TestAutogenRouteBuilderScenarios(t *testing.T) {
17 tests := map[string]struct {
18 run func(t *testing.T)
19 }{
20 "build scalar autogen route": {
21 run: runTestBuildScalarAutogenRoute,
22 },
23 "build histogram bucket autogen route": {
24 run: runTestBuildHistogramBucketAutogenRoute,
25 },
26 "build summary quantile autogen route": {
27 run: runTestBuildSummaryQuantileAutogenRoute,
28 },
29 "build state-set autogen route": {
30 run: runTestBuildStateSetAutogenRoute,
31 },
32 "build measure-set autogen route": {
33 run: runTestBuildMeasureSetAutogenRoute,
34 },
35 }
36
37 for name, tc := range tests {
38 t.Run(name, tc.run)
39 }
40 }
41
42 func runTestBuildScalarAutogenRoute(t *testing.T) {
43 tests := map[string]struct {
44 metricName string
45 labels map[string]string
46 meta metrix.SeriesMeta
47 wantID string
48 wantDim string
49 wantAlg program.Algorithm
50 wantUnits string
51 }{
52 "counter metric uses incremental algorithm and static dimension": {
53 metricName: "svc.requests_total",
54 labels: map[string]string{
55 "instance": "db1",
56 "job": "mysql",
57 },
58 meta: metrix.SeriesMeta{Kind: metrix.MetricKindCounter},
59 wantID: "svc.requests_total-instance=db1-job=mysql",
60 wantDim: "requests_total",
61 wantAlg: program.AlgorithmIncremental,
62 wantUnits: "events/s",
63 },
64 "gauge metric uses absolute algorithm": {
65 metricName: "svc.temperature_celsius",
66 labels: map[string]string{
67 "instance": "db1",
68 },
69 meta: metrix.SeriesMeta{Kind: metrix.MetricKindGauge},
70 wantID: "svc.temperature_celsius-instance=db1",
71 wantDim: "temperature_celsius",
72 wantAlg: program.AlgorithmAbsolute,
73 wantUnits: "celsius",
74 },
75 }
76
77 for name, tc := range tests {
78 t.Run(name, func(t *testing.T) {
79 route, ok, err := buildScalarAutogenRoute(
80 tc.metricName,
81 sortedLabelView(tc.labels),
82 tc.meta,
83 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
84 "",
85 )
86 require.NoError(t, err)
87 require.True(t, ok)
88
89 assert.Equal(t, tc.wantID, route.chartID)
90 assert.Equal(t, tc.wantDim, route.dimensionName)
91 assert.Equal(t, tc.wantAlg, route.algorithm)
92 assert.Equal(t, tc.wantUnits, route.units)
93 assert.True(t, route.staticDimension)
94 })
95 }
96 }
97
98 func runTestBuildHistogramBucketAutogenRoute(t *testing.T) {
99 tests := map[string]struct {
100 metricName string
101 labels map[string]string
102 wantID string
103 wantDim string
104 }{
105 "histogram bucket excludes le from chart id and uses bucket dimension": {
106 metricName: "svc.latency_seconds_bucket",
107 labels: map[string]string{
108 "instance": "db1",
109 "le": "0.5",
110 "method": "GET",
111 },
112 wantID: "svc.latency_seconds-instance=db1-method=GET",
113 wantDim: "bucket_0.5",
114 },
115 }
116
117 for name, tc := range tests {
118 t.Run(name, func(t *testing.T) {
119 route, ok, err := buildHistogramBucketAutogenRoute(
120 tc.metricName,
121 sortedLabelView(tc.labels),
122 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
123 "",
124 )
125 require.NoError(t, err)
126 require.True(t, ok)
127
128 assert.Equal(t, tc.wantID, route.chartID)
129 assert.Equal(t, tc.wantDim, route.dimensionName)
130 assert.Equal(t, metrix.HistogramBucketLabel, route.dimensionKeyLabel)
131 assert.Equal(t, program.AlgorithmIncremental, route.algorithm)
132 assert.False(t, route.staticDimension)
133 })
134 }
135 }
136
137 func runTestBuildSummaryQuantileAutogenRoute(t *testing.T) {
138 tests := map[string]struct {
139 metricName string
140 labels map[string]string
141 wantID string
142 wantDim string
143 wantUnits string
144 }{
145 "summary quantile excludes quantile label and keeps absolute algorithm": {
146 metricName: "svc.request_duration_seconds",
147 labels: map[string]string{
148 "instance": "db1",
149 "method": "GET",
150 "quantile": "0.99",
151 },
152 wantID: "svc.request_duration_seconds-instance=db1-method=GET",
153 wantDim: "quantile_0.99",
154 wantUnits: "seconds",
155 },
156 }
157
158 for name, tc := range tests {
159 t.Run(name, func(t *testing.T) {
160 route, ok, err := buildSummaryQuantileAutogenRoute(
161 tc.metricName,
162 sortedLabelView(tc.labels),
163 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
164 "",
165 )
166 require.NoError(t, err)
167 require.True(t, ok)
168
169 assert.Equal(t, tc.wantID, route.chartID)
170 assert.Equal(t, tc.wantDim, route.dimensionName)
171 assert.Equal(t, tc.wantUnits, route.units)
172 assert.Equal(t, metrix.SummaryQuantileLabel, route.dimensionKeyLabel)
173 assert.Equal(t, program.AlgorithmAbsolute, route.algorithm)
174 assert.False(t, route.staticDimension)
175 })
176 }
177 }
178
179 func runTestBuildStateSetAutogenRoute(t *testing.T) {
180 tests := map[string]struct {
181 metricName string
182 labels map[string]string
183 meta metrix.SeriesMeta
184 wantID string
185 wantDim string
186 }{
187 "stateset uses metric-name label as dimension key and excludes it from chart id": {
188 metricName: "service_status",
189 labels: map[string]string{
190 "instance": "db1",
191 "service_status": "ready",
192 },
193 meta: metrix.SeriesMeta{FlattenRole: metrix.FlattenRoleStateSetState},
194 wantID: "service_status-instance=db1",
195 wantDim: "ready",
196 },
197 }
198
199 for name, tc := range tests {
200 t.Run(name, func(t *testing.T) {
201 route, ok, err := buildStateSetAutogenRoute(
202 tc.metricName,
203 sortedLabelView(tc.labels),
204 tc.meta,
205 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
206 "",
207 )
208 require.NoError(t, err)
209 require.True(t, ok)
210
211 assert.Equal(t, tc.wantID, route.chartID)
212 assert.Equal(t, tc.wantDim, route.dimensionName)
213 assert.Equal(t, "service_status", route.dimensionKeyLabel)
214 assert.Equal(t, "state", route.units)
215 assert.Equal(t, program.AlgorithmAbsolute, route.algorithm)
216 assert.False(t, route.staticDimension)
217 })
218 }
219 }
220
221 func runTestBuildMeasureSetAutogenRoute(t *testing.T) {
222 tests := map[string]struct {
223 metricName string
224 labels map[string]string
225 meta metrix.SeriesMeta
226 wantOK bool
227 wantID string
228 wantDim string
229 wantAlg program.Algorithm
230 wantUnits string
231 }{
232 "MeasureSet gauge uses synthetic field label and absolute algorithm": {
233 metricName: "service_latency_seconds_value",
234 labels: map[string]string{
235 "instance": "db1",
236 metrix.MeasureSetFieldLabel: "value",
237 },
238 meta: metrix.SeriesMeta{
239 Kind: metrix.MetricKindGauge,
240 SourceKind: metrix.MetricKindMeasureSet,
241 FlattenRole: metrix.FlattenRoleMeasureSetField,
242 },
243 wantOK: true,
244 wantID: "service_latency_seconds-instance=db1",
245 wantDim: "value",
246 wantAlg: program.AlgorithmAbsolute,
247 wantUnits: "seconds",
248 },
249 "MeasureSet counter uses synthetic field label and incremental algorithm": {
250 metricName: "svc_requests_total_ok",
251 labels: map[string]string{
252 "instance": "db1",
253 metrix.MeasureSetFieldLabel: "ok",
254 },
255 meta: metrix.SeriesMeta{
256 Kind: metrix.MetricKindCounter,
257 SourceKind: metrix.MetricKindMeasureSet,
258 FlattenRole: metrix.FlattenRoleMeasureSetField,
259 },
260 wantOK: true,
261 wantID: "svc_requests_total-instance=db1",
262 wantDim: "ok",
263 wantAlg: program.AlgorithmIncremental,
264 wantUnits: "requests/s",
265 },
266 "MeasureSet ignores unrelated matching labels and uses reserved field label": {
267 metricName: "svc_requests_total_ok",
268 labels: map[string]string{
269 "instance": "db1",
270 "svc_requests": "total_ok",
271 metrix.MeasureSetFieldLabel: "ok",
272 },
273 meta: metrix.SeriesMeta{
274 Kind: metrix.MetricKindCounter,
275 SourceKind: metrix.MetricKindMeasureSet,
276 FlattenRole: metrix.FlattenRoleMeasureSetField,
277 },
278 wantOK: true,
279 wantID: "svc_requests_total-instance=db1-svc_requests=total_ok",
280 wantDim: "ok",
281 wantAlg: program.AlgorithmIncremental,
282 wantUnits: "requests/s",
283 },
284 "MeasureSet without reserved field label does not route": {
285 metricName: "svc_requests_total_ok",
286 labels: map[string]string{
287 "instance": "db1",
288 "svc_requests": "total_ok",
289 },
290 meta: metrix.SeriesMeta{
291 Kind: metrix.MetricKindCounter,
292 SourceKind: metrix.MetricKindMeasureSet,
293 FlattenRole: metrix.FlattenRoleMeasureSetField,
294 },
295 wantOK: false,
296 },
297 "MeasureSet with mismatched reserved field label does not route": {
298 metricName: "svc_requests_total_ok",
299 labels: map[string]string{
300 "instance": "db1",
301 metrix.MeasureSetFieldLabel: "failed",
302 },
303 meta: metrix.SeriesMeta{
304 Kind: metrix.MetricKindCounter,
305 SourceKind: metrix.MetricKindMeasureSet,
306 FlattenRole: metrix.FlattenRoleMeasureSetField,
307 },
308 wantOK: false,
309 },
310 }
311
312 for name, tc := range tests {
313 t.Run(name, func(t *testing.T) {
314 route, ok, err := buildMeasureSetAutogenRoute(
315 tc.metricName,
316 sortedLabelView(tc.labels),
317 tc.meta,
318 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
319 "",
320 )
321 require.NoError(t, err)
322 require.Equal(t, tc.wantOK, ok)
323 if !tc.wantOK {
324 return
325 }
326
327 assert.Equal(t, tc.wantID, route.chartID)
328 assert.Equal(t, tc.wantDim, route.dimensionName)
329 assert.Equal(t, tc.wantAlg, route.algorithm)
330 assert.Equal(t, tc.wantUnits, route.units)
331 assert.Equal(t, metrix.MeasureSetFieldLabel, route.dimensionKeyLabel)
332 assert.False(t, route.staticDimension)
333 })
334 }
335 }
336
337 func TestFitsTypeIDBudget(t *testing.T) {
338 tests := map[string]struct {
339 maxLen int
340 typeIDPrefix string
341 chartID string
342 want bool
343 }{
344 "empty type id at exact limit passes": {
345 maxLen: 5,
346 chartID: "abcde",
347 want: true,
348 },
349 "empty type id over limit fails": {
350 maxLen: 5,
351 chartID: "abcdef",
352 want: false,
353 },
354 "type id includes separator in budget": {
355 maxLen: 16,
356 typeIDPrefix: "collector.job",
357 chartID: "abc",
358 want: false, // len("collector.job")+1+len("abc") == 17 > 16
359 },
360 "type id budget overflow fails": {
361 maxLen: 15,
362 typeIDPrefix: "collector.job",
363 chartID: "abc",
364 want: false,
365 },
366 }
367
368 for name, tc := range tests {
369 t.Run(name, func(t *testing.T) {
370 assert.Equal(t, tc.want, fitsTypeIDBudget(tc.maxLen, tc.typeIDPrefix, tc.chartID))
371 })
372 }
373 }
374
375 func TestGetAutogenChartContext(t *testing.T) {
376 tests := map[string]struct {
377 namespace string
378 metric string
379 want string
380 }{
381 "empty namespace returns bare metric": {namespace: "", metric: "foo", want: "foo"},
382 "single-segment namespace is prefixed": {namespace: "prometheus", metric: "foo", want: "prometheus.foo"},
383 "multi-segment namespace composes with dot": {namespace: "prometheus.app", metric: "foo", want: "prometheus.app.foo"},
384 "whitespace-only namespace is treated empty": {namespace: " ", metric: "foo", want: "foo"},
385 "empty metric falls back to metric": {namespace: "", metric: "", want: "metric"},
386 "namespace with empty metric": {namespace: "prometheus", metric: "", want: "prometheus.metric"},
387 }
388
389 for name, tc := range tests {
390 t.Run(name, func(t *testing.T) {
391 assert.Equal(t, tc.want, getAutogenChartContext(tc.namespace, tc.metric))
392 })
393 }
394 }
395
396 func sortedLabelView(labels map[string]string) metrix.LabelView {
397 items := make([]metrix.Label, 0, len(labels))
398 for key, value := range labels {
399 items = append(items, metrix.Label{Key: key, Value: value})
400 }
401 sort.Slice(items, func(i, j int) bool {
402 return items[i].Key < items[j].Key
403 })
404 return labelSliceView{items: items}
405 }
406
407 func TestBuildScalarAutogenRouteSanitizesDotLabelValues(t *testing.T) {
408 route, ok, err := buildScalarAutogenRoute(
409 "svc.requests_total",
410 sortedLabelView(map[string]string{
411 "instance": "db1.eu",
412 "job": "mysql.prod",
413 }),
414 metrix.SeriesMeta{Kind: metrix.MetricKindCounter},
415 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
416 "",
417 )
418 require.NoError(t, err)
419 require.True(t, ok)
420 assert.Equal(t, "svc.requests_total-instance=db1_eu-job=mysql_prod", route.chartID)
421 }
422
423 func TestBuildScalarAutogenRouteSanitizesLegacyLabelChars(t *testing.T) {
424 tests := []struct {
425 name string
426 value string
427 want string
428 }{
429 {name: "space", value: "a b", want: "a_b"},
430 {name: "backslash", value: "a\\b", want: "a_b"},
431 {name: "apostrophe", value: "a'b", want: "ab"},
432 }
433
434 for _, tc := range tests {
435 t.Run(tc.name, func(t *testing.T) {
436 route, ok, err := buildScalarAutogenRoute(
437 "svc.requests_total",
438 sortedLabelView(map[string]string{"instance": tc.value}),
439 metrix.SeriesMeta{Kind: metrix.MetricKindCounter},
440 AutogenPolicy{Enabled: true, MaxTypeIDLen: defaultMaxTypeIDLen},
441 "",
442 )
443 require.NoError(t, err)
444 require.True(t, ok)
445 assert.Equal(t, "svc.requests_total-instance="+tc.want, route.chartID)
446 })
447 }
448 }