github.com/thanos-io/thanos@v0.32.5/pkg/store/storepb/testutil/series.go (about) 1 // Copyright (c) The Thanos Authors. 2 // Licensed under the Apache License 2.0. 3 4 package storetestutil 5 6 import ( 7 "context" 8 "fmt" 9 "math" 10 "math/rand" 11 "os" 12 "path/filepath" 13 "runtime" 14 "sort" 15 "testing" 16 "time" 17 18 "github.com/cespare/xxhash" 19 "github.com/efficientgo/core/testutil" 20 "github.com/go-kit/log" 21 "github.com/gogo/protobuf/types" 22 "github.com/oklog/ulid" 23 "github.com/prometheus/prometheus/model/histogram" 24 "github.com/prometheus/prometheus/model/labels" 25 "github.com/prometheus/prometheus/storage" 26 "github.com/prometheus/prometheus/tsdb" 27 "github.com/prometheus/prometheus/tsdb/chunkenc" 28 "github.com/prometheus/prometheus/tsdb/chunks" 29 "github.com/prometheus/prometheus/tsdb/index" 30 "github.com/prometheus/prometheus/tsdb/wlog" 31 "go.uber.org/atomic" 32 33 "github.com/thanos-io/thanos/pkg/store/hintspb" 34 "github.com/thanos-io/thanos/pkg/store/labelpb" 35 "github.com/thanos-io/thanos/pkg/store/storepb" 36 ) 37 38 const ( 39 // LabelLongSuffix is a label with ~50B in size, to emulate real-world high cardinality. 40 LabelLongSuffix = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd" 41 ) 42 43 func allPostings(t testing.TB, ix tsdb.IndexReader) index.Postings { 44 k, v := index.AllPostingsKey() 45 p, err := ix.Postings(k, v) 46 testutil.Ok(t, err) 47 return p 48 } 49 50 type HeadGenOptions struct { 51 TSDBDir string 52 SamplesPerSeries, Series int 53 ScrapeInterval time.Duration 54 55 WithWAL bool 56 PrependLabels labels.Labels 57 SkipChunks bool // Skips chunks in returned slice (not in generated head!). 58 SampleType chunkenc.ValueType 59 60 Random *rand.Rand 61 } 62 63 func CreateBlockFromHead(t testing.TB, dir string, head *tsdb.Head) ulid.ULID { 64 compactor, err := tsdb.NewLeveledCompactor(context.Background(), nil, log.NewNopLogger(), []int64{1000000}, nil, nil) 65 testutil.Ok(t, err) 66 67 testutil.Ok(t, os.MkdirAll(dir, 0777)) 68 69 // Add +1 millisecond to block maxt because block intervals are half-open: [b.MinTime, b.MaxTime). 70 // Because of this block intervals are always +1 than the total samples it includes. 71 ulid, err := compactor.Write(dir, head, head.MinTime(), head.MaxTime()+1, nil) 72 testutil.Ok(t, err) 73 return ulid 74 } 75 76 // CreateHeadWithSeries returns head filled with given samples and same series returned in separate list for assertion purposes. 77 // Returned series list has "ext1"="1" prepended. Each series looks as follows: 78 // {foo=bar,i=000001aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd} <random value> where number indicate sample number from 0. 79 // Returned series are framed in the same way as remote read would frame them. 80 func CreateHeadWithSeries(t testing.TB, j int, opts HeadGenOptions) (*tsdb.Head, []*storepb.Series) { 81 if opts.SamplesPerSeries < 1 || opts.Series < 1 { 82 t.Fatal("samples and series has to be 1 or more") 83 } 84 if opts.ScrapeInterval == 0 { 85 opts.ScrapeInterval = 1 * time.Millisecond 86 } 87 // Use float type if sample type is not set. 88 if opts.SampleType == chunkenc.ValNone { 89 opts.SampleType = chunkenc.ValFloat 90 } 91 92 fmt.Printf( 93 "Creating %d %d-sample series with %s interval in %s\n", 94 opts.Series, 95 opts.SamplesPerSeries, 96 opts.ScrapeInterval.String(), 97 opts.TSDBDir, 98 ) 99 100 var w *wlog.WL 101 var err error 102 if opts.WithWAL { 103 w, err = wlog.New(nil, nil, filepath.Join(opts.TSDBDir, "wal"), wlog.ParseCompressionType(true, string(wlog.CompressionSnappy))) 104 testutil.Ok(t, err) 105 } else { 106 testutil.Ok(t, os.MkdirAll(filepath.Join(opts.TSDBDir, "wal"), os.ModePerm)) 107 } 108 109 headOpts := tsdb.DefaultHeadOptions() 110 headOpts.ChunkDirRoot = opts.TSDBDir 111 headOpts.EnableNativeHistograms = *atomic.NewBool(true) 112 h, err := tsdb.NewHead(nil, nil, w, nil, headOpts, nil) 113 testutil.Ok(t, err) 114 115 app := h.Appender(context.Background()) 116 for i := 0; i < opts.Series; i++ { 117 tsLabel := j*opts.Series*opts.SamplesPerSeries + i*opts.SamplesPerSeries 118 switch opts.SampleType { 119 case chunkenc.ValFloat: 120 appendFloatSamples(t, app, tsLabel, opts) 121 case chunkenc.ValHistogram: 122 appendHistogramSamples(t, app, tsLabel, opts) 123 } 124 } 125 testutil.Ok(t, app.Commit()) 126 127 return h, ReadSeriesFromBlock(t, h, opts.PrependLabels, opts.SkipChunks) 128 } 129 130 func ReadSeriesFromBlock(t testing.TB, h tsdb.BlockReader, extLabels labels.Labels, skipChunks bool) []*storepb.Series { 131 // Use TSDB and get all series for assertion. 132 chks, err := h.Chunks() 133 testutil.Ok(t, err) 134 defer func() { testutil.Ok(t, chks.Close()) }() 135 136 ir, err := h.Index() 137 testutil.Ok(t, err) 138 defer func() { testutil.Ok(t, ir.Close()) }() 139 140 var ( 141 lset labels.Labels 142 chunkMetas []chunks.Meta 143 expected = make([]*storepb.Series, 0) 144 ) 145 146 var builder labels.ScratchBuilder 147 148 all := allPostings(t, ir) 149 for all.Next() { 150 testutil.Ok(t, ir.Series(all.At(), &builder, &chunkMetas)) 151 lset = builder.Labels() 152 expected = append(expected, &storepb.Series{Labels: labelpb.ZLabelsFromPromLabels(append(extLabels.Copy(), lset...))}) 153 154 if skipChunks { 155 continue 156 } 157 158 for _, c := range chunkMetas { 159 chEnc, err := chks.Chunk(c) 160 testutil.Ok(t, err) 161 162 // Open Chunk. 163 if c.MaxTime == math.MaxInt64 { 164 c.MaxTime = c.MinTime + int64(chEnc.NumSamples()) - 1 165 } 166 167 expected[len(expected)-1].Chunks = append(expected[len(expected)-1].Chunks, storepb.AggrChunk{ 168 MinTime: c.MinTime, 169 MaxTime: c.MaxTime, 170 Raw: &storepb.Chunk{ 171 Data: chEnc.Bytes(), 172 Type: storepb.Chunk_Encoding(chEnc.Encoding() - 1), 173 Hash: xxhash.Sum64(chEnc.Bytes()), 174 }, 175 }) 176 } 177 } 178 testutil.Ok(t, all.Err()) 179 return expected 180 } 181 182 func appendFloatSamples(t testing.TB, app storage.Appender, tsLabel int, opts HeadGenOptions) { 183 ref, err := app.Append( 184 0, 185 labels.FromStrings("foo", "bar", "i", fmt.Sprintf("%07d%s", tsLabel, LabelLongSuffix), "j", fmt.Sprintf("%v", tsLabel)), 186 int64(tsLabel)*opts.ScrapeInterval.Milliseconds(), 187 opts.Random.Float64(), 188 ) 189 testutil.Ok(t, err) 190 191 for is := 1; is < opts.SamplesPerSeries; is++ { 192 _, err := app.Append(ref, nil, int64(tsLabel+is)*opts.ScrapeInterval.Milliseconds(), opts.Random.Float64()) 193 testutil.Ok(t, err) 194 } 195 } 196 197 func appendHistogramSamples(t testing.TB, app storage.Appender, tsLabel int, opts HeadGenOptions) { 198 sample := &histogram.Histogram{ 199 Schema: 0, 200 Count: 9, 201 Sum: -3.1415, 202 ZeroCount: 12, 203 ZeroThreshold: 0.001, 204 NegativeSpans: []histogram.Span{ 205 {Offset: 0, Length: 4}, 206 {Offset: 1, Length: 1}, 207 }, 208 NegativeBuckets: []int64{1, 2, -2, 1, -1}, 209 } 210 211 ref, err := app.AppendHistogram( 212 0, 213 labels.FromStrings("foo", "bar", "i", fmt.Sprintf("%07d%s", tsLabel, LabelLongSuffix), "j", fmt.Sprintf("%v", tsLabel)), 214 int64(tsLabel)*opts.ScrapeInterval.Milliseconds(), 215 sample, 216 nil, 217 ) 218 testutil.Ok(t, err) 219 220 for is := 1; is < opts.SamplesPerSeries; is++ { 221 _, err := app.AppendHistogram(ref, nil, int64(tsLabel+is)*opts.ScrapeInterval.Milliseconds(), sample, nil) 222 testutil.Ok(t, err) 223 } 224 } 225 226 // SeriesServer is test gRPC storeAPI series server. 227 type SeriesServer struct { 228 // This field just exist to pseudo-implement the unused methods of the interface. 229 storepb.Store_SeriesServer 230 231 ctx context.Context 232 233 SeriesSet []*storepb.Series 234 Warnings []string 235 HintsSet []*types.Any 236 237 Size int64 238 } 239 240 func NewSeriesServer(ctx context.Context) *SeriesServer { 241 return &SeriesServer{ctx: ctx} 242 } 243 244 func (s *SeriesServer) Send(r *storepb.SeriesResponse) error { 245 s.Size += int64(r.Size()) 246 247 if r.GetWarning() != "" { 248 s.Warnings = append(s.Warnings, r.GetWarning()) 249 return nil 250 } 251 252 if r.GetSeries() != nil { 253 s.SeriesSet = append(s.SeriesSet, r.GetSeries()) 254 return nil 255 } 256 257 if r.GetHints() != nil { 258 s.HintsSet = append(s.HintsSet, r.GetHints()) 259 return nil 260 } 261 // Unsupported field, skip. 262 return nil 263 } 264 265 func (s *SeriesServer) Context() context.Context { 266 return s.ctx 267 } 268 269 func RunSeriesInterestingCases(t testutil.TB, maxSamples, maxSeries int, f func(t testutil.TB, samplesPerSeries, series int)) { 270 for _, tc := range []struct { 271 samplesPerSeries int 272 series int 273 }{ 274 { 275 samplesPerSeries: 1, 276 series: maxSeries, 277 }, 278 { 279 samplesPerSeries: maxSamples / (maxSeries / 10), 280 series: maxSeries / 10, 281 }, 282 { 283 samplesPerSeries: maxSamples, 284 series: 1, 285 }, 286 } { 287 if ok := t.Run(fmt.Sprintf("%dSeriesWith%dSamples", tc.series, tc.samplesPerSeries), func(t testutil.TB) { 288 f(t, tc.samplesPerSeries, tc.series) 289 }); !ok { 290 return 291 } 292 runtime.GC() 293 } 294 } 295 296 // SeriesCase represents single test/benchmark case for testing storepb series. 297 type SeriesCase struct { 298 Name string 299 Req *storepb.SeriesRequest 300 301 // Exact expectations are checked only for tests. For benchmarks only length is assured. 302 ExpectedSeries []*storepb.Series 303 ExpectedWarnings []string 304 ExpectedHints []hintspb.SeriesResponseHints 305 HintsCompareFunc func(t testutil.TB, expected, actual hintspb.SeriesResponseHints) 306 } 307 308 // TestServerSeries runs tests against given cases. 309 func TestServerSeries(t testutil.TB, store storepb.StoreServer, cases ...*SeriesCase) { 310 for _, c := range cases { 311 t.Run(c.Name, func(t testutil.TB) { 312 t.ResetTimer() 313 for i := 0; i < t.N(); i++ { 314 srv := NewSeriesServer(context.Background()) 315 testutil.Ok(t, store.Series(c.Req, srv)) 316 testutil.Equals(t, len(c.ExpectedWarnings), len(srv.Warnings), "%v", srv.Warnings) 317 testutil.Equals(t, len(c.ExpectedSeries), len(srv.SeriesSet)) 318 testutil.Equals(t, len(c.ExpectedHints), len(srv.HintsSet)) 319 320 if !t.IsBenchmark() { 321 if len(c.ExpectedSeries) == 1 { 322 // For bucketStoreAPI chunks are not sorted within response. TODO: Investigate: Is this fine? 323 sort.Slice(srv.SeriesSet[0].Chunks, func(i, j int) bool { 324 return srv.SeriesSet[0].Chunks[i].MinTime < srv.SeriesSet[0].Chunks[j].MinTime 325 }) 326 } 327 328 // Huge responses can produce unreadable diffs - make it more human readable. 329 if len(c.ExpectedSeries) > 4 { 330 for j := range c.ExpectedSeries { 331 testutil.Equals(t, c.ExpectedSeries[j].Labels, srv.SeriesSet[j].Labels, "%v series chunks mismatch", j) 332 333 // Check chunks when it is not a skip chunk query 334 if !c.Req.SkipChunks { 335 if len(c.ExpectedSeries[j].Chunks) > 20 { 336 testutil.Equals(t, len(c.ExpectedSeries[j].Chunks), len(srv.SeriesSet[j].Chunks), "%v series chunks number mismatch", j) 337 } 338 for ci := range c.ExpectedSeries[j].Chunks { 339 testutil.Equals(t, c.ExpectedSeries[j].Chunks[ci], srv.SeriesSet[j].Chunks[ci], "%v series chunks mismatch %v", j, ci) 340 } 341 } 342 } 343 } else { 344 testutil.Equals(t, c.ExpectedSeries, srv.SeriesSet) 345 } 346 347 var actualHints []hintspb.SeriesResponseHints 348 for _, anyHints := range srv.HintsSet { 349 hints := hintspb.SeriesResponseHints{} 350 testutil.Ok(t, types.UnmarshalAny(anyHints, &hints)) 351 actualHints = append(actualHints, hints) 352 } 353 testutil.Equals(t, len(c.ExpectedHints), len(actualHints)) 354 for i, hint := range actualHints { 355 if c.HintsCompareFunc == nil { 356 testutil.Equals(t, c.ExpectedHints[i], hint) 357 } else { 358 c.HintsCompareFunc(t, c.ExpectedHints[i], hint) 359 } 360 } 361 } 362 } 363 }) 364 } 365 }