master
go 485 lines 18.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package nagios
4
5 import (
6 "context"
7 "math"
8 "strings"
9 "testing"
10
11 "github.com/netdata/netdata/go/plugins/pkg/metrix"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/charttpl"
14 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/collecttest"
15 "github.com/netdata/netdata/go/plugins/plugin/scripts.d/collector/nagios/internal/output"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18 )
19
20 const gateCheckName = "check_gate"
21
22 func TestV2Gate_G1_TemplateCompileProof(t *testing.T) {
23 templateYAML := New().ChartTemplateYAML()
24 collecttest.AssertChartTemplateSchema(t, templateYAML)
25
26 specYAML, err := charttpl.DecodeYAML([]byte(templateYAML))
27 require.NoError(t, err)
28 require.NoError(t, specYAML.Validate())
29 _, err = chartengine.Compile(specYAML, 1)
30 require.NoError(t, err)
31 }
32
33 func TestV2Gate_G2_PerfdataRouting(t *testing.T) {
34 router := newPerfdataRouter(64)
35 warnLow := 100.0
36 warnHigh := 500.0
37 critLow := 200.0
38 critHigh := 900.0
39 samples := router.route(gateCheckName, []output.PerfDatum{
40 {
41 Label: "latency", Unit: "ms", Value: 120,
42 Warn: &output.ThresholdRange{Inclusive: true, Low: &warnLow, High: &warnHigh},
43 Crit: &output.ThresholdRange{Inclusive: true, Low: &critLow, High: &critHigh},
44 },
45 {Label: "throughput", Unit: "KB", Value: 30},
46 {Label: "wire_rate", Unit: "kb", Value: 80},
47 {Label: "free_pct", Unit: "%", Value: 40},
48 {Label: "requests", Unit: "c", Value: 42},
49 {Label: "custom", Unit: "widgets", Value: 3.14},
50 {Label: "dup-one", Unit: "widgets", Value: 11}, // collides with dup_one
51 {Label: "dup_one", Unit: "widgets", Value: 22},
52 })
53
54 byName := valueSampleMap(samples.values)
55 byUnit := valueSampleUnits(samples.values)
56 byThreshold := thresholdStateMap(samples.thresholdStates)
57 assertNear(t, byName["perfdata.check_gate.time_latency_value"], 0.12)
58 assertNear(t, byName["perfdata.check_gate.bytes_throughput_value"], 30000)
59 assertNear(t, byName["perfdata.check_gate.bits_wire_rate_value"], 80000)
60 assertNear(t, byName["perfdata.check_gate.percent_free_pct_value"], 40)
61 assertNear(t, byName["perfdata.check_gate.counter_requests_value"], 42)
62 assertNear(t, byName["perfdata.check_gate.generic_custom_value"], 3.14)
63 assertNear(t, byName["perfdata.check_gate.generic_dup_one_value"], 11)
64 assertString(t, byThreshold["perfdata.check_gate.time_latency_threshold_state"], perfThresholdStateWarning)
65 assertString(t, byThreshold["perfdata.check_gate.bytes_throughput_threshold_state"], perfThresholdStateNone)
66 assertString(t, byThreshold["perfdata.check_gate.bits_wire_rate_threshold_state"], perfThresholdStateNone)
67 assertString(t, byThreshold["perfdata.check_gate.percent_free_pct_threshold_state"], perfThresholdStateNone)
68 assertString(t, byThreshold["perfdata.check_gate.generic_custom_threshold_state"], perfThresholdStateNone)
69 _, hasCounterThreshold := byThreshold["perfdata.check_gate.counter_requests_threshold_state"]
70 assert.False(t, hasCounterThreshold)
71
72 assertString(t, byUnit["perfdata.check_gate.time_latency_value"], "seconds")
73 assertString(t, byUnit["perfdata.check_gate.bytes_throughput_value"], "bytes")
74 assertString(t, byUnit["perfdata.check_gate.bits_wire_rate_value"], "bits")
75 assertString(t, byUnit["perfdata.check_gate.percent_free_pct_value"], "%")
76 assertString(t, byUnit["perfdata.check_gate.counter_requests_value"], "c")
77 assertString(t, byUnit["perfdata.check_gate.generic_custom_value"], "generic")
78
79 store := metrix.NewCollectorStore()
80 cc := gateCycleController(t, store)
81 cc.BeginCycle()
82 sm := store.Write().SnapshotMeter("")
83 labels := sm.LabelSet(
84 metrix.Label{Key: "nagios_job", Value: "gate_job"},
85 )
86 for _, measureSet := range samples.values {
87 fields := perfMeasureSetValues(measureSet.value)
88 if measureSet.counter {
89 sm.MeasureSetCounter(
90 measureSet.name,
91 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
92 metrix.WithChartFamily(perfdataFamily(measureSet.checkName)),
93 metrix.WithUnit(measureSet.unit),
94 ).ObserveTotalFields(fields, labels)
95 continue
96 }
97 sm.MeasureSetGauge(
98 measureSet.name,
99 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
100 metrix.WithChartFamily(perfdataFamily(measureSet.checkName)),
101 metrix.WithUnit(measureSet.unit),
102 ).ObserveFields(fields, labels)
103 }
104 for _, thresholdState := range samples.thresholdStates {
105 sm.WithLabelSet(labels).StateSet(
106 thresholdState.name,
107 metrix.WithStateSetMode(metrix.ModeBitSet),
108 metrix.WithStateSetStates(perfThresholdStateNames...),
109 metrix.WithChartFamily(perfdataFamily(thresholdState.checkName)),
110 metrix.WithUnit("state"),
111 ).Enable(thresholdState.state)
112 sm.WithLabelSet(labels).WithLabels(
113 metrix.Label{Key: perfdataValueLabelKey, Value: thresholdState.perfdataValue},
114 ).StateSet(
115 jobPerfdataThresholdMetricName,
116 metrix.WithStateSetMode(metrix.ModeBitSet),
117 metrix.WithStateSetStates(perfThresholdAlertStateNames...),
118 metrix.WithUnit("state"),
119 ).Enable(thresholdState.state)
120 }
121 cc.CommitCycleSuccess()
122
123 reader := store.Read(metrix.ReadFlatten())
124 assertMetricMeta(t, reader, "perfdata.check_gate.time_latency_value", "seconds", true)
125 assertMetricMeta(t, reader, "perfdata.check_gate.bytes_throughput_value", "bytes", true)
126 assertMetricMeta(t, reader, "perfdata.check_gate.bits_wire_rate_value", "bits", true)
127 assertMetricMeta(t, reader, "perfdata.check_gate.percent_free_pct_value", "%", true)
128 assertMetricMeta(t, reader, "perfdata.check_gate.counter_requests_value", "c", true)
129 assertMetricMeta(t, reader, "perfdata.check_gate.generic_custom_value", "generic", true)
130 assertMetricMeta(t, reader, "perfdata.check_gate.time_latency_threshold_state", "state", false)
131 assertMetricMeta(t, reader, "job.perfdata.threshold_state", "state", false)
132 assertMetricChartFamily(t, reader, "perfdata.check_gate.time_latency_value", "Perfdata/check_gate")
133 assertMetricChartFamily(t, reader, "perfdata.check_gate.time_latency_threshold_state", "Perfdata/check_gate")
134 assertMetricValue(t, reader, "perfdata.check_gate.time_latency_threshold_state", metrix.Labels{
135 "nagios_job": "gate_job",
136 "perfdata.check_gate.time_latency_threshold_state": perfThresholdStateWarning,
137 }, 1)
138 assertMetricValue(t, reader, "job.perfdata.threshold_state", metrix.Labels{
139 "nagios_job": "gate_job",
140 perfdataValueLabelKey: "time_latency",
141 "job.perfdata.threshold_state": perfThresholdStateWarning,
142 }, 1)
143 assertMetricValue(t, reader, "job.perfdata.threshold_state", metrix.Labels{
144 "nagios_job": "gate_job",
145 perfdataValueLabelKey: "time_latency",
146 "job.perfdata.threshold_state": perfThresholdStateRetry,
147 }, 0)
148 assertSeriesKind(t, reader, "perfdata.check_gate.time_latency_value", metrix.Labels{
149 "nagios_job": "gate_job",
150 metrix.MeasureSetFieldLabel: perfFieldValue,
151 }, metrix.MetricKindGauge)
152 assertSeriesKind(t, reader, "perfdata.check_gate.counter_requests_value", metrix.Labels{
153 "nagios_job": "gate_job",
154 metrix.MeasureSetFieldLabel: perfFieldValue,
155 }, metrix.MetricKindCounter)
156
157 changedClass := router.route(gateCheckName, []output.PerfDatum{
158 {Label: "latency", Unit: "%", Value: 1}, // same label, different class => new identity
159 })
160 changedSamples := valueSampleMap(changedClass.values)
161 assertNear(t, changedSamples["perfdata.check_gate.percent_latency_value"], 1)
162 }
163
164 func TestV2Gate_G3_ChartLifecycleChurn(t *testing.T) {
165 newHarness := func(t *testing.T) (*chartengine.Engine, metrix.CollectorStore, func(includeB bool) chartengine.Plan) {
166 t.Helper()
167 engine, err := chartengine.New()
168 require.NoError(t, err)
169 require.NoError(t, engine.LoadYAML([]byte(New().ChartTemplateYAML()), 1))
170
171 store := metrix.NewCollectorStore()
172 emit := func(includeB bool) chartengine.Plan {
173 cc := gateCycleController(t, store)
174 cc.BeginCycle()
175 sm := store.Write().SnapshotMeter("")
176 ls := sm.LabelSet(
177 metrix.Label{Key: "nagios_job", Value: "gate_job"},
178 )
179 aFields := defaultPerfMeasureSetValues()
180 aFields[perfFieldValue] = 1
181 sm.MeasureSetGauge(
182 "perfdata.check_gate.bytes_a",
183 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
184 metrix.WithChartFamily(perfdataFamily("check_gate")),
185 metrix.WithUnit("bytes"),
186 ).ObserveFields(aFields, ls)
187 if includeB {
188 bFields := defaultPerfMeasureSetValues()
189 bFields[perfFieldValue] = 2
190 sm.MeasureSetGauge(
191 "perfdata.check_gate.bytes_b",
192 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
193 metrix.WithChartFamily(perfdataFamily("check_gate")),
194 metrix.WithUnit("bytes"),
195 ).ObserveFields(bFields, ls)
196 }
197 cc.CommitCycleSuccess()
198
199 plan, err := prepareCommittedPlan(engine, store.Read(metrix.ReadFlatten()))
200 require.NoError(t, err)
201 return plan
202 }
203 return engine, store, emit
204 }
205
206 t.Run("abort-cycle does not remove", func(t *testing.T) {
207 engine, store, emit := newHarness(t)
208
209 plan1 := emit(true)
210 assert.NotZero(t, countActions[chartengine.CreateChartAction](plan1.Actions))
211
212 cc := gateCycleController(t, store)
213 cc.BeginCycle()
214 sm := store.Write().SnapshotMeter("")
215 ls := sm.LabelSet(
216 metrix.Label{Key: "nagios_job", Value: "gate_job"},
217 )
218 aFields := defaultPerfMeasureSetValues()
219 aFields[perfFieldValue] = 1
220 sm.MeasureSetGauge(
221 "perfdata.check_gate.bytes_a",
222 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
223 metrix.WithChartFamily(perfdataFamily("check_gate")),
224 metrix.WithUnit("bytes"),
225 ).ObserveFields(aFields, ls)
226 cc.AbortCycle()
227 assert.Equal(t, metrix.CollectStatusFailed, store.Read(metrix.ReadRaw()).CollectMeta().LastAttemptStatus)
228
229 planAbort, err := prepareCommittedPlan(engine, store.Read(metrix.ReadFlatten()))
230 require.NoError(t, err)
231 assert.Zero(t, removeActionsCount(planAbort.Actions))
232 })
233
234 t.Run("failed-attempt gap does not count toward expiry", func(t *testing.T) {
235 _, store, emit := newHarness(t)
236
237 plan1 := emit(true)
238 assert.NotZero(t, countActions[chartengine.CreateChartAction](plan1.Actions))
239 plan2 := emit(false)
240 assert.Zero(t, removeActionsCount(plan2.Actions))
241 assertPlanHasUpdateForTarget(t, plan2, "perfdata.check_gate.bytes_a")
242 assertPlanHasNoRemoveForTarget(t, plan2, "perfdata.check_gate.bytes_b")
243
244 cc := gateCycleController(t, store)
245 cc.BeginCycle()
246 sm := store.Write().SnapshotMeter("")
247 ls := sm.LabelSet(
248 metrix.Label{Key: "nagios_job", Value: "gate_job"},
249 )
250 aFields := defaultPerfMeasureSetValues()
251 aFields[perfFieldValue] = 1
252 sm.MeasureSetGauge(
253 "perfdata.check_gate.bytes_a",
254 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
255 metrix.WithChartFamily(perfdataFamily("check_gate")),
256 metrix.WithUnit("bytes"),
257 ).ObserveFields(aFields, ls)
258 cc.AbortCycle()
259 assert.Equal(t, metrix.CollectStatusFailed, store.Read(metrix.ReadRaw()).CollectMeta().LastAttemptStatus)
260
261 plan3 := emit(false)
262 assert.Zero(t, removeActionsCount(plan3.Actions))
263 assertPlanHasUpdateForTarget(t, plan3, "perfdata.check_gate.bytes_a")
264 assertPlanHasNoRemoveForTarget(t, plan3, "perfdata.check_gate.bytes_b")
265 plan4 := emit(false)
266 assert.Zero(t, removeActionsCount(plan4.Actions))
267 })
268 }
269
270 func TestV2Gate_G5_ScalingPrecisionEquivalence(t *testing.T) {
271 tests := map[string]struct {
272 unit string
273 raw float64
274 expectedUnit string
275 }{
276 "time": {unit: "ms", raw: 5.2, expectedUnit: "seconds"},
277 "bytes": {unit: "KB", raw: 1024, expectedUnit: "bytes"},
278 "bits": {unit: "kb", raw: 8, expectedUnit: "bits"},
279 "percent": {unit: "%", raw: 99.5, expectedUnit: "%"},
280 "counter": {unit: "c", raw: 42, expectedUnit: "c"},
281 "generic": {unit: "widgets", raw: 3.14, expectedUnit: "generic"},
282 }
283
284 for name, tc := range tests {
285 t.Run(name, func(t *testing.T) {
286 router := newPerfdataRouter(64)
287 displayV1 := legacyDisplayValue(tc.unit, tc.raw)
288
289 samples := router.route(gateCheckName, []output.PerfDatum{
290 {Label: "sample", Unit: tc.unit, Value: tc.raw},
291 })
292 var (
293 candidate float64
294 candidateKey string
295 candidateUnit string
296 )
297 found := false
298 for key, value := range valueSampleMap(samples.values) {
299 if len(key) >= 6 && key[len(key)-6:] == "_value" {
300 candidate = value
301 candidateKey = key
302 candidateUnit = valueSampleUnits(samples.values)[key]
303 found = true
304 break
305 }
306 }
307 require.True(t, found, "missing routed value sample")
308
309 if displayV1 == 0 {
310 assert.InDelta(t, displayV1, candidate, 1e-9)
311 return
312 }
313 rel := math.Abs(candidate-displayV1) / math.Abs(displayV1)
314 assert.LessOrEqual(t, rel, 0.001)
315 assert.Equal(t, tc.expectedUnit, candidateUnit)
316 assert.True(t, perfMeasureFieldFloat(perfFieldValue))
317
318 store := metrix.NewCollectorStore()
319 cc := gateCycleController(t, store)
320 cc.BeginCycle()
321 sm := store.Write().SnapshotMeter("")
322 for _, measureSet := range samples.values {
323 fields := perfMeasureSetValues(measureSet.value)
324 if measureSet.counter {
325 sm.MeasureSetCounter(
326 measureSet.name,
327 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
328 metrix.WithChartFamily(perfdataFamily(measureSet.checkName)),
329 metrix.WithUnit(measureSet.unit),
330 ).ObserveTotalFields(fields, sm.LabelSet())
331 continue
332 }
333 sm.MeasureSetGauge(
334 measureSet.name,
335 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
336 metrix.WithChartFamily(perfdataFamily(measureSet.checkName)),
337 metrix.WithUnit(measureSet.unit),
338 ).ObserveFields(fields, sm.LabelSet())
339 }
340 cc.CommitCycleSuccess()
341 flat := store.Read(metrix.ReadFlatten())
342 assertMetricMeta(t, flat, candidateKey, tc.expectedUnit, true)
343 assertMetricChartFamily(t, flat, candidateKey, "Perfdata/check_gate")
344 })
345 }
346 }
347
348 // With an empty meter prefix and context_namespace: nagios, an autogen perfdata chart's context
349 // is single-namespaced (nagios.perfdata.*) — never doubled (nagios.nagios.*) and never bare
350 // (perfdata.*). The chart ID intentionally drops the redundant nagios. prefix; the collector
351 // identity is already in the chart type (the job full name).
352 func TestV2Gate_AutogenPerfdataContextSingleNamespaced(t *testing.T) {
353 engine, err := chartengine.New()
354 require.NoError(t, err)
355 require.NoError(t, engine.LoadYAML([]byte(New().ChartTemplateYAML()), 1))
356
357 store := metrix.NewCollectorStore()
358 cc := gateCycleController(t, store)
359 cc.BeginCycle()
360 sm := store.Write().SnapshotMeter("")
361 ls := sm.LabelSet(metrix.Label{Key: "nagios_job", Value: "check_mem"})
362 fields := defaultPerfMeasureSetValues()
363 fields[perfFieldValue] = 30000
364 sm.MeasureSetGauge(
365 "perfdata.check_memory.bytes_used",
366 metrix.WithMeasureSetFields(perfMeasureSetFieldSpecs()...),
367 metrix.WithChartFamily(perfdataFamily("check_memory")),
368 metrix.WithUnit("bytes"),
369 ).ObserveFields(fields, ls)
370 cc.CommitCycleSuccess()
371
372 plan, err := prepareCommittedPlan(engine, store.Read(metrix.ReadFlatten()))
373 require.NoError(t, err)
374
375 var found bool
376 for _, action := range plan.Actions {
377 create, ok := action.(chartengine.CreateChartAction)
378 if !ok || !strings.Contains(create.ChartID, "perfdata.check_memory") {
379 continue
380 }
381 found = true
382 assert.Equal(t, "nagios.perfdata.check_memory.bytes_used", create.Meta.Context,
383 "autogen perfdata context must be single-namespaced via context_namespace")
384 assert.NotContains(t, create.Meta.Context, "nagios.nagios.",
385 "autogen context must not double-prefix the namespace")
386 assert.Falsef(t, strings.HasPrefix(create.ChartID, "nagios."),
387 "autogen chart ID should drop the redundant nagios. prefix, got %q", create.ChartID)
388 }
389 require.True(t, found, "expected an autogen create-chart action for the perfdata measureset")
390 }
391
392 func countActions[T any](actions []chartengine.EngineAction) int {
393 n := 0
394 for _, action := range actions {
395 if _, ok := action.(T); ok {
396 n++
397 }
398 }
399 return n
400 }
401
402 func removeActionsCount(actions []chartengine.EngineAction) int {
403 return countActions[chartengine.RemoveChartAction](actions) + countActions[chartengine.RemoveDimensionAction](actions)
404 }
405
406 func gateCycleController(t *testing.T, store metrix.CollectorStore) metrix.CycleController {
407 t.Helper()
408 managed, ok := metrix.AsCycleManagedStore(store)
409 require.True(t, ok)
410 return managed.CycleController()
411 }
412
413 func prepareCommittedPlan(engine *chartengine.Engine, reader metrix.Reader) (chartengine.Plan, error) {
414 attempt, err := engine.PreparePlan(reader)
415 if err != nil {
416 return chartengine.Plan{}, err
417 }
418 defer attempt.Abort()
419
420 plan := attempt.Plan()
421 if err := attempt.Commit(); err != nil {
422 return chartengine.Plan{}, err
423 }
424 return plan, nil
425 }
426
427 func assertMetricMeta(t *testing.T, reader metrix.Reader, metricName, unit string, isFloat bool) {
428 t.Helper()
429 meta, ok := reader.MetricMeta(metricName)
430 require.True(t, ok, "missing metric metadata for %q", metricName)
431 assert.Equal(t, unit, meta.Unit)
432 assert.Equal(t, isFloat, meta.Float)
433 }
434
435 func assertMetricChartFamily(t *testing.T, reader metrix.Reader, metricName, chartFamily string) {
436 t.Helper()
437 meta, ok := reader.MetricMeta(metricName)
438 require.True(t, ok, "missing metric metadata for %q", metricName)
439 assert.Equal(t, chartFamily, meta.ChartFamily)
440 }
441
442 func assertSeriesKind(t *testing.T, reader metrix.Reader, metricName string, labels metrix.Labels, want metrix.MetricKind) {
443 t.Helper()
444 meta, ok := reader.SeriesMeta(metricName, labels)
445 require.True(t, ok, "missing series metadata for %q with labels %v", metricName, labels)
446 assert.Equal(t, want, meta.Kind)
447 }
448
449 func assertPlanHasUpdateForTarget(t *testing.T, plan chartengine.Plan, updateMetricPrefix string) {
450 t.Helper()
451 for _, action := range plan.Actions {
452 update, ok := action.(chartengine.UpdateChartAction)
453 if !ok {
454 continue
455 }
456 if strings.HasPrefix(update.ChartID, updateMetricPrefix) {
457 return
458 }
459 }
460 assert.FailNow(t, "expected update action", "%q", updateMetricPrefix)
461 }
462
463 func assertPlanHasNoRemoveForTarget(t *testing.T, plan chartengine.Plan, removeMetricPrefix string) {
464 t.Helper()
465 for _, action := range plan.Actions {
466 switch a := action.(type) {
467 case chartengine.RemoveDimensionAction:
468 if strings.HasPrefix(a.ChartID, removeMetricPrefix) {
469 assert.FailNow(t, "unexpected remove dimension action", "%q", removeMetricPrefix)
470 }
471 case chartengine.RemoveChartAction:
472 if strings.HasPrefix(a.ChartID, removeMetricPrefix) {
473 assert.FailNow(t, "unexpected remove chart action", "%q", removeMetricPrefix)
474 }
475 }
476 }
477 }
478
479 func TestV2Gate_SmokeCollect(t *testing.T) {
480 coll := newTestCollector()
481 coll.runner = &fakeRunner{}
482 coll.Config.JobConfig.Plugin = writeTestPluginFile(t, "true")
483 coll.Config.JobConfig.Name = "smoke"
484 require.NoError(t, coll.Check(context.Background()))
485 }