master
go 397 lines 14 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package metrix
4
5 import (
6 "errors"
7 "sync"
8 "testing"
9
10 "github.com/stretchr/testify/require"
11 )
12
13 func TestHostScopePartitionsSeriesIdentity(t *testing.T) {
14 store := NewCollectorStore()
15 cc := cycleController(t, store)
16
17 scope := HostScope{
18 ScopeKey: "workload/api",
19 GUID: "guid-api",
20 Hostname: "api",
21 Labels: map[string]string{"_vnode_type": "azure_workload"},
22 }
23
24 meter := store.Write().SnapshotMeter("azure")
25 defaultGauge := meter.Gauge("requests")
26 scopedGauge := meter.WithHostScope(scope).Gauge("requests")
27 labels := meter.LabelSet(Label{Key: "resource", Value: "vm1"})
28
29 cc.BeginCycle()
30 defaultGauge.Observe(1, labels)
31 scopedGauge.Observe(2, labels)
32 require.NoError(t, cc.CommitCycleSuccess())
33
34 mustValue(t, store.Read(), "azure.requests", Labels{"resource": "vm1"}, 1)
35 mustValue(t, store.Read(ReadHostScope(scope.ScopeKey)), "azure.requests", Labels{"resource": "vm1"}, 2)
36
37 _, ok := store.Read().Value("azure.requests", Labels{"resource": "vm2"})
38 require.False(t, ok)
39 _, ok = store.Read(ReadHostScope(scope.ScopeKey)).Value("azure.requests", Labels{"resource": "vm2"})
40 require.False(t, ok)
41
42 scopes := store.Read().HostScopes()
43 require.Len(t, scopes, 2)
44 require.True(t, scopes[0].IsDefault())
45 require.Equal(t, scope, scopes[1])
46
47 filteredScopes := store.Read(ReadHostScope(scope.ScopeKey)).HostScopes()
48 require.Equal(t, scopes, filteredScopes)
49
50 var defaultID, scopedID SeriesID
51 store.Read().ForEachSeriesIdentity(func(identity SeriesIdentity, _ SeriesMeta, name string, _ LabelView, _ SampleValue) {
52 if name == "azure.requests" {
53 defaultID = identity.ID
54 }
55 })
56 store.Read(ReadHostScope(scope.ScopeKey)).ForEachSeriesIdentity(func(identity SeriesIdentity, _ SeriesMeta, name string, _ LabelView, _ SampleValue) {
57 if name == "azure.requests" {
58 scopedID = identity.ID
59 }
60 })
61 require.NotEmpty(t, defaultID)
62 require.NotEmpty(t, scopedID)
63 require.NotEqual(t, defaultID, scopedID)
64 }
65
66 func TestHostScopeMetadataRefreshesRetainedSeries(t *testing.T) {
67 store := NewCollectorStore()
68 cc := cycleController(t, store)
69
70 scopeV1 := HostScope{
71 ScopeKey: "workload/api",
72 GUID: "guid-api",
73 Hostname: "api",
74 Labels: map[string]string{"_vnode_type": "azure_workload"},
75 }
76 scopeV2 := HostScope{
77 ScopeKey: "workload/api",
78 GUID: "guid-api",
79 Hostname: "api-v2",
80 Labels: map[string]string{"_vnode_type": "azure_workload", "region": "eastus"},
81 }
82 meter := store.Write().SnapshotMeter("azure")
83 labels := meter.LabelSet(Label{Key: "resource", Value: "vm1"})
84
85 cc.BeginCycle()
86 meter.WithHostScope(scopeV1).Gauge("requests").Observe(1, labels)
87 meter.WithHostScope(scopeV1).Gauge("errors").Observe(2, labels)
88 require.NoError(t, cc.CommitCycleSuccess())
89
90 cc.BeginCycle()
91 meter.WithHostScope(scopeV2).Gauge("requests").Observe(3, labels)
92 require.NoError(t, cc.CommitCycleSuccess())
93
94 require.Equal(t, []HostScope{scopeV2}, store.Read().HostScopes())
95
96 snapshot := store.(*storeView).core.snapshot.Load()
97 seen := 0
98 for _, series := range snapshot.series {
99 if series.hostScopeKey != scopeV2.ScopeKey {
100 continue
101 }
102 seen++
103 require.Equal(t, scopeV2, series.hostScope)
104 }
105 require.Equal(t, 2, seen)
106 }
107
108 func TestHostScopeVecDerivationPartitionsSeries(t *testing.T) {
109 store := NewCollectorStore()
110 cc := cycleController(t, store)
111
112 scopeA := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
113 scopeB := HostScope{ScopeKey: "workload/db", GUID: "guid-db", Hostname: "db"}
114 vec := store.Write().SnapshotMeter("azure").Vec("resource").Gauge("cpu")
115
116 cc.BeginCycle()
117 vec.WithLabelValues("vm1").Observe(10)
118 vec.WithHostScope(scopeA).WithLabelValues("vm1").Observe(20)
119 vec.WithHostScope(scopeB).WithLabelValues("vm1").Observe(30)
120 require.NoError(t, cc.CommitCycleSuccess())
121
122 mustValue(t, store.Read(), "azure.cpu", Labels{"resource": "vm1"}, 10)
123 mustValue(t, store.Read(ReadHostScope(scopeA.ScopeKey)), "azure.cpu", Labels{"resource": "vm1"}, 20)
124 mustValue(t, store.Read(ReadHostScope(scopeB.ScopeKey)), "azure.cpu", Labels{"resource": "vm1"}, 30)
125
126 missingScopeReader := store.Read(ReadHostScope("workload/missing"))
127 _, ok := missingScopeReader.Value("azure.cpu", Labels{"resource": "vm1"})
128 require.False(t, ok)
129 _, ok = missingScopeReader.Family("azure.cpu")
130 require.False(t, ok)
131 }
132
133 func TestHostScopeFlattenPreservesScopeScenarios(t *testing.T) {
134 cases := map[string]struct {
135 run func(t *testing.T)
136 }{
137 "histogram": {
138 run: func(t *testing.T) {
139 store := NewCollectorStore()
140 cc := cycleController(t, store)
141
142 scope := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
143 hist := store.Write().SnapshotMeter("svc").WithHostScope(scope).Histogram("latency")
144 labels := store.Write().SnapshotMeter("").LabelSet(Label{Key: "route", Value: "/api"})
145
146 cc.BeginCycle()
147 hist.ObservePoint(HistogramPoint{
148 Count: 3,
149 Sum: 6,
150 Buckets: []BucketPoint{
151 {UpperBound: 1, CumulativeCount: 1},
152 {UpperBound: 5, CumulativeCount: 3},
153 },
154 }, labels)
155 require.NoError(t, cc.CommitCycleSuccess())
156
157 defaultReader := store.Read(ReadFlatten())
158 _, ok := defaultReader.Value("svc.latency_count", Labels{"route": "/api"})
159 require.False(t, ok)
160
161 scopedReader := store.Read(ReadFlatten(), ReadHostScope(scope.ScopeKey))
162 mustValue(t, scopedReader, "svc.latency_count", Labels{"route": "/api"}, 3)
163 mustValue(t, scopedReader, "svc.latency_sum", Labels{"route": "/api"}, 6)
164 mustValue(t, scopedReader, "svc.latency_bucket", Labels{"route": "/api", HistogramBucketLabel: "1"}, 1)
165 mustValue(t, scopedReader, "svc.latency_bucket", Labels{"route": "/api", HistogramBucketLabel: "5"}, 3)
166 },
167 },
168 "composite instruments": {
169 run: func(t *testing.T) {
170 store := NewCollectorStore()
171 cc := cycleController(t, store)
172
173 scope := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
174 meter := store.Write().SnapshotMeter("svc")
175 labels := meter.LabelSet(Label{Key: "route", Value: "/api"})
176 summary := meter.WithHostScope(scope).Summary("latency", WithSummaryQuantiles(0.5))
177 stateSet := meter.WithHostScope(scope).StateSet("mode", WithStateSetStates("idle", "busy"))
178 measureSet := meter.WithHostScope(scope).MeasureSetGauge(
179 "usage",
180 WithMeasureSetFields(
181 MeasureFieldSpec{Name: "used"},
182 MeasureFieldSpec{Name: "limit"},
183 ),
184 )
185
186 cc.BeginCycle()
187 summary.ObservePoint(SummaryPoint{
188 Count: 2,
189 Sum: 1,
190 Quantiles: []QuantilePoint{
191 {Quantile: 0.5, Value: 0.4},
192 },
193 }, labels)
194 stateSet.ObserveStateSet(StateSetPoint{States: map[string]bool{"busy": true}}, labels)
195 measureSet.ObserveFields(map[string]SampleValue{"used": 7, "limit": 10}, labels)
196 require.NoError(t, cc.CommitCycleSuccess())
197
198 defaultReader := store.Read(ReadFlatten())
199 _, ok := defaultReader.Value("svc.latency_count", Labels{"route": "/api"})
200 require.False(t, ok)
201 _, ok = defaultReader.Value("svc.mode", Labels{"route": "/api", "svc.mode": "busy"})
202 require.False(t, ok)
203 _, ok = defaultReader.Value("svc.usage_used", Labels{"route": "/api", MeasureSetFieldLabel: "used"})
204 require.False(t, ok)
205
206 scopedReader := store.Read(ReadFlatten(), ReadHostScope(scope.ScopeKey))
207 mustValue(t, scopedReader, "svc.latency_count", Labels{"route": "/api"}, 2)
208 mustValue(t, scopedReader, "svc.latency_sum", Labels{"route": "/api"}, 1)
209 mustValue(t, scopedReader, "svc.latency", Labels{"route": "/api", SummaryQuantileLabel: "0.5"}, 0.4)
210 mustValue(t, scopedReader, "svc.mode", Labels{"route": "/api", "svc.mode": "idle"}, 0)
211 mustValue(t, scopedReader, "svc.mode", Labels{"route": "/api", "svc.mode": "busy"}, 1)
212 mustValue(t, scopedReader, "svc.usage_used", Labels{"route": "/api", MeasureSetFieldLabel: "used"}, 7)
213 mustValue(t, scopedReader, "svc.usage_limit", Labels{"route": "/api", MeasureSetFieldLabel: "limit"}, 10)
214 },
215 },
216 }
217
218 for name, tc := range cases {
219 t.Run(name, tc.run)
220 }
221 }
222
223 func TestHostScopeStatefulBaselinesArePerScope(t *testing.T) {
224 store := NewCollectorStore()
225 cc := cycleController(t, store)
226
227 scopeA := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
228 scopeB := HostScope{ScopeKey: "workload/db", GUID: "guid-db", Hostname: "db"}
229 meter := store.Write().StatefulMeter("svc")
230 gaugeA := meter.WithHostScope(scopeA).Gauge("load")
231 gaugeB := meter.WithHostScope(scopeB).Gauge("load")
232 counterA := meter.WithHostScope(scopeA).Counter("requests")
233 counterB := meter.WithHostScope(scopeB).Counter("requests")
234 labels := meter.LabelSet(Label{Key: "resource", Value: "vm1"})
235
236 cc.BeginCycle()
237 gaugeA.Add(1, labels)
238 gaugeB.Add(10, labels)
239 counterA.Add(1, labels)
240 counterB.Add(10, labels)
241 require.NoError(t, cc.CommitCycleSuccess())
242
243 cc.BeginCycle()
244 gaugeA.Add(2, labels)
245 gaugeB.Add(3, labels)
246 counterA.Add(2, labels)
247 counterB.Add(3, labels)
248 require.NoError(t, cc.CommitCycleSuccess())
249
250 mustValue(t, store.Read(ReadHostScope(scopeA.ScopeKey)), "svc.load", Labels{"resource": "vm1"}, 3)
251 mustValue(t, store.Read(ReadHostScope(scopeB.ScopeKey)), "svc.load", Labels{"resource": "vm1"}, 13)
252 mustValue(t, store.Read(ReadHostScope(scopeA.ScopeKey)), "svc.requests", Labels{"resource": "vm1"}, 3)
253 mustValue(t, store.Read(ReadHostScope(scopeB.ScopeKey)), "svc.requests", Labels{"resource": "vm1"}, 13)
254 mustDelta(t, store.Read(ReadHostScope(scopeA.ScopeKey)), "svc.requests", Labels{"resource": "vm1"}, 2)
255 mustDelta(t, store.Read(ReadHostScope(scopeB.ScopeKey)), "svc.requests", Labels{"resource": "vm1"}, 3)
256
257 _, ok := store.Read().Value("svc.load", Labels{"resource": "vm1"})
258 require.False(t, ok)
259 }
260
261 func TestHostScopeStatefulHistogramCumulativeWindowIsPerScope(t *testing.T) {
262 store := NewCollectorStore()
263 cc := cycleController(t, store)
264
265 scopeA := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
266 scopeB := HostScope{ScopeKey: "workload/db", GUID: "guid-db", Hostname: "db"}
267 meter := store.Write().StatefulMeter("svc")
268 histA := meter.WithHostScope(scopeA).Histogram("latency", WithHistogramBounds(1, 2))
269 histB := meter.WithHostScope(scopeB).Histogram("latency", WithHistogramBounds(1, 2))
270 labels := meter.LabelSet(Label{Key: "resource", Value: "vm1"})
271
272 cc.BeginCycle()
273 histA.Observe(0.5, labels)
274 histA.Observe(1.5, labels)
275 histB.Observe(3, labels)
276 require.NoError(t, cc.CommitCycleSuccess())
277
278 cc.BeginCycle()
279 histA.Observe(0.2, labels)
280 histB.Observe(0.8, labels)
281 require.NoError(t, cc.CommitCycleSuccess())
282
283 mustHistogram(t, store.Read(ReadHostScope(scopeA.ScopeKey)), "svc.latency", Labels{"resource": "vm1"}, HistogramPoint{
284 Count: 3,
285 Sum: 2.2,
286 Buckets: []BucketPoint{
287 {UpperBound: 1, CumulativeCount: 2},
288 {UpperBound: 2, CumulativeCount: 3},
289 },
290 })
291 mustHistogram(t, store.Read(ReadHostScope(scopeB.ScopeKey)), "svc.latency", Labels{"resource": "vm1"}, HistogramPoint{
292 Count: 2,
293 Sum: 3.8,
294 Buckets: []BucketPoint{
295 {UpperBound: 1, CumulativeCount: 1},
296 {UpperBound: 2, CumulativeCount: 1},
297 },
298 })
299 }
300
301 func TestHostScopeConflictScenarios(t *testing.T) {
302 cases := map[string]struct {
303 run func(t *testing.T)
304 }{
305 "commit fails without publishing": {
306 run: func(t *testing.T) {
307 store := NewCollectorStore()
308 cc := cycleController(t, store)
309
310 scopeA := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
311 scopeB := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api-v2"}
312 meter := store.Write().SnapshotMeter("azure")
313 a := meter.WithHostScope(scopeA).Gauge("requests")
314 b := meter.WithHostScope(scopeB).Gauge("requests")
315
316 cc.BeginCycle()
317 a.Observe(1)
318 b.Observe(2)
319 err := cc.CommitCycleSuccess()
320 require.Error(t, err)
321 require.True(t, errors.Is(err, ErrHostScopeConflict), "unexpected error: %v", err)
322
323 meta := store.Read().CollectMeta()
324 require.Equal(t, CollectStatusFailed, meta.LastAttemptStatus)
325 require.Equal(t, uint64(1), meta.LastAttemptSeq)
326 require.Equal(t, uint64(0), meta.LastSuccessSeq)
327
328 _, ok := store.Read(ReadRaw(), ReadHostScope(scopeA.ScopeKey)).Value("azure.requests", nil)
329 require.False(t, ok)
330 },
331 },
332 "multiple conflicts join errors": {
333 run: func(t *testing.T) {
334 store := NewCollectorStore()
335 cc := cycleController(t, store)
336
337 scopeA := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"}
338 scopeAConflict := HostScope{ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api-v2"}
339 scopeB := HostScope{ScopeKey: "workload/db", GUID: "guid-db", Hostname: "db"}
340 scopeBConflict := HostScope{ScopeKey: "workload/db", GUID: "guid-db-v2", Hostname: "db"}
341 meter := store.Write().SnapshotMeter("azure")
342
343 cc.BeginCycle()
344 meter.WithHostScope(scopeA).Gauge("requests").Observe(1)
345 meter.WithHostScope(scopeAConflict).Gauge("requests").Observe(2)
346 meter.WithHostScope(scopeB).Gauge("requests").Observe(3)
347 meter.WithHostScope(scopeBConflict).Gauge("requests").Observe(4)
348 err := cc.CommitCycleSuccess()
349 require.Error(t, err)
350 require.True(t, errors.Is(err, ErrHostScopeConflict), "unexpected error: %v", err)
351 require.Contains(t, err.Error(), "scope_key=\"workload/api\"")
352 require.Contains(t, err.Error(), "scope_key=\"workload/db\"")
353 },
354 },
355 }
356
357 for name, tc := range cases {
358 t.Run(name, tc.run)
359 }
360 }
361
362 func TestHostScopeConcurrentWrites(t *testing.T) {
363 store := NewCollectorStore()
364 cc := cycleController(t, store)
365
366 scopes := []HostScope{
367 {ScopeKey: "workload/api", GUID: "guid-api", Hostname: "api"},
368 {ScopeKey: "workload/db", GUID: "guid-db", Hostname: "db"},
369 {ScopeKey: "workload/cache", GUID: "guid-cache", Hostname: "cache"},
370 }
371 meter := store.Write().StatefulMeter("svc")
372 counters := make([]StatefulCounter, len(scopes))
373 for i, scope := range scopes {
374 counters[i] = meter.WithHostScope(scope).Counter("requests")
375 }
376
377 const writersPerScope = 8
378 const writesPerWriter = 100
379
380 var wg sync.WaitGroup
381 cc.BeginCycle()
382 for i := range scopes {
383 for range writersPerScope {
384 wg.Go(func() {
385 for range writesPerWriter {
386 counters[i].Add(1)
387 }
388 })
389 }
390 }
391 wg.Wait()
392 require.NoError(t, cc.CommitCycleSuccess())
393
394 for _, scope := range scopes {
395 mustValue(t, store.Read(ReadHostScope(scope.ScopeKey)), "svc.requests", nil, writersPerScope*writesPerWriter)
396 }
397 }