diff --git a/pkg/store/acceptance_test.go b/pkg/store/acceptance_test.go index b3619dd86e..d90d893da2 100644 --- a/pkg/store/acceptance_test.go +++ b/pkg/store/acceptance_test.go @@ -5,16 +5,32 @@ package store import ( "context" + "fmt" + "net/url" + "os" + "path/filepath" "testing" "time" + "github.com/go-kit/log" "github.com/pkg/errors" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" "github.com/prometheus/prometheus/storage" + "github.com/prometheus/prometheus/tsdb" + "golang.org/x/exp/slices" "github.com/efficientgo/core/testutil" + "github.com/thanos-io/objstore" + "github.com/thanos-io/objstore/providers/filesystem" + "github.com/thanos-io/thanos/pkg/block" + "github.com/thanos-io/thanos/pkg/block/metadata" + "github.com/thanos-io/thanos/pkg/component" + "github.com/thanos-io/thanos/pkg/promclient" "github.com/thanos-io/thanos/pkg/store/storepb" + "github.com/thanos-io/thanos/pkg/stringset" + "github.com/thanos-io/thanos/pkg/testutil/custom" + "github.com/thanos-io/thanos/pkg/testutil/e2eutil" ) type labelNameCallCase struct { @@ -37,8 +53,17 @@ type labelValuesCallCase struct { expectErr error } -// testLabelAPIs tests labels methods from StoreAPI from closed box perspective. -func testLabelAPIs(t *testing.T, startStore func(t *testing.T, extLset labels.Labels, append func(app storage.Appender)) storepb.StoreServer) { +type seriesCallCase struct { + matchers []storepb.LabelMatcher + start int64 + end int64 + + expectedLabels []labels.Labels + expectErr error +} + +// testStoreAPIsAcceptance tests StoreAPI from closed box perspective. +func testStoreAPIsAcceptance(t *testing.T, startStore func(t *testing.T, extLset labels.Labels, append func(app storage.Appender)) storepb.StoreServer) { t.Helper() now := time.Now() @@ -48,6 +73,7 @@ func testLabelAPIs(t *testing.T, startStore func(t *testing.T, extLset labels.La appendFn func(app storage.Appender) labelNameCalls []labelNameCallCase labelValuesCalls []labelValuesCallCase + seriesCalls []seriesCallCase }{ { desc: "no label in tsdb, empty results", @@ -187,6 +213,198 @@ func testLabelAPIs(t *testing.T, startStore func(t *testing.T, extLset labels.La }, }, }, + { + // Tests mostly taken from https://github.com/prometheus/prometheus/blob/95e705612c1d557f1681bd081a841b78f93ee158/tsdb/querier_test.go#L1898, though some are still missing + desc: "matching behavior", + appendFn: func(app storage.Appender) { + _, err := app.Append(0, labels.FromStrings("n", "1"), 0, 0) + testutil.Ok(t, err) + _, err = app.Append(0, labels.FromStrings("n", "1", "i", "a"), 0, 0) + testutil.Ok(t, err) + _, err = app.Append(0, labels.FromStrings("n", "1", "i", "b"), 0, 0) + testutil.Ok(t, err) + _, err = app.Append(0, labels.FromStrings("n", "2"), 0, 0) + testutil.Ok(t, err) + _, err = app.Append(0, labels.FromStrings("n", "2.5"), 0, 0) + testutil.Ok(t, err) + + testutil.Ok(t, app.Commit()) + }, + seriesCalls: []seriesCallCase{ + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + {Type: storepb.LabelMatcher_EQ, Name: "i", Value: "a"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + {Type: storepb.LabelMatcher_EQ, Name: "i", Value: "missing"}, + }, + expectedLabels: []labels.Labels{}, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "missing", Value: ""}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + labels.FromStrings("n", "2", "region", "eu-west"), + labels.FromStrings("n", "2.5", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_NEQ, Name: "n", Value: "1"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "2", "region", "eu-west"), + labels.FromStrings("n", "2.5", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_RE, Name: "i", Value: ".+"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_RE, Name: "i", Value: ".*"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + labels.FromStrings("n", "2", "region", "eu-west"), + labels.FromStrings("n", "2.5", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "i", Value: ""}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "2", "region", "eu-west"), + labels.FromStrings("n", "2.5", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_NEQ, Name: "i", Value: ""}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_NEQ, Name: "missing", Value: ""}, + }, + expectedLabels: []labels.Labels{}, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + {Type: storepb.LabelMatcher_NEQ, Name: "i", Value: "a"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_RE, Name: "n", Value: "^1$"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "b", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + {Type: storepb.LabelMatcher_RE, Name: "i", Value: "^a$"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_EQ, Name: "n", Value: "1"}, + {Type: storepb.LabelMatcher_RE, Name: "i", Value: "^a?$"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "1", "i", "a", "region", "eu-west"), + }, + }, + { + start: timestamp.FromTime(minTime), + end: timestamp.FromTime(maxTime), + matchers: []storepb.LabelMatcher{ + {Type: storepb.LabelMatcher_RE, Name: "i", Value: "^$"}, + }, + expectedLabels: []labels.Labels{ + labels.FromStrings("n", "1", "region", "eu-west"), + labels.FromStrings("n", "2", "region", "eu-west"), + labels.FromStrings("n", "2.5", "region", "eu-west"), + }, + }, + }, + }, } { t.Run(tc.desc, func(t *testing.T) { appendFn := tc.appendFn @@ -236,6 +454,150 @@ func testLabelAPIs(t *testing.T, startStore func(t *testing.T, extLset labels.La testutil.Equals(t, c.expectedValues, resp.Values) }) } + for _, c := range tc.seriesCalls { + t.Run("series", func(t *testing.T) { + srv := newStoreSeriesServer(context.Background()) + err := store.Series(&storepb.SeriesRequest{ + MinTime: c.start, + MaxTime: c.end, + Matchers: c.matchers, + }, srv) + if c.expectErr != nil { + testutil.NotOk(t, err) + testutil.Equals(t, c.expectErr.Error(), err.Error()) + return + } + testutil.Ok(t, err) + + receivedLabels := make([]labels.Labels, 0) + for _, s := range srv.SeriesSet { + receivedLabels = append(receivedLabels, s.PromLabels()) + } + slices.SortFunc(c.expectedLabels, func(a, b labels.Labels) bool { return labels.Compare(a, b) < 0 }) + slices.SortFunc(receivedLabels, func(a, b labels.Labels) bool { return labels.Compare(a, b) < 0 }) + testutil.Equals(t, c.expectedLabels, receivedLabels) + }) + } }) } } + +func TestBucketStore_Acceptance(t *testing.T) { + t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) + + testStoreAPIsAcceptance(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { + tmpDir := tt.TempDir() + bktDir := filepath.Join(tmpDir, "bkt") + auxDir := filepath.Join(tmpDir, "aux") + metaDir := filepath.Join(tmpDir, "meta") + + testutil.Ok(tt, os.MkdirAll(metaDir, os.ModePerm)) + testutil.Ok(tt, os.MkdirAll(auxDir, os.ModePerm)) + + bkt, err := filesystem.NewBucket(bktDir) + testutil.Ok(tt, err) + tt.Cleanup(func() { testutil.Ok(tt, bkt.Close()) }) + + headOpts := tsdb.DefaultHeadOptions() + headOpts.ChunkDirRoot = tmpDir + headOpts.ChunkRange = 1000 + h, err := tsdb.NewHead(nil, nil, nil, nil, headOpts, nil) + testutil.Ok(tt, err) + tt.Cleanup(func() { testutil.Ok(tt, h.Close()) }) + logger := log.NewNopLogger() + + appendFn(h.Appender(context.Background())) + + if h.NumSeries() == 0 { + tt.Skip("Bucket Store cannot handle empty HEAD") + } + + id := createBlockFromHead(tt, auxDir, h) + + auxBlockDir := filepath.Join(auxDir, id.String()) + _, err = metadata.InjectThanos(log.NewNopLogger(), auxBlockDir, metadata.Thanos{ + Labels: extLset.Map(), + Downsample: metadata.ThanosDownsample{Resolution: 0}, + Source: metadata.TestSource, + }, nil) + testutil.Ok(tt, err) + + testutil.Ok(tt, block.Upload(context.Background(), logger, bkt, auxBlockDir, metadata.NoneFunc)) + testutil.Ok(tt, block.Upload(context.Background(), logger, bkt, auxBlockDir, metadata.NoneFunc)) + + chunkPool, err := NewDefaultChunkBytesPool(2e5) + testutil.Ok(tt, err) + + metaFetcher, err := block.NewMetaFetcher(logger, 20, objstore.WithNoopInstr(bkt), metaDir, nil, []block.MetadataFilter{ + block.NewTimePartitionMetaFilter(allowAllFilterConf.MinTime, allowAllFilterConf.MaxTime), + }) + testutil.Ok(tt, err) + + bucketStore, err := NewBucketStore( + objstore.WithNoopInstr(bkt), + metaFetcher, + "", + NewChunksLimiterFactory(10e6), + NewSeriesLimiterFactory(10e6), + NewBytesLimiterFactory(10e6), + NewGapBasedPartitioner(PartitionerMaxGapSize), + 20, + true, + DefaultPostingOffsetInMemorySampling, + false, + false, + 1*time.Minute, + WithChunkPool(chunkPool), + WithFilterConfig(allowAllFilterConf), + ) + testutil.Ok(tt, err) + tt.Cleanup(func() { testutil.Ok(tt, bucketStore.Close()) }) + + testutil.Ok(tt, bucketStore.SyncBlocks(context.Background())) + + return bucketStore + }) +} + +func TestPrometheusStore_Acceptance(t *testing.T) { + t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) + + testStoreAPIsAcceptance(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { + p, err := e2eutil.NewPrometheus() + testutil.Ok(tt, err) + tt.Cleanup(func() { testutil.Ok(tt, p.Stop()) }) + + appendFn(p.Appender()) + + testutil.Ok(tt, p.Start()) + u, err := url.Parse(fmt.Sprintf("http://%s", p.Addr())) + testutil.Ok(tt, err) + + version, err := promclient.NewDefaultClient().BuildVersion(context.Background(), u) + testutil.Ok(tt, err) + + promStore, err := NewPrometheusStore(nil, nil, promclient.NewDefaultClient(), u, component.Sidecar, + func() labels.Labels { return extLset }, + func() (int64, int64) { return timestamp.FromTime(minTime), timestamp.FromTime(maxTime) }, + func() stringset.Set { return stringset.AllStrings() }, + func() string { return version }) + testutil.Ok(tt, err) + + return promStore + }) +} + +func TestTSDBStore_Acceptance(t *testing.T) { + t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) + + testStoreAPIsAcceptance(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { + db, err := e2eutil.NewTSDB() + testutil.Ok(tt, err) + tt.Cleanup(func() { testutil.Ok(tt, db.Close()) }) + + tsdbStore := NewTSDBStore(nil, db, component.Rule, extLset) + + appendFn(db.Appender(context.Background())) + return tsdbStore + }) +} diff --git a/pkg/store/bucket_test.go b/pkg/store/bucket_test.go index 544b46da63..54dd9a09e4 100644 --- a/pkg/store/bucket_test.go +++ b/pkg/store/bucket_test.go @@ -3341,80 +3341,3 @@ func TestBucketIndexReader_decodeCachedPostingsErrors(t *testing.T) { testutil.NotOk(t, err) }) } - -func TestBucketStore_LabelAPIs(t *testing.T) { - t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) - - testLabelAPIs(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { - tmpDir := tt.TempDir() - bktDir := filepath.Join(tmpDir, "bkt") - auxDir := filepath.Join(tmpDir, "aux") - metaDir := filepath.Join(tmpDir, "meta") - - testutil.Ok(tt, os.MkdirAll(metaDir, os.ModePerm)) - testutil.Ok(tt, os.MkdirAll(auxDir, os.ModePerm)) - - bkt, err := filesystem.NewBucket(bktDir) - testutil.Ok(tt, err) - tt.Cleanup(func() { testutil.Ok(tt, bkt.Close()) }) - - headOpts := tsdb.DefaultHeadOptions() - headOpts.ChunkDirRoot = tmpDir - headOpts.ChunkRange = 1000 - h, err := tsdb.NewHead(nil, nil, nil, nil, headOpts, nil) - testutil.Ok(tt, err) - tt.Cleanup(func() { testutil.Ok(tt, h.Close()) }) - logger := log.NewNopLogger() - - appendFn(h.Appender(context.Background())) - - if h.NumSeries() == 0 { - tt.Skip("Bucket Store cannot handle empty HEAD") - } - - id := createBlockFromHead(tt, auxDir, h) - - auxBlockDir := filepath.Join(auxDir, id.String()) - _, err = metadata.InjectThanos(log.NewNopLogger(), auxBlockDir, metadata.Thanos{ - Labels: extLset.Map(), - Downsample: metadata.ThanosDownsample{Resolution: 0}, - Source: metadata.TestSource, - }, nil) - testutil.Ok(tt, err) - - testutil.Ok(tt, block.Upload(context.Background(), logger, bkt, auxBlockDir, metadata.NoneFunc)) - testutil.Ok(tt, block.Upload(context.Background(), logger, bkt, auxBlockDir, metadata.NoneFunc)) - - chunkPool, err := NewDefaultChunkBytesPool(2e5) - testutil.Ok(tt, err) - - metaFetcher, err := block.NewMetaFetcher(logger, 20, objstore.WithNoopInstr(bkt), metaDir, nil, []block.MetadataFilter{ - block.NewTimePartitionMetaFilter(allowAllFilterConf.MinTime, allowAllFilterConf.MaxTime), - }) - testutil.Ok(tt, err) - - bucketStore, err := NewBucketStore( - objstore.WithNoopInstr(bkt), - metaFetcher, - "", - NewChunksLimiterFactory(10e6), - NewSeriesLimiterFactory(10e6), - NewBytesLimiterFactory(10e6), - NewGapBasedPartitioner(PartitionerMaxGapSize), - 20, - true, - DefaultPostingOffsetInMemorySampling, - false, - false, - 1*time.Minute, - WithChunkPool(chunkPool), - WithFilterConfig(allowAllFilterConf), - ) - testutil.Ok(tt, err) - tt.Cleanup(func() { testutil.Ok(tt, bucketStore.Close()) }) - - testutil.Ok(tt, bucketStore.SyncBlocks(context.Background())) - - return bucketStore - }) -} diff --git a/pkg/store/prometheus_test.go b/pkg/store/prometheus_test.go index 4696a81d8a..82965672c7 100644 --- a/pkg/store/prometheus_test.go +++ b/pkg/store/prometheus_test.go @@ -388,33 +388,6 @@ func TestPrometheusStore_SeriesLabels_e2e(t *testing.T) { } } -func TestPrometheusStore_LabelAPIs(t *testing.T) { - t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) - testLabelAPIs(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { - p, err := e2eutil.NewPrometheus() - testutil.Ok(tt, err) - tt.Cleanup(func() { testutil.Ok(tt, p.Stop()) }) - - appendFn(p.Appender()) - - testutil.Ok(tt, p.Start()) - u, err := url.Parse(fmt.Sprintf("http://%s", p.Addr())) - testutil.Ok(tt, err) - - version, err := promclient.NewDefaultClient().BuildVersion(context.Background(), u) - testutil.Ok(tt, err) - - promStore, err := NewPrometheusStore(nil, nil, promclient.NewDefaultClient(), u, component.Sidecar, - func() labels.Labels { return extLset }, - nil, - func() stringset.Set { return stringset.AllStrings() }, - func() string { return version }) - testutil.Ok(tt, err) - - return promStore - }) -} - func TestPrometheusStore_Series_MatchExternalLabel(t *testing.T) { defer custom.TolerantVerifyLeak(t) diff --git a/pkg/store/tsdb_test.go b/pkg/store/tsdb_test.go index 90c99d4d6d..6dcc033c1c 100644 --- a/pkg/store/tsdb_test.go +++ b/pkg/store/tsdb_test.go @@ -15,7 +15,6 @@ import ( "github.com/cespare/xxhash" "github.com/go-kit/log" "github.com/prometheus/prometheus/model/labels" - "github.com/prometheus/prometheus/storage" "github.com/prometheus/prometheus/tsdb" "github.com/efficientgo/core/testutil" @@ -229,20 +228,6 @@ func TestTSDBStore_Series(t *testing.T) { } } -func TestTSDBStore_LabelAPIs(t *testing.T) { - t.Cleanup(func() { custom.TolerantVerifyLeak(t) }) - testLabelAPIs(t, func(tt *testing.T, extLset labels.Labels, appendFn func(app storage.Appender)) storepb.StoreServer { - db, err := e2eutil.NewTSDB() - testutil.Ok(tt, err) - tt.Cleanup(func() { testutil.Ok(tt, db.Close()) }) - - tsdbStore := NewTSDBStore(nil, db, component.Rule, extLset) - - appendFn(db.Appender(context.Background())) - return tsdbStore - }) -} - // Regression test for https://github.com/thanos-io/thanos/issues/1038. func TestTSDBStore_Series_SplitSamplesIntoChunksWithMaxSizeOf120(t *testing.T) { defer custom.TolerantVerifyLeak(t)