mirror of https://github.com/grafana/loki
feat(metastore): Implement Labels() and Values() on ObjectMetastore (#16734)
parent
9691f3e5dc
commit
a57a80ea32
@ -0,0 +1,260 @@ |
||||
package metastore |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"os" |
||||
"slices" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/go-kit/log" |
||||
"github.com/grafana/dskit/user" |
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"github.com/prometheus/prometheus/model/labels" |
||||
"github.com/stretchr/testify/require" |
||||
"github.com/thanos-io/objstore" |
||||
|
||||
"github.com/grafana/loki/v3/pkg/dataobj" |
||||
"github.com/grafana/loki/v3/pkg/dataobj/uploader" |
||||
"github.com/grafana/loki/v3/pkg/logproto" |
||||
) |
||||
|
||||
const ( |
||||
tenantID = "test-tenant" |
||||
) |
||||
|
||||
var ( |
||||
now = time.Now().UTC() |
||||
|
||||
// our streams won't use any log lines, therefore leave them out of the Entry structs
|
||||
streams = []logproto.Stream{ |
||||
{ |
||||
Labels: `{app="foo", env="prod"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(-2 * time.Hour)}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="foo", env="dev"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="bar", env="prod"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(5 * time.Second)}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="bar", env="dev"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(8 * time.Minute)}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="baz", env="prod", team="a"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(12 * time.Minute)}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="foo", env="prod"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(-12 * time.Hour)}}, |
||||
}, |
||||
{ |
||||
Labels: `{app="foo", env="prod"}`, |
||||
Entries: []logproto.Entry{{Timestamp: now.Add(12 * time.Hour)}}, |
||||
}, |
||||
} |
||||
) |
||||
|
||||
// Similar to store_test.go -- we need a populated dataobj/builder/metastore to test labels and values
|
||||
type testDataBuilder struct { |
||||
t *testing.T |
||||
bucket objstore.Bucket |
||||
|
||||
builder *dataobj.Builder |
||||
meta *Updater |
||||
uploader *uploader.Uploader |
||||
} |
||||
|
||||
func (b *testDataBuilder) addStreamAndFlush(stream logproto.Stream) { |
||||
err := b.builder.Append(stream) |
||||
require.NoError(b.t, err) |
||||
|
||||
buf := bytes.NewBuffer(make([]byte, 0, 1024*1024)) |
||||
stats, err := b.builder.Flush(buf) |
||||
require.NoError(b.t, err) |
||||
|
||||
path, err := b.uploader.Upload(context.Background(), buf) |
||||
require.NoError(b.t, err) |
||||
|
||||
err = b.meta.Update(context.Background(), path, stats) |
||||
require.NoError(b.t, err) |
||||
|
||||
b.builder.Reset() |
||||
} |
||||
|
||||
func TestLabels(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "prod"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedLabels, err := mstore.Labels(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedLabels, len(matchers)) |
||||
}) |
||||
} |
||||
|
||||
func TestNonExistentLabels(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "invalid"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "ops"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedLabels, err := mstore.Labels(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedLabels, 0) |
||||
}) |
||||
} |
||||
|
||||
func TestMixedLabels(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "invalid"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedLabels, err := mstore.Labels(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedLabels, 0) |
||||
}) |
||||
} |
||||
|
||||
func TestLabelsSingleMatcher(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "prod"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedLabels, err := mstore.Labels(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
|
||||
require.Len(t, matchedLabels, 3) |
||||
for _, expectedLabel := range []string{"env", "team", "app"} { |
||||
require.NotEqual(t, slices.Index(matchedLabels, expectedLabel), -1) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func TestLabelsEmptyMatcher(t *testing.T) { |
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedLabels, err := mstore.Labels(ctx, start, end) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedLabels, 3) |
||||
}) |
||||
} |
||||
|
||||
func TestValues(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "prod"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedValues, err := mstore.Values(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedValues, len(matchers)) |
||||
}) |
||||
} |
||||
|
||||
func TestNonExistentValues(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "invalid"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "ops"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedValues, err := mstore.Values(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedValues, 0) |
||||
}) |
||||
} |
||||
|
||||
func TestMixedValues(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "app", "foo"), |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "ops"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedValues, err := mstore.Values(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedValues, 0) |
||||
}) |
||||
} |
||||
|
||||
func TestValuesSingleMatcher(t *testing.T) { |
||||
matchers := []*labels.Matcher{ |
||||
labels.MustNewMatcher(labels.MatchEqual, "env", "prod"), |
||||
} |
||||
|
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedValues, err := mstore.Values(ctx, start, end, matchers...) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedValues, 5) |
||||
}) |
||||
} |
||||
|
||||
func TestValuesEmptyMatcher(t *testing.T) { |
||||
queryMetastore(t, tenantID, func(ctx context.Context, start, end time.Time, mstore Metastore) { |
||||
matchedValues, err := mstore.Values(ctx, start, end) |
||||
require.NoError(t, err) |
||||
require.Len(t, matchedValues, 6) |
||||
for _, expectedValue := range []string{"foo", "prod", "bar", "dev", "baz", "a"} { |
||||
require.NotEqual(t, slices.Index(matchedValues, expectedValue), -1) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func queryMetastore(t *testing.T, tenantID string, mfunc func(context.Context, time.Time, time.Time, Metastore)) { |
||||
now := time.Now().UTC() |
||||
start := now.Add(-time.Hour * 5) |
||||
end := now.Add(time.Hour * 5) |
||||
|
||||
builder := newTestDataBuilder(t, tenantID) |
||||
|
||||
for _, stream := range streams { |
||||
builder.addStreamAndFlush(stream) |
||||
} |
||||
|
||||
mstore := NewObjectMetastore(builder.bucket) |
||||
defer func() { |
||||
require.NoError(t, mstore.bucket.Close()) |
||||
}() |
||||
|
||||
ctx := user.InjectOrgID(context.Background(), tenantID) |
||||
|
||||
mfunc(ctx, start, end, mstore) |
||||
} |
||||
|
||||
func newTestDataBuilder(t *testing.T, tenantID string) *testDataBuilder { |
||||
bucket := objstore.NewInMemBucket() |
||||
|
||||
builder, err := dataobj.NewBuilder(dataobj.BuilderConfig{ |
||||
TargetPageSize: 1024 * 1024, // 1MB
|
||||
TargetObjectSize: 10 * 1024 * 1024, // 10MB
|
||||
TargetSectionSize: 1024 * 1024, // 1MB
|
||||
BufferSize: 1024 * 1024, // 1MB
|
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
meta := NewUpdater(bucket, tenantID, log.NewLogfmtLogger(os.Stdout)) |
||||
require.NoError(t, meta.RegisterMetrics(prometheus.NewPedanticRegistry())) |
||||
|
||||
uploader := uploader.New(uploader.Config{SHAPrefixSize: 2}, bucket, tenantID) |
||||
require.NoError(t, uploader.RegisterMetrics(prometheus.NewPedanticRegistry())) |
||||
|
||||
return &testDataBuilder{ |
||||
t: t, |
||||
bucket: bucket, |
||||
builder: builder, |
||||
meta: meta, |
||||
uploader: uploader, |
||||
} |
||||
} |
Loading…
Reference in new issue