LabelNames API with matchers (#9083)

* Push the matchers for LabelNames all the way into the index.

NB This doesn't actually implement it in the index, just plumbs it through for now...

Signed-off-by: Tom Wilkie <tom@grafana.com>

* Hack it up.  Does not work.

Signed-off-by: Tom Wilkie <tom@grafana.com>

* Revert changes I don't understand

Can't see why do we need to hold a mutex on symbols, and the purpose of
the LabelNamesFor method.

Maybe I'll need to re-add this later.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Implement LabelNamesFor

This method provides the label names that appear in the postings
provided. We do that deeper than the label values because we know
beforehand that most of the label names we'll be the same across
different postings, and we don't want to go down an up looking up the
same symbols for all different series.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Mutex on symbols should be unlocked

However, I still don't understand why do we need a mutex here.

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Fix head.LabelNamesFor

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Implement mockIndex LabelNames with matchers

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Nitpick on slice initialisation

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Add tests for LabelNamesWithMatchers

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Fix the mutex mess on head.LabelValues/LabelNames

I still don't see why we need to grab that unrelated mutex, but at least
now we're grabbing it consistently

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Check error after iterating postings

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Use the error from posting when there was en error in postings

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>
Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Update storage/interface.go comment

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Update tsdb/index/index.go comment

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Update tsdb/index/index.go wrapped error msg

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Update tsdb/index/index.go wrapped error msg

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Update tsdb/index/index.go warpped error msg

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>

* Remove unneeded comment

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Add testcases for LabelNames w/matchers in api.go

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

* Use t.Cleanup() instead of defer in tests

Signed-off-by: Oleg Zaytsev <mail@olegzaytsev.com>

Co-authored-by: Tom Wilkie <tom@grafana.com>
Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com>
This commit is contained in:
Oleg Zaytsev 2021-07-20 14:38:08 +02:00 committed by GitHub
parent 7337ecf0d3
commit b1ed4a0a66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 402 additions and 68 deletions

View file

@ -184,7 +184,9 @@ func (q *errQuerier) Select(bool, *storage.SelectHints, ...*labels.Matcher) stor
func (*errQuerier) LabelValues(string, ...*labels.Matcher) ([]string, storage.Warnings, error) { func (*errQuerier) LabelValues(string, ...*labels.Matcher) ([]string, storage.Warnings, error) {
return nil, nil, nil return nil, nil, nil
} }
func (*errQuerier) LabelNames() ([]string, storage.Warnings, error) { return nil, nil, nil } func (*errQuerier) LabelNames(...*labels.Matcher) ([]string, storage.Warnings, error) {
return nil, nil, nil
}
func (*errQuerier) Close() error { return nil } func (*errQuerier) Close() error { return nil }
// errSeriesSet implements storage.SeriesSet which always returns error. // errSeriesSet implements storage.SeriesSet which always returns error.

View file

@ -234,7 +234,7 @@ func (errQuerier) LabelValues(name string, matchers ...*labels.Matcher) ([]strin
return nil, nil, errors.New("label values error") return nil, nil, errors.New("label values error")
} }
func (errQuerier) LabelNames() ([]string, storage.Warnings, error) { func (errQuerier) LabelNames(...*labels.Matcher) ([]string, storage.Warnings, error) {
return nil, nil, errors.New("label names error") return nil, nil, errors.New("label names error")
} }

View file

