diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go index fe490bbeaf..f512728ac9 100644 --- a/cmd/promtool/tsdb.go +++ b/cmd/promtool/tsdb.go @@ -552,7 +552,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten postingInfos = postingInfos[:0] for _, n := range allLabelNames { - values, err := ir.SortedLabelValues(ctx, n, selectors...) + values, err := ir.SortedLabelValues(ctx, n, nil, selectors...) if err != nil { return err } @@ -568,7 +568,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten postingInfos = postingInfos[:0] for _, n := range allLabelNames { - lv, err := ir.SortedLabelValues(ctx, n, selectors...) + lv, err := ir.SortedLabelValues(ctx, n, nil, selectors...) if err != nil { return err } @@ -578,7 +578,7 @@ func analyzeBlock(ctx context.Context, path, blockID string, limit int, runExten printInfo(postingInfos) postingInfos = postingInfos[:0] - lv, err := ir.SortedLabelValues(ctx, "__name__", selectors...) + lv, err := ir.SortedLabelValues(ctx, "__name__", nil, selectors...) if err != nil { return err } diff --git a/tsdb/block.go b/tsdb/block.go index 7f7d993800..7d243f8bf7 100644 --- a/tsdb/block.go +++ b/tsdb/block.go @@ -66,10 +66,10 @@ type IndexReader interface { Symbols() index.StringIter // SortedLabelValues returns sorted possible label values. - SortedLabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) + SortedLabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) // LabelValues returns possible label values which may not be sorted. - LabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) + LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) // Postings returns the postings list iterator for the label pairs. // The Postings here contain the offsets to the series inside the index. @@ -475,14 +475,14 @@ func (r blockIndexReader) Symbols() index.StringIter { return r.ir.Symbols() } -func (r blockIndexReader) SortedLabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (r blockIndexReader) SortedLabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { var st []string var err error if len(matchers) == 0 { - st, err = r.ir.SortedLabelValues(ctx, name) + st, err = r.ir.SortedLabelValues(ctx, name, hints) } else { - st, err = r.LabelValues(ctx, name, matchers...) + st, err = r.LabelValues(ctx, name, hints, matchers...) if err == nil { slices.Sort(st) } @@ -493,16 +493,16 @@ func (r blockIndexReader) SortedLabelValues(ctx context.Context, name string, ma return st, nil } -func (r blockIndexReader) LabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (r blockIndexReader) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { if len(matchers) == 0 { - st, err := r.ir.LabelValues(ctx, name) + st, err := r.ir.LabelValues(ctx, name, hints) if err != nil { return st, fmt.Errorf("block: %s: %w", r.b.Meta().ULID, err) } return st, nil } - return labelValuesWithMatchers(ctx, r.ir, name, matchers...) + return labelValuesWithMatchers(ctx, r.ir, name, hints, matchers...) } func (r blockIndexReader) LabelNames(ctx context.Context, matchers ...*labels.Matcher) ([]string, error) { diff --git a/tsdb/block_test.go b/tsdb/block_test.go index 776beb4396..0f892a3782 100644 --- a/tsdb/block_test.go +++ b/tsdb/block_test.go @@ -299,11 +299,11 @@ func TestLabelValuesWithMatchers(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - actualValues, err := indexReader.SortedLabelValues(ctx, tt.labelName, tt.matchers...) + actualValues, err := indexReader.SortedLabelValues(ctx, tt.labelName, nil, tt.matchers...) require.NoError(t, err) require.Equal(t, tt.expectedValues, actualValues) - actualValues, err = indexReader.LabelValues(ctx, tt.labelName, tt.matchers...) + actualValues, err = indexReader.LabelValues(ctx, tt.labelName, nil, tt.matchers...) sort.Strings(actualValues) require.NoError(t, err) require.Equal(t, tt.expectedValues, actualValues) @@ -459,7 +459,7 @@ func BenchmarkLabelValuesWithMatchers(b *testing.B) { b.ReportAllocs() for benchIdx := 0; benchIdx < b.N; benchIdx++ { - actualValues, err := indexReader.LabelValues(ctx, "b_tens", matchers...) + actualValues, err := indexReader.LabelValues(ctx, "b_tens", nil, matchers...) require.NoError(b, err) require.Len(b, actualValues, 9) } diff --git a/tsdb/head_read.go b/tsdb/head_read.go index f37fd17d60..20495c30b3 100644 --- a/tsdb/head_read.go +++ b/tsdb/head_read.go @@ -61,8 +61,8 @@ func (h *headIndexReader) Symbols() index.StringIter { // specific label name that are within the time range mint to maxt. // If matchers are specified the returned result set is reduced // to label values of metrics matching the matchers. -func (h *headIndexReader) SortedLabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { - values, err := h.LabelValues(ctx, name, matchers...) +func (h *headIndexReader) SortedLabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { + values, err := h.LabelValues(ctx, name, hints, matchers...) if err == nil { slices.Sort(values) } @@ -73,16 +73,16 @@ func (h *headIndexReader) SortedLabelValues(ctx context.Context, name string, ma // specific label name that are within the time range mint to maxt. // If matchers are specified the returned result set is reduced // to label values of metrics matching the matchers. -func (h *headIndexReader) LabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (h *headIndexReader) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { if h.maxt < h.head.MinTime() || h.mint > h.head.MaxTime() { return []string{}, nil } if len(matchers) == 0 { - return h.head.postings.LabelValues(ctx, name), nil + return h.head.postings.LabelValues(ctx, name, hints), nil } - return labelValuesWithMatchers(ctx, h, name, matchers...) + return labelValuesWithMatchers(ctx, h, name, hints, matchers...) } // LabelNames returns all the unique label names present in the head diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 561c8c789d..485b8b7b1f 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -1216,7 +1216,7 @@ func TestHead_Truncate(t *testing.T) { ss = map[string]struct{}{} values[name] = ss } - for _, value := range h.postings.LabelValues(ctx, name) { + for _, value := range h.postings.LabelValues(ctx, name, nil) { ss[value] = struct{}{} } } @@ -3136,7 +3136,7 @@ func TestHeadLabelNamesValuesWithMinMaxRange(t *testing.T) { require.Equal(t, tt.expectedNames, actualLabelNames) if len(tt.expectedValues) > 0 { for i, name := range expectedLabelNames { - actualLabelValue, err := headIdxReader.SortedLabelValues(ctx, name) + actualLabelValue, err := headIdxReader.SortedLabelValues(ctx, name, nil) require.NoError(t, err) require.Equal(t, []string{tt.expectedValues[i]}, actualLabelValue) } @@ -3209,11 +3209,11 @@ func TestHeadLabelValuesWithMatchers(t *testing.T) { t.Run(tt.name, func(t *testing.T) { headIdxReader := head.indexRange(0, 200) - actualValues, err := headIdxReader.SortedLabelValues(ctx, tt.labelName, tt.matchers...) + actualValues, err := headIdxReader.SortedLabelValues(ctx, tt.labelName, nil, tt.matchers...) require.NoError(t, err) require.Equal(t, tt.expectedValues, actualValues) - actualValues, err = headIdxReader.LabelValues(ctx, tt.labelName, tt.matchers...) + actualValues, err = headIdxReader.LabelValues(ctx, tt.labelName, nil, tt.matchers...) sort.Strings(actualValues) require.NoError(t, err) require.Equal(t, tt.expectedValues, actualValues) @@ -3472,7 +3472,7 @@ func BenchmarkHeadLabelValuesWithMatchers(b *testing.B) { b.ReportAllocs() for benchIdx := 0; benchIdx < b.N; benchIdx++ { - actualValues, err := headIdxReader.LabelValues(ctx, "b_tens", matchers...) + actualValues, err := headIdxReader.LabelValues(ctx, "b_tens", nil, matchers...) require.NoError(b, err) require.Len(b, actualValues, 9) } diff --git a/tsdb/index/index.go b/tsdb/index/index.go index 42ecd7245d..edcb92a719 100644 --- a/tsdb/index/index.go +++ b/tsdb/index/index.go @@ -1493,8 +1493,8 @@ func (r *Reader) SymbolTableSize() uint64 { // SortedLabelValues returns value tuples that exist for the given label name. // It is not safe to use the return value beyond the lifetime of the byte slice // passed into the Reader. -func (r *Reader) SortedLabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { - values, err := r.LabelValues(ctx, name, matchers...) +func (r *Reader) SortedLabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { + values, err := r.LabelValues(ctx, name, hints, matchers...) if err == nil && r.version == FormatV1 { slices.Sort(values) } @@ -1505,7 +1505,7 @@ func (r *Reader) SortedLabelValues(ctx context.Context, name string, matchers .. // It is not safe to use the return value beyond the lifetime of the byte slice // passed into the Reader. // TODO(replay): Support filtering by matchers. -func (r *Reader) LabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (r *Reader) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { if len(matchers) > 0 { return nil, fmt.Errorf("matchers parameter is not implemented: %+v", matchers) } @@ -1517,6 +1517,9 @@ func (r *Reader) LabelValues(ctx context.Context, name string, matchers ...*labe } values := make([]string, 0, len(e)) for k := range e { + if hints != nil && hints.Limit > 0 && len(values) >= hints.Limit { + break + } values = append(values, k) } return values, nil @@ -1529,9 +1532,16 @@ func (r *Reader) LabelValues(ctx context.Context, name string, matchers ...*labe return nil, nil } - values := make([]string, 0, len(e)*symbolFactor) + valuesLength := len(e) * symbolFactor + if hints != nil && hints.Limit > 0 && valuesLength > hints.Limit { + valuesLength = hints.Limit + } + values := make([]string, 0, valuesLength) lastVal := e[len(e)-1].value err := r.traversePostingOffsets(ctx, e[0].off, func(val string, _ uint64) (bool, error) { + if hints != nil && hints.Limit > 0 && len(values) >= hints.Limit { + return false, nil + } values = append(values, val) return val != lastVal, nil }) diff --git a/tsdb/index/index_test.go b/tsdb/index/index_test.go index e3fe5a41fd..17b4cc88dd 100644 --- a/tsdb/index/index_test.go +++ b/tsdb/index/index_test.go @@ -421,7 +421,7 @@ func TestPersistence_index_e2e(t *testing.T) { for k, v := range labelPairs { sort.Strings(v) - res, err := ir.SortedLabelValues(ctx, k) + res, err := ir.SortedLabelValues(ctx, k, nil) require.NoError(t, err) require.Len(t, res, len(v)) diff --git a/tsdb/index/postings.go b/tsdb/index/postings.go index e3ba5d64b4..7fdf64acca 100644 --- a/tsdb/index/postings.go +++ b/tsdb/index/postings.go @@ -168,11 +168,15 @@ func (p *MemPostings) LabelNames() []string { } // LabelValues returns label values for the given name. -func (p *MemPostings) LabelValues(_ context.Context, name string) []string { +func (p *MemPostings) LabelValues(_ context.Context, name string, hints *storage.LabelHints) []string { p.mtx.RLock() values := p.lvs[name] p.mtx.RUnlock() + if hints != nil && hints.Limit > 0 && len(values) > hints.Limit { + values = values[:hints.Limit] + } + // The slice from p.lvs[name] is shared between all readers, and it is append-only. // Since it's shared, we need to make a copy of it before returning it to make // sure that no caller modifies the original one by sorting it or filtering it. diff --git a/tsdb/ooo_head_read.go b/tsdb/ooo_head_read.go index 5eb63edfd5..ddc5376df0 100644 --- a/tsdb/ooo_head_read.go +++ b/tsdb/ooo_head_read.go @@ -176,16 +176,16 @@ type multiMeta struct { // LabelValues needs to be overridden from the headIndexReader implementation // so we can return labels within either in-order range or ooo range. -func (oh *HeadAndOOOIndexReader) LabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (oh *HeadAndOOOIndexReader) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { if oh.maxt < oh.head.MinTime() && oh.maxt < oh.head.MinOOOTime() || oh.mint > oh.head.MaxTime() && oh.mint > oh.head.MaxOOOTime() { return []string{}, nil } if len(matchers) == 0 { - return oh.head.postings.LabelValues(ctx, name), nil + return oh.head.postings.LabelValues(ctx, name, hints), nil } - return labelValuesWithMatchers(ctx, oh, name, matchers...) + return labelValuesWithMatchers(ctx, oh, name, hints, matchers...) } func lessByMinTimeAndMinRef(a, b chunks.Meta) int { @@ -484,11 +484,11 @@ func (ir *OOOCompactionHeadIndexReader) Series(ref storage.SeriesRef, builder *l return getOOOSeriesChunks(s, ir.ch.mint, ir.ch.maxt, 0, ir.ch.lastMmapRef, false, 0, chks) } -func (ir *OOOCompactionHeadIndexReader) SortedLabelValues(_ context.Context, _ string, _ ...*labels.Matcher) ([]string, error) { +func (ir *OOOCompactionHeadIndexReader) SortedLabelValues(_ context.Context, _ string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, error) { return nil, errors.New("not implemented") } -func (ir *OOOCompactionHeadIndexReader) LabelValues(_ context.Context, _ string, _ ...*labels.Matcher) ([]string, error) { +func (ir *OOOCompactionHeadIndexReader) LabelValues(_ context.Context, _ string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, error) { return nil, errors.New("not implemented") } diff --git a/tsdb/ooo_head_read_test.go b/tsdb/ooo_head_read_test.go index 9dcf125b92..4fd29d0b1b 100644 --- a/tsdb/ooo_head_read_test.go +++ b/tsdb/ooo_head_read_test.go @@ -452,24 +452,24 @@ func testOOOHeadChunkReader_LabelValues(t *testing.T, scenario sampleTypeScenari // We first want to test using a head index reader that covers the biggest query interval oh := NewHeadAndOOOIndexReader(head, tc.queryMinT, tc.queryMinT, tc.queryMaxT, 0) matchers := []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1")} - values, err := oh.LabelValues(ctx, "foo", matchers...) + values, err := oh.LabelValues(ctx, "foo", nil, matchers...) sort.Strings(values) require.NoError(t, err) require.Equal(t, tc.expValues1, values) matchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchNotRegexp, "foo", "^bar.")} - values, err = oh.LabelValues(ctx, "foo", matchers...) + values, err = oh.LabelValues(ctx, "foo", nil, matchers...) sort.Strings(values) require.NoError(t, err) require.Equal(t, tc.expValues2, values) matchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.")} - values, err = oh.LabelValues(ctx, "foo", matchers...) + values, err = oh.LabelValues(ctx, "foo", nil, matchers...) sort.Strings(values) require.NoError(t, err) require.Equal(t, tc.expValues3, values) - values, err = oh.LabelValues(ctx, "foo") + values, err = oh.LabelValues(ctx, "foo", nil) sort.Strings(values) require.NoError(t, err) require.Equal(t, tc.expValues4, values) diff --git a/tsdb/querier.go b/tsdb/querier.go index f7d564a2dd..0943c760cd 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -77,8 +77,8 @@ func newBlockBaseQuerier(b BlockReader, mint, maxt int64) (*blockBaseQuerier, er }, nil } -func (q *blockBaseQuerier) LabelValues(ctx context.Context, name string, _ *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) { - res, err := q.index.SortedLabelValues(ctx, name, matchers...) +func (q *blockBaseQuerier) LabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, annotations.Annotations, error) { + res, err := q.index.SortedLabelValues(ctx, name, hints, matchers...) return res, nil, err } @@ -390,8 +390,9 @@ func inversePostingsForMatcher(ctx context.Context, ix IndexReader, m *labels.Ma return it, it.Err() } -func labelValuesWithMatchers(ctx context.Context, r IndexReader, name string, matchers ...*labels.Matcher) ([]string, error) { - allValues, err := r.LabelValues(ctx, name) +func labelValuesWithMatchers(ctx context.Context, r IndexReader, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { + // Limit is applied at the end, after filtering. + allValues, err := r.LabelValues(ctx, name, nil) if err != nil { return nil, fmt.Errorf("fetching values of label %s: %w", name, err) } @@ -428,6 +429,9 @@ func labelValuesWithMatchers(ctx context.Context, r IndexReader, name string, ma // If we don't have any matchers for other labels, then we're done. if !hasMatchersForOtherLabels { + if hints != nil && hints.Limit > 0 && len(allValues) > hints.Limit { + allValues = allValues[:hints.Limit] + } return allValues, nil } @@ -451,6 +455,9 @@ func labelValuesWithMatchers(ctx context.Context, r IndexReader, name string, ma values := make([]string, 0, len(indexes)) for _, idx := range indexes { values = append(values, allValues[idx]) + if hints != nil && hints.Limit > 0 && len(values) >= hints.Limit { + break + } } return values, nil diff --git a/tsdb/querier_bench_test.go b/tsdb/querier_bench_test.go index f5cc62d961..511166d2b5 100644 --- a/tsdb/querier_bench_test.go +++ b/tsdb/querier_bench_test.go @@ -228,7 +228,7 @@ func benchmarkLabelValuesWithMatchers(b *testing.B, ir IndexReader) { for _, c := range cases { b.Run(c.name, func(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := labelValuesWithMatchers(ctx, ir, c.labelName, c.matchers...) + _, err := labelValuesWithMatchers(ctx, ir, c.labelName, nil, c.matchers...) require.NoError(b, err) } }) diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go index cb96fa3716..cd3b15abc4 100644 --- a/tsdb/querier_test.go +++ b/tsdb/querier_test.go @@ -2252,19 +2252,22 @@ func (m mockIndex) Close() error { return nil } -func (m mockIndex) SortedLabelValues(ctx context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { - values, _ := m.LabelValues(ctx, name, matchers...) +func (m mockIndex) SortedLabelValues(ctx context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { + values, _ := m.LabelValues(ctx, name, hints, matchers...) sort.Strings(values) return values, nil } -func (m mockIndex) LabelValues(_ context.Context, name string, matchers ...*labels.Matcher) ([]string, error) { +func (m mockIndex) LabelValues(_ context.Context, name string, hints *storage.LabelHints, matchers ...*labels.Matcher) ([]string, error) { var values []string if len(matchers) == 0 { for l := range m.postings { if l.Name == name { values = append(values, l.Value) + if hints != nil && hints.Limit > 0 && len(values) >= hints.Limit { + break + } } } return values, nil @@ -2275,6 +2278,9 @@ func (m mockIndex) LabelValues(_ context.Context, name string, matchers ...*labe if matcher.Matches(series.l.Get(matcher.Name)) { // TODO(colega): shouldn't we check all the matchers before adding this to the values? values = append(values, series.l.Get(name)) + if hints != nil && hints.Limit > 0 && len(values) >= hints.Limit { + break + } } } } @@ -3299,12 +3305,12 @@ func (m mockMatcherIndex) Symbols() index.StringIter { return nil } func (m mockMatcherIndex) Close() error { return nil } // SortedLabelValues will return error if it is called. -func (m mockMatcherIndex) SortedLabelValues(context.Context, string, ...*labels.Matcher) ([]string, error) { +func (m mockMatcherIndex) SortedLabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, error) { return []string{}, errors.New("sorted label values called") } // LabelValues will return error if it is called. -func (m mockMatcherIndex) LabelValues(context.Context, string, ...*labels.Matcher) ([]string, error) { +func (m mockMatcherIndex) LabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, error) { return []string{}, errors.New("label values called") } @@ -3736,7 +3742,7 @@ func TestReader_PostingsForLabelMatchingHonorsContextCancel(t *testing.T) { failAfter := uint64(mockReaderOfLabelsSeriesCount / 2 / checkContextEveryNIterations) ctx := &testutil.MockContextErrAfter{FailAfter: failAfter} - _, err := labelValuesWithMatchers(ctx, ir, "__name__", labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".+")) + _, err := labelValuesWithMatchers(ctx, ir, "__name__", nil, labels.MustNewMatcher(labels.MatchRegexp, "__name__", ".+")) require.Error(t, err) require.Equal(t, failAfter+1, ctx.Count()) // Plus one for the Err() call that puts the error in the result. @@ -3746,7 +3752,7 @@ type mockReaderOfLabels struct{} const mockReaderOfLabelsSeriesCount = checkContextEveryNIterations * 10 -func (m mockReaderOfLabels) LabelValues(context.Context, string, ...*labels.Matcher) ([]string, error) { +func (m mockReaderOfLabels) LabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, error) { return make([]string, mockReaderOfLabelsSeriesCount), nil } @@ -3754,7 +3760,7 @@ func (m mockReaderOfLabels) LabelValueFor(context.Context, storage.SeriesRef, st panic("LabelValueFor called") } -func (m mockReaderOfLabels) SortedLabelValues(context.Context, string, ...*labels.Matcher) ([]string, error) { +func (m mockReaderOfLabels) SortedLabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, error) { panic("SortedLabelValues called") }