| 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 | } |