master
go 359 lines 9.35 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartengine
4
5 import (
6 "testing"
7
8 "github.com/netdata/netdata/go/plugins/pkg/metrix"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11 )
12
13 func TestEngineLoadScenarios(t *testing.T) {
14 tests := map[string]struct {
15 initialYAML string
16 initialRev uint64
17 reloadYAML string
18 reloadRev uint64
19 reloadErr bool
20 assert func(t *testing.T, e *Engine)
21 }{
22 "load from yaml populates program and revision": {
23 initialYAML: validTemplateYAML(),
24 initialRev: 100,
25 assert: func(t *testing.T, e *Engine) {
26 t.Helper()
27 require.True(t, e.ready())
28 p := e.program()
29 require.NotNil(t, p)
30 assert.Equal(t, uint64(100), p.Revision())
31 assert.Equal(t, "v1", p.Version())
32 assert.Equal(t, []string{"mysql_queries_total"}, p.MetricNames())
33 },
34 },
35 "failed reload keeps previous compiled program": {
36 initialYAML: validTemplateYAML(),
37 initialRev: 200,
38 reloadYAML: `
39 version: v1
40 groups:
41 - family: Database
42 charts:
43 - title: Broken
44 context: broken
45 units: "1"
46 dimensions:
47 - selector: mysql_queries_total
48 `,
49 reloadRev: 201,
50 reloadErr: true,
51 assert: func(t *testing.T, e *Engine) {
52 t.Helper()
53 p := e.program()
54 require.NotNil(t, p)
55 assert.Equal(t, uint64(200), p.Revision())
56 assert.Equal(t, []string{"mysql_queries_total"}, p.MetricNames())
57 },
58 },
59 "invalid selector syntax on reload is rejected and keeps previous program": {
60 initialYAML: validTemplateYAML(),
61 initialRev: 300,
62 reloadYAML: `
63 version: v1
64 groups:
65 - family: Database
66 metrics:
67 - mysql_queries_total
68 charts:
69 - title: Broken selector
70 context: broken_selector
71 units: queries/s
72 dimensions:
73 - selector: mysql_queries_total{method="GET",}
74 name: total
75 `,
76 reloadRev: 301,
77 reloadErr: true,
78 assert: func(t *testing.T, e *Engine) {
79 t.Helper()
80 p := e.program()
81 require.NotNil(t, p)
82 assert.Equal(t, uint64(300), p.Revision())
83 assert.Equal(t, []string{"mysql_queries_total"}, p.MetricNames())
84 },
85 },
86 "invalid engine selector on reload is rejected and keeps previous program": {
87 initialYAML: validTemplateYAML(),
88 initialRev: 400,
89 reloadYAML: `
90 version: v1
91 engine:
92 selector:
93 allow:
94 - mysql_queries_total{db="main",}
95 groups:
96 - family: Database
97 metrics:
98 - mysql_queries_total
99 charts:
100 - title: Queries
101 context: queries_total
102 units: queries/s
103 dimensions:
104 - selector: mysql_queries_total
105 name: total
106 `,
107 reloadRev: 401,
108 reloadErr: true,
109 assert: func(t *testing.T, e *Engine) {
110 t.Helper()
111 p := e.program()
112 require.NotNil(t, p)
113 assert.Equal(t, uint64(400), p.Revision())
114 assert.Equal(t, []string{"mysql_queries_total"}, p.MetricNames())
115 },
116 },
117 }
118
119 for name, tc := range tests {
120 t.Run(name, func(t *testing.T) {
121 e, err := New()
122 require.NoError(t, err)
123
124 require.NoError(t, e.LoadYAML([]byte(tc.initialYAML), tc.initialRev))
125 if tc.reloadYAML != "" {
126 err = e.LoadYAML([]byte(tc.reloadYAML), tc.reloadRev)
127 if tc.reloadErr {
128 require.Error(t, err)
129 } else {
130 require.NoError(t, err)
131 }
132 }
133 if tc.assert != nil {
134 tc.assert(t, e)
135 }
136 })
137 }
138 }
139
140 func validTemplateYAML() string {
141 return `
142 version: v1
143 groups:
144 - family: Database
145 metrics:
146 - mysql_queries_total
147 charts:
148 - title: Queries
149 context: queries_total
150 units: queries/s
151 dimensions:
152 - selector: mysql_queries_total
153 name: total
154 `
155 }
156
157 func TestEngineResetMaterializedPreservesProgram(t *testing.T) {
158 e, err := New()
159 require.NoError(t, err)
160 require.NoError(t, e.LoadYAML([]byte(validTemplateYAML()), 42))
161
162 store := metrix.NewCollectorStore()
163 cc := mustCycleController(t, store)
164 cc.BeginCycle()
165 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(7)
166 cc.CommitCycleSuccess()
167
168 plan1, err := buildPlan(e, store.Read())
169 require.NoError(t, err)
170 require.NotNil(t, findCreateChartActionInEngineTests(plan1))
171
172 p := e.program()
173 require.NotNil(t, p)
174 require.Equal(t, uint64(42), p.Revision())
175
176 e.ResetMaterialized()
177
178 p = e.program()
179 require.NotNil(t, p)
180 require.Equal(t, uint64(42), p.Revision())
181
182 plan2, err := buildPlan(e, store.Read())
183 require.NoError(t, err)
184 require.NotNil(t, findCreateChartActionInEngineTests(plan2))
185 }
186
187 func TestEnginePreparePlanLifecycleScenarios(t *testing.T) {
188 tests := map[string]struct {
189 run func(t *testing.T)
190 }{
191 "outstanding attempt blocks prepare until abort": {
192 run: func(t *testing.T) {
193 e, err := New()
194 require.NoError(t, err)
195 require.NoError(t, e.LoadYAML([]byte(validTemplateYAML()), 1))
196
197 store := metrix.NewCollectorStore()
198 cc := mustCycleController(t, store)
199 cc.BeginCycle()
200 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(7)
201 cc.CommitCycleSuccess()
202
203 attempt, err := e.PreparePlan(store.Read())
204 require.NoError(t, err)
205
206 _, err = e.PreparePlan(store.Read())
207 require.ErrorIs(t, err, ErrOutstandingPlanAttempt)
208
209 attempt.Abort()
210 require.Empty(t, e.state.materialized.charts)
211
212 cc.BeginCycle()
213 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(8)
214 cc.CommitCycleSuccess()
215
216 plan, err := buildPlan(e, store.Read())
217 require.NoError(t, err)
218 require.NotNil(t, findCreateChartActionInEngineTests(plan))
219 },
220 },
221 "aborted attempt does not advance materialized lifecycle": {
222 run: func(t *testing.T) {
223 e, err := New()
224 require.NoError(t, err)
225 require.NoError(t, e.LoadYAML([]byte(validTemplateYAML()), 1))
226
227 store := metrix.NewCollectorStore()
228 cc := mustCycleController(t, store)
229 cc.BeginCycle()
230 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(9)
231 cc.CommitCycleSuccess()
232
233 attempt, err := e.PreparePlan(store.Read())
234 require.NoError(t, err)
235 attempt.Abort()
236 require.Empty(t, e.state.materialized.charts)
237
238 cc.BeginCycle()
239 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(12)
240 cc.CommitCycleSuccess()
241
242 plan, err := buildPlan(e, store.Read())
243 require.NoError(t, err)
244 require.NotNil(t, findCreateChartActionInEngineTests(plan))
245 },
246 },
247 "reset materialized makes prepared commit stale": {
248 run: func(t *testing.T) {
249 e, err := New()
250 require.NoError(t, err)
251 require.NoError(t, e.LoadYAML([]byte(validTemplateYAML()), 1))
252
253 store := metrix.NewCollectorStore()
254 cc := mustCycleController(t, store)
255 cc.BeginCycle()
256 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(11)
257 cc.CommitCycleSuccess()
258
259 attempt, err := e.PreparePlan(store.Read())
260 require.NoError(t, err)
261
262 e.ResetMaterialized()
263
264 require.ErrorIs(t, attempt.Commit(), ErrStalePlanAttempt)
265 nextAttempt, err := e.PreparePlan(store.Read())
266 require.NoError(t, err)
267 nextAttempt.Abort()
268 },
269 },
270 "repeated commit is rejected after successful commit": {
271 run: func(t *testing.T) {
272 e, err := New()
273 require.NoError(t, err)
274 require.NoError(t, e.LoadYAML([]byte(validTemplateYAML()), 1))
275
276 store := metrix.NewCollectorStore()
277 cc := mustCycleController(t, store)
278 cc.BeginCycle()
279 store.Write().SnapshotMeter("").Gauge("mysql_queries_total").Observe(13)
280 cc.CommitCycleSuccess()
281
282 attempt, err := e.PreparePlan(store.Read())
283 require.NoError(t, err)
284 require.NotNil(t, findCreateChartActionInEngineTests(attempt.Plan()))
285
286 require.NoError(t, attempt.Commit())
287 require.NotEmpty(t, e.state.materialized.charts)
288 require.ErrorIs(t, attempt.Commit(), ErrFinishedPlanAttempt)
289 },
290 },
291 }
292
293 for name, tc := range tests {
294 t.Run(name, tc.run)
295 }
296 }
297
298 func TestEngineResetMaterializedKeepsRouteCacheWarm(t *testing.T) {
299 e, err := New()
300 require.NoError(t, err)
301 require.NoError(t, e.LoadYAML([]byte(dynamicDimensionTemplateYAML()), 42))
302
303 store := metrix.NewCollectorStore()
304 cc := mustCycleController(t, store)
305 vec := store.Write().StatefulMeter("component").Vec("id").Gauge("load")
306
307 cc.BeginCycle()
308 vec.WithLabelValues("a").Set(1)
309 vec.WithLabelValues("b").Set(2)
310 cc.CommitCycleSuccess()
311
312 plan1, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
313 require.NoError(t, err)
314 require.NotNil(t, findCreateChartActionInEngineTests(plan1))
315 stats1 := e.stats()
316 require.Greater(t, stats1.RouteCacheMisses, uint64(0))
317
318 plan2, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
319 require.NoError(t, err)
320 require.Nil(t, findCreateChartActionInEngineTests(plan2))
321 stats2 := e.stats()
322 require.Greater(t, stats2.RouteCacheHits, stats1.RouteCacheHits)
323
324 e.ResetMaterialized()
325
326 plan3, err := buildPlan(e, store.Read(metrix.ReadFlatten()))
327 require.NoError(t, err)
328 require.NotNil(t, findCreateChartActionInEngineTests(plan3))
329 stats3 := e.stats()
330 require.Greater(t, stats3.RouteCacheHits, stats2.RouteCacheHits)
331 }
332
333 func findCreateChartActionInEngineTests(plan Plan) *CreateChartAction {
334 for _, action := range plan.Actions {
335 create, ok := action.(CreateChartAction)
336 if ok {
337 return &create
338 }
339 }
340 return nil
341 }
342
343 func dynamicDimensionTemplateYAML() string {
344 return `
345 version: v1
346 groups:
347 - family: Runtime
348 metrics:
349 - component.load
350 charts:
351 - id: component_load
352 title: Component Load
353 context: netdata.go.plugin.component.component_load
354 units: load
355 dimensions:
356 - selector: component.load
357 name_from_label: id
358 `
359 }