@ -113,8 +113,9 @@ type LabelQuerier interface {
LabelValues(name string, matchers ...*labels.Matcher) ([]string, Warnings, error) LabelValues(name string, matchers ...*labels.Matcher) ([]string, Warnings, error)
// LabelNames returns all the unique label names present in the block in sorted order. // LabelNames returns all the unique label names present in the block in sorted order.
// TODO(yeya24): support matchers or hints. // If matchers are specified the returned result set is reduced
LabelNames() ([]string, Warnings, error) // to label names of metrics matching the matchers.
LabelNames(matchers ...*labels.Matcher) ([]string, Warnings, error)
// Close releases the resources of the Querier. // Close releases the resources of the Querier.
Close() error Close() error

View file

@ -218,13 +218,13 @@ func mergeStrings(a, b []string) []string {
} }
// LabelNames returns all the unique label names present in all queriers in sorted order. // LabelNames returns all the unique label names present in all queriers in sorted order.
func (q *mergeGenericQuerier) LabelNames() ([]string, Warnings, error) { func (q *mergeGenericQuerier) LabelNames(matchers ...*labels.Matcher) ([]string, Warnings, error) {
var ( var (
labelNamesMap = make(map[string]struct{}) labelNamesMap = make(map[string]struct{})
warnings Warnings warnings Warnings
) )
for _, querier := range q.queriers { for _, querier := range q.queriers {
names, wrn, err := querier.LabelNames() names, wrn, err := querier.LabelNames(matchers...)
if wrn != nil { if wrn != nil {
// TODO(bwplotka): We could potentially wrap warnings. // TODO(bwplotka): We could potentially wrap warnings.
warnings = append(warnings, wrn...) warnings = append(warnings, wrn...)

View file

@ -778,7 +778,7 @@ func (m *mockGenericQuerier) LabelValues(name string, matchers ...*labels.Matche
return m.resp, m.warnings, m.err return m.resp, m.warnings, m.err
} }
func (m *mockGenericQuerier) LabelNames() ([]string, Warnings, error) { func (m *mockGenericQuerier) LabelNames(...*labels.Matcher) ([]string, Warnings, error) {
m.mtx.Lock() m.mtx.Lock()
m.labelNamesCalls++ m.labelNamesCalls++
m.mtx.Unlock() m.mtx.Unlock()

View file

@ -32,7 +32,7 @@ func (noopQuerier) LabelValues(string, ...*labels.Matcher) ([]string, Warnings,
return nil, nil, nil return nil, nil, nil
} }
func (noopQuerier) LabelNames() ([]string, Warnings, error) { func (noopQuerier) LabelNames(...*labels.Matcher) ([]string, Warnings, error) {
return nil, nil, nil return nil, nil, nil
} }
@ -55,7 +55,7 @@ func (noopChunkQuerier) LabelValues(string, ...*labels.Matcher) ([]string, Warni
return nil, nil, nil return nil, nil, nil
} }
func (noopChunkQuerier) LabelNames() ([]string, Warnings, error) { func (noopChunkQuerier) LabelNames(...*labels.Matcher) ([]string, Warnings, error) {
return nil, nil, nil return nil, nil, nil
} }

View file

@ -212,7 +212,7 @@ func (q *querier) LabelValues(string, ...*labels.Matcher) ([]string, storage.War
} }
// LabelNames implements storage.Querier and is a noop. // LabelNames implements storage.Querier and is a noop.
func (q *querier) LabelNames() ([]string, storage.Warnings, error) { func (q *querier) LabelNames(...*labels.Matcher) ([]string, storage.Warnings, error) {
// TODO: Implement: https://github.com/prometheus/prometheus/issues/3351 // TODO: Implement: https://github.com/prometheus/prometheus/issues/3351
return nil, nil, errors.New("not implemented") return nil, nil, errors.New("not implemented")
} }

View file

@ -55,8 +55,8 @@ func (s *secondaryQuerier) LabelValues(name string, matchers ...*labels.Matcher)
return vals, w, nil return vals, w, nil
} }
func (s *secondaryQuerier) LabelNames() ([]string, Warnings, error) { func (s *secondaryQuerier) LabelNames(matchers ...*labels.Matcher) ([]string, Warnings, error) {
names, w, err := s.genericQuerier.LabelNames() names, w, err := s.genericQuerier.LabelNames(matchers...)
if err != nil { if err != nil {
return nil, append([]error{err}, w...), nil return nil, append([]error{err}, w...), nil
} }

View file

@ -85,13 +85,17 @@ type IndexReader interface {
Series(ref uint64, lset *labels.Labels, chks *[]chunks.Meta) error Series(ref uint64, lset *labels.Labels, chks *[]chunks.Meta) error
// LabelNames returns all the unique label names present in the index in sorted order. // LabelNames returns all the unique label names present in the index in sorted order.
LabelNames() ([]string, error) LabelNames(matchers ...*labels.Matcher) ([]string, error)
// LabelValueFor returns label value for the given label name in the series referred to by ID. // LabelValueFor returns label value for the given label name in the series referred to by ID.
// If the series couldn't be found or the series doesn't have the requested label a // If the series couldn't be found or the series doesn't have the requested label a
// storage.ErrNotFound is returned as error. // storage.ErrNotFound is returned as error.
LabelValueFor(id uint64, label string) (string, error) LabelValueFor(id uint64, label string) (string, error)
// LabelNamesFor returns all the label names for the series referred to by IDs.
// The names returned are sorted.
LabelNamesFor(ids ...uint64) ([]string, error)
// Close releases the underlying resources of the reader. // Close releases the underlying resources of the reader.
Close() error Close() error
} }
@ -443,7 +447,15 @@ func (r blockIndexReader) LabelValues(name string, matchers ...*labels.Matcher)
return st, errors.Wrapf(err, "block: %s", r.b.Meta().ULID) return st, errors.Wrapf(err, "block: %s", r.b.Meta().ULID)
} }
return labelValuesWithMatchers(r, name, matchers...) return labelValuesWithMatchers(r.ir, name, matchers...)
}
func (r blockIndexReader) LabelNames(matchers ...*labels.Matcher) ([]string, error) {
if len(matchers) == 0 {
return r.b.LabelNames()
}
return labelNamesWithMatchers(r.ir, matchers...)
} }
func (r blockIndexReader) Postings(name string, values ...string) (index.Postings, error) { func (r blockIndexReader) Postings(name string, values ...string) (index.Postings, error) {
@ -465,10 +477,6 @@ func (r blockIndexReader) Series(ref uint64, lset *labels.Labels, chks *[]chunks
return nil return nil
} }
func (r blockIndexReader) LabelNames() ([]string, error) {
return r.b.LabelNames()
}
func (r blockIndexReader) Close() error { func (r blockIndexReader) Close() error {
r.b.pendingReaders.Done() r.b.pendingReaders.Done()
return nil return nil
@ -479,6 +487,12 @@ func (r blockIndexReader) LabelValueFor(id uint64, label string) (string, error)
return r.ir.LabelValueFor(id, label) return r.ir.LabelValueFor(id, label)
} }
// LabelNamesFor returns all the label names for the series referred to by IDs.
// The names returned are sorted.
func (r blockIndexReader) LabelNamesFor(ids ...uint64) ([]string, error) {
return r.ir.LabelNamesFor(ids...)
}
type blockTombstoneReader struct { type blockTombstoneReader struct {
tombstones.Reader tombstones.Reader
b *Block b *Block

View file

@ -418,6 +418,82 @@ func BenchmarkLabelValuesWithMatchers(b *testing.B) {
} }
} }
func TestLabelNamesWithMatchers(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "test_block_label_names_with_matchers")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, os.RemoveAll(tmpdir)) })
var seriesEntries []storage.Series
for i := 0; i < 100; i++ {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
}, []tsdbutil.Sample{sample{100, 0}}))
if i%10 == 0 {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
{Name: "tens", Value: fmt.Sprintf("value%d", i/10)},
}, []tsdbutil.Sample{sample{100, 0}}))
}
if i%20 == 0 {
seriesEntries = append(seriesEntries, storage.NewListSeries(labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
{Name: "tens", Value: fmt.Sprintf("value%d", i/10)},
{Name: "twenties", Value: fmt.Sprintf("value%d", i/20)},
}, []tsdbutil.Sample{sample{100, 0}}))
}
}
blockDir := createBlock(t, tmpdir, seriesEntries)
files, err := sequenceFiles(chunkDir(blockDir))
require.NoError(t, err)
require.Greater(t, len(files), 0, "No chunk created.")
// Check open err.
block, err := OpenBlock(nil, blockDir, nil)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, block.Close()) })
indexReader, err := block.Index()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, indexReader.Close()) })
testCases := []struct {
name string
labelName string
matchers []*labels.Matcher
expectedNames []string
}{
{
name: "get with non-empty unique: all",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchNotEqual, "unique", "")},
expectedNames: []string{"tens", "twenties", "unique"},
}, {
name: "get with unique ending in 1: only unique",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "unique", "value.*1")},
expectedNames: []string{"unique"},
}, {
name: "get with unique = value20: all",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "unique", "value20")},
expectedNames: []string{"tens", "twenties", "unique"},
}, {
name: "get tens = 1: unique & tens",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "tens", "value1")},
expectedNames: []string{"tens", "unique"},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
actualNames, err := indexReader.LabelNames(tt.matchers...)
require.NoError(t, err)
require.Equal(t, tt.expectedNames, actualNames)
})
}
}
// createBlock creates a block with given set of series and returns its dir. // createBlock creates a block with given set of series and returns its dir.
func createBlock(tb testing.TB, dir string, series []storage.Series) string { func createBlock(tb testing.TB, dir string, series []storage.Series) string {
blockDir, err := CreateBlock(series, dir, 0, log.NewNopLogger()) blockDir, err := CreateBlock(series, dir, 0, log.NewNopLogger())

View file

@ -2019,13 +2019,13 @@ func (h *headIndexReader) LabelValues(name string, matchers ...*labels.Matcher)
// LabelNames returns all the unique label names present in the head // LabelNames returns all the unique label names present in the head
// that are within the time range mint to maxt. // that are within the time range mint to maxt.
func (h *headIndexReader) LabelNames() ([]string, error) { func (h *headIndexReader) LabelNames(matchers ...*labels.Matcher) ([]string, error) {
h.head.symMtx.RLock()
if h.maxt < h.head.MinTime() || h.mint > h.head.MaxTime() { if h.maxt < h.head.MinTime() || h.mint > h.head.MaxTime() {
h.head.symMtx.RUnlock()
return []string{}, nil return []string{}, nil
} }
if len(matchers) == 0 {
h.head.symMtx.RLock()
labelNames := h.head.postings.LabelNames() labelNames := h.head.postings.LabelNames()
h.head.symMtx.RUnlock() h.head.symMtx.RUnlock()
@ -2033,6 +2033,9 @@ func (h *headIndexReader) LabelNames() ([]string, error) {
return labelNames, nil return labelNames, nil
} }
return labelNamesWithMatchers(h, matchers...)
}
// Postings returns the postings list iterator for the label pairs. // Postings returns the postings list iterator for the label pairs.
func (h *headIndexReader) Postings(name string, values ...string) (index.Postings, error) { func (h *headIndexReader) Postings(name string, values ...string) (index.Postings, error) {
res := make([]index.Postings, 0, len(values)) res := make([]index.Postings, 0, len(values))
@ -2122,6 +2125,27 @@ func (h *headIndexReader) LabelValueFor(id uint64, label string) (string, error)
return value, nil return value, nil
} }
// LabelNamesFor returns all the label names for the series referred to by IDs.
// The names returned are sorted.
func (h *headIndexReader) LabelNamesFor(ids ...uint64) ([]string, error) {
namesMap := make(map[string]struct{})
for _, id := range ids {
memSeries := h.head.series.getByID(id)
if memSeries == nil {
return nil, storage.ErrNotFound
}
for _, lbl := range memSeries.lset {
namesMap[lbl.Name] = struct{}{}
}
}
names := make([]string, 0, len(namesMap))
for name := range namesMap {
names = append(names, name)
}
sort.Strings(names)
return names, nil
}
func (h *Head) getOrCreate(hash uint64, lset labels.Labels) (*memSeries, bool, error) { func (h *Head) getOrCreate(hash uint64, lset labels.Labels) (*memSeries, bool, error) {
// Just using `getOrCreateWithID` below would be semantically sufficient, but we'd create // Just using `getOrCreateWithID` below would be semantically sufficient, but we'd create
// a new series on every sample inserted via Add(), which causes allocations // a new series on every sample inserted via Add(), which causes allocations

View file

@ -1934,9 +1934,7 @@ func TestHeadLabelNamesValuesWithMinMaxRange(t *testing.T) {
func TestHeadLabelValuesWithMatchers(t *testing.T) { func TestHeadLabelValuesWithMatchers(t *testing.T) {
head, _ := newTestHead(t, 1000, false) head, _ := newTestHead(t, 1000, false)
defer func() { t.Cleanup(func() { require.NoError(t, head.Close()) })
require.NoError(t, head.Close())
}()
app := head.Appender(context.Background()) app := head.Appender(context.Background())
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
@ -1993,6 +1991,74 @@ func TestHeadLabelValuesWithMatchers(t *testing.T) {
} }
} }
func TestHeadLabelNamesWithMatchers(t *testing.T) {
head, _ := newTestHead(t, 1000, false)
defer func() {
require.NoError(t, head.Close())
}()
app := head.Appender(context.Background())
for i := 0; i < 100; i++ {
_, err := app.Append(0, labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
}, 100, 0)
require.NoError(t, err)
if i%10 == 0 {
_, err := app.Append(0, labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
{Name: "tens", Value: fmt.Sprintf("value%d", i/10)},
}, 100, 0)
require.NoError(t, err)
}
if i%20 == 0 {
_, err := app.Append(0, labels.Labels{
{Name: "unique", Value: fmt.Sprintf("value%d", i)},
{Name: "tens", Value: fmt.Sprintf("value%d", i/10)},
{Name: "twenties", Value: fmt.Sprintf("value%d", i/20)},
}, 100, 0)
require.NoError(t, err)
}
}
require.NoError(t, app.Commit())
testCases := []struct {
name string
labelName string
matchers []*labels.Matcher
expectedNames []string
}{
{
name: "get with non-empty unique: all",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchNotEqual, "unique", "")},
expectedNames: []string{"tens", "twenties", "unique"},
}, {
name: "get with unique ending in 1: only unique",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "unique", "value.*1")},
expectedNames: []string{"unique"},
}, {
name: "get with unique = value20: all",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "unique", "value20")},
expectedNames: []string{"tens", "twenties", "unique"},
}, {
name: "get tens = 1: unique & tens",
matchers: []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "tens", "value1")},
expectedNames: []string{"tens", "unique"},
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
headIdxReader := head.indexRange(0, 200)
actualNames, err := headIdxReader.LabelNames(tt.matchers...)
require.NoError(t, err)
require.Equal(t, tt.expectedNames, actualNames)
})
}
}
func TestErrReuseAppender(t *testing.T) { func TestErrReuseAppender(t *testing.T) {
head, _ := newTestHead(t, 1000, false) head, _ := newTestHead(t, 1000, false)
defer func() { defer func() {

View file

@ -1517,6 +1517,49 @@ func (r *Reader) LabelValues(name string, matchers ...*labels.Matcher) ([]string
return values, nil return values, nil
} }
// LabelNamesFor returns all the label names for the series referred to by IDs.
// The names returned are sorted.
func (r *Reader) LabelNamesFor(ids ...uint64) ([]string, error) {
// Gather offsetsMap the name offsetsMap in the symbol table first
offsetsMap := make(map[uint32]struct{})
for _, id := range ids {
offset := id
// In version 2 series IDs are no longer exact references but series are 16-byte padded
// and the ID is the multiple of 16 of the actual position.
if r.version == FormatV2 {
offset = id * 16
}
d := encoding.NewDecbufUvarintAt(r.b, int(offset), castagnoliTable)
buf := d.Get()
if d.Err() != nil {
return nil, errors.Wrap(d.Err(), "get buffer for series")
}
offsets, err := r.dec.LabelNamesOffsetsFor(buf)
if err != nil {
return nil, errors.Wrap(err, "get label name offsets")
}
for _, off := range offsets {
offsetsMap[off] = struct{}{}
}
}
// Lookup the unique symbols.
names := make([]string, 0, len(offsetsMap))
for off := range offsetsMap {
name, err := r.lookupSymbol(off)
if err != nil {
return nil, errors.Wrap(err, "lookup symbol in LabelNamesFor")
}
names = append(names, name)
}
sort.Strings(names)
return names, nil
}
// LabelValueFor returns label value for the given label name in the series referred to by ID. // LabelValueFor returns label value for the given label name in the series referred to by ID.
func (r *Reader) LabelValueFor(id uint64, label string) (string, error) { func (r *Reader) LabelValueFor(id uint64, label string) (string, error) {
offset := id offset := id
@ -1670,7 +1713,12 @@ func (r *Reader) Size() int64 {
} }
// LabelNames returns all the unique label names present in the index. // LabelNames returns all the unique label names present in the index.
func (r *Reader) LabelNames() ([]string, error) { // TODO(twilkie) implement support for matchers
func (r *Reader) LabelNames(matchers ...*labels.Matcher) ([]string, error) {
if len(matchers) > 0 {
return nil, errors.Errorf("matchers parameter is not implemented: %+v", matchers)
}
labelNames := make([]string, 0, len(r.postings)) labelNames := make([]string, 0, len(r.postings))
for name := range r.postings { for name := range r.postings {
if name == allPostingsKey.Name { if name == allPostingsKey.Name {
@ -1721,6 +1769,25 @@ func (dec *Decoder) Postings(b []byte) (int, Postings, error) {
return n, newBigEndianPostings(l), d.Err() return n, newBigEndianPostings(l), d.Err()
} }
// LabelNamesOffsetsFor decodes the offsets of the name symbols for a given series.
// They are returned in the same order they're stored, which should be sorted lexicographically.
func (dec *Decoder) LabelNamesOffsetsFor(b []byte) ([]uint32, error) {
d := encoding.Decbuf{B: b}
k := d.Uvarint()
offsets := make([]uint32, k)
for i := 0; i < k; i++ {
offsets[i] = uint32(d.Uvarint())
_ = d.Uvarint() // skip the label value
if d.Err() != nil {
return nil, errors.Wrap(d.Err(), "read series label offsets")
}
}
return offsets, d.Err()
}
// LabelValueFor decodes a label for a given series. // LabelValueFor decodes a label for a given series.
func (dec *Decoder) LabelValueFor(b []byte, label string) (string, error) { func (dec *Decoder) LabelValueFor(b []byte, label string) (string, error) {
d := encoding.Decbuf{B: b} d := encoding.Decbuf{B: b}

View file

@ -88,8 +88,8 @@ func (q *blockBaseQuerier) LabelValues(name string, matchers ...*labels.Matcher)
return res, nil, err return res, nil, err
} }
func (q *blockBaseQuerier) LabelNames() ([]string, storage.Warnings, error) { func (q *blockBaseQuerier) LabelNames(matchers ...*labels.Matcher) ([]string, storage.Warnings, error) {
res, err := q.index.LabelNames() res, err := q.index.LabelNames(matchers...)
return res, nil, err return res, nil, err
} }
@ -407,6 +407,23 @@ func labelValuesWithMatchers(r IndexReader, name string, matchers ...*labels.Mat
return values, nil return values, nil
} }
func labelNamesWithMatchers(r IndexReader, matchers ...*labels.Matcher) ([]string, error) {
p, err := PostingsForMatchers(r, matchers...)
if err != nil {
return nil, err
}
var postings []uint64
for p.Next() {
postings = append(postings, p.At())
}
if p.Err() != nil {
return nil, errors.Wrapf(p.Err(), "postings for label names with matchers")
}
return r.LabelNamesFor(postings...)
}
// blockBaseSeriesSet allows to iterate over all series in the single block. // blockBaseSeriesSet allows to iterate over all series in the single block.
// Iterated series are trimmed with given min and max time as well as tombstones. // Iterated series are trimmed with given min and max time as well as tombstones.
// See newBlockSeriesSet and newBlockChunkSeriesSet to use it for either sample or chunk iterating. // See newBlockSeriesSet and newBlockChunkSeriesSet to use it for either sample or chunk iterating.

View file

@ -1154,7 +1154,7 @@ func (m mockIndex) SortedLabelValues(name string, matchers ...*labels.Matcher) (
} }
func (m mockIndex) LabelValues(name string, matchers ...*labels.Matcher) ([]string, error) { func (m mockIndex) LabelValues(name string, matchers ...*labels.Matcher) ([]string, error) {
values := []string{} var values []string
if len(matchers) == 0 { if len(matchers) == 0 {
for l := range m.postings { for l := range m.postings {
@ -1168,6 +1168,7 @@ func (m mockIndex) LabelValues(name string, matchers ...*labels.Matcher) ([]stri
for _, series := range m.series { for _, series := range m.series {
for _, matcher := range matchers { for _, matcher := range matchers {
if matcher.Matches(series.l.Get(matcher.Name)) { 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)) values = append(values, series.l.Get(name))
} }
} }
@ -1180,6 +1181,20 @@ func (m mockIndex) LabelValueFor(id uint64, label string) (string, error) {
return m.series[id].l.Get(label), nil return m.series[id].l.Get(label), nil
} }
func (m mockIndex) LabelNamesFor(ids ...uint64) ([]string, error) {
namesMap := make(map[string]bool)
for _, id := range ids {
for _, lbl := range m.series[id].l {
namesMap[lbl.Name] = true
}
}
names := make([]string, 0, len(namesMap))
for name := range namesMap {
names = append(names, name)
}
return names, nil
}
func (m mockIndex) Postings(name string, values ...string) (index.Postings, error) { func (m mockIndex) Postings(name string, values ...string) (index.Postings, error) {
res := make([]index.Postings, 0, len(values)) res := make([]index.Postings, 0, len(values))
for _, value := range values { for _, value := range values {
@ -1212,11 +1227,28 @@ func (m mockIndex) Series(ref uint64, lset *labels.Labels, chks *[]chunks.Meta)
return nil return nil
} }
func (m mockIndex) LabelNames() ([]string, error) { func (m mockIndex) LabelNames(matchers ...*labels.Matcher) ([]string, error) {
names := map[string]struct{}{} names := map[string]struct{}{}
if len(matchers) == 0 {
for l := range m.postings { for l := range m.postings {
names[l.Name] = struct{}{} names[l.Name] = struct{}{}
} }
} else {
for _, series := range m.series {
matches := true
for _, matcher := range matchers {
matches = matches || matcher.Matches(series.l.Get(matcher.Name))
if !matches {
break
}
}
if matches {
for _, lbl := range series.l {
names[lbl.Name] = struct{}{}
}
}
}
}
l := make([]string, 0, len(names)) l := make([]string, 0, len(names))
for name := range names { for name := range names {
l = append(l, name) l = append(l, name)
@ -2007,6 +2039,10 @@ func (m mockMatcherIndex) LabelValueFor(id uint64, label string) (string, error)
return "", errors.New("label value for called") return "", errors.New("label value for called")
} }
func (m mockMatcherIndex) LabelNamesFor(ids ...uint64) ([]string, error) {
return nil, errors.New("label names for for called")
}
func (m mockMatcherIndex) Postings(name string, values ...string) (index.Postings, error) { func (m mockMatcherIndex) Postings(name string, values ...string) (index.Postings, error) {
return index.EmptyPostings(), nil return index.EmptyPostings(), nil
} }
@ -2019,7 +2055,9 @@ func (m mockMatcherIndex) Series(ref uint64, lset *labels.Labels, chks *[]chunks
return nil return nil
} }
func (m mockMatcherIndex) LabelNames() ([]string, error) { return []string{}, nil } func (m mockMatcherIndex) LabelNames(...*labels.Matcher) ([]string, error) {
return []string{}, nil
}
func TestPostingsForMatcher(t *testing.T) { func TestPostingsForMatcher(t *testing.T) {
cases := []struct { cases := []struct {

View file

@ -559,25 +559,17 @@ func (api *API) labelNames(r *http.Request) apiFuncResult {
warnings storage.Warnings warnings storage.Warnings
) )
if len(matcherSets) > 0 { if len(matcherSets) > 0 {
hints := &storage.SelectHints{ labelNamesSet := make(map[string]struct{})
Start: timestamp.FromTime(start),
End: timestamp.FromTime(end), for _, matchers := range matcherSets {
Func: "series", // There is no series function, this token is used for lookups that don't need samples. vals, callWarnings, err := q.LabelNames(matchers...)
if err != nil {
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil}
} }
labelNamesSet := make(map[string]struct{}) warnings = append(warnings, callWarnings...)
// Get all series which match matchers. for _, val := range vals {
for _, mset := range matcherSets { labelNamesSet[val] = struct{}{}
s := q.Select(false, hints, mset...)
for s.Next() {
series := s.At()
for _, lb := range series.Labels() {
labelNamesSet[lb.Name] = struct{}{}
}
}
warnings = append(warnings, s.Warnings()...)
if err := s.Err(); err != nil {
return apiFuncResult{nil, &apiError{errorExec, err}, warnings, nil}
} }
} }

View file

@ -464,6 +464,7 @@ func TestLabelNames(t *testing.T) {
test_metric1{foo2="boo"} 1+0x100 test_metric1{foo2="boo"} 1+0x100
test_metric2{foo="boo"} 1+0x100 test_metric2{foo="boo"} 1+0x100
test_metric2{foo="boo", xyz="qwerty"} 1+0x100 test_metric2{foo="boo", xyz="qwerty"} 1+0x100
test_metric2{foo="baz", abc="qwerty"} 1+0x100
`) `)
require.NoError(t, err) require.NoError(t, err)
defer suite.Close() defer suite.Close()
@ -472,21 +473,57 @@ func TestLabelNames(t *testing.T) {
api := &API{ api := &API{
Queryable: suite.Storage(), Queryable: suite.Storage(),
} }
request := func(m string) (*http.Request, error) { request := func(method string, matchers ...string) (*http.Request, error) {
if m == http.MethodPost { u, err := url.Parse("http://example.com")
r, err := http.NewRequest(m, "http://example.com", nil) require.NoError(t, err)
q := u.Query()
for _, matcher := range matchers {
q.Add("match[]", matcher)
}
u.RawQuery = q.Encode()
r, err := http.NewRequest(method, u.String(), nil)
if method == http.MethodPost {
r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
return r, err return r, err
} }
return http.NewRequest(m, "http://example.com", nil)
} for _, tc := range []struct {
name string
matchers []string
expected []string
}{
{
name: "no matchers",
expected: []string{"__name__", "abc", "baz", "foo", "foo1", "foo2", "xyz"},
},
{
name: "non empty label matcher",
matchers: []string{`{foo=~".+"}`},
expected: []string{"__name__", "abc", "foo", "xyz"},
},
{
name: "exact label matcher",
matchers: []string{`{foo="boo"}`},
expected: []string{"__name__", "foo", "xyz"},
},
{
name: "two matchers",
matchers: []string{`{foo="boo"}`, `{foo="baz"}`},
expected: []string{"__name__", "abc", "foo", "xyz"},
},
} {
t.Run(tc.name, func(t *testing.T) {
for _, method := range []string{http.MethodGet, http.MethodPost} { for _, method := range []string{http.MethodGet, http.MethodPost} {
ctx := context.Background() ctx := context.Background()
req, err := request(method) req, err := request(method, tc.matchers...)
require.NoError(t, err) require.NoError(t, err)
res := api.labelNames(req.WithContext(ctx)) res := api.labelNames(req.WithContext(ctx))
assertAPIError(t, res.err, "") assertAPIError(t, res.err, "")
assertAPIResponse(t, res.data, []string{"__name__", "baz", "foo", "foo1", "foo2", "xyz"}) assertAPIResponse(t, res.data, tc.expected)
}
})
} }
} }