master
go 2,536 lines 73.5 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartengine
4
5 import (
6 "math"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "github.com/netdata/netdata/go/plugins/pkg/metrix"
13 metrixselector "github.com/netdata/netdata/go/plugins/pkg/metrix/selector"
14 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine/internal/program"
15 )
16
17 func TestInferDimensionLabelKeyScenarios(t *testing.T) {
18 tests := map[string]struct {
19 metricName string
20 meta metrix.SeriesMeta
21 wantKey string
22 wantOK bool
23 wantErr bool
24 }{
25 "histogram bucket uses le label": {
26 meta: metrix.SeriesMeta{
27 FlattenRole: metrix.FlattenRoleHistogramBucket,
28 },
29 wantKey: "le",
30 wantOK: true,
31 },
32 "summary quantile uses quantile label": {
33 meta: metrix.SeriesMeta{
34 FlattenRole: metrix.FlattenRoleSummaryQuantile,
35 },
36 wantKey: "quantile",
37 wantOK: true,
38 },
39 "stateset uses metric family name label": {
40 metricName: "system_status",
41 meta: metrix.SeriesMeta{
42 FlattenRole: metrix.FlattenRoleStateSetState,
43 },
44 wantKey: "system_status",
45 wantOK: true,
46 },
47 "histogram count does not infer dynamic key": {
48 meta: metrix.SeriesMeta{
49 FlattenRole: metrix.FlattenRoleHistogramCount,
50 },
51 wantOK: false,
52 },
53 "non flattened role is an inference error": {
54 meta: metrix.SeriesMeta{
55 FlattenRole: metrix.FlattenRoleNone,
56 },
57 wantErr: true,
58 },
59 }
60
61 for name, tc := range tests {
62 t.Run(name, func(t *testing.T) {
63 key, ok, err := inferDimensionLabelKey(tc.metricName, tc.meta)
64 if tc.wantErr {
65 require.Error(t, err)
66 return
67 }
68 require.NoError(t, err)
69 assert.Equal(t, tc.wantKey, key)
70 assert.Equal(t, tc.wantOK, ok)
71 })
72 }
73 }
74
75 func TestBuildPlanResolvesInferDimensionNames(t *testing.T) {
76 tests := map[string]struct {
77 yaml string
78 setup func(t *testing.T, s metrix.CollectorStore)
79 wantNames []string
80 wantKinds []ActionKind
81 }{
82 "histogram bucket inference resolves bucket names from le": {
83 yaml: `
84 version: v1
85 groups:
86 - family: Latency
87 metrics:
88 - svc.latency_seconds_bucket
89 charts:
90 - title: Latency buckets
91 context: latency_bucket
92 units: observations
93 dimensions:
94 - selector: svc.latency_seconds_bucket
95 `,
96 setup: func(t *testing.T, s metrix.CollectorStore) {
97 t.Helper()
98 cc := mustCycleController(t, s)
99 h := s.Write().SnapshotMeter("svc").Histogram("latency_seconds", metrix.WithHistogramBounds(1, 2))
100 cc.BeginCycle()
101 h.ObservePoint(metrix.HistogramPoint{
102 Count: 2,
103 Sum: 3,
104 Buckets: []metrix.BucketPoint{
105 {UpperBound: 1, CumulativeCount: 1},
106 {UpperBound: 2, CumulativeCount: 2},
107 },
108 })
109 cc.CommitCycleSuccess()
110 },
111 wantNames: []string{"+Inf", "1", "2"},
112 wantKinds: []ActionKind{ActionCreateChart, ActionCreateDimension, ActionCreateDimension, ActionCreateDimension, ActionUpdateChart},
113 },
114 "summary quantile inference resolves quantile labels": {
115 yaml: `
116 version: v1
117 groups:
118 - family: Latency
119 metrics:
120 - svc.request_time
121 charts:
122 - title: Request time quantiles
123 context: request_time_quantile
124 units: seconds
125 dimensions:
126 - selector: svc.request_time{quantile=~".+"}
127 `,
128 setup: func(t *testing.T, s metrix.CollectorStore) {
129 t.Helper()
130 cc := mustCycleController(t, s)
131 sm := s.Write().SnapshotMeter("svc")
132 sum := sm.Summary("request_time", metrix.WithSummaryQuantiles(0.5, 0.9))
133
134 cc.BeginCycle()
135 sum.ObservePoint(metrix.SummaryPoint{
136 Count: 10,
137 Sum: 8.8,
138 Quantiles: []metrix.QuantilePoint{
139 {Quantile: 0.5, Value: 0.4},
140 {Quantile: 0.9, Value: 1.2},
141 },
142 })
143 cc.CommitCycleSuccess()
144 },
145 wantNames: []string{"0.5", "0.9"},
146 wantKinds: []ActionKind{ActionCreateChart, ActionCreateDimension, ActionCreateDimension, ActionUpdateChart},
147 },
148 "stateset inference resolves state names from metric-family label key": {
149 yaml: `
150 version: v1
151 groups:
152 - family: Service
153 metrics:
154 - system.status
155 charts:
156 - title: System status
157 context: system_status
158 units: state
159 dimensions:
160 - selector: system.status
161 `,
162 setup: func(t *testing.T, s metrix.CollectorStore) {
163 t.Helper()
164 cc := mustCycleController(t, s)
165 ss := s.Write().SnapshotMeter("system").StateSet(
166 "status",
167 metrix.WithStateSetStates("ok", "failed"),
168 metrix.WithStateSetMode(metrix.ModeEnum),
169 )
170 cc.BeginCycle()
171 ss.Enable("ok")
172 cc.CommitCycleSuccess()
173 },
174 wantNames: []string{"failed", "ok"},
175 wantKinds: []ActionKind{ActionCreateChart, ActionCreateDimension, ActionCreateDimension, ActionUpdateChart},
176 },
177 }
178
179 for name, tc := range tests {
180 t.Run(name, func(t *testing.T) {
181 e, err := New()
182 require.NoError(t, err)
183 require.NoError(t, e.LoadYAML([]byte(tc.yaml), 1))
184
185 store := metrix.NewCollectorStore()
186 tc.setup(t, store)
187
188 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
189 require.NoError(t, err)
190
191 got := make([]string, 0, len(plan.InferredDimensions))
192 for _, dim := range plan.InferredDimensions {
193 got = append(got, dim.Name)
194 }
195 assert.Equal(t, tc.wantNames, got)
196 assert.Equal(t, tc.wantKinds, actionKinds(plan.Actions))
197 })
198 }
199 }
200
201 func TestBuildPlanLegacySingleScenarioCases(t *testing.T) {
202 tests := map[string]struct {
203 run func(t *testing.T)
204 }{
205 "BuildPlanRequiresFlattenedReaderForInference": {run: runTestBuildPlanRequiresFlattenedReaderForInference},
206 "BuildPlanUsesRouteCacheReuse": {run: runTestBuildPlanUsesRouteCacheReuse},
207 "BuildPlanLifecycleDimensionExpiry": {run: runTestBuildPlanLifecycleDimensionExpiry},
208 "BuildPlanLifecycleChartExpiry": {run: runTestBuildPlanLifecycleChartExpiry},
209 "BuildPlanLifecycleNoRemovalOnFailedCycle": {run: runTestBuildPlanLifecycleNoRemovalOnFailedCycle},
210 "BuildPlanRendersChartIDsFromInstances": {run: runTestBuildPlanRendersChartIDsFromInstances},
211 "BuildPlanEnforcesMaxInstancesDeterministically": {run: runTestBuildPlanEnforcesMaxInstancesDeterministically},
212 "BuildPlanEnforcesMaxDimsDeterministically": {run: runTestBuildPlanEnforcesMaxDimsDeterministically},
213 "BuildPlanComputesChartLabelsIntersectionAndExclusions": {run: runTestBuildPlanComputesChartLabelsIntersectionAndExclusions},
214 "BuildPlanAutogenDisabledSkipsUnmatchedSeries": {run: runTestBuildPlanAutogenDisabledSkipsUnmatchedSeries},
215 "BuildPlanEnginePolicySelectorFiltersSeriesBeforeRouting": {run: runTestBuildPlanEnginePolicySelectorFiltersSeriesBeforeRouting},
216 "BuildPlanTemplateEnginePolicyControlsSelectorAndAutogen": {run: runTestBuildPlanTemplateEnginePolicyControlsSelectorAndAutogen},
217 "BuildPlanEnginePolicyOptionOverridesTemplatePolicy": {run: runTestBuildPlanEnginePolicyOptionOverridesTemplatePolicy},
218 "BuildPlanAutogenOptionKeepsTemplateSelector": {run: runTestBuildPlanAutogenOptionKeepsTemplateSelector},
219 "BuildPlanAutogenCreatesChartForUnmatchedScalar": {run: runTestBuildPlanAutogenCreatesChartForUnmatchedScalar},
220 "BuildPlanAutogenUsesMetricMetadataForScalar": {run: runTestBuildPlanAutogenUsesMetricMetadataForScalar},
221 "BuildPlanAutogenUsesMetricPriorityMetadataForScalar": {run: runTestBuildPlanAutogenUsesMetricPriorityMetadataForScalar},
222 "BuildPlanAutogenUsesMetricMetadataForHistogram": {run: runTestBuildPlanAutogenUsesMetricMetadataForHistogram},
223 "BuildPlanAutogenUsesMetricFloatMetadataForScalar": {run: runTestBuildPlanAutogenUsesMetricFloatMetadataForScalar},
224 "BuildPlanAutogenUsesMetricMetadataForSummaryWithoutQuantiles": {run: runTestBuildPlanAutogenUsesMetricMetadataForSummaryWithoutQuantiles},
225 "BuildPlanTemplatePrecedenceOverAutogen": {run: runTestBuildPlanTemplatePrecedenceOverAutogen},
226 "BuildPlanAutogenStrictOverflowDrop": {run: runTestBuildPlanAutogenStrictOverflowDrop},
227 "BuildPlanAutogenUsesFlattenMetadataForHistogramBuckets": {run: runTestBuildPlanAutogenUsesFlattenMetadataForHistogramBuckets},
228 "BuildPlanAutogenCreatesChartForUnmatchedGauge": {run: runTestBuildPlanAutogenCreatesChartForUnmatchedGauge},
229 "BuildPlanAutogenCreatesChartForUnmatchedStateSet": {run: runTestBuildPlanAutogenCreatesChartForUnmatchedStateSet},
230 "BuildPlanAutogenKeepsStateSetUnitsWhenMetricMetaUnitIsSet": {run: runTestBuildPlanAutogenKeepsStateSetUnitsWhenMetricMetaUnitIsSet},
231 "BuildPlanAutogenCreatesChartForUnmatchedMeasureSetGauge": {run: runTestBuildPlanAutogenCreatesChartForUnmatchedMeasureSetGauge},
232 "BuildPlanAutogenCreatesChartForUnmatchedMeasureSetCounter": {run: runTestBuildPlanAutogenCreatesChartForUnmatchedMeasureSetCounter},
233 "BuildPlanTemplateWinsOnAutogenChartIDCollisionAcrossSeries": {run: runTestBuildPlanTemplateWinsOnAutogenChartIDCollisionAcrossSeries},
234 "BuildPlanAutogenRemovalLifecycleExpiry": {run: runTestBuildPlanAutogenRemovalLifecycleExpiry},
235 "BuildPlanFirstWriterWinsAndAccumulatesRepeatedRoutes": {run: runTestBuildPlanFirstWriterWinsAndAccumulatesRepeatedRoutes},
236 "BuildPlanEmptyEmissionAndScratchReusePruneAcrossCycles": {run: runTestBuildPlanEmptyEmissionAndScratchReusePruneAcrossCycles},
237 "BuildPlanAutogenContextNamespacePrefixesContext": {run: runTestBuildPlanAutogenContextNamespacePrefixesContext},
238 "BuildPlanAutogenContextNamespaceStubGroupOnly": {run: runTestBuildPlanAutogenContextNamespaceStubGroupOnly},
239 "BuildPlanSummaryNaNQuantileGaps": {run: runTestBuildPlanSummaryNaNQuantileGaps},
240 "BuildPlanSummaryMixedFiniteNaNQuantileGaps": {run: runTestBuildPlanSummaryMixedFiniteNaNQuantileGaps},
241 }
242
243 for name, tc := range tests {
244 t.Run(name, tc.run)
245 }
246 }
247
248 func runTestBuildPlanRequiresFlattenedReaderForInference(t *testing.T) {
249 e, err := New()
250 require.NoError(t, err)
251
252 yaml := `
253 version: v1
254 groups:
255 - family: Service
256 metrics:
257 - system.status
258 charts:
259 - title: System status
260 context: system_status
261 units: state
262 dimensions:
263 - selector: system.status
264 `
265 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
266
267 store := metrix.NewCollectorStore()
268 cc := mustCycleController(t, store)
269 ss := store.Write().SnapshotMeter("system").StateSet(
270 "status",
271 metrix.WithStateSetStates("ok", "failed"),
272 metrix.WithStateSetMode(metrix.ModeEnum),
273 )
274
275 cc.BeginCycle()
276 ss.Enable("ok")
277 cc.CommitCycleSuccess()
278
279 _, err = buildPlan(e, store.Read())
280 require.Error(t, err)
281 assert.ErrorContains(t, err, "Read(metrix.ReadFlatten())")
282
283 _, err = buildPlan(e, store.Read(metrix.ReadFlatten()))
284 require.NoError(t, err)
285 }
286
287 func runTestBuildPlanUsesRouteCacheReuse(t *testing.T) {
288 e, err := New()
289 require.NoError(t, err)
290
291 yaml := `
292 version: v1
293 groups:
294 - family: Service
295 metrics:
296 - svc.requests_total
297 charts:
298 - title: Requests
299 context: requests
300 units: requests/s
301 dimensions:
302 - selector: svc.requests_total
303 name: total
304 `
305 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
306
307 store := metrix.NewCollectorStore()
308 cc := mustCycleController(t, store)
309 c := store.Write().SnapshotMeter("svc").Counter("requests_total")
310
311 cc.BeginCycle()
312 c.ObserveTotal(10)
313 cc.CommitCycleSuccess()
314
315 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
316 require.NoError(t, err)
317 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
318 stats1 := e.stats()
319 assert.Equal(t, uint64(0), stats1.RouteCacheHits)
320 assert.Equal(t, uint64(1), stats1.RouteCacheMisses)
321 require.NotNil(t, findUpdateAction(plan1))
322 assert.Equal(t, float64(10), findUpdateAction(plan1).Values[0].Float64)
323
324 cc.BeginCycle()
325 c.ObserveTotal(20)
326 cc.CommitCycleSuccess()
327
328 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
329 require.NoError(t, err)
330 assert.Equal(t, []ActionKind{ActionUpdateChart}, actionKinds(plan2.Actions))
331 stats2 := e.stats()
332 assert.Equal(t, uint64(1), stats2.RouteCacheHits)
333 assert.Equal(t, uint64(1), stats2.RouteCacheMisses)
334 require.NotNil(t, findUpdateAction(plan2))
335 assert.Equal(t, float64(20), findUpdateAction(plan2).Values[0].Float64)
336 }
337
338 func runTestBuildPlanLifecycleDimensionExpiry(t *testing.T) {
339 e, err := New()
340 require.NoError(t, err)
341
342 yaml := `
343 version: v1
344 groups:
345 - family: Service
346 metrics:
347 - svc.total
348 - svc.mode_metric
349 charts:
350 - title: Service status
351 context: service_status
352 units: state
353 lifecycle:
354 dimensions:
355 expire_after_cycles: 1
356 dimensions:
357 - selector: svc.total
358 name: total
359 - selector: svc.mode_metric
360 name_from_label: mode
361 `
362 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
363
364 store := metrix.NewCollectorStore()
365 cc := mustCycleController(t, store)
366 sm := store.Write().SnapshotMeter("svc")
367 total := sm.Gauge("total")
368 modeMetric := sm.Gauge("mode_metric")
369 modeOK := sm.LabelSet(metrix.Label{Key: "mode", Value: "ok"})
370
371 cc.BeginCycle()
372 total.Observe(100)
373 modeMetric.Observe(1, modeOK)
374 cc.CommitCycleSuccess()
375
376 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
377 require.NoError(t, err)
378 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
379
380 cc.BeginCycle()
381 total.Observe(101)
382 cc.CommitCycleSuccess()
383
384 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
385 require.NoError(t, err)
386 assert.Equal(t, []ActionKind{ActionUpdateChart, ActionRemoveDimension}, actionKinds(plan2.Actions))
387 removeDim := findRemoveDimensionAction(plan2)
388 require.NotNil(t, removeDim)
389 assert.Equal(t, "ok", removeDim.Name)
390 }
391
392 func runTestBuildPlanLifecycleChartExpiry(t *testing.T) {
393 e, err := New()
394 require.NoError(t, err)
395
396 yaml := `
397 version: v1
398 groups:
399 - family: Service
400 metrics:
401 - svc.requests_total
402 charts:
403 - title: Requests
404 context: requests
405 units: requests/s
406 lifecycle:
407 expire_after_cycles: 1
408 dimensions:
409 - selector: svc.requests_total
410 name: total
411 `
412 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
413
414 store := metrix.NewCollectorStore()
415 cc := mustCycleController(t, store)
416 c := store.Write().SnapshotMeter("svc").Counter("requests_total")
417
418 cc.BeginCycle()
419 c.ObserveTotal(10)
420 cc.CommitCycleSuccess()
421
422 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
423 require.NoError(t, err)
424 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
425
426 cc.BeginCycle()
427 cc.CommitCycleSuccess()
428
429 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
430 require.NoError(t, err)
431 assert.Equal(t, []ActionKind{ActionRemoveChart}, actionKinds(plan2.Actions))
432 }
433
434 func runTestBuildPlanLifecycleNoRemovalOnFailedCycle(t *testing.T) {
435 e, err := New()
436 require.NoError(t, err)
437
438 yaml := `
439 version: v1
440 groups:
441 - family: Service
442 metrics:
443 - svc.requests_total
444 charts:
445 - title: Requests
446 context: requests
447 units: requests/s
448 lifecycle:
449 expire_after_cycles: 1
450 dimensions:
451 - selector: svc.requests_total
452 name: total
453 `
454 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
455
456 store := metrix.NewCollectorStore()
457 cc := mustCycleController(t, store)
458 c := store.Write().SnapshotMeter("svc").Counter("requests_total")
459
460 cc.BeginCycle()
461 c.ObserveTotal(10)
462 cc.CommitCycleSuccess()
463
464 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
465 require.NoError(t, err)
466 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
467
468 cc.BeginCycle()
469 cc.AbortCycle()
470
471 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
472 require.NoError(t, err)
473 assert.Empty(t, plan2.Actions)
474
475 cc.BeginCycle()
476 cc.CommitCycleSuccess()
477
478 plan3, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
479 require.NoError(t, err)
480 assert.Equal(t, []ActionKind{ActionRemoveChart}, actionKinds(plan3.Actions))
481 }
482
483 func runTestBuildPlanRendersChartIDsFromInstances(t *testing.T) {
484 e, err := New()
485 require.NoError(t, err)
486
487 yaml := `
488 version: v1
489 groups:
490 - family: Net
491 metrics:
492 - windows_net_bytes_received_total
493 charts:
494 - id: win_nic_traffic
495 title: NIC traffic
496 context: nic_traffic
497 units: bytes/s
498 instances:
499 by_labels: [nic]
500 dimensions:
501 - selector: windows_net_bytes_received_total
502 name: received
503 `
504 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
505
506 store := metrix.NewCollectorStore()
507 cc := mustCycleController(t, store)
508 sm := store.Write().SnapshotMeter("")
509 rx := sm.Counter("windows_net_bytes_received_total")
510
511 eth0 := sm.LabelSet(metrix.Label{Key: "nic", Value: "eth0"})
512 eth1 := sm.LabelSet(metrix.Label{Key: "nic", Value: "eth1"})
513
514 cc.BeginCycle()
515 rx.ObserveTotal(10, eth1)
516 rx.ObserveTotal(20, eth0)
517 cc.CommitCycleSuccess()
518
519 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
520 require.NoError(t, err)
521 assert.Equal(t, []ActionKind{
522 ActionCreateChart, ActionCreateDimension, ActionUpdateChart,
523 ActionCreateChart, ActionCreateDimension, ActionUpdateChart,
524 }, actionKinds(plan1.Actions))
525
526 createChartIDs := make([]string, 0, 2)
527 createChartLabels := make(map[string]map[string]string, 2)
528 updateChartIDs := make([]string, 0, 2)
529 for _, action := range plan1.Actions {
530 switch v := action.(type) {
531 case CreateChartAction:
532 createChartIDs = append(createChartIDs, v.ChartID)
533 createChartLabels[v.ChartID] = v.Labels
534 case UpdateChartAction:
535 updateChartIDs = append(updateChartIDs, v.ChartID)
536 }
537 }
538 assert.Equal(t, []string{"win_nic_traffic_eth0", "win_nic_traffic_eth1"}, createChartIDs)
539 assert.Equal(t, "eth0", createChartLabels["win_nic_traffic_eth0"]["nic"])
540 assert.Equal(t, "eth1", createChartLabels["win_nic_traffic_eth1"]["nic"])
541 assert.Equal(t, []string{"win_nic_traffic_eth0", "win_nic_traffic_eth1"}, updateChartIDs)
542
543 cc.BeginCycle()
544 rx.ObserveTotal(21, eth0)
545 rx.ObserveTotal(11, eth1)
546 cc.CommitCycleSuccess()
547
548 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
549 require.NoError(t, err)
550 assert.Equal(t, []ActionKind{ActionUpdateChart, ActionUpdateChart}, actionKinds(plan2.Actions))
551 }
552
553 func runTestBuildPlanEnforcesMaxInstancesDeterministically(t *testing.T) {
554 e, err := New()
555 require.NoError(t, err)
556
557 yaml := `
558 version: v1
559 groups:
560 - family: Net
561 metrics:
562 - windows_net_bytes_received_total
563 charts:
564 - id: win_nic_traffic
565 title: NIC traffic
566 context: nic_traffic
567 units: bytes/s
568 lifecycle:
569 max_instances: 1
570 instances:
571 by_labels: [nic]
572 dimensions:
573 - selector: windows_net_bytes_received_total
574 name: received
575 `
576 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
577
578 store := metrix.NewCollectorStore()
579 cc := mustCycleController(t, store)
580 sm := store.Write().SnapshotMeter("")
581 rx := sm.Counter("windows_net_bytes_received_total")
582
583 eth0 := sm.LabelSet(metrix.Label{Key: "nic", Value: "eth0"})
584 eth1 := sm.LabelSet(metrix.Label{Key: "nic", Value: "eth1"})
585
586 cc.BeginCycle()
587 rx.ObserveTotal(10, eth0)
588 cc.CommitCycleSuccess()
589
590 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
591 require.NoError(t, err)
592 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
593
594 cc.BeginCycle()
595 rx.ObserveTotal(11, eth0)
596 rx.ObserveTotal(20, eth1)
597 cc.CommitCycleSuccess()
598
599 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
600 require.NoError(t, err)
601 // eth0 exists and is seen, so eth1 is dropped under max_instances=1.
602 assert.Equal(t, []ActionKind{ActionUpdateChart}, actionKinds(plan2.Actions))
603 update2 := findUpdateAction(plan2)
604 require.NotNil(t, update2)
605 assert.Equal(t, "win_nic_traffic_eth0", update2.ChartID)
606
607 cc.BeginCycle()
608 rx.ObserveTotal(21, eth1)
609 cc.CommitCycleSuccess()
610
611 plan3, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
612 require.NoError(t, err)
613 assert.Equal(t, []ActionKind{
614 ActionRemoveChart,
615 ActionCreateChart,
616 ActionCreateDimension,
617 ActionUpdateChart,
618 }, actionKinds(plan3.Actions))
619 }
620
621 func runTestBuildPlanEnforcesMaxDimsDeterministically(t *testing.T) {
622 e, err := New()
623 require.NoError(t, err)
624
625 yaml := `
626 version: v1
627 groups:
628 - family: Service
629 metrics:
630 - svc_mode
631 charts:
632 - id: service_mode
633 title: Service mode
634 context: service_mode
635 units: state
636 lifecycle:
637 dimensions:
638 max_dims: 2
639 dimensions:
640 - selector: svc_mode
641 name_from_label: mode
642 `
643 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
644
645 store := metrix.NewCollectorStore()
646 cc := mustCycleController(t, store)
647 sm := store.Write().SnapshotMeter("")
648 g := sm.Gauge("svc_mode")
649
650 modeA := sm.LabelSet(metrix.Label{Key: "mode", Value: "a"})
651 modeB := sm.LabelSet(metrix.Label{Key: "mode", Value: "b"})
652 modeC := sm.LabelSet(metrix.Label{Key: "mode", Value: "c"})
653
654 cc.BeginCycle()
655 g.Observe(1, modeA)
656 g.Observe(1, modeB)
657 cc.CommitCycleSuccess()
658
659 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
660 require.NoError(t, err)
661 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
662
663 cc.BeginCycle()
664 g.Observe(1, modeA)
665 g.Observe(1, modeB)
666 g.Observe(1, modeC)
667 cc.CommitCycleSuccess()
668
669 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
670 require.NoError(t, err)
671 // a,b seen; c is dropped under max_dims=2.
672 assert.Equal(t, []ActionKind{ActionUpdateChart}, actionKinds(plan2.Actions))
673 update2 := findUpdateAction(plan2)
674 require.NotNil(t, update2)
675 assert.Len(t, update2.Values, 2)
676
677 cc.BeginCycle()
678 g.Observe(1, modeB)
679 g.Observe(1, modeC)
680 cc.CommitCycleSuccess()
681
682 plan3, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
683 require.NoError(t, err)
684 assert.Equal(t, []ActionKind{
685 ActionRemoveDimension,
686 ActionCreateDimension,
687 ActionUpdateChart,
688 }, actionKinds(plan3.Actions))
689 }
690
691 func runTestBuildPlanComputesChartLabelsIntersectionAndExclusions(t *testing.T) {
692 e, err := New()
693 require.NoError(t, err)
694
695 yaml := `
696 version: v1
697 groups:
698 - family: Net
699 metrics:
700 - windows_net_bytes
701 charts:
702 - id: win_nic_traffic
703 title: NIC traffic
704 context: nic_traffic
705 units: bytes/s
706 instances:
707 by_labels: [nic]
708 dimensions:
709 - selector: windows_net_bytes{direction="in"}
710 name: received
711 - selector: windows_net_bytes{direction="out"}
712 name: sent
713 `
714 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
715
716 store := metrix.NewCollectorStore()
717 cc := mustCycleController(t, store)
718 sm := store.Write().SnapshotMeter("")
719 m := sm.Counter("windows_net_bytes")
720 in := sm.LabelSet(
721 metrix.Label{Key: "nic", Value: "eth0"},
722 metrix.Label{Key: "direction", Value: "in"},
723 metrix.Label{Key: "interface_type", Value: "ethernet"},
724 )
725 out := sm.LabelSet(
726 metrix.Label{Key: "nic", Value: "eth0"},
727 metrix.Label{Key: "direction", Value: "out"},
728 metrix.Label{Key: "interface_type", Value: "ethernet"},
729 )
730
731 cc.BeginCycle()
732 m.ObserveTotal(10, in)
733 m.ObserveTotal(20, out)
734 cc.CommitCycleSuccess()
735
736 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
737 require.NoError(t, err)
738
739 var create *CreateChartAction
740 for _, action := range plan.Actions {
741 if v, ok := action.(CreateChartAction); ok {
742 create = &v
743 break
744 }
745 }
746 require.NotNil(t, create)
747 assert.Equal(t, "eth0", create.Labels["nic"])
748 assert.Equal(t, "ethernet", create.Labels["interface_type"])
749 _, hasDirection := create.Labels["direction"]
750 assert.False(t, hasDirection)
751 }
752
753 func runTestBuildPlanAutogenDisabledSkipsUnmatchedSeries(t *testing.T) {
754 e, err := New()
755 require.NoError(t, err)
756
757 yaml := `
758 version: v1
759 groups:
760 - family: Service
761 metrics:
762 - svc.requests_total
763 charts:
764 - title: Requests
765 context: requests
766 units: requests/s
767 dimensions:
768 - selector: svc.requests_total
769 name: total
770 `
771 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
772
773 store := metrix.NewCollectorStore()
774 cc := mustCycleController(t, store)
775 unmatched := store.Write().SnapshotMeter("svc").Counter("errors_total")
776
777 cc.BeginCycle()
778 unmatched.ObserveTotal(10)
779 cc.CommitCycleSuccess()
780
781 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
782 require.NoError(t, err)
783 assert.Empty(t, plan.Actions)
784 }
785
786 func runTestBuildPlanEnginePolicySelectorFiltersSeriesBeforeRouting(t *testing.T) {
787 selectorExpr := metrixselector.Expr{
788 Allow: []string{`svc.errors_total{method="GET"}`},
789 }
790 e, err := New(WithEnginePolicy(EnginePolicy{
791 Selector: &selectorExpr,
792 Autogen: &AutogenPolicy{Enabled: true},
793 }))
794 require.NoError(t, err)
795
796 yaml := `
797 version: v1
798 groups:
799 - family: Service
800 metrics:
801 - svc.requests_total
802 charts:
803 - title: Requests
804 context: requests
805 units: requests/s
806 dimensions:
807 - selector: svc.requests_total
808 name: total
809 `
810 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
811
812 store := metrix.NewCollectorStore()
813 cc := mustCycleController(t, store)
814 sm := store.Write().SnapshotMeter("svc")
815 unmatched := sm.Counter("errors_total")
816 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
817 methodPOST := sm.LabelSet(metrix.Label{Key: "method", Value: "POST"})
818
819 cc.BeginCycle()
820 unmatched.ObserveTotal(10, methodGET)
821 unmatched.ObserveTotal(20, methodPOST)
822 cc.CommitCycleSuccess()
823
824 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
825 require.NoError(t, err)
826
827 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
828 create := findCreateChartAction(plan)
829 require.NotNil(t, create)
830 assert.Equal(t, "svc.errors_total-method=GET", create.ChartID)
831 assert.Equal(t, "GET", create.Labels["method"])
832
833 update := findUpdateAction(plan)
834 require.NotNil(t, update)
835 require.Len(t, update.Values, 1)
836 assert.Equal(t, float64(10), update.Values[0].Float64)
837 }
838
839 func runTestBuildPlanTemplateEnginePolicyControlsSelectorAndAutogen(t *testing.T) {
840 e, err := New()
841 require.NoError(t, err)
842
843 yaml := `
844 version: v1
845 engine:
846 selector:
847 allow:
848 - svc.errors_total{method="GET"}
849 autogen:
850 enabled: true
851 groups:
852 - family: Service
853 `
854 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
855
856 store := metrix.NewCollectorStore()
857 cc := mustCycleController(t, store)
858 sm := store.Write().SnapshotMeter("svc")
859 unmatched := sm.Counter("errors_total")
860 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
861 methodPOST := sm.LabelSet(metrix.Label{Key: "method", Value: "POST"})
862
863 cc.BeginCycle()
864 unmatched.ObserveTotal(10, methodGET)
865 unmatched.ObserveTotal(20, methodPOST)
866 cc.CommitCycleSuccess()
867
868 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
869 require.NoError(t, err)
870
871 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
872 create := findCreateChartAction(plan)
873 require.NotNil(t, create)
874 assert.Equal(t, "svc.errors_total-method=GET", create.ChartID)
875 }
876
877 func runTestBuildPlanEnginePolicyOptionOverridesTemplatePolicy(t *testing.T) {
878 overrideSelector := metrixselector.Expr{
879 Allow: []string{`svc.errors_total{method="POST"}`},
880 }
881 e, err := New(WithEnginePolicy(EnginePolicy{
882 Selector: &overrideSelector,
883 Autogen: &AutogenPolicy{Enabled: true},
884 }))
885 require.NoError(t, err)
886
887 yaml := `
888 version: v1
889 engine:
890 selector:
891 allow:
892 - svc.errors_total{method="GET"}
893 autogen:
894 enabled: false
895 groups:
896 - family: Service
897 `
898 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
899
900 store := metrix.NewCollectorStore()
901 cc := mustCycleController(t, store)
902 sm := store.Write().SnapshotMeter("svc")
903 unmatched := sm.Counter("errors_total")
904 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
905 methodPOST := sm.LabelSet(metrix.Label{Key: "method", Value: "POST"})
906
907 cc.BeginCycle()
908 unmatched.ObserveTotal(10, methodGET)
909 unmatched.ObserveTotal(20, methodPOST)
910 cc.CommitCycleSuccess()
911
912 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
913 require.NoError(t, err)
914
915 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
916 create := findCreateChartAction(plan)
917 require.NotNil(t, create)
918 assert.Equal(t, "svc.errors_total-method=POST", create.ChartID)
919 }
920
921 func runTestBuildPlanAutogenOptionKeepsTemplateSelector(t *testing.T) {
922 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
923 require.NoError(t, err)
924
925 yaml := `
926 version: v1
927 engine:
928 selector:
929 allow:
930 - svc.errors_total{method="GET"}
931 autogen:
932 enabled: false
933 groups:
934 - family: Service
935 `
936 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
937
938 store := metrix.NewCollectorStore()
939 cc := mustCycleController(t, store)
940 sm := store.Write().SnapshotMeter("svc")
941 unmatched := sm.Counter("errors_total")
942 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
943 methodPOST := sm.LabelSet(metrix.Label{Key: "method", Value: "POST"})
944
945 cc.BeginCycle()
946 unmatched.ObserveTotal(10, methodGET)
947 unmatched.ObserveTotal(20, methodPOST)
948 cc.CommitCycleSuccess()
949
950 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
951 require.NoError(t, err)
952
953 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
954 create := findCreateChartAction(plan)
955 require.NotNil(t, create)
956 assert.Equal(t, "svc.errors_total-method=GET", create.ChartID)
957 }
958
959 func runTestBuildPlanAutogenCreatesChartForUnmatchedScalar(t *testing.T) {
960 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
961 require.NoError(t, err)
962
963 yaml := `
964 version: v1
965 groups:
966 - family: Service
967 metrics:
968 - svc.requests_total
969 charts:
970 - title: Requests
971 context: requests
972 units: requests/s
973 dimensions:
974 - selector: svc.requests_total
975 name: total
976 `
977 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
978
979 store := metrix.NewCollectorStore()
980 cc := mustCycleController(t, store)
981 sm := store.Write().SnapshotMeter("svc")
982 unmatched := sm.Counter("errors_total")
983 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
984
985 cc.BeginCycle()
986 unmatched.ObserveTotal(10, methodGET)
987 cc.CommitCycleSuccess()
988
989 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
990 require.NoError(t, err)
991
992 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
993 create := findCreateChartAction(plan)
994 require.NotNil(t, create)
995 assert.Equal(t, "svc.errors_total-method=GET", create.ChartID)
996 assert.Equal(t, "svc.errors_total", create.Meta.Context)
997 assert.Equal(t, "events/s", create.Meta.Units)
998 assert.Equal(t, "GET", create.Labels["method"])
999 update := findUpdateAction(plan)
1000 require.NotNil(t, update)
1001 assert.Equal(t, "svc.errors_total-method=GET", update.ChartID)
1002 require.Len(t, update.Values, 1)
1003 assert.Equal(t, "errors_total", update.Values[0].Name)
1004 assert.Equal(t, float64(10), update.Values[0].Float64)
1005 }
1006
1007 func runTestBuildPlanAutogenContextNamespacePrefixesContext(t *testing.T) {
1008 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1009 require.NoError(t, err)
1010
1011 // Root context_namespace must prefix autogen (unmatched-series) chart contexts,
1012 // joined with "." like the template compiler ("prometheus" + "svc.errors_total").
1013 yaml := `
1014 version: v1
1015 context_namespace: prometheus
1016 groups:
1017 - family: Service
1018 metrics:
1019 - svc.requests_total
1020 charts:
1021 - title: Requests
1022 context: requests
1023 units: requests/s
1024 dimensions:
1025 - selector: svc.requests_total
1026 name: total
1027 `
1028 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1029
1030 store := metrix.NewCollectorStore()
1031 cc := mustCycleController(t, store)
1032 sm := store.Write().SnapshotMeter("svc")
1033 unmatched := sm.Counter("errors_total")
1034 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1035
1036 cc.BeginCycle()
1037 unmatched.ObserveTotal(10, methodGET)
1038 cc.CommitCycleSuccess()
1039
1040 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1041 require.NoError(t, err)
1042
1043 create := findCreateChartAction(plan)
1044 require.NotNil(t, create)
1045 assert.Equal(t, "prometheus.svc.errors_total", create.Meta.Context)
1046 }
1047
1048 // Mirrors the autogen-only collector shape: a stub group satisfies the
1049 // required groups[] but declares no charts, so every series is unmatched and handled by autogen,
1050 // with contexts prefixed by the top-level context_namespace.
1051 func runTestBuildPlanAutogenContextNamespaceStubGroupOnly(t *testing.T) {
1052 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1053 require.NoError(t, err)
1054
1055 yaml := `
1056 version: v1
1057 context_namespace: prometheus
1058 groups:
1059 - family: Prometheus
1060 `
1061 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1062
1063 store := metrix.NewCollectorStore()
1064 cc := mustCycleController(t, store)
1065 sm := store.Write().SnapshotMeter("")
1066 unmatched := sm.Counter("requests_total")
1067 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1068
1069 cc.BeginCycle()
1070 unmatched.ObserveTotal(10, methodGET)
1071 cc.CommitCycleSuccess()
1072
1073 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1074 require.NoError(t, err)
1075
1076 create := findCreateChartAction(plan)
1077 require.NotNil(t, create)
1078 assert.Equal(t, "prometheus.requests_total", create.Meta.Context)
1079 }
1080
1081 func runTestBuildPlanAutogenUsesMetricMetadataForScalar(t *testing.T) {
1082 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1083 require.NoError(t, err)
1084
1085 yaml := `
1086 version: v1
1087 groups:
1088 - family: Service
1089 metrics:
1090 - svc.requests_total
1091 charts:
1092 - title: Requests
1093 context: requests
1094 units: requests/s
1095 dimensions:
1096 - selector: svc.requests_total
1097 name: total
1098 `
1099 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1100
1101 store := metrix.NewCollectorStore()
1102 cc := mustCycleController(t, store)
1103 unmatched := store.Write().SnapshotMeter("svc").Counter(
1104 "bytes_total",
1105 metrix.WithDescription("HTTP traffic"),
1106 metrix.WithChartFamily("Traffic"),
1107 metrix.WithUnit("bytes"),
1108 )
1109
1110 cc.BeginCycle()
1111 unmatched.ObserveTotal(10)
1112 cc.CommitCycleSuccess()
1113
1114 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1115 require.NoError(t, err)
1116
1117 create := findCreateChartAction(plan)
1118 require.NotNil(t, create)
1119 assert.Equal(t, "HTTP traffic", create.Meta.Title)
1120 assert.Equal(t, "Traffic", create.Meta.Family)
1121 assert.Equal(t, "bytes/s", create.Meta.Units)
1122 assert.Equal(t, Priority, create.Meta.Priority)
1123 }
1124
1125 func runTestBuildPlanAutogenUsesMetricPriorityMetadataForScalar(t *testing.T) {
1126 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1127 require.NoError(t, err)
1128
1129 yaml := `
1130 version: v1
1131 groups:
1132 - family: Service
1133 metrics:
1134 - svc.requests_total
1135 charts:
1136 - title: Requests
1137 context: requests
1138 units: requests/s
1139 dimensions:
1140 - selector: svc.requests_total
1141 name: total
1142 `
1143 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1144
1145 store := metrix.NewCollectorStore()
1146 cc := mustCycleController(t, store)
1147 unmatched := store.Write().SnapshotMeter("svc").Counter(
1148 "bytes_total",
1149 metrix.WithDescription("HTTP traffic"),
1150 metrix.WithChartFamily("Traffic"),
1151 metrix.WithChartPriority(Priority+321),
1152 metrix.WithUnit("bytes"),
1153 )
1154
1155 cc.BeginCycle()
1156 unmatched.ObserveTotal(10)
1157 cc.CommitCycleSuccess()
1158
1159 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1160 require.NoError(t, err)
1161
1162 create := findCreateChartAction(plan)
1163 require.NotNil(t, create)
1164 assert.Equal(t, Priority+321, create.Meta.Priority)
1165 }
1166
1167 func runTestBuildPlanAutogenUsesMetricMetadataForHistogram(t *testing.T) {
1168 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1169 require.NoError(t, err)
1170
1171 yaml := `
1172 version: v1
1173 groups:
1174 - family: Service
1175 metrics:
1176 - svc.requests_total
1177 charts:
1178 - title: Requests
1179 context: requests
1180 units: requests/s
1181 dimensions:
1182 - selector: svc.requests_total
1183 name: total
1184 `
1185 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1186
1187 store := metrix.NewCollectorStore()
1188 cc := mustCycleController(t, store)
1189 h := store.Write().SnapshotMeter("svc").Histogram(
1190 "request_duration_ms",
1191 metrix.WithHistogramBounds(1, 2),
1192 metrix.WithDescription("Request duration"),
1193 metrix.WithChartFamily("Latency"),
1194 metrix.WithUnit("ms"),
1195 )
1196
1197 cc.BeginCycle()
1198 h.ObservePoint(metrix.HistogramPoint{
1199 Count: 3,
1200 Sum: 5,
1201 Buckets: []metrix.BucketPoint{
1202 {UpperBound: 1, CumulativeCount: 1},
1203 {UpperBound: 2, CumulativeCount: 3},
1204 },
1205 })
1206 cc.CommitCycleSuccess()
1207
1208 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1209 require.NoError(t, err)
1210
1211 buckets := findCreateChartActionByID(plan, "svc.request_duration_ms")
1212 require.NotNil(t, buckets)
1213 assert.Equal(t, "Request duration", buckets.Meta.Title)
1214 assert.Equal(t, "Latency", buckets.Meta.Family)
1215 assert.Equal(t, "observations/s", buckets.Meta.Units)
1216
1217 sum := findCreateChartActionByID(plan, "svc.request_duration_ms_sum")
1218 require.NotNil(t, sum)
1219 assert.Equal(t, "Request duration", sum.Meta.Title)
1220 assert.Equal(t, "Latency", sum.Meta.Family)
1221 assert.Equal(t, "ms/s", sum.Meta.Units)
1222 }
1223
1224 func runTestBuildPlanAutogenUsesMetricFloatMetadataForScalar(t *testing.T) {
1225 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1226 require.NoError(t, err)
1227
1228 yaml := `
1229 version: v1
1230 groups:
1231 - family: Service
1232 metrics:
1233 - svc.requests_total
1234 charts:
1235 - title: Requests
1236 context: requests
1237 units: requests/s
1238 dimensions:
1239 - selector: svc.requests_total
1240 name: total
1241 `
1242 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1243
1244 store := metrix.NewCollectorStore()
1245 cc := mustCycleController(t, store)
1246 unmatched := store.Write().SnapshotMeter("svc").Gauge(
1247 "temperature_celsius",
1248 metrix.WithFloat(true),
1249 )
1250
1251 cc.BeginCycle()
1252 unmatched.Observe(10.5)
1253 cc.CommitCycleSuccess()
1254
1255 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1256 require.NoError(t, err)
1257
1258 var created *CreateDimensionAction
1259 for _, action := range plan.Actions {
1260 dim, ok := action.(CreateDimensionAction)
1261 if !ok || dim.ChartID != "svc.temperature_celsius" {
1262 continue
1263 }
1264 created = &dim
1265 break
1266 }
1267 require.NotNil(t, created)
1268 assert.True(t, created.Float)
1269 update := findUpdateAction(plan)
1270 require.NotNil(t, update)
1271 require.Len(t, update.Values, 1)
1272 assert.True(t, update.Values[0].IsFloat)
1273 assert.Equal(t, float64(10.5), update.Values[0].Float64)
1274 }
1275
1276 func runTestBuildPlanAutogenUsesMetricMetadataForSummaryWithoutQuantiles(t *testing.T) {
1277 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1278 require.NoError(t, err)
1279
1280 yaml := `
1281 version: v1
1282 groups:
1283 - family: Service
1284 metrics:
1285 - svc.requests_total
1286 charts:
1287 - title: Requests
1288 context: requests
1289 units: requests/s
1290 dimensions:
1291 - selector: svc.requests_total
1292 name: total
1293 `
1294 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1295
1296 store := metrix.NewCollectorStore()
1297 cc := mustCycleController(t, store)
1298 s := store.Write().SnapshotMeter("svc").Summary(
1299 "query_duration_ms",
1300 metrix.WithDescription("Query duration"),
1301 metrix.WithChartFamily("Latency"),
1302 metrix.WithUnit("ms"),
1303 )
1304
1305 cc.BeginCycle()
1306 s.ObservePoint(metrix.SummaryPoint{
1307 Count: 4,
1308 Sum: 8,
1309 })
1310 cc.CommitCycleSuccess()
1311
1312 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1313 require.NoError(t, err)
1314
1315 sum := findCreateChartActionByID(plan, "svc.query_duration_ms_sum")
1316 require.NotNil(t, sum)
1317 assert.Equal(t, "Query duration", sum.Meta.Title)
1318 assert.Equal(t, "Latency", sum.Meta.Family)
1319 assert.Equal(t, "ms/s", sum.Meta.Units)
1320 }
1321
1322 func runTestBuildPlanTemplatePrecedenceOverAutogen(t *testing.T) {
1323 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1324 require.NoError(t, err)
1325
1326 yaml := `
1327 version: v1
1328 groups:
1329 - family: Service
1330 metrics:
1331 - svc.requests_total
1332 charts:
1333 - id: svc_requests
1334 title: Requests
1335 context: requests
1336 units: requests/s
1337 dimensions:
1338 - selector: svc.requests_total
1339 name: total
1340 `
1341 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1342
1343 store := metrix.NewCollectorStore()
1344 cc := mustCycleController(t, store)
1345 sm := store.Write().SnapshotMeter("svc")
1346 m := sm.Counter("requests_total", metrix.WithFloat(true))
1347 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1348
1349 cc.BeginCycle()
1350 m.ObserveTotal(10, methodGET)
1351 cc.CommitCycleSuccess()
1352
1353 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1354 require.NoError(t, err)
1355 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
1356 create := findCreateChartAction(plan)
1357 require.NotNil(t, create)
1358 assert.Equal(t, "svc_requests", create.ChartID)
1359 assert.NotEqual(t, "svc.requests_total-method=GET", create.ChartID)
1360 var createdDim *CreateDimensionAction
1361 for _, action := range plan.Actions {
1362 dim, ok := action.(CreateDimensionAction)
1363 if !ok || dim.ChartID != "svc_requests" {
1364 continue
1365 }
1366 createdDim = &dim
1367 break
1368 }
1369 require.NotNil(t, createdDim)
1370 assert.False(t, createdDim.Float)
1371 update := findUpdateAction(plan)
1372 require.NotNil(t, update)
1373 require.Len(t, update.Values, 1)
1374 assert.False(t, update.Values[0].IsFloat)
1375 assert.Equal(t, int64(10), update.Values[0].Int64)
1376 }
1377
1378 func runTestBuildPlanAutogenStrictOverflowDrop(t *testing.T) {
1379 e, err := New(
1380 WithEmitTypeIDBudgetPrefix("collector.job"),
1381 WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{
1382 Enabled: true,
1383 MaxTypeIDLen: 32,
1384 }}),
1385 )
1386 require.NoError(t, err)
1387
1388 yaml := `
1389 version: v1
1390 groups:
1391 - family: Service
1392 metrics:
1393 - svc.requests_total
1394 charts:
1395 - title: Requests
1396 context: requests
1397 units: requests/s
1398 dimensions:
1399 - selector: svc.requests_total
1400 name: total
1401 `
1402 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1403
1404 store := metrix.NewCollectorStore()
1405 cc := mustCycleController(t, store)
1406 sm := store.Write().SnapshotMeter("svc")
1407 metric := sm.Counter("this_metric_name_is_long_total")
1408 ls := sm.LabelSet(metrix.Label{Key: "tenant", Value: "a_very_long_tenant_name"})
1409
1410 cc.BeginCycle()
1411 metric.ObserveTotal(10, ls)
1412 cc.CommitCycleSuccess()
1413
1414 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1415 require.NoError(t, err)
1416 assert.Empty(t, plan.Actions)
1417 }
1418
1419 func runTestBuildPlanAutogenUsesFlattenMetadataForHistogramBuckets(t *testing.T) {
1420 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1421 require.NoError(t, err)
1422
1423 yaml := `
1424 version: v1
1425 groups:
1426 - family: Service
1427 metrics:
1428 - svc.requests_total
1429 charts:
1430 - title: Requests
1431 context: requests
1432 units: requests/s
1433 dimensions:
1434 - selector: svc.requests_total
1435 name: total
1436 `
1437 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1438
1439 store := metrix.NewCollectorStore()
1440 cc := mustCycleController(t, store)
1441 sm := store.Write().SnapshotMeter("svc")
1442 h := sm.Histogram("latency_seconds", metrix.WithHistogramBounds(1, 2))
1443 method := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1444
1445 cc.BeginCycle()
1446 h.ObservePoint(metrix.HistogramPoint{
1447 Count: 3,
1448 Sum: 4,
1449 Buckets: []metrix.BucketPoint{
1450 {UpperBound: 1, CumulativeCount: 1},
1451 {UpperBound: 2, CumulativeCount: 3},
1452 },
1453 }, method)
1454 cc.CommitCycleSuccess()
1455
1456 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1457 require.NoError(t, err)
1458
1459 var bucketChart *CreateChartAction
1460 for _, action := range plan.Actions {
1461 create, ok := action.(CreateChartAction)
1462 if !ok {
1463 continue
1464 }
1465 if create.ChartID == "svc.latency_seconds-method=GET" {
1466 bucketChart = &create
1467 break
1468 }
1469 }
1470 require.NotNil(t, bucketChart)
1471 assert.Equal(t, "GET", bucketChart.Labels["method"])
1472 _, hasLE := bucketChart.Labels["le"]
1473 assert.False(t, hasLE)
1474
1475 dims := map[string]struct{}{}
1476 for _, action := range plan.Actions {
1477 create, ok := action.(CreateDimensionAction)
1478 if !ok || create.ChartID != "svc.latency_seconds-method=GET" {
1479 continue
1480 }
1481 dims[create.Name] = struct{}{}
1482 }
1483 assert.Contains(t, dims, "bucket_1")
1484 assert.Contains(t, dims, "bucket_2")
1485 assert.Contains(t, dims, "bucket_+Inf")
1486 }
1487
1488 func runTestBuildPlanAutogenCreatesChartForUnmatchedGauge(t *testing.T) {
1489 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1490 require.NoError(t, err)
1491
1492 yaml := `
1493 version: v1
1494 groups:
1495 - family: Service
1496 metrics:
1497 - svc.requests_total
1498 charts:
1499 - title: Requests
1500 context: requests
1501 units: requests/s
1502 dimensions:
1503 - selector: svc.requests_total
1504 name: total
1505 `
1506 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1507
1508 store := metrix.NewCollectorStore()
1509 cc := mustCycleController(t, store)
1510 sm := store.Write().SnapshotMeter("svc")
1511 g := sm.Gauge("queue_depth")
1512 queueMain := sm.LabelSet(metrix.Label{Key: "queue", Value: "main"})
1513
1514 cc.BeginCycle()
1515 g.Observe(7, queueMain)
1516 cc.CommitCycleSuccess()
1517
1518 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1519 require.NoError(t, err)
1520
1521 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
1522 create := findCreateChartAction(plan)
1523 require.NotNil(t, create)
1524 assert.Equal(t, "svc.queue_depth-queue=main", create.ChartID)
1525 assert.Equal(t, "svc.queue_depth", create.Meta.Context)
1526 assert.Equal(t, "depth", create.Meta.Units)
1527 assert.Equal(t, program.AlgorithmAbsolute, create.Meta.Algorithm)
1528
1529 update := findUpdateAction(plan)
1530 require.NotNil(t, update)
1531 require.Len(t, update.Values, 1)
1532 assert.Equal(t, "queue_depth", update.Values[0].Name)
1533 assert.Equal(t, float64(7), update.Values[0].Float64)
1534 }
1535
1536 func runTestBuildPlanAutogenCreatesChartForUnmatchedStateSet(t *testing.T) {
1537 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1538 require.NoError(t, err)
1539
1540 yaml := `
1541 version: v1
1542 groups:
1543 - family: Service
1544 metrics:
1545 - svc.requests_total
1546 charts:
1547 - title: Requests
1548 context: requests
1549 units: requests/s
1550 dimensions:
1551 - selector: svc.requests_total
1552 name: total
1553 `
1554 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1555
1556 store := metrix.NewCollectorStore()
1557 cc := mustCycleController(t, store)
1558 sm := store.Write().SnapshotMeter("svc")
1559 ss := sm.StateSet("service_mode",
1560 metrix.WithStateSetStates("maintenance", "operational"),
1561 metrix.WithStateSetMode(metrix.ModeEnum),
1562 )
1563
1564 cc.BeginCycle()
1565 ss.Enable("operational")
1566 cc.CommitCycleSuccess()
1567
1568 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1569 require.NoError(t, err)
1570
1571 assert.Equal(t, []ActionKind{
1572 ActionCreateChart,
1573 ActionCreateDimension,
1574 ActionCreateDimension,
1575 ActionUpdateChart,
1576 }, actionKinds(plan.Actions))
1577 create := findCreateChartAction(plan)
1578 require.NotNil(t, create)
1579 assert.Equal(t, "svc.service_mode", create.ChartID)
1580 assert.Equal(t, "svc.service_mode", create.Meta.Context)
1581 assert.Equal(t, "state", create.Meta.Units)
1582 _, hasStateLabel := create.Labels["svc.service_mode"]
1583 assert.False(t, hasStateLabel)
1584
1585 dims := map[string]struct{}{}
1586 for _, action := range plan.Actions {
1587 dim, ok := action.(CreateDimensionAction)
1588 if !ok || dim.ChartID != "svc.service_mode" {
1589 continue
1590 }
1591 dims[dim.Name] = struct{}{}
1592 }
1593 assert.Contains(t, dims, "maintenance")
1594 assert.Contains(t, dims, "operational")
1595 }
1596
1597 func runTestBuildPlanAutogenKeepsStateSetUnitsWhenMetricMetaUnitIsSet(t *testing.T) {
1598 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1599 require.NoError(t, err)
1600
1601 yaml := `
1602 version: v1
1603 groups:
1604 - family: Service
1605 metrics:
1606 - svc.requests_total
1607 charts:
1608 - title: Requests
1609 context: requests
1610 units: requests/s
1611 dimensions:
1612 - selector: svc.requests_total
1613 name: total
1614 `
1615 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1616
1617 store := metrix.NewCollectorStore()
1618 cc := mustCycleController(t, store)
1619 ss := store.Write().SnapshotMeter("svc").StateSet(
1620 "service_mode",
1621 metrix.WithStateSetStates("maintenance", "operational"),
1622 metrix.WithDescription("Service mode"),
1623 metrix.WithChartFamily("Service"),
1624 metrix.WithUnit("watts"),
1625 )
1626
1627 cc.BeginCycle()
1628 ss.Enable("operational")
1629 cc.CommitCycleSuccess()
1630
1631 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1632 require.NoError(t, err)
1633
1634 create := findCreateChartAction(plan)
1635 require.NotNil(t, create)
1636 assert.Equal(t, "svc.service_mode", create.ChartID)
1637 assert.Equal(t, "Service mode", create.Meta.Title)
1638 assert.Equal(t, "Service", create.Meta.Family)
1639 assert.Equal(t, "state", create.Meta.Units)
1640 }
1641
1642 func runTestBuildPlanAutogenCreatesChartForUnmatchedMeasureSetGauge(t *testing.T) {
1643 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1644 require.NoError(t, err)
1645
1646 yaml := `
1647 version: v1
1648 groups:
1649 - family: Service
1650 metrics:
1651 - svc.requests_total
1652 charts:
1653 - title: Requests
1654 context: requests
1655 units: requests/s
1656 dimensions:
1657 - selector: svc.requests_total
1658 name: total
1659 `
1660 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1661
1662 store := metrix.NewCollectorStore()
1663 cc := mustCycleController(t, store)
1664 ms := store.Write().SnapshotMeter("svc").MeasureSetGauge(
1665 "latency_seconds",
1666 metrix.WithMeasureSetFields(
1667 metrix.MeasureFieldSpec{Name: "value"},
1668 metrix.MeasureFieldSpec{Name: "ratio", Float: true},
1669 ),
1670 metrix.WithDescription("Latency"),
1671 metrix.WithChartFamily("Service"),
1672 metrix.WithUnit("seconds"),
1673 )
1674
1675 cc.BeginCycle()
1676 ms.ObservePoint(metrix.MeasureSetPoint{Values: []metrix.SampleValue{1.5, 0.5}})
1677 cc.CommitCycleSuccess()
1678
1679 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1680 require.NoError(t, err)
1681
1682 assert.Equal(t, []ActionKind{
1683 ActionCreateChart,
1684 ActionCreateDimension,
1685 ActionCreateDimension,
1686 ActionUpdateChart,
1687 }, actionKinds(plan.Actions))
1688
1689 create := findCreateChartAction(plan)
1690 require.NotNil(t, create)
1691 assert.Equal(t, "svc.latency_seconds", create.ChartID)
1692 assert.Equal(t, "Latency", create.Meta.Title)
1693 assert.Equal(t, "Service", create.Meta.Family)
1694 assert.Equal(t, "svc.latency_seconds", create.Meta.Context)
1695 assert.Equal(t, "seconds", create.Meta.Units)
1696 _, hasFieldLabel := create.Labels[metrix.MeasureSetFieldLabel]
1697 assert.False(t, hasFieldLabel)
1698
1699 dims := map[string]CreateDimensionAction{}
1700 for _, action := range plan.Actions {
1701 dim, ok := action.(CreateDimensionAction)
1702 if !ok || dim.ChartID != "svc.latency_seconds" {
1703 continue
1704 }
1705 dims[dim.Name] = dim
1706 }
1707 require.Len(t, dims, 2)
1708 assert.Equal(t, program.AlgorithmAbsolute, dims["value"].Algorithm)
1709 assert.False(t, dims["value"].Float)
1710 assert.Equal(t, program.AlgorithmAbsolute, dims["ratio"].Algorithm)
1711 assert.True(t, dims["ratio"].Float)
1712
1713 update := findUpdateAction(plan)
1714 require.NotNil(t, update)
1715 require.Len(t, update.Values, 2)
1716 names := map[string]struct{}{}
1717 for _, value := range update.Values {
1718 names[value.Name] = struct{}{}
1719 }
1720 assert.Contains(t, names, "ratio")
1721 assert.Contains(t, names, "value")
1722 }
1723
1724 func runTestBuildPlanAutogenCreatesChartForUnmatchedMeasureSetCounter(t *testing.T) {
1725 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1726 require.NoError(t, err)
1727
1728 yaml := `
1729 version: v1
1730 groups:
1731 - family: Service
1732 metrics:
1733 - svc.requests_total
1734 charts:
1735 - title: Requests
1736 context: requests
1737 units: requests/s
1738 dimensions:
1739 - selector: svc.requests_total
1740 name: total
1741 `
1742 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1743
1744 store := metrix.NewCollectorStore()
1745 cc := mustCycleController(t, store)
1746 ms := store.Write().SnapshotMeter("svc").MeasureSetCounter(
1747 "requests_total",
1748 metrix.WithMeasureSetFields(
1749 metrix.MeasureFieldSpec{Name: "ok"},
1750 metrix.MeasureFieldSpec{Name: "failed"},
1751 ),
1752 metrix.WithDescription("Requests"),
1753 metrix.WithChartFamily("Service"),
1754 metrix.WithUnit("requests"),
1755 )
1756
1757 cc.BeginCycle()
1758 ms.ObserveTotalPoint(metrix.MeasureSetPoint{Values: []metrix.SampleValue{10, 2}})
1759 cc.CommitCycleSuccess()
1760
1761 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1762 require.NoError(t, err)
1763
1764 assert.Equal(t, []ActionKind{
1765 ActionCreateChart,
1766 ActionCreateDimension,
1767 ActionCreateDimension,
1768 ActionUpdateChart,
1769 }, actionKinds(plan.Actions))
1770
1771 create := findCreateChartAction(plan)
1772 require.NotNil(t, create)
1773 assert.Equal(t, "svc.requests_total", create.ChartID)
1774 assert.Equal(t, "Requests", create.Meta.Title)
1775 assert.Equal(t, "Service", create.Meta.Family)
1776 assert.Equal(t, "svc.requests_total", create.Meta.Context)
1777 assert.Equal(t, "requests/s", create.Meta.Units)
1778 _, hasFieldLabel := create.Labels[metrix.MeasureSetFieldLabel]
1779 assert.False(t, hasFieldLabel)
1780
1781 dims := map[string]CreateDimensionAction{}
1782 for _, action := range plan.Actions {
1783 dim, ok := action.(CreateDimensionAction)
1784 if !ok || dim.ChartID != "svc.requests_total" {
1785 continue
1786 }
1787 dims[dim.Name] = dim
1788 }
1789 require.Len(t, dims, 2)
1790 assert.Equal(t, program.AlgorithmIncremental, dims["ok"].Algorithm)
1791 assert.Equal(t, program.AlgorithmIncremental, dims["failed"].Algorithm)
1792 assert.False(t, dims["ok"].Float)
1793 assert.False(t, dims["failed"].Float)
1794
1795 update := findUpdateAction(plan)
1796 require.NotNil(t, update)
1797 require.Len(t, update.Values, 2)
1798 names := map[string]struct{}{}
1799 for _, value := range update.Values {
1800 names[value.Name] = struct{}{}
1801 }
1802 assert.Contains(t, names, "failed")
1803 assert.Contains(t, names, "ok")
1804 }
1805
1806 func runTestBuildPlanTemplateWinsOnAutogenChartIDCollisionAcrossSeries(t *testing.T) {
1807 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
1808 require.NoError(t, err)
1809
1810 yaml := `
1811 version: v1
1812 groups:
1813 - family: Service
1814 metrics:
1815 - svc.foo_total
1816 charts:
1817 - id: svc.errors_total-method=GET
1818 title: Foo requests
1819 context: foo_requests
1820 units: requests/s
1821 dimensions:
1822 - selector: svc.foo_total{method="GET"}
1823 name: total
1824 `
1825 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1826
1827 store := metrix.NewCollectorStore()
1828 cc := mustCycleController(t, store)
1829 sm := store.Write().SnapshotMeter("svc")
1830 errorsTotal := sm.Counter("errors_total")
1831 fooTotal := sm.Counter("foo_total")
1832 methodGET := sm.LabelSet(metrix.Label{Key: "method", Value: "GET"})
1833
1834 cc.BeginCycle()
1835 errorsTotal.ObserveTotal(10, methodGET)
1836 fooTotal.ObserveTotal(7, methodGET)
1837 cc.CommitCycleSuccess()
1838
1839 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1840 require.NoError(t, err)
1841
1842 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan.Actions))
1843 create := findCreateChartAction(plan)
1844 require.NotNil(t, create)
1845 assert.Equal(t, "svc.errors_total-method=GET", create.ChartID)
1846 assert.Equal(t, "foo_requests", create.Meta.Context)
1847
1848 update := findUpdateAction(plan)
1849 require.NotNil(t, update)
1850 require.Len(t, update.Values, 1)
1851 assert.Equal(t, "total", update.Values[0].Name)
1852 assert.Equal(t, float64(7), update.Values[0].Float64)
1853 }
1854
1855 func runTestBuildPlanAutogenRemovalLifecycleExpiry(t *testing.T) {
1856 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{
1857 Enabled: true,
1858 ExpireAfterSuccessCycles: 1,
1859 }}))
1860 require.NoError(t, err)
1861
1862 yaml := `
1863 version: v1
1864 groups:
1865 - family: Service
1866 metrics:
1867 - svc.requests_total
1868 charts:
1869 - title: Requests
1870 context: requests
1871 units: requests/s
1872 dimensions:
1873 - selector: svc.requests_total
1874 name: total
1875 `
1876 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1877
1878 store := metrix.NewCollectorStore()
1879 cc := mustCycleController(t, store)
1880 c := store.Write().SnapshotMeter("svc").Counter("errors_total")
1881
1882 cc.BeginCycle()
1883 c.ObserveTotal(10)
1884 cc.CommitCycleSuccess()
1885
1886 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1887 require.NoError(t, err)
1888 assert.Equal(t, []ActionKind{ActionCreateChart, ActionCreateDimension, ActionUpdateChart}, actionKinds(plan1.Actions))
1889
1890 cc.BeginCycle()
1891 cc.CommitCycleSuccess()
1892
1893 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
1894 require.NoError(t, err)
1895 assert.Equal(t, []ActionKind{ActionRemoveChart}, actionKinds(plan2.Actions))
1896 }
1897
1898 func runTestBuildPlanFirstWriterWinsAndAccumulatesRepeatedRoutes(t *testing.T) {
1899 e, err := New()
1900 require.NoError(t, err)
1901
1902 yaml := `
1903 version: v1
1904 groups:
1905 - family: Service
1906 metrics:
1907 - m_a
1908 - m_b
1909 charts:
1910 - id: conflict_total
1911 title: Conflict total
1912 context: conflict_total
1913 units: value
1914 dimensions:
1915 - selector: m_a
1916 name_from_label: mode
1917 options:
1918 hidden: true
1919 float: true
1920 - selector: m_b
1921 name_from_label: mode
1922 options:
1923 hidden: false
1924 float: false
1925 `
1926 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1927
1928 store := metrix.NewCollectorStore()
1929 cc := mustCycleController(t, store)
1930 m := store.Write().SnapshotMeter("")
1931 a := m.Gauge("m_a")
1932 b := m.Gauge("m_b")
1933 total := m.LabelSet(metrix.Label{Key: "mode", Value: "total"})
1934
1935 cc.BeginCycle()
1936 a.Observe(5, total)
1937 b.Observe(3, total)
1938 cc.CommitCycleSuccess()
1939
1940 plan, err := buildPlan(e, store.Read())
1941 require.NoError(t, err)
1942
1943 assert.Equal(t, []ActionKind{
1944 ActionCreateChart,
1945 ActionCreateDimension,
1946 ActionUpdateChart,
1947 }, actionKinds(plan.Actions))
1948
1949 var created *CreateDimensionAction
1950 for _, action := range plan.Actions {
1951 dim, ok := action.(CreateDimensionAction)
1952 if !ok {
1953 continue
1954 }
1955 created = &dim
1956 break
1957 }
1958 require.NotNil(t, created)
1959 assert.Equal(t, "total", created.Name)
1960 assert.True(t, created.Hidden)
1961 assert.True(t, created.Float)
1962
1963 update := findUpdateAction(plan)
1964 require.NotNil(t, update)
1965 require.Len(t, update.Values, 1)
1966 assert.True(t, update.Values[0].IsFloat)
1967 assert.Equal(t, "total", update.Values[0].Name)
1968 assert.Equal(t, float64(8), update.Values[0].Float64)
1969 }
1970
1971 func runTestBuildPlanEmptyEmissionAndScratchReusePruneAcrossCycles(t *testing.T) {
1972 e, err := New()
1973 require.NoError(t, err)
1974
1975 yaml := `
1976 version: v1
1977 groups:
1978 - family: Service
1979 metrics:
1980 - svc_mode
1981 charts:
1982 - id: service_mode
1983 title: Service mode
1984 context: service_mode
1985 units: state
1986 lifecycle:
1987 dimensions:
1988 expire_after_cycles: 1
1989 dimensions:
1990 - selector: svc_mode
1991 name_from_label: mode
1992 `
1993 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
1994
1995 store := metrix.NewCollectorStore()
1996 cc := mustCycleController(t, store)
1997 m := store.Write().SnapshotMeter("")
1998 mode := m.Gauge("svc_mode")
1999 okSet := m.LabelSet(metrix.Label{Key: "mode", Value: "ok"})
2000 warnSet := m.LabelSet(metrix.Label{Key: "mode", Value: "warn"})
2001
2002 cc.BeginCycle()
2003 mode.Observe(1, okSet)
2004 mode.Observe(2, warnSet)
2005 cc.CommitCycleSuccess()
2006
2007 plan1, err := buildPlan(e, store.Read())
2008 require.NoError(t, err)
2009 require.NotNil(t, findUpdateAction(plan1))
2010
2011 matChart := e.state.materialized.charts["service_mode"]
2012 require.NotNil(t, matChart)
2013 require.Contains(t, matChart.scratchEntries, "ok")
2014 require.Contains(t, matChart.scratchEntries, "warn")
2015 require.NotNil(t, matChart.scratchEntries["ok"])
2016
2017 cc.BeginCycle()
2018 mode.Observe(3, okSet)
2019 cc.CommitCycleSuccess()
2020
2021 plan2, err := buildPlan(e, store.Read())
2022 require.NoError(t, err)
2023
2024 update2 := findUpdateAction(plan2)
2025 require.NotNil(t, update2)
2026 got := make(map[string]UpdateDimensionValue, len(update2.Values))
2027 for _, v := range update2.Values {
2028 got[v.Name] = v
2029 }
2030 require.Contains(t, got, "ok")
2031 require.Contains(t, got, "warn")
2032 assert.Equal(t, float64(3), got["ok"].Float64)
2033 assert.True(t, got["warn"].IsEmpty)
2034 require.NotNil(t, findRemoveDimensionAction(plan2))
2035
2036 matChart = e.state.materialized.charts["service_mode"]
2037 require.NotNil(t, matChart)
2038 assert.NotContains(t, matChart.dimensions, "warn")
2039 require.Contains(t, matChart.scratchEntries, "warn")
2040 require.Contains(t, matChart.scratchEntries, "ok")
2041
2042 cc.BeginCycle()
2043 mode.Observe(4, okSet)
2044 cc.CommitCycleSuccess()
2045
2046 plan3, err := buildPlan(e, store.Read())
2047 require.NoError(t, err)
2048 update3 := findUpdateAction(plan3)
2049 require.NotNil(t, update3)
2050 require.Len(t, update3.Values, 1)
2051 assert.Equal(t, "ok", update3.Values[0].Name)
2052
2053 matChart = e.state.materialized.charts["service_mode"]
2054 require.NotNil(t, matChart)
2055 assert.NotContains(t, matChart.scratchEntries, "warn")
2056 require.Contains(t, matChart.scratchEntries, "ok")
2057 }
2058
2059 func TestBuildPlanSequenceModeScenarios(t *testing.T) {
2060 tests := map[string]struct {
2061 run func(t *testing.T)
2062 }{
2063 "collector mode keeps static success-seq dedupe semantics": {
2064 run: func(t *testing.T) {
2065 e, err := New()
2066 require.NoError(t, err)
2067 require.NoError(t, e.LoadYAML([]byte(`
2068 version: v1
2069 groups:
2070 - family: Service
2071 metrics:
2072 - component.load
2073 charts:
2074 - id: component_load
2075 title: Component Load
2076 context: component_load
2077 units: load
2078 dimensions:
2079 - selector: component.load
2080 name: value
2081 `), 1))
2082
2083 store := metrix.NewCollectorStore()
2084 cc := mustCycleController(t, store)
2085 g := store.Write().SnapshotMeter("component").Gauge("load")
2086
2087 cc.BeginCycle()
2088 g.Observe(5)
2089 cc.CommitCycleSuccess()
2090
2091 plan1, err := buildPlan(e, store.Read())
2092 require.NoError(t, err)
2093 require.NotNil(t, findUpdateAction(plan1))
2094
2095 plan2, err := buildPlan(e, store.Read())
2096 require.NoError(t, err)
2097 assert.Empty(t, plan2.Actions)
2098 },
2099 },
2100 "runtime mode re-emits updates on no-write ticks and keeps scratch entries": {
2101 run: func(t *testing.T) {
2102 e, err := New(WithSeriesSelectionAllVisible(), WithRuntimePlannerMode())
2103 require.NoError(t, err)
2104 require.NoError(t, e.LoadYAML([]byte(`
2105 version: v1
2106 groups:
2107 - family: Runtime
2108 metrics:
2109 - component.load
2110 charts:
2111 - id: component_load
2112 title: Component Load
2113 context: component_load
2114 units: load
2115 dimensions:
2116 - selector: component.load
2117 name_from_label: id
2118 `), 1))
2119
2120 store := metrix.NewRuntimeStore()
2121 vec := store.Write().StatefulMeter("component").Vec("id").Gauge("load")
2122 vec.WithLabelValues("ok").Set(1)
2123 vec.WithLabelValues("warn").Set(2)
2124
2125 reader := store.Read(metrix.ReadRaw(), metrix.ReadFlatten())
2126 plan1, err := buildPlan(e, reader)
2127 require.NoError(t, err)
2128 require.NotNil(t, findUpdateAction(plan1))
2129
2130 matChart := e.state.materialized.charts["component_load"]
2131 require.NotNil(t, matChart)
2132 require.Contains(t, matChart.scratchEntries, "ok")
2133 require.Contains(t, matChart.scratchEntries, "warn")
2134 require.NotNil(t, matChart.scratchEntries["ok"])
2135
2136 plan2, err := buildPlan(e, reader)
2137 require.NoError(t, err)
2138 assert.Equal(t, []ActionKind{ActionUpdateChart}, actionKinds(plan2.Actions))
2139 require.NotNil(t, findUpdateAction(plan2))
2140 metricsReader := e.RuntimeStore().Read(metrix.ReadRaw())
2141 cacheHits, ok := metricsReader.Value("netdata.go.plugin.framework.chartengine.route_cache_hits_total", nil)
2142 require.True(t, ok)
2143 assert.GreaterOrEqual(t, cacheHits, float64(1))
2144 fullDrops, fullDropsSeen := metricsReader.Value("netdata.go.plugin.framework.chartengine.route_cache_full_drops_total", nil)
2145 require.True(t, fullDropsSeen)
2146 assert.Equal(t, float64(0), fullDrops)
2147
2148 matChart = e.state.materialized.charts["component_load"]
2149 require.NotNil(t, matChart)
2150 require.Contains(t, matChart.scratchEntries, "ok")
2151 require.Contains(t, matChart.scratchEntries, "warn")
2152 },
2153 },
2154 }
2155
2156 for name, tc := range tests {
2157 t.Run(name, tc.run)
2158 }
2159 }
2160
2161 func TestPlannerStageBoundaries(t *testing.T) {
2162 tests := map[string]func(t *testing.T){
2163 "scan stage accumulates per-chart state": func(t *testing.T) {
2164 e, err := New()
2165 require.NoError(t, err)
2166
2167 yaml := `
2168 version: v1
2169 groups:
2170 - family: Service
2171 metrics:
2172 - svc_mode
2173 charts:
2174 - id: service_mode
2175 title: Service mode
2176 context: service_mode
2177 units: state
2178 dimensions:
2179 - selector: svc_mode
2180 name_from_label: mode
2181 `
2182 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
2183
2184 store := metrix.NewCollectorStore()
2185 cc := mustCycleController(t, store)
2186 sm := store.Write().SnapshotMeter("")
2187 mode := sm.Gauge("svc_mode")
2188 a := sm.LabelSet(metrix.Label{Key: "mode", Value: "a"})
2189 b := sm.LabelSet(metrix.Label{Key: "mode", Value: "b"})
2190
2191 cc.BeginCycle()
2192 mode.Observe(1, a)
2193 mode.Observe(2, b)
2194 cc.CommitCycleSuccess()
2195
2196 out := Plan{
2197 Actions: make([]EngineAction, 0),
2198 InferredDimensions: make([]InferredDimension, 0),
2199 }
2200 reader := store.Read()
2201 meta := reader.CollectMeta()
2202 materialized := e.state.materialized.clone()
2203 ctx, err := e.preparePlanBuildContext(reader, &out, meta, meta.LastSuccessSeq, &materialized)
2204 require.NoError(t, err)
2205 require.NoError(t, e.scanPlanSeries(ctx))
2206
2207 require.Len(t, ctx.chartsByID, 1)
2208 cs, ok := ctx.chartsByID["service_mode"]
2209 require.True(t, ok)
2210 require.NotNil(t, cs.entries["a"])
2211 require.NotNil(t, cs.entries["b"])
2212 assert.Equal(t, metrix.SampleValue(1), cs.entries["a"].value)
2213 assert.Equal(t, metrix.SampleValue(2), cs.entries["b"].value)
2214 assert.Empty(t, out.InferredDimensions)
2215 },
2216 "materialize stage emits create and update actions": func(t *testing.T) {
2217 e, err := New()
2218 require.NoError(t, err)
2219
2220 yaml := `
2221 version: v1
2222 groups:
2223 - family: Service
2224 metrics:
2225 - svc_mode
2226 charts:
2227 - id: service_mode
2228 title: Service mode
2229 context: service_mode
2230 units: state
2231 dimensions:
2232 - selector: svc_mode
2233 name_from_label: mode
2234 `
2235 require.NoError(t, e.LoadYAML([]byte(yaml), 1))
2236
2237 store := metrix.NewCollectorStore()
2238 cc := mustCycleController(t, store)
2239 sm := store.Write().SnapshotMeter("")
2240 mode := sm.Gauge("svc_mode")
2241 okLabel := sm.LabelSet(metrix.Label{Key: "mode", Value: "ok"})
2242
2243 cc.BeginCycle()
2244 mode.Observe(1, okLabel)
2245 cc.CommitCycleSuccess()
2246
2247 out := Plan{
2248 Actions: make([]EngineAction, 0),
2249 InferredDimensions: make([]InferredDimension, 0),
2250 }
2251 reader := store.Read()
2252 meta := reader.CollectMeta()
2253 materialized := e.state.materialized.clone()
2254 ctx, err := e.preparePlanBuildContext(reader, &out, meta, meta.LastSuccessSeq, &materialized)
2255 require.NoError(t, err)
2256 require.NoError(t, e.scanPlanSeries(ctx))
2257 require.NoError(t, e.materializePlanCharts(ctx))
2258
2259 assert.Equal(t, []ActionKind{
2260 ActionCreateChart,
2261 ActionCreateDimension,
2262 ActionUpdateChart,
2263 }, actionKinds(out.Actions))
2264
2265 update := findUpdateAction(out)
2266 require.NotNil(t, update)
2267 require.Len(t, update.Values, 1)
2268 assert.Equal(t, "ok", update.Values[0].Name)
2269 assert.Equal(t, float64(1), update.Values[0].Float64)
2270 },
2271 "caps stage evicts deterministically": func(t *testing.T) {
2272 lifecycle := program.LifecyclePolicy{
2273 MaxInstances: 1,
2274 }
2275 meta := program.ChartMeta{
2276 Title: "Requests",
2277 Context: "requests",
2278 Family: "Service",
2279 Units: "requests/s",
2280 Algorithm: program.AlgorithmIncremental,
2281 Type: program.ChartTypeLine,
2282 }
2283 chartsByID := map[string]*chartState{
2284 "svc_a": {
2285 templateID: "tpl.requests",
2286 chartID: "svc_a",
2287 meta: meta,
2288 lifecycle: lifecycle,
2289 currentBuildSeq: 2,
2290 observedCount: 1,
2291 entries: map[string]*dimBuildEntry{
2292 "total": {
2293 seenSeq: 2,
2294 value: 10,
2295 dimensionState: dimensionState{
2296 static: true,
2297 order: 0,
2298 },
2299 },
2300 },
2301 },
2302 "svc_b": {
2303 templateID: "tpl.requests",
2304 chartID: "svc_b",
2305 meta: meta,
2306 lifecycle: lifecycle,
2307 currentBuildSeq: 2,
2308 observedCount: 1,
2309 entries: map[string]*dimBuildEntry{
2310 "total": {
2311 seenSeq: 2,
2312 value: 20,
2313 dimensionState: dimensionState{
2314 static: true,
2315 order: 0,
2316 },
2317 },
2318 },
2319 },
2320 }
2321
2322 state := newMaterializedState()
2323 oldChart, created := state.ensureChart("svc_old", "tpl.requests", meta, lifecycle)
2324 require.True(t, created)
2325 oldChart.lastSeenSuccessSeq = 1
2326
2327 removeDims, removeCharts := enforceLifecycleCaps(2, chartsByID, &state)
2328 assert.Empty(t, removeDims)
2329 require.Len(t, removeCharts, 1)
2330 assert.Equal(t, "svc_old", removeCharts[0].ChartID)
2331
2332 assert.Contains(t, chartsByID, "svc_a")
2333 assert.NotContains(t, chartsByID, "svc_b")
2334 },
2335 "expiry stage removes stale dimensions and charts": func(t *testing.T) {
2336 state := newMaterializedState()
2337
2338 liveMeta := program.ChartMeta{
2339 Title: "Service mode",
2340 Context: "service_mode",
2341 }
2342 liveChart, created := state.ensureChart("svc_mode", "tpl.mode", liveMeta, program.LifecyclePolicy{
2343 Dimensions: program.DimensionLifecyclePolicy{ExpireAfterCycles: 1},
2344 })
2345 require.True(t, created)
2346 liveChart.lastSeenSuccessSeq = 3
2347 liveDim, dimCreated := liveChart.ensureDimension("stale_mode", dimensionState{
2348 static: false,
2349 order: 1,
2350 algorithm: program.AlgorithmAbsolute,
2351 multiplier: 1,
2352 divisor: 1,
2353 })
2354 require.True(t, dimCreated)
2355 liveDim.lastSeenSuccessSeq = 1
2356
2357 oldMeta := program.ChartMeta{
2358 Title: "Old chart",
2359 Context: "old_chart",
2360 }
2361 oldChart, oldCreated := state.ensureChart("old_chart", "tpl.old", oldMeta, program.LifecyclePolicy{
2362 ExpireAfterCycles: 1,
2363 })
2364 require.True(t, oldCreated)
2365 oldChart.lastSeenSuccessSeq = 1
2366
2367 removeDims, removeCharts := collectExpiryRemovals(3, &state)
2368 require.Len(t, removeDims, 1)
2369 assert.Equal(t, "svc_mode", removeDims[0].ChartID)
2370 assert.Equal(t, "stale_mode", removeDims[0].Name)
2371
2372 require.Len(t, removeCharts, 1)
2373 assert.Equal(t, "old_chart", removeCharts[0].ChartID)
2374 },
2375 }
2376
2377 for name, run := range tests {
2378 t.Run(name, run)
2379 }
2380 }
2381
2382 func actionKinds(actions []EngineAction) []ActionKind {
2383 out := make([]ActionKind, 0, len(actions))
2384 for _, action := range actions {
2385 out = append(out, action.Kind())
2386 }
2387 return out
2388 }
2389
2390 // A summary scraped with NaN quantile values is still an OBSERVED point, so its quantile chart is
2391 // created (not skipped by the observedCount==0 path) and each NaN quantile dim renders as a gap
2392 // (IsEmpty → SETEMPTY), never a 0 value.
2393 func runTestBuildPlanSummaryNaNQuantileGaps(t *testing.T) {
2394 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
2395 require.NoError(t, err)
2396 require.NoError(t, e.LoadYAML([]byte(`
2397 version: v1
2398 groups:
2399 - family: Service
2400 `), 1))
2401
2402 store := metrix.NewCollectorStore()
2403 cc := mustCycleController(t, store)
2404 sum := store.Write().SnapshotMeter("svc").Summary("latency", metrix.WithSummaryQuantiles(0.5, 0.9))
2405
2406 cc.BeginCycle()
2407 sum.ObservePoint(metrix.SummaryPoint{
2408 Count: 0,
2409 Sum: 0,
2410 Quantiles: []metrix.QuantilePoint{
2411 {Quantile: 0.5, Value: metrix.SampleValue(math.NaN())},
2412 {Quantile: 0.9, Value: metrix.SampleValue(math.NaN())},
2413 },
2414 })
2415 cc.CommitCycleSuccess()
2416
2417 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
2418 require.NoError(t, err)
2419
2420 require.NotNil(t, findCreateChartActionByID(plan, "svc.latency"),
2421 "summary quantile chart should be created from an observed all-NaN point")
2422
2423 var quantileUpdate *UpdateChartAction
2424 for i := range plan.Actions {
2425 if u, ok := plan.Actions[i].(UpdateChartAction); ok && u.ChartID == "svc.latency" {
2426 cp := u
2427 quantileUpdate = &cp
2428 break
2429 }
2430 }
2431 require.NotNil(t, quantileUpdate, "expected an update action for the quantile chart")
2432 require.NotEmpty(t, quantileUpdate.Values)
2433 for _, v := range quantileUpdate.Values {
2434 assert.Truef(t, v.IsEmpty, "NaN quantile dim %q must gap (IsEmpty), got %+v", v.Name, v)
2435 }
2436 }
2437
2438 func runTestBuildPlanSummaryMixedFiniteNaNQuantileGaps(t *testing.T) {
2439 e, err := New(WithEnginePolicy(EnginePolicy{Autogen: &AutogenPolicy{Enabled: true}}))
2440 require.NoError(t, err)
2441 require.NoError(t, e.LoadYAML([]byte(`
2442 version: v1
2443 groups:
2444 - family: Service
2445 `), 1))
2446
2447 store := metrix.NewCollectorStore()
2448 cc := mustCycleController(t, store)
2449 sum := store.Write().SnapshotMeter("svc").Summary("latency", metrix.WithSummaryQuantiles(0.5, 0.9))
2450
2451 // One quantile carries a finite value, the other is NaN: the planner must gap
2452 // only the NaN dimension and keep the finite one (per-dimension, same chart).
2453 cc.BeginCycle()
2454 sum.ObservePoint(metrix.SummaryPoint{
2455 Count: 1,
2456 Sum: 0.4,
2457 Quantiles: []metrix.QuantilePoint{
2458 {Quantile: 0.5, Value: 0.4},
2459 {Quantile: 0.9, Value: metrix.SampleValue(math.NaN())},
2460 },
2461 })
2462 cc.CommitCycleSuccess()
2463
2464 plan, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
2465 require.NoError(t, err)
2466
2467 require.NotNil(t, findCreateChartActionByID(plan, "svc.latency"),
2468 "summary quantile chart should be created")
2469
2470 var quantileUpdate *UpdateChartAction
2471 for i := range plan.Actions {
2472 if u, ok := plan.Actions[i].(UpdateChartAction); ok && u.ChartID == "svc.latency" {
2473 cp := u
2474 quantileUpdate = &cp
2475 break
2476 }
2477 }
2478 require.NotNil(t, quantileUpdate, "expected an update action for the quantile chart")
2479 require.Len(t, quantileUpdate.Values, 2, "expected both quantile dimensions")
2480
2481 var empty, finite int
2482 for _, v := range quantileUpdate.Values {
2483 if v.IsEmpty {
2484 empty++
2485 } else {
2486 finite++
2487 }
2488 }
2489 assert.Equalf(t, 1, empty, "exactly the NaN quantile dim must gap, got %+v", quantileUpdate.Values)
2490 assert.Equalf(t, 1, finite, "exactly the finite quantile dim must carry a value, got %+v", quantileUpdate.Values)
2491 }
2492
2493 func findUpdateAction(plan Plan) *UpdateChartAction {
2494 for _, action := range plan.Actions {
2495 if update, ok := action.(UpdateChartAction); ok {
2496 return &update
2497 }
2498 }
2499 return nil
2500 }
2501
2502 func findCreateChartAction(plan Plan) *CreateChartAction {
2503 for _, action := range plan.Actions {
2504 if create, ok := action.(CreateChartAction); ok {
2505 return &create
2506 }
2507 }
2508 return nil
2509 }
2510
2511 func findCreateChartActionByID(plan Plan, chartID string) *CreateChartAction {
2512 for _, action := range plan.Actions {
2513 create, ok := action.(CreateChartAction)
2514 if !ok || create.ChartID != chartID {
2515 continue
2516 }
2517 return &create
2518 }
2519 return nil
2520 }
2521
2522 func findRemoveDimensionAction(plan Plan) *RemoveDimensionAction {
2523 for _, action := range plan.Actions {
2524 if remove, ok := action.(RemoveDimensionAction); ok {
2525 return &remove
2526 }
2527 }
2528 return nil
2529 }
2530
2531 func mustCycleController(t *testing.T, s metrix.CollectorStore) metrix.CycleController {
2532 t.Helper()
2533 managed, ok := metrix.AsCycleManagedStore(s)
2534 require.True(t, ok)
2535 return managed.CycleController()
2536 }