| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package metrix |
| 4 | |
| 5 | import ( |
| 6 | "math" |
| 7 | "testing" |
| 8 | |
| 9 | "github.com/stretchr/testify/require" |
| 10 | ) |
| 11 | |
| 12 | func TestSummaryStoreScenarios(t *testing.T) { |
| 13 | tests := map[string]struct { |
| 14 | run func(t *testing.T) |
| 15 | }{ |
| 16 | "snapshot summary stores NaN quantile values (sparse window -> gap downstream)": { |
| 17 | run: func(t *testing.T) { |
| 18 | s := NewCollectorStore() |
| 19 | cc := cycleController(t, s) |
| 20 | sum := s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryQuantiles(0.5, 0.9)) |
| 21 | |
| 22 | cc.BeginCycle() |
| 23 | require.NotPanics(t, func() { |
| 24 | sum.ObservePoint(SummaryPoint{ |
| 25 | Count: 0, |
| 26 | Sum: 0, |
| 27 | Quantiles: []QuantilePoint{ |
| 28 | {Quantile: 0.5, Value: SampleValue(math.NaN())}, |
| 29 | {Quantile: 0.9, Value: SampleValue(math.NaN())}, |
| 30 | }, |
| 31 | }) |
| 32 | }) |
| 33 | cc.CommitCycleSuccess() |
| 34 | |
| 35 | point, ok := s.Read().Summary("svc.latency", nil) |
| 36 | require.True(t, ok) |
| 37 | require.Len(t, point.Quantiles, 2) |
| 38 | for _, q := range point.Quantiles { |
| 39 | require.Truef(t, math.IsNaN(float64(q.Value)), |
| 40 | "quantile %v value must be stored as NaN, got %v", q.Quantile, q.Value) |
| 41 | } |
| 42 | }, |
| 43 | }, |
| 44 | "snapshot summary count sum read and flatten": { |
| 45 | run: func(t *testing.T) { |
| 46 | s := NewCollectorStore() |
| 47 | cc := cycleController(t, s) |
| 48 | sm := s.Write().SnapshotMeter("svc") |
| 49 | sum := sm.Summary("latency") |
| 50 | |
| 51 | cc.BeginCycle() |
| 52 | sum.ObservePoint(SummaryPoint{Count: 3, Sum: 1.2}) |
| 53 | cc.CommitCycleSuccess() |
| 54 | |
| 55 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{Count: 3, Sum: 1.2}) |
| 56 | _, ok := s.Read().Value("svc.latency", nil) |
| 57 | require.False(t, ok, "expected non-scalar summary unavailable via Value") |
| 58 | |
| 59 | fr := s.Read(ReadFlatten()) |
| 60 | mustValue(t, fr, "svc.latency_count", nil, 3) |
| 61 | mustValue(t, fr, "svc.latency_sum", nil, 1.2) |
| 62 | _, ok = fr.Summary("svc.latency", nil) |
| 63 | require.False(t, ok, "expected flattened reader to hide typed summary getter") |
| 64 | }, |
| 65 | }, |
| 66 | "snapshot summary quantiles are validated and flattened": { |
| 67 | run: func(t *testing.T) { |
| 68 | s := NewCollectorStore() |
| 69 | cc := cycleController(t, s) |
| 70 | sum := s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryQuantiles(0.5, 0.9)) |
| 71 | |
| 72 | cc.BeginCycle() |
| 73 | sum.ObservePoint(SummaryPoint{ |
| 74 | Count: 2, |
| 75 | Sum: 1.0, |
| 76 | Quantiles: []QuantilePoint{ |
| 77 | {Quantile: 0.9, Value: 0.8}, |
| 78 | {Quantile: 0.5, Value: 0.4}, |
| 79 | }, |
| 80 | }) |
| 81 | cc.CommitCycleSuccess() |
| 82 | |
| 83 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{ |
| 84 | Count: 2, |
| 85 | Sum: 1.0, |
| 86 | Quantiles: []QuantilePoint{ |
| 87 | {Quantile: 0.5, Value: 0.4}, |
| 88 | {Quantile: 0.9, Value: 0.8}, |
| 89 | }, |
| 90 | }) |
| 91 | |
| 92 | fr := s.Read(ReadFlatten()) |
| 93 | mustValue(t, fr, "svc.latency", Labels{"quantile": "0.5"}, 0.4) |
| 94 | mustValue(t, fr, "svc.latency", Labels{"quantile": "0.9"}, 0.8) |
| 95 | mustValue(t, fr, "svc.latency_count", nil, 2) |
| 96 | mustValue(t, fr, "svc.latency_sum", nil, 1.0) |
| 97 | }, |
| 98 | }, |
| 99 | "snapshot summary point quantiles must match declaration": { |
| 100 | run: func(t *testing.T) { |
| 101 | s := NewCollectorStore() |
| 102 | cc := cycleController(t, s) |
| 103 | sum := s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryQuantiles(0.5, 0.9)) |
| 104 | |
| 105 | cc.BeginCycle() |
| 106 | expectPanic(t, func() { |
| 107 | sum.ObservePoint(SummaryPoint{ |
| 108 | Count: 1, |
| 109 | Sum: 0.2, |
| 110 | Quantiles: []QuantilePoint{ |
| 111 | {Quantile: 0.5, Value: 0.2}, |
| 112 | }, |
| 113 | }) |
| 114 | }) |
| 115 | cc.AbortCycle() |
| 116 | }, |
| 117 | }, |
| 118 | "stateful summary cumulative accumulates count sum and quantiles": { |
| 119 | run: func(t *testing.T) { |
| 120 | s := NewCollectorStore() |
| 121 | cc := cycleController(t, s) |
| 122 | sum := s.Write().StatefulMeter("svc").Summary("latency", WithSummaryQuantiles(0.5, 1.0)) |
| 123 | |
| 124 | cc.BeginCycle() |
| 125 | sum.Observe(1) |
| 126 | sum.Observe(2) |
| 127 | sum.Observe(10) |
| 128 | cc.CommitCycleSuccess() |
| 129 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{ |
| 130 | Count: 3, |
| 131 | Sum: 13, |
| 132 | Quantiles: []QuantilePoint{ |
| 133 | {Quantile: 0.5, Value: 2}, |
| 134 | {Quantile: 1.0, Value: 10}, |
| 135 | }, |
| 136 | }) |
| 137 | |
| 138 | cc.BeginCycle() |
| 139 | sum.Observe(3) |
| 140 | cc.CommitCycleSuccess() |
| 141 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{ |
| 142 | Count: 4, |
| 143 | Sum: 16, |
| 144 | Quantiles: []QuantilePoint{ |
| 145 | {Quantile: 0.5, Value: 2.5}, |
| 146 | {Quantile: 1.0, Value: 10}, |
| 147 | }, |
| 148 | }) |
| 149 | }, |
| 150 | }, |
| 151 | "stateful summary cycle window resets and uses freshness cycle": { |
| 152 | run: func(t *testing.T) { |
| 153 | s := NewCollectorStore() |
| 154 | cc := cycleController(t, s) |
| 155 | sum := s.Write().StatefulMeter("svc").Summary( |
| 156 | "latency", |
| 157 | WithSummaryQuantiles(0.5), |
| 158 | WithWindow(WindowCycle), |
| 159 | ) |
| 160 | |
| 161 | cc.BeginCycle() |
| 162 | sum.Observe(2) |
| 163 | sum.Observe(4) |
| 164 | cc.CommitCycleSuccess() |
| 165 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{ |
| 166 | Count: 2, |
| 167 | Sum: 6, |
| 168 | Quantiles: []QuantilePoint{ |
| 169 | {Quantile: 0.5, Value: 3}, |
| 170 | }, |
| 171 | }) |
| 172 | |
| 173 | cc.BeginCycle() |
| 174 | sum.Observe(1) |
| 175 | cc.CommitCycleSuccess() |
| 176 | mustSummary(t, s.Read(), "svc.latency", nil, SummaryPoint{ |
| 177 | Count: 1, |
| 178 | Sum: 1, |
| 179 | Quantiles: []QuantilePoint{ |
| 180 | {Quantile: 0.5, Value: 1}, |
| 181 | }, |
| 182 | }) |
| 183 | |
| 184 | cc.BeginCycle() |
| 185 | cc.CommitCycleSuccess() |
| 186 | _, ok := s.Read().Summary("svc.latency", nil) |
| 187 | require.False(t, ok, "expected stale cycle-window summary hidden from Read") |
| 188 | mustSummary(t, s.Read(ReadRaw()), "svc.latency", nil, SummaryPoint{ |
| 189 | Count: 1, |
| 190 | Sum: 1, |
| 191 | Quantiles: []QuantilePoint{ |
| 192 | {Quantile: 0.5, Value: 1}, |
| 193 | }, |
| 194 | }) |
| 195 | _, ok = s.Read(ReadFlatten()).Value("svc.latency_count", nil) |
| 196 | require.False(t, ok, "expected stale cycle-window summary flatten hidden from Read(ReadFlatten())") |
| 197 | mustValue(t, s.Read(ReadRaw(), ReadFlatten()), "svc.latency_count", nil, 1) |
| 198 | }, |
| 199 | }, |
| 200 | "window option on snapshot summary panics": { |
| 201 | run: func(t *testing.T) { |
| 202 | s := NewCollectorStore() |
| 203 | expectPanic(t, func() { |
| 204 | _ = s.Write().SnapshotMeter("svc").Summary("latency", WithWindow(WindowCycle)) |
| 205 | }) |
| 206 | }, |
| 207 | }, |
| 208 | "summary reservoir option validation and custom size": { |
| 209 | run: func(t *testing.T) { |
| 210 | s := NewCollectorStore() |
| 211 | cc := cycleController(t, s) |
| 212 | |
| 213 | expectPanic(t, func() { |
| 214 | _ = s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryReservoirSize(128)) |
| 215 | }) |
| 216 | expectPanic(t, func() { |
| 217 | _ = s.Write().StatefulMeter("svc").Summary("latency_bad", WithSummaryQuantiles(0.9), WithSummaryReservoirSize(0)) |
| 218 | }) |
| 219 | |
| 220 | sum := s.Write().StatefulMeter("svc").Summary( |
| 221 | "latency_small_reservoir", |
| 222 | WithSummaryQuantiles(0.5), |
| 223 | WithSummaryReservoirSize(8), |
| 224 | ) |
| 225 | cc.BeginCycle() |
| 226 | for i := range 100 { |
| 227 | sum.Observe(SampleValue(i)) |
| 228 | } |
| 229 | cc.CommitCycleSuccess() |
| 230 | |
| 231 | p, ok := s.Read().Summary("svc.latency_small_reservoir", nil) |
| 232 | require.True(t, ok, "expected summary for custom reservoir series") |
| 233 | require.Equal(t, SampleValue(100), p.Count, "unexpected count") |
| 234 | require.Len(t, p.Quantiles, 1) |
| 235 | require.False(t, math.IsNaN(p.Quantiles[0].Value), "expected non-NaN quantile value with custom reservoir") |
| 236 | }, |
| 237 | }, |
| 238 | "summary flatten quantile label collision panics": { |
| 239 | run: func(t *testing.T) { |
| 240 | s := NewCollectorStore() |
| 241 | cc := cycleController(t, s) |
| 242 | sum := s.Write().SnapshotMeter("svc"). |
| 243 | WithLabels(Label{Key: "quantile", Value: "x"}). |
| 244 | Summary("latency", WithSummaryQuantiles(0.5)) |
| 245 | |
| 246 | cc.BeginCycle() |
| 247 | expectPanic(t, func() { |
| 248 | sum.ObservePoint(SummaryPoint{ |
| 249 | Count: 1, |
| 250 | Sum: 0.2, |
| 251 | Quantiles: []QuantilePoint{ |
| 252 | {Quantile: 0.5, Value: 0.2}, |
| 253 | }, |
| 254 | }) |
| 255 | }) |
| 256 | cc.AbortCycle() |
| 257 | }, |
| 258 | }, |
| 259 | "summary schema mismatch and mode mixing panic": { |
| 260 | run: func(t *testing.T) { |
| 261 | s := NewCollectorStore() |
| 262 | _ = s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryQuantiles(0.5)) |
| 263 | |
| 264 | expectPanic(t, func() { |
| 265 | _ = s.Write().SnapshotMeter("svc").Summary("latency", WithSummaryQuantiles(0.9)) |
| 266 | }) |
| 267 | expectPanic(t, func() { |
| 268 | _ = s.Write().StatefulMeter("svc").Summary("latency", WithSummaryQuantiles(0.5)) |
| 269 | }) |
| 270 | }, |
| 271 | }, |
| 272 | } |
| 273 | |
| 274 | for name, tc := range tests { |
| 275 | t.Run(name, tc.run) |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | func mustSummary(t *testing.T, r Reader, name string, labels Labels, want SummaryPoint) { |
| 280 | t.Helper() |
| 281 | got, ok := r.Summary(name, labels) |
| 282 | require.True(t, ok, "expected summary for %s", name) |
| 283 | require.True(t, equalSample(got.Count, want.Count), "unexpected summary count for %s: got=%v want=%v", name, got.Count, want.Count) |
| 284 | require.True(t, equalSample(got.Sum, want.Sum), "unexpected summary sum for %s: got=%v want=%v", name, got.Sum, want.Sum) |
| 285 | require.Len(t, got.Quantiles, len(want.Quantiles), "unexpected summary quantiles length for %s", name) |
| 286 | for i := range want.Quantiles { |
| 287 | gq := got.Quantiles[i] |
| 288 | wq := want.Quantiles[i] |
| 289 | require.Equal(t, wq.Quantile, gq.Quantile, "unexpected summary quantile[%d] for %s", i, name) |
| 290 | require.True(t, equalSample(gq.Value, wq.Value), "unexpected summary quantile value[%d] for %s: got=%v want=%v", i, name, gq.Value, wq.Value) |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | func equalSample(a, b SampleValue) bool { |
| 295 | if math.IsNaN(a) || math.IsNaN(b) { |
| 296 | return math.IsNaN(a) && math.IsNaN(b) |
| 297 | } |
| 298 | const eps = 1e-9 |
| 299 | return math.Abs(a-b) <= eps |
| 300 | } |