Like Prometheus, but for logs.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loki/pkg/querier/multi_tenant_querier.go

484 lines
13 KiB

package querier
import (
"context"
"fmt"
"strings"
"github.com/grafana/loki/v3/pkg/querier/plan"
"github.com/grafana/loki/v3/pkg/storage/stores/index/seriesvolume"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/grafana/dskit/user"
"github.com/prometheus/prometheus/model/labels"
"github.com/grafana/dskit/tenant"
"github.com/grafana/loki/v3/pkg/iter"
"github.com/grafana/loki/v3/pkg/loghttp"
"github.com/grafana/loki/v3/pkg/logproto"
"github.com/grafana/loki/v3/pkg/logql"
"github.com/grafana/loki/v3/pkg/logql/syntax"
"github.com/grafana/loki/v3/pkg/storage/stores/index/stats"
)
const (
defaultTenantLabel = "__tenant_id__"
retainExistingPrefix = "original_"
)
// MultiTenantQuerier is able to query across different tenants.
type MultiTenantQuerier struct {
Querier
logger log.Logger
}
// NewMultiTenantQuerier returns a new querier able to query across different tenants.
func NewMultiTenantQuerier(querier Querier, logger log.Logger) *MultiTenantQuerier {
return &MultiTenantQuerier{
Querier: querier,
logger: logger,
}
}
func (q *MultiTenantQuerier) SelectLogs(ctx context.Context, params logql.SelectLogParams) (iter.EntryIterator, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.SelectLogs(ctx, params)
}
selector, err := params.LogSelector()
if err != nil {
return nil, err
}
matchedTenants, filteredMatchers := filterValuesByMatchers(defaultTenantLabel, tenantIDs, selector.Matchers()...)
params.Selector = replaceMatchers(selector, filteredMatchers).String()
parsed, err := syntax.ParseLogSelector(params.Selector, true)
if err != nil {
return nil, fmt.Errorf("log selector is invalid after matcher update: %w", err)
}
params.Plan = &plan.QueryPlan{
AST: parsed,
}
// in case of multiple tenants, we need to filter the store chunks by tenant if they are provided
storeOverridesByTenant := make(map[string][]*logproto.ChunkRef)
if overrides := params.GetStoreChunks(); overrides != nil {
storeOverridesByTenant = partitionChunkRefsByTenant(overrides.Refs)
}
iters := make([]iter.EntryIterator, len(matchedTenants))
i := 0
for id := range matchedTenants {
singleContext := user.InjectOrgID(ctx, id)
tenantParams := params
if tenantChunkOverrides, ok := storeOverridesByTenant[id]; ok {
tenantParams = tenantParams.WithStoreChunks(&logproto.ChunkRefGroup{Refs: tenantChunkOverrides})
}
iter, err := q.Querier.SelectLogs(singleContext, tenantParams)
if err != nil {
return nil, err
}
iters[i] = NewTenantEntryIterator(iter, id)
i++
}
return iter.NewSortEntryIterator(iters, params.Direction), nil
}
func (q *MultiTenantQuerier) SelectSamples(ctx context.Context, params logql.SelectSampleParams) (iter.SampleIterator, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.SelectSamples(ctx, params)
}
matchedTenants, updatedSelector, err := removeTenantSelector(params, tenantIDs)
if err != nil {
return nil, err
}
params.Selector = updatedSelector.String()
// in case of multiple tenants, we need to filter the store chunks by tenant if they are provided
storeOverridesByTenant := make(map[string][]*logproto.ChunkRef)
if overrides := params.GetStoreChunks(); overrides != nil {
storeOverridesByTenant = partitionChunkRefsByTenant(params.GetStoreChunks().Refs)
}
iters := make([]iter.SampleIterator, len(matchedTenants))
i := 0
for id := range matchedTenants {
singleContext := user.InjectOrgID(ctx, id)
tenantParams := params
if tenantChunkOverrides, ok := storeOverridesByTenant[id]; ok {
tenantParams = tenantParams.WithStoreChunks(&logproto.ChunkRefGroup{Refs: tenantChunkOverrides})
}
iter, err := q.Querier.SelectSamples(singleContext, tenantParams)
if err != nil {
return nil, err
}
iters[i] = NewTenantSampleIterator(iter, id)
i++
}
return iter.NewSortSampleIterator(iters), nil
}
func (q *MultiTenantQuerier) Label(ctx context.Context, req *logproto.LabelRequest) (*logproto.LabelResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if req.Values && req.Name == defaultTenantLabel {
return &logproto.LabelResponse{Values: tenantIDs}, nil
}
if len(tenantIDs) == 1 {
return q.Querier.Label(ctx, req)
}
responses := make([]*logproto.LabelResponse, len(tenantIDs))
for i, id := range tenantIDs {
singleContext := user.InjectOrgID(ctx, id)
resp, err := q.Querier.Label(singleContext, req)
if err != nil {
return nil, err
}
responses[i] = resp
}
// Append tenant ID label name if label names are requested.
if !req.Values {
responses = append(responses, &logproto.LabelResponse{Values: []string{defaultTenantLabel}})
}
return logproto.MergeLabelResponses(responses)
}
func (q *MultiTenantQuerier) Series(ctx context.Context, req *logproto.SeriesRequest) (*logproto.SeriesResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.Series(ctx, req)
}
responses := make([]*logproto.SeriesResponse, len(tenantIDs))
for i, id := range tenantIDs {
singleContext := user.InjectOrgID(ctx, id)
resp, err := q.Querier.Series(singleContext, req)
if err != nil {
return nil, err
}
Optimize series response format by using repeated fileds. (#11498) **What this PR does / why we need it**: The Protobuf map type is encodied as a repeated field of map entries. Decoding them to a slice is much faster than decoding them into a map. Since Loki is not using the fast key check for a map we can use the slice decoding. This change also allows us to decode the JSON directly into the right protobuf struct. This doulbes the JSON decoding speed and reduces the memory pressure by ~40%. ``` › go test -bench=. -run=^$ -count=10 ./pkg/querier/queryrange > before.log › go test -bench=. -run=^$ -count=10 ./pkg/querier/queryrange > after.log › benchstat before.log after.log goos: darwin goarch: arm64 pkg: github.com/grafana/loki/pkg/querier/queryrange │ before.log │ after.log │ │ sec/op │ sec/op vs base │ ResponseMerge/mergeStreams_unlimited-10 32.36m ± 0% 32.63m ± 2% ~ (p=0.393 n=10) ResponseMerge/mergeOrderedNonOverlappingStreams_unlimited-10 1.050m ± 1% 1.080m ± 3% +2.84% (p=0.005 n=10) ResponseMerge/mergeStreams_limited-10 33.02m ± 0% 32.60m ± 1% -1.29% (p=0.004 n=10) ResponseMerge/mergeOrderedNonOverlappingStreams_limited-10 15.11m ± 0% 15.07m ± 0% ~ (p=0.075 n=10) _CodecDecodeLogs-10 4.395m ± 1% 4.364m ± 0% -0.72% (p=0.005 n=10) _CodecDecodeSamples-10 16.97m ± 0% 16.84m ± 2% -0.77% (p=0.023 n=10) _CodecDecodeSeries/application/vnd.google.protobuf-10 745.8µ ± 8% 736.8µ ± 12% ~ (p=0.739 n=10) _CodecDecodeSeries/application/json;_charset=utf-8-10 15.37m ± 1% 10.60m ± 0% -31.03% (p=0.000 n=10) _MergeResponses-10 1186.9m ± 2% 149.8m ± 1% -87.38% (p=0.000 n=10) _UnwrapSeries-10 9.399m ± 1% 4.049m ± 0% -56.92% (p=0.000 n=10) _DecodeMergeEncodeCycle-10 666.0m ± 3% 194.6m ± 0% -70.79% (p=0.000 n=10) geomean 18.87m 12.51m -33.70% │ before.log │ after.log │ │ B/op │ B/op vs base │ _CodecDecodeLogs-10 3.649Mi ± 0% 3.649Mi ± 0% ~ (p=0.364 n=10) _CodecDecodeSamples-10 18.12Mi ± 0% 18.12Mi ± 0% ~ (p=0.926 n=10) _CodecDecodeSeries/application/vnd.google.protobuf-10 7.647Mi ± 0% 7.647Mi ± 0% ~ (p=0.587 n=10) _CodecDecodeSeries/application/json;_charset=utf-8-10 27.94Mi ± 0% 16.99Mi ± 0% -39.18% (p=0.000 n=10) _MergeResponses-10 2.362Mi ± 0% 2.408Mi ± 0% +1.98% (p=0.000 n=10) _UnwrapSeries-10 19.495Mi ± 0% 8.595Mi ± 0% -55.91% (p=0.000 n=10) _DecodeMergeEncodeCycle-10 772.3Mi ± 0% 772.3Mi ± 0% ~ (p=0.912 n=10) geomean 17.50Mi 14.54Mi -16.91% │ before.log │ after.log │ │ allocs/op │ allocs/op vs base │ _CodecDecodeLogs-10 41.10k ± 0% 41.10k ± 0% ~ (p=1.000 n=10) ¹ _CodecDecodeSamples-10 411.9k ± 0% 411.9k ± 0% ~ (p=1.000 n=10) _CodecDecodeSeries/application/vnd.google.protobuf-10 32.00 ± 0% 32.00 ± 0% ~ (p=1.000 n=10) ¹ _CodecDecodeSeries/application/json;_charset=utf-8-10 304.2k ± 0% 298.1k ± 0% -2.01% (p=0.000 n=10) _MergeResponses-10 100.1k ± 0% 100.1k ± 0% -0.00% (p=0.002 n=10) _UnwrapSeries-10 201.1k ± 0% 198.0k ± 0% -1.54% (p=0.000 n=10) _DecodeMergeEncodeCycle-10 203.1k ± 0% 203.1k ± 0% ~ (p=0.621 n=10) geomean 48.95k 48.70k -0.51% ¹ all samples are equal ``` **Checklist** - [x] Reviewed the [`CONTRIBUTING.md`](https://github.com/grafana/loki/blob/main/CONTRIBUTING.md) guide (**required**) - [ ] Documentation added - [x] Tests updated - [ ] `CHANGELOG.md` updated - [ ] If the change is worth mentioning in the release notes, add `add-to-release-notes` label - [ ] Changes that require user attention or interaction to upgrade are documented in `docs/sources/setup/upgrade/_index.md` - [ ] For Helm chart changes bump the Helm chart version in `production/helm/loki/Chart.yaml` and update `production/helm/loki/CHANGELOG.md` and `production/helm/loki/README.md`. [Example PR](https://github.com/grafana/loki/commit/d10549e3ece02120974929894ee333d07755d213) - [ ] If the change is deprecating or removing a configuration option, update the `deprecated-config.yaml` and `deleted-config.yaml` files respectively in the `tools/deprecated-config-checker` directory. [Example PR](https://github.com/grafana/loki/pull/10840/commits/0d4416a4b03739583349934b96f272fb4f685d15)
1 year ago
for i := range resp.GetSeries() {
s := &resp.Series[i]
if s.Get(defaultTenantLabel) == "" {
s.Labels = append(s.Labels, logproto.SeriesIdentifier_LabelsEntry{Key: defaultTenantLabel, Value: id})
}
}
responses[i] = resp
}
return logproto.MergeSeriesResponses(responses)
}
func (q *MultiTenantQuerier) IndexStats(ctx context.Context, req *loghttp.RangeQuery) (*stats.Stats, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.IndexStats(ctx, req)
}
responses := make([]*stats.Stats, len(tenantIDs))
for i, id := range tenantIDs {
singleContext := user.InjectOrgID(ctx, id)
resp, err := q.Querier.IndexStats(singleContext, req)
if err != nil {
return nil, err
}
responses[i] = resp
}
merged := stats.MergeStats(responses...)
return &merged, nil
}
func (q *MultiTenantQuerier) IndexShards(
ctx context.Context,
req *loghttp.RangeQuery,
targetBytesPerShard uint64,
) (*logproto.ShardsResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.IndexShards(ctx, req, targetBytesPerShard)
}
responses := make([]*logproto.ShardsResponse, len(tenantIDs))
for i, id := range tenantIDs {
singleContext := user.InjectOrgID(ctx, id)
resp, err := q.Querier.IndexShards(singleContext, req, targetBytesPerShard)
if err != nil {
return nil, err
}
responses[i] = resp
}
// TODO(owen-d): better merging
var highestIdx int
var highestVal int
for i, resp := range responses {
if len(resp.Shards) > highestVal {
highestIdx = i
highestVal = len(resp.Shards)
}
}
return responses[highestIdx], nil
}
func (q *MultiTenantQuerier) Volume(ctx context.Context, req *logproto.VolumeRequest) (*logproto.VolumeResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
responses := make([]*logproto.VolumeResponse, len(tenantIDs))
for i, id := range tenantIDs {
singleContext := user.InjectOrgID(ctx, id)
resp, err := q.Querier.Volume(singleContext, req)
if err != nil {
return nil, err
}
responses[i] = resp
}
merged := seriesvolume.Merge(responses, req.Limit)
return merged, nil
}
func (q *MultiTenantQuerier) DetectedFields(ctx context.Context, req *logproto.DetectedFieldsRequest) (*logproto.DetectedFieldsResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.DetectedFields(ctx, req)
}
level.Debug(q.logger).Log(
"msg", "detected fields requested for multiple tenants, but not yet supported",
"tenantIDs", strings.Join(tenantIDs, ","),
)
return &logproto.DetectedFieldsResponse{
Fields: []*logproto.DetectedField{},
Limit: req.GetLimit(),
}, nil
}
func (q *MultiTenantQuerier) DetectedLabels(ctx context.Context, req *logproto.DetectedLabelsRequest) (*logproto.DetectedLabelsResponse, error) {
tenantIDs, err := tenant.TenantIDs(ctx)
if err != nil {
return nil, err
}
if len(tenantIDs) == 1 {
return q.Querier.DetectedLabels(ctx, req)
}
level.Debug(q.logger).Log(
"msg", "detected labels requested for multiple tenants, but not yet supported. returning static labels",
"tenantIDs", strings.Join(tenantIDs, ","),
)
return &logproto.DetectedLabelsResponse{
DetectedLabels: []*logproto.DetectedLabel{
{Label: "multi_tenant_querier_not_implemented"},
},
}, nil
}
// removeTenantSelector filters the given tenant IDs based on any tenant ID filter the in passed selector.
func removeTenantSelector(params logql.SelectSampleParams, tenantIDs []string) (map[string]struct{}, syntax.Expr, error) {
expr, err := params.Expr()
if err != nil {
return nil, nil, err
}
selector, err := expr.Selector()
if err != nil {
return nil, nil, err
}
matchedTenants, filteredMatchers := filterValuesByMatchers(defaultTenantLabel, tenantIDs, selector.Matchers()...)
updatedExpr := replaceMatchers(expr, filteredMatchers)
return matchedTenants, updatedExpr, nil
}
// replaceMatchers traverses the passed expression and replaces all matchers.
func replaceMatchers(expr syntax.Expr, matchers []*labels.Matcher) syntax.Expr {
expr, _ = syntax.Clone(expr)
expr.Walk(func(e syntax.Expr) {
switch concrete := e.(type) {
case *syntax.MatchersExpr:
concrete.Mts = matchers
}
})
return expr
}
// See https://github.com/grafana/mimir/blob/114ab88b50638a2047e2ca2a60640f6ca6fe8c17/pkg/querier/tenantfederation/tenant_federation.go#L29-L69
// filterValuesByMatchers applies matchers to inputted `idLabelName` and
// `ids`. A set of matched IDs is returned and also all label matchers not
// targeting the `idLabelName` label.
//
// In case a label matcher is set on a label conflicting with `idLabelName`, we
// need to rename this labelMatcher's name to its original name. This is used
// to as part of Select in the mergeQueryable, to ensure only relevant queries
// are considered and the forwarded matchers do not contain matchers on the
// `idLabelName`.
func filterValuesByMatchers(idLabelName string, ids []string, matchers ...*labels.Matcher) (matchedIDs map[string]struct{}, unrelatedMatchers []*labels.Matcher) {
// this contains the matchers which are not related to idLabelName
unrelatedMatchers = make([]*labels.Matcher, 0, len(matchers))
// build map of values to consider for the matchers
matchedIDs = sliceToSet(ids)
for _, m := range matchers {
switch m.Name {
// matcher has idLabelName to target a specific tenant(s)
case idLabelName:
for value := range matchedIDs {
if !m.Matches(value) {
delete(matchedIDs, value)
}
}
// check if has the retained label name
case retainExistingPrefix + idLabelName:
// rewrite label to the original name, by copying matcher and
// replacing the label name
rewrittenM := *m
rewrittenM.Name = idLabelName
unrelatedMatchers = append(unrelatedMatchers, &rewrittenM)
default:
unrelatedMatchers = append(unrelatedMatchers, m)
}
}
return matchedIDs, unrelatedMatchers
}
func sliceToSet(values []string) map[string]struct{} {
out := make(map[string]struct{}, len(values))
for _, v := range values {
out[v] = struct{}{}
}
return out
}
type relabel struct {
tenantID string
cache map[string]labels.Labels
}
func (r relabel) relabel(original string) string {
lbls, ok := r.cache[original]
if ok {
return lbls.String()
}
lbls, _ = syntax.ParseLabels(original)
builder := labels.NewBuilder(lbls).Del(defaultTenantLabel)
// Prefix label if it conflicts with the tenant label.
if lbls.Has(defaultTenantLabel) {
builder.Set(retainExistingPrefix+defaultTenantLabel, lbls.Get(defaultTenantLabel))
}
builder.Set(defaultTenantLabel, r.tenantID)
lbls = builder.Labels()
r.cache[original] = lbls
return lbls.String()
}
// TenantEntry Iterator wraps an entry iterator and adds the tenant label.
type TenantEntryIterator struct {
iter.EntryIterator
relabel
}
func NewTenantEntryIterator(iter iter.EntryIterator, id string) *TenantEntryIterator {
return &TenantEntryIterator{
EntryIterator: iter,
relabel: relabel{
tenantID: id,
cache: map[string]labels.Labels{},
},
}
}
func (i *TenantEntryIterator) Labels() string {
return i.relabel.relabel(i.EntryIterator.Labels())
}
// TenantEntry Iterator wraps a sample iterator and adds the tenant label.
type TenantSampleIterator struct {
iter.SampleIterator
relabel
}
func NewTenantSampleIterator(iter iter.SampleIterator, id string) *TenantSampleIterator {
return &TenantSampleIterator{
SampleIterator: iter,
relabel: relabel{
tenantID: id,
cache: map[string]labels.Labels{},
},
}
}
func (i *TenantSampleIterator) Labels() string {
return i.relabel.relabel(i.SampleIterator.Labels())
}
func partitionChunkRefsByTenant(refs []*logproto.ChunkRef) map[string][]*logproto.ChunkRef {
filtered := make(map[string][]*logproto.ChunkRef)
for _, ref := range refs {
filtered[ref.UserID] = append(filtered[ref.UserID], ref)
}
return filtered
}