master
go 474 lines 14.3 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package metrix
4
5 import (
6 "testing"
7
8 "github.com/stretchr/testify/require"
9 )
10
11 func TestMeasureSetDeclarationValidation(t *testing.T) {
12 tests := map[string]struct {
13 run func(t *testing.T)
14 }{
15 "snapshot MeasureSetGauge declaration requires WithMeasureSetFields": {
16 run: func(t *testing.T) {
17 s := NewCollectorStore()
18 expectPanic(t, func() {
19 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency")
20 })
21 },
22 },
23 "stateful MeasureSetCounter declaration requires WithMeasureSetFields": {
24 run: func(t *testing.T) {
25 s := NewCollectorStore()
26 expectPanic(t, func() {
27 _ = s.Write().StatefulMeter("svc").MeasureSetCounter("requests")
28 })
29 },
30 },
31 "MeasureSet declaration rejects duplicate field names": {
32 run: func(t *testing.T) {
33 s := NewCollectorStore()
34 expectPanic(t, func() {
35 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
36 WithMeasureSetFields(
37 MeasureFieldSpec{Name: "value"},
38 MeasureFieldSpec{Name: "value"},
39 ),
40 )
41 })
42 },
43 },
44 "MeasureSet declaration rejects empty field names": {
45 run: func(t *testing.T) {
46 s := NewCollectorStore()
47 expectPanic(t, func() {
48 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
49 WithMeasureSetFields(MeasureFieldSpec{Name: " "}),
50 )
51 })
52 },
53 },
54 "MeasureSet schema mismatch panics on field drift": {
55 run: func(t *testing.T) {
56 s := NewCollectorStore()
57 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
58 WithMeasureSetFields(
59 MeasureFieldSpec{Name: "value"},
60 MeasureFieldSpec{Name: "max"},
61 ),
62 )
63 expectPanic(t, func() {
64 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
65 WithMeasureSetFields(
66 MeasureFieldSpec{Name: "value"},
67 MeasureFieldSpec{Name: "min"},
68 ),
69 )
70 })
71 },
72 },
73 "MeasureSet schema mismatch panics on field float drift": {
74 run: func(t *testing.T) {
75 s := NewCollectorStore()
76 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
77 WithMeasureSetFields(MeasureFieldSpec{Name: "value", Float: false}),
78 )
79 expectPanic(t, func() {
80 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
81 WithMeasureSetFields(MeasureFieldSpec{Name: "value", Float: true}),
82 )
83 })
84 },
85 },
86 "MeasureSet schema mismatch panics on semantics drift": {
87 run: func(t *testing.T) {
88 s := NewCollectorStore()
89 _ = s.Write().SnapshotMeter("svc").MeasureSetGauge("latency",
90 WithMeasureSetFields(MeasureFieldSpec{Name: "value"}),
91 )
92 expectPanic(t, func() {
93 _ = s.Write().SnapshotMeter("svc").MeasureSetCounter("latency",
94 WithMeasureSetFields(MeasureFieldSpec{Name: "value"}),
95 )
96 })
97 },
98 },
99 "MeasureSet options are invalid for other instrument kinds": {
100 run: func(t *testing.T) {
101 s := NewCollectorStore()
102 expectPanic(t, func() {
103 _ = s.Write().SnapshotMeter("svc").Gauge("latency",
104 WithMeasureSetFields(MeasureFieldSpec{Name: "value"}),
105 )
106 })
107 },
108 },
109 }
110
111 for name, tc := range tests {
112 t.Run(name, tc.run)
113 }
114 }
115
116 func TestMeasureSetStoreScenarios(t *testing.T) {
117 tests := map[string]struct {
118 run func(t *testing.T)
119 }{
120 "snapshot MeasureSet gauge read and flatten metadata": {
121 run: func(t *testing.T) {
122 s := NewCollectorStore()
123 cc := cycleController(t, s)
124 ms := s.Write().SnapshotMeter("svc").MeasureSetGauge(
125 "latency",
126 WithMeasureSetFields(
127 MeasureFieldSpec{Name: "value"},
128 MeasureFieldSpec{Name: "ratio", Float: true},
129 ),
130 WithDescription("Latency"),
131 WithChartFamily("Service"),
132 WithUnit("seconds"),
133 )
134
135 cc.BeginCycle()
136 ms.ObserveFields(map[string]SampleValue{
137 "value": 1.5,
138 "ratio": 0.5,
139 })
140 cc.CommitCycleSuccess()
141
142 mustMeasureSet(t, s.Read(), "svc.latency", nil, []SampleValue{1.5, 0.5})
143
144 rawMeta, ok := s.Read().SeriesMeta("svc.latency", nil)
145 require.True(t, ok)
146 require.Equal(t, MetricKindMeasureSet, rawMeta.Kind)
147 require.Equal(t, MetricKindMeasureSet, rawMeta.SourceKind)
148 require.Equal(t, FlattenRoleNone, rawMeta.FlattenRole)
149
150 flat := s.Read(ReadFlatten())
151 mustValue(t, flat, "svc.latency_value", measureSetFieldLabels("value"), 1.5)
152 mustValue(t, flat, "svc.latency_ratio", measureSetFieldLabels("ratio"), 0.5)
153 _, ok = flat.Value("svc.latency_value", nil)
154 require.False(t, ok, "expected flattened MeasureSet scalar lookup without synthetic field label to miss")
155 _, ok = flat.MeasureSet("svc.latency", nil)
156 require.False(t, ok, "expected flattened view to hide typed MeasureSet getter")
157
158 flatMeta, ok := flat.SeriesMeta("svc.latency_ratio", measureSetFieldLabels("ratio"))
159 require.True(t, ok)
160 require.Equal(t, MetricKindGauge, flatMeta.Kind)
161 require.Equal(t, MetricKindMeasureSet, flatMeta.SourceKind)
162 require.Equal(t, FlattenRoleMeasureSetField, flatMeta.FlattenRole)
163
164 meta, ok := flat.MetricMeta("svc.latency_ratio")
165 require.True(t, ok)
166 require.Equal(t, "Latency", meta.Description)
167 require.Equal(t, "Service", meta.ChartFamily)
168 require.Equal(t, "seconds", meta.Unit)
169 require.True(t, meta.Float)
170 },
171 },
172 "stateful MeasureSet gauge add baselines from committed and remains visible": {
173 run: func(t *testing.T) {
174 s := NewCollectorStore()
175 cc := cycleController(t, s)
176 ms := s.Write().StatefulMeter("svc").MeasureSetGauge(
177 "usage",
178 WithMeasureSetFields(
179 MeasureFieldSpec{Name: "value"},
180 MeasureFieldSpec{Name: "limit"},
181 ),
182 )
183
184 cc.BeginCycle()
185 ms.SetPoint(MeasureSetPoint{Values: []SampleValue{10, 20}})
186 cc.CommitCycleSuccess()
187
188 cc.BeginCycle()
189 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{2, 3}})
190 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{1, 0}})
191 cc.CommitCycleSuccess()
192 mustMeasureSet(t, s.Read(), "svc.usage", nil, []SampleValue{13, 23})
193
194 cc.BeginCycle()
195 cc.CommitCycleSuccess()
196 mustMeasureSet(t, s.Read(), "svc.usage", nil, []SampleValue{13, 23})
197 mustValue(t, s.Read(ReadFlatten()), "svc.usage_value", measureSetFieldLabels("value"), 13)
198 mustValue(t, s.Read(ReadFlatten()), "svc.usage_limit", measureSetFieldLabels("limit"), 23)
199 },
200 },
201 "snapshot MeasureSet counter flatten delta and reset-aware semantics": {
202 run: func(t *testing.T) {
203 s := NewCollectorStore()
204 cc := cycleController(t, s)
205 ms := s.Write().SnapshotMeter("svc").MeasureSetCounter(
206 "requests",
207 WithMeasureSetFields(
208 MeasureFieldSpec{Name: "ok"},
209 MeasureFieldSpec{Name: "failed"},
210 ),
211 )
212
213 cc.BeginCycle()
214 ms.ObserveTotalFields(map[string]SampleValue{
215 "ok": 100,
216 "failed": 40,
217 })
218 cc.CommitCycleSuccess()
219 mustMeasureSet(t, s.Read(), "svc.requests", nil, []SampleValue{100, 40})
220 mustNoDelta(t, s.Read(ReadFlatten()), "svc.requests_ok", measureSetFieldLabels("ok"))
221
222 cc.BeginCycle()
223 ms.ObserveTotalFields(map[string]SampleValue{
224 "ok": 150,
225 "failed": 50,
226 })
227 cc.CommitCycleSuccess()
228 mustDelta(t, s.Read(ReadFlatten()), "svc.requests_ok", measureSetFieldLabels("ok"), 50)
229 mustDelta(t, s.Read(ReadFlatten()), "svc.requests_failed", measureSetFieldLabels("failed"), 10)
230
231 cc.BeginCycle()
232 ms.ObserveTotalFields(map[string]SampleValue{
233 "ok": 20,
234 "failed": 5,
235 })
236 cc.CommitCycleSuccess()
237 mustDelta(t, s.Read(ReadFlatten()), "svc.requests_ok", measureSetFieldLabels("ok"), 20)
238 mustDelta(t, s.Read(ReadFlatten()), "svc.requests_failed", measureSetFieldLabels("failed"), 5)
239 },
240 },
241 "snapshot MeasureSet counter delta unavailable on attempt gap": {
242 run: func(t *testing.T) {
243 s := NewCollectorStore()
244 cc := cycleController(t, s)
245 ms := s.Write().SnapshotMeter("svc").MeasureSetCounter(
246 "jobs",
247 WithMeasureSetFields(MeasureFieldSpec{Name: "done"}),
248 )
249
250 cc.BeginCycle()
251 ms.ObserveTotalFields(map[string]SampleValue{"done": 10})
252 cc.CommitCycleSuccess()
253
254 cc.BeginCycle()
255 ms.ObserveTotalFields(map[string]SampleValue{"done": 20})
256 cc.CommitCycleSuccess()
257 mustDelta(t, s.Read(ReadFlatten()), "svc.jobs_done", measureSetFieldLabels("done"), 10)
258
259 cc.BeginCycle()
260 ms.ObserveTotalFields(map[string]SampleValue{"done": 30})
261 cc.AbortCycle()
262
263 cc.BeginCycle()
264 ms.ObserveTotalFields(map[string]SampleValue{"done": 40})
265 cc.CommitCycleSuccess()
266 mustNoDelta(t, s.Read(ReadFlatten()), "svc.jobs_done", measureSetFieldLabels("done"))
267 },
268 },
269 "MeasureSet flatten label key collision panics": {
270 run: func(t *testing.T) {
271 s := NewCollectorStore()
272 cc := cycleController(t, s)
273 ms := s.Write().SnapshotMeter("svc").
274 WithLabels(Label{Key: MeasureSetFieldLabel, Value: "already-present"}).
275 MeasureSetGauge(
276 "latency",
277 WithMeasureSetFields(MeasureFieldSpec{Name: "value"}),
278 )
279
280 cc.BeginCycle()
281 expectPanic(t, func() {
282 ms.ObservePoint(MeasureSetPoint{Values: []SampleValue{1}})
283 })
284 cc.AbortCycle()
285 },
286 },
287 "stateful MeasureSet counter add accumulates and flattened delta works": {
288 run: func(t *testing.T) {
289 s := NewCollectorStore()
290 cc := cycleController(t, s)
291 ms := s.Write().StatefulMeter("svc").MeasureSetCounter(
292 "events",
293 WithMeasureSetFields(
294 MeasureFieldSpec{Name: "ok"},
295 MeasureFieldSpec{Name: "failed"},
296 ),
297 )
298
299 cc.BeginCycle()
300 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{5, 1}})
301 cc.CommitCycleSuccess()
302 mustNoDelta(t, s.Read(ReadFlatten()), "svc.events_ok", measureSetFieldLabels("ok"))
303
304 cc.BeginCycle()
305 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{2, 3}})
306 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{1, 0}})
307 cc.CommitCycleSuccess()
308 mustMeasureSet(t, s.Read(), "svc.events", nil, []SampleValue{8, 4})
309 mustDelta(t, s.Read(ReadFlatten()), "svc.events_ok", measureSetFieldLabels("ok"), 3)
310 mustDelta(t, s.Read(ReadFlatten()), "svc.events_failed", measureSetFieldLabels("failed"), 3)
311 },
312 },
313 "stateful MeasureSet counter negative add panics": {
314 run: func(t *testing.T) {
315 s := NewCollectorStore()
316 cc := cycleController(t, s)
317 ms := s.Write().StatefulMeter("svc").MeasureSetCounter(
318 "events",
319 WithMeasureSetFields(
320 MeasureFieldSpec{Name: "ok"},
321 MeasureFieldSpec{Name: "failed"},
322 ),
323 )
324
325 cc.BeginCycle()
326 expectPanic(t, func() {
327 ms.AddPoint(MeasureSetPoint{Values: []SampleValue{1, -1}})
328 })
329 cc.AbortCycle()
330 },
331 },
332 "snapshot MeasureSet named full writes require exact field set": {
333 run: func(t *testing.T) {
334 s := NewCollectorStore()
335 cc := cycleController(t, s)
336 ms := s.Write().SnapshotMeter("svc").MeasureSetGauge(
337 "latency",
338 WithMeasureSetFields(
339 MeasureFieldSpec{Name: "value"},
340 MeasureFieldSpec{Name: "max"},
341 ),
342 )
343
344 cc.BeginCycle()
345 expectPanic(t, func() {
346 ms.ObserveFields(map[string]SampleValue{"value": 1})
347 })
348 cc.AbortCycle()
349
350 cc.BeginCycle()
351 expectPanic(t, func() {
352 ms.ObserveFields(map[string]SampleValue{
353 "value": 1,
354 "max": 2,
355 "min": 0,
356 })
357 })
358 cc.AbortCycle()
359 },
360 },
361 "stateful MeasureSet gauge named writes support full and singular updates": {
362 run: func(t *testing.T) {
363 s := NewCollectorStore()
364 cc := cycleController(t, s)
365 ms := s.Write().StatefulMeter("svc").MeasureSetGauge(
366 "usage",
367 WithMeasureSetFields(
368 MeasureFieldSpec{Name: "value"},
369 MeasureFieldSpec{Name: "limit"},
370 ),
371 )
372
373 cc.BeginCycle()
374 ms.SetFields(map[string]SampleValue{
375 "value": 10,
376 "limit": 20,
377 })
378 cc.CommitCycleSuccess()
379
380 cc.BeginCycle()
381 ms.SetField("value", 15)
382 ms.AddField("limit", 3)
383 cc.CommitCycleSuccess()
384
385 mustMeasureSet(t, s.Read(), "svc.usage", nil, []SampleValue{15, 23})
386 mustValue(t, s.Read(ReadFlatten()), "svc.usage_value", measureSetFieldLabels("value"), 15)
387 mustValue(t, s.Read(ReadFlatten()), "svc.usage_limit", measureSetFieldLabels("limit"), 23)
388 },
389 },
390 "stateful MeasureSet counter named writes support full and singular deltas": {
391 run: func(t *testing.T) {
392 s := NewCollectorStore()
393 cc := cycleController(t, s)
394 ms := s.Write().StatefulMeter("svc").MeasureSetCounter(
395 "events",
396 WithMeasureSetFields(
397 MeasureFieldSpec{Name: "ok"},
398 MeasureFieldSpec{Name: "failed"},
399 ),
400 )
401
402 cc.BeginCycle()
403 ms.AddFields(map[string]SampleValue{
404 "ok": 5,
405 "failed": 1,
406 })
407 cc.CommitCycleSuccess()
408 mustNoDelta(t, s.Read(ReadFlatten()), "svc.events_ok", measureSetFieldLabels("ok"))
409
410 cc.BeginCycle()
411 ms.AddField("ok", 2)
412 ms.AddField("failed", 3)
413 cc.CommitCycleSuccess()
414
415 mustMeasureSet(t, s.Read(), "svc.events", nil, []SampleValue{7, 4})
416 mustDelta(t, s.Read(ReadFlatten()), "svc.events_ok", measureSetFieldLabels("ok"), 2)
417 mustDelta(t, s.Read(ReadFlatten()), "svc.events_failed", measureSetFieldLabels("failed"), 3)
418 },
419 },
420 "MeasureSet direct read returns a copy": {
421 run: func(t *testing.T) {
422 s := NewCollectorStore()
423 cc := cycleController(t, s)
424 ms := s.Write().SnapshotMeter("svc").MeasureSetGauge(
425 "latency",
426 WithMeasureSetFields(MeasureFieldSpec{Name: "value"}),
427 )
428
429 cc.BeginCycle()
430 ms.ObservePoint(MeasureSetPoint{Values: []SampleValue{7}})
431 cc.CommitCycleSuccess()
432
433 p, ok := s.Read().MeasureSet("svc.latency", nil)
434 require.True(t, ok)
435 p.Values[0] = 99
436
437 mustMeasureSet(t, s.Read(), "svc.latency", nil, []SampleValue{7})
438 },
439 },
440 "MeasureSet point length mismatch panics": {
441 run: func(t *testing.T) {
442 s := NewCollectorStore()
443 cc := cycleController(t, s)
444 ms := s.Write().SnapshotMeter("svc").MeasureSetGauge(
445 "latency",
446 WithMeasureSetFields(
447 MeasureFieldSpec{Name: "value"},
448 MeasureFieldSpec{Name: "max"},
449 ),
450 )
451
452 cc.BeginCycle()
453 expectPanic(t, func() {
454 ms.ObservePoint(MeasureSetPoint{Values: []SampleValue{1}})
455 })
456 cc.AbortCycle()
457 },
458 },
459 }
460
461 for name, tc := range tests {
462 t.Run(name, tc.run)
463 }
464 }
465
466 func mustMeasureSet(t *testing.T, r Reader, name string, labels Labels, want []SampleValue) {
467 t.Helper()
468 got, ok := r.MeasureSet(name, labels)
469 require.True(t, ok, "expected measureset for %s", name)
470 require.Len(t, got.Values, len(want), "unexpected measureset size for %s", name)
471 for i, w := range want {
472 require.Equal(t, w, got.Values[i], "unexpected measureset value %d for %s", i, name)
473 }
474 }