master
go 290 lines 7.45 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package runtimechartemit
4
5 import (
6 "bytes"
7 "testing"
8
9 "github.com/netdata/netdata/go/plugins/plugin/framework/chartemit"
10 "github.com/netdata/netdata/go/plugins/plugin/framework/runtimecomp"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/netdata/netdata/go/plugins/pkg/metrix"
15 )
16
17 func TestRuntimeComponentRegistrationScenarios(t *testing.T) {
18 tests := map[string]struct {
19 run func(t *testing.T)
20 }{
21 "registration applies defaults and stores normalized emit env": {
22 run: func(t *testing.T) {
23 svc := New(nil)
24 svc.pluginName = "go.d"
25 store := metrix.NewRuntimeStore()
26
27 err := svc.RegisterComponent(ComponentConfig{
28 Name: "chartengine",
29 Store: store,
30 TemplateYAML: []byte(runtimeGaugeTemplateYAML()),
31 })
32 require.NoError(t, err)
33
34 specs := svc.registry.snapshot()
35 require.Len(t, specs, 1)
36
37 spec := specs[0]
38 assert.Equal(t, "chartengine", spec.Name)
39 assert.Equal(t, 1, spec.UpdateEvery)
40 assert.Equal(t, "netdata.go.d.internal.chartengine", spec.EmitEnv.TypeID)
41 assert.Equal(t, "go.d", spec.EmitEnv.Plugin)
42 assert.Equal(t, "internal", spec.EmitEnv.Module)
43 assert.Equal(t, "chartengine", spec.EmitEnv.JobName)
44 },
45 },
46 "registration allows empty template when autogen is enabled": {
47 run: func(t *testing.T) {
48 svc := New(nil)
49 svc.pluginName = "go.d"
50 store := metrix.NewRuntimeStore()
51
52 err := svc.RegisterComponent(ComponentConfig{
53 Name: "component",
54 Store: store,
55 Autogen: runtimecomp.AutogenPolicy{
56 Enabled: true,
57 },
58 })
59 require.NoError(t, err)
60
61 specs := svc.registry.snapshot()
62 require.Len(t, specs, 1)
63 assert.NotEmpty(t, specs[0].TemplateYAML)
64 },
65 },
66 "registration rejects empty template when autogen is disabled": {
67 run: func(t *testing.T) {
68 svc := New(nil)
69 svc.pluginName = "go.d"
70 store := metrix.NewRuntimeStore()
71
72 err := svc.RegisterComponent(ComponentConfig{
73 Name: "component",
74 Store: store,
75 })
76 require.Error(t, err)
77 assert.Contains(t, err.Error(), "template is required when autogen is disabled")
78 },
79 },
80 }
81
82 for name, tc := range tests {
83 t.Run(name, tc.run)
84 }
85 }
86
87 func TestRuntimeMetricsJobScenarios(t *testing.T) {
88 tests := map[string]struct {
89 run func(t *testing.T)
90 }{
91 "runtime job respects component cadence and emits on scheduled tick": {
92 run: func(t *testing.T) {
93 reg := newComponentRegistry()
94 store := metrix.NewRuntimeStore()
95 g := store.Write().StatefulMeter("component").Gauge("load")
96 g.Set(5)
97
98 reg.upsert(componentSpec{
99 Name: "component",
100 Store: store,
101 TemplateYAML: []byte(runtimeGaugeTemplateYAML()),
102 UpdateEvery: 2,
103 EmitEnv: chartemit.EmitEnv{
104 TypeID: "netdata.go.d.internal.component",
105 UpdateEvery: 2,
106 Plugin: "go.d",
107 Module: "internal",
108 JobName: "component",
109 },
110 })
111
112 var out bytes.Buffer
113 job := newRuntimeMetricsJob(&out, reg, nil)
114
115 job.runOnce(1)
116 assert.Equal(t, 0, out.Len())
117
118 job.runOnce(2)
119 result := out.String()
120 requireInOrder(t, result, "HOST ''", "CHART", "BEGIN")
121 },
122 },
123 "runtime job observes all visible runtime series (not only latest seq)": {
124 run: func(t *testing.T) {
125 reg := newComponentRegistry()
126 store := metrix.NewRuntimeStore()
127 vec := store.Write().StatefulMeter("component").Vec("id").Gauge("load")
128 vec.WithLabelValues("a").Set(1)
129 vec.WithLabelValues("b").Set(2)
130
131 reg.upsert(componentSpec{
132 Name: "component",
133 Store: store,
134 TemplateYAML: []byte(runtimeDynamicDimTemplateYAML()),
135 UpdateEvery: 1,
136 EmitEnv: chartemit.EmitEnv{
137 TypeID: "netdata.go.d.internal.component",
138 UpdateEvery: 1,
139 Plugin: "go.d",
140 Module: "internal",
141 JobName: "component",
142 },
143 })
144
145 var out bytes.Buffer
146 job := newRuntimeMetricsJob(&out, reg, nil)
147 job.runOnce(1)
148
149 result := out.String()
150 assert.Contains(t, result, "DIMENSION 'a' 'a' 'absolute'")
151 assert.Contains(t, result, "DIMENSION 'b' 'b' 'absolute'")
152 },
153 },
154 "runtime job keeps emitting on no-write ticks": {
155 run: func(t *testing.T) {
156 reg := newComponentRegistry()
157 store := metrix.NewRuntimeStore()
158 store.Write().StatefulMeter("component").Gauge("load").Set(7)
159
160 reg.upsert(componentSpec{
161 Name: "component",
162 Store: store,
163 TemplateYAML: []byte(runtimeGaugeTemplateYAML()),
164 UpdateEvery: 1,
165 EmitEnv: chartemit.EmitEnv{
166 TypeID: "netdata.go.d.internal.component",
167 UpdateEvery: 1,
168 Plugin: "go.d",
169 Module: "internal",
170 JobName: "component",
171 },
172 })
173
174 var out bytes.Buffer
175 job := newRuntimeMetricsJob(&out, reg, nil)
176 job.runOnce(1)
177 require.Contains(t, out.String(), "BEGIN")
178
179 out.Reset()
180 job.runOnce(2)
181 assert.Contains(t, out.String(), "BEGIN")
182 },
183 },
184 "runtime job emits obsolete chart when component is removed": {
185 run: func(t *testing.T) {
186 reg := newComponentRegistry()
187 store := metrix.NewRuntimeStore()
188 store.Write().StatefulMeter("component").Gauge("load").Set(3)
189
190 reg.upsert(componentSpec{
191 Name: "component",
192 Store: store,
193 TemplateYAML: []byte(runtimeGaugeTemplateYAML()),
194 UpdateEvery: 1,
195 EmitEnv: chartemit.EmitEnv{
196 TypeID: "netdata.go.d.internal.component",
197 UpdateEvery: 1,
198 Plugin: "go.d",
199 Module: "internal",
200 JobName: "component",
201 },
202 })
203
204 var out bytes.Buffer
205 job := newRuntimeMetricsJob(&out, reg, nil)
206 job.runOnce(1)
207 require.Contains(t, out.String(), "CHART 'netdata.go.d.internal.component.component_load'")
208
209 out.Reset()
210 reg.remove("component")
211 job.runOnce(2)
212 result := out.String()
213 requireInOrder(t, result, "HOST ''", "CHART 'netdata.go.d.internal.component.component_load'", "'obsolete'")
214 },
215 },
216 }
217
218 for name, tc := range tests {
219 t.Run(name, tc.run)
220 }
221 }
222
223 func TestRuntimeProducerRegistrationScenarios(t *testing.T) {
224 tests := map[string]struct {
225 run func(t *testing.T)
226 }{
227 "register producer validates input and unregisters cleanly": {
228 run: func(t *testing.T) {
229 svc := New(nil)
230 svc.Start("go.d", &bytes.Buffer{})
231 defer svc.Stop()
232
233 require.Error(t, svc.RegisterProducer("", func() error { return nil }))
234 require.Error(t, svc.RegisterProducer("x", nil))
235
236 var calls int
237 require.NoError(t, svc.RegisterProducer("x", func() error {
238 calls++
239 return nil
240 }))
241 svc.Tick(1)
242 assert.Equal(t, 1, calls)
243
244 svc.UnregisterProducer("x")
245 svc.Tick(2)
246 assert.Equal(t, 1, calls)
247 },
248 },
249 }
250
251 for name, tc := range tests {
252 t.Run(name, tc.run)
253 }
254 }
255
256 func runtimeGaugeTemplateYAML() string {
257 return `
258 version: v1
259 groups:
260 - family: Runtime
261 metrics:
262 - component.load
263 charts:
264 - id: component_load
265 title: Component Load
266 context: netdata.go.plugin.component.component_load
267 units: load
268 dimensions:
269 - selector: component.load
270 name: value
271 `
272 }
273
274 func runtimeDynamicDimTemplateYAML() string {
275 return `
276 version: v1
277 groups:
278 - family: Runtime
279 metrics:
280 - component.load
281 charts:
282 - id: component_load
283 title: Component Load
284 context: netdata.go.plugin.component.component_load
285 units: load
286 dimensions:
287 - selector: component.load
288 name_from_label: id
289 `
290 }