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  }