Files
traces/modules/ingester/instance_search.go
Carles GarciaandGitHub 25606a1054 frontend, distributor: Add tenant ID validation (#5786)
* frontend, distributor: Add tenant ID validation

Use the same validation than in Mimir for consistency.
In addition, reject empty IDs ("").

* Validate tenantID everywhere except in frontend handlers

We can't do it in frontend handlers because we need to wait for the multitenant middleware,
but we rely on the validation middleware.

Validating everywhere follows the same pattern as Mimir and ensures we don't miss any edge case.

* Check status codes

* Add test for user overrides API

* add comment
2025-11-11 14:51:26 +01:00

657 lines
21 KiB
Go

/*
* ingester is used as the template for livestore/instance_search.go any changes here should be reflected there and vice versa.
*/
package ingester
import (
"context"
"errors"
"fmt"
"sync"
"github.com/gogo/protobuf/proto"
"github.com/segmentio/fasthash/fnv1a"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/atomic"
"github.com/go-kit/log/level"
"github.com/grafana/tempo/pkg/api"
"github.com/grafana/tempo/pkg/boundedwaitgroup"
"github.com/grafana/tempo/pkg/collector"
"github.com/grafana/tempo/pkg/tempopb"
"github.com/grafana/tempo/pkg/traceql"
"github.com/grafana/tempo/pkg/util"
"github.com/grafana/tempo/pkg/util/log"
"github.com/grafana/tempo/pkg/validation"
"github.com/grafana/tempo/tempodb/backend"
"github.com/grafana/tempo/tempodb/encoding/common"
)
func (i *instance) Search(ctx context.Context, req *tempopb.SearchRequest) (*tempopb.SearchResponse, error) {
ctx, span := tracer.Start(ctx, "instance.Search")
defer span.End()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
maxResults := int(req.Limit)
// if limit is not set, use a safe default
if maxResults == 0 {
maxResults = 20
}
span.AddEvent("SearchRequest", trace.WithAttributes(attribute.String("request", req.String())))
mostRecent := false
if len(req.Query) > 0 {
rootExpr, err := traceql.Parse(req.Query)
if err != nil {
return nil, fmt.Errorf("error parsing query: %w", err)
}
ok := false
if mostRecent, ok = rootExpr.Hints.GetBool(traceql.HintMostRecent, false); !ok {
mostRecent = false
}
}
var (
resultsMtx = sync.Mutex{}
combiner = traceql.NewMetadataCombiner(maxResults, mostRecent)
metrics = &tempopb.SearchMetrics{}
opts = common.DefaultSearchOptions()
anyErr atomic.Error
)
search := func(blockMeta *backend.BlockMeta, block common.Searcher, spanName string) {
ctx, span := tracer.Start(ctx, "instance.searchBlock."+spanName)
defer span.End()
span.AddEvent("block entry mtx acquired")
span.SetAttributes(attribute.String("blockID", blockMeta.BlockID.String()))
var resp *tempopb.SearchResponse
var err error
// if the combiner is complete for the block's end time, we can skip searching it
if combiner.IsCompleteFor(uint32(blockMeta.EndTime.Unix())) {
return
}
if api.IsTraceQLQuery(req) {
// note: we are creating new engine for each wal block,
// and engine.ExecuteSearch is parsing the query for each block
resp, err = traceql.NewEngine().ExecuteSearch(ctx, req, traceql.NewSpansetFetcherWrapper(func(ctx context.Context, req traceql.FetchSpansRequest) (traceql.FetchSpansResponse, error) {
return block.Fetch(ctx, req, opts)
}), i.overrides.UnsafeQueryHints(i.instanceID))
} else {
resp, err = block.Search(ctx, req, opts)
}
if errors.Is(err, util.ErrUnsupported) {
level.Warn(log.Logger).Log("msg", "block does not support search", "blockID", blockMeta.BlockID)
return
}
if errors.Is(err, context.Canceled) {
// Ignore
return
}
if err != nil {
level.Error(log.Logger).Log("msg", "error searching block", "blockID", blockMeta.BlockID, "err", err)
anyErr.Store(err)
return
}
if resp == nil {
return
}
resultsMtx.Lock()
defer resultsMtx.Unlock()
if resp.Metrics != nil {
metrics.InspectedTraces += resp.Metrics.InspectedTraces
metrics.InspectedBytes += resp.Metrics.InspectedBytes
}
for _, tr := range resp.Traces {
combiner.AddMetadata(tr)
if combiner.IsCompleteFor(traceql.TimestampNever) {
cancel()
return
}
}
}
// Search headblock (synchronously)
// Lock headblock separately from other blocks and release it as quickly as possible.
// A warning about deadlocks!! This area does a hard-acquire of both mutexes.
// To avoid deadlocks this function and all others must acquire them in
// the ** same_order ** or else!!! i.e. another function can't acquire blocksMtx
// then headblockMtx. Even if the likelihood is low it is a statistical certainly
// that eventually a deadlock will occur.
i.headBlockMtx.RLock()
span.AddEvent("acquired headblock mtx")
if includeBlock(i.headBlock.BlockMeta(), req) {
search(i.headBlock.BlockMeta(), i.headBlock, "headBlock")
}
i.headBlockMtx.RUnlock()
if err := anyErr.Load(); err != nil {
return nil, err
}
// Search all other blocks (concurrently)
// Lock blocks mutex until all search tasks are finished and this function exits. This avoids
// deadlocking with other activity (ingest, flushing), caused by releasing
// and then attempting to retake the lock.
i.blocksMtx.RLock()
defer i.blocksMtx.RUnlock()
span.AddEvent("acquired blocks mtx")
wg := sync.WaitGroup{}
for _, b := range i.completingBlocks {
if !includeBlock(b.BlockMeta(), req) {
continue
}
wg.Add(1)
go func(b common.WALBlock) {
defer wg.Done()
search(b.BlockMeta(), b, "completingBlock")
}(b)
}
for _, b := range i.completeBlocks {
if !includeBlock(b.BlockMeta(), req) {
continue
}
wg.Add(1)
go func(b *LocalBlock) {
defer wg.Done()
search(b.BlockMeta(), b, "completeBlock")
}(b)
}
wg.Wait()
if err := anyErr.Load(); err != nil {
return nil, err
}
return &tempopb.SearchResponse{
Traces: combiner.Metadata(),
Metrics: metrics,
}, nil
}
func (i *instance) SearchTags(ctx context.Context, scope string) (*tempopb.SearchTagsResponse, error) {
v2Response, err := i.SearchTagsV2(ctx, &tempopb.SearchTagsRequest{Scope: scope})
if err != nil {
return nil, err
}
distinctValues := collector.NewDistinctString(0, 0, 0) // search tags v2 enforces the limit
// flatten v2 response
for _, s := range v2Response.Scopes {
for _, t := range s.Tags {
distinctValues.Collect(t)
}
}
return &tempopb.SearchTagsResponse{
TagNames: distinctValues.Strings(),
Metrics: v2Response.Metrics, // send metrics with response
}, nil
}
// SearchTagsV2 calls SearchTags for each scope and returns the results.
func (i *instance) SearchTagsV2(ctx context.Context, req *tempopb.SearchTagsRequest) (*tempopb.SearchTagsV2Response, error) {
ctx, span := tracer.Start(ctx, "instance.SearchTagsV2")
defer span.End()
userID, err := validation.ExtractValidTenantID(ctx)
if err != nil {
return nil, err
}
scope := req.Scope
if scope == api.ParamScopeIntrinsic {
// For the intrinsic scope there is nothing to do in the ingester,
// these are always added by the frontend.
return &tempopb.SearchTagsV2Response{}, nil
}
// parse for normal scopes
attributeScope := traceql.AttributeScopeFromString(scope)
if attributeScope == traceql.AttributeScopeUnknown {
return nil, fmt.Errorf("unknown scope: %s", scope)
}
maxBytestPerTags := i.limiter.Limits().MaxBytesPerTagValuesQuery(userID)
distinctValues := collector.NewScopedDistinctString(maxBytestPerTags, req.MaxTagsPerScope, req.StaleValuesThreshold)
mc := collector.NewMetricsCollector()
engine := traceql.NewEngine()
query := traceql.ExtractMatchers(req.Query)
searchBlock := func(ctx context.Context, s common.Searcher, spanName string) error {
ctx, span := tracer.Start(ctx, "instance.SearchTagsV2."+spanName)
defer span.End()
if s == nil {
return nil
}
if distinctValues.Exceeded() {
return nil
}
// if the query is empty, use the old search
if traceql.IsEmptyQuery(query) {
err = s.SearchTags(ctx, attributeScope, func(t string, scope traceql.AttributeScope) {
distinctValues.Collect(scope.String(), t)
}, mc.Add, common.DefaultSearchOptions())
if err != nil && !errors.Is(err, util.ErrUnsupported) {
return fmt.Errorf("unexpected error searching tags: %w", err)
}
return nil
}
// otherwise use the filtered search
fetcher := traceql.NewTagNamesFetcherWrapper(func(ctx context.Context, req traceql.FetchTagsRequest, cb traceql.FetchTagsCallback) error {
return s.FetchTagNames(ctx, req, cb, mc.Add, common.DefaultSearchOptions())
})
return engine.ExecuteTagNames(ctx, attributeScope, query, func(tag string, scope traceql.AttributeScope) bool {
return distinctValues.Collect(scope.String(), tag)
}, fetcher)
}
func() { // anon function to defer unlock right after search
i.headBlockMtx.RLock()
defer i.headBlockMtx.RUnlock()
span.AddEvent("acquired headblock mtx")
err = searchBlock(ctx, i.headBlock, "headBlock")
}()
if err != nil {
return nil, fmt.Errorf("unexpected error searching head block (%s): %w", i.headBlock.BlockMeta().BlockID, err)
}
i.blocksMtx.RLock()
defer i.blocksMtx.RUnlock()
span.AddEvent("acquired blocks mtx")
for _, b := range i.completingBlocks {
if err = searchBlock(ctx, b, "completingBlock"); err != nil {
return nil, fmt.Errorf("unexpected error searching completing block (%s): %w", b.BlockMeta().BlockID, err)
}
}
for _, b := range i.completeBlocks {
if err = searchBlock(ctx, b, "completeBlock"); err != nil {
return nil, fmt.Errorf("unexpected error searching complete block (%s): %w", b.BlockMeta().BlockID, err)
}
}
if distinctValues.Exceeded() {
level.Warn(log.Logger).Log("msg", "Search of tags exceeded limit, reduce cardinality or size of tags", "orgID", userID, "stopReason", distinctValues.StopReason())
}
collected := distinctValues.Strings()
resp := &tempopb.SearchTagsV2Response{
Scopes: make([]*tempopb.SearchTagsV2Scope, 0, len(collected)+1), // +1 for intrinsic below
Metrics: &tempopb.MetadataMetrics{
InspectedBytes: mc.TotalValue(), // capture metrics
},
}
for scope, vals := range collected {
resp.Scopes = append(resp.Scopes, &tempopb.SearchTagsV2Scope{
Name: scope,
Tags: vals,
})
}
return resp, nil
}
func (i *instance) SearchTagValues(ctx context.Context, tagName string, limit uint32, staleValueThreshold uint32) (*tempopb.SearchTagValuesResponse, error) {
userID, err := validation.ExtractValidTenantID(ctx)
if err != nil {
return nil, err
}
maxBytesPerTagValues := i.limiter.Limits().MaxBytesPerTagValuesQuery(userID)
distinctValues := collector.NewDistinctString(maxBytesPerTagValues, limit, staleValueThreshold)
mc := collector.NewMetricsCollector()
var inspectedBlocks, maxBlocks int
if limit := i.limiter.Limits().MaxBlocksPerTagValuesQuery(userID); limit > 0 {
maxBlocks = limit
}
search := func(s common.Searcher, dv *collector.DistinctString) error {
if maxBlocks > 0 && inspectedBlocks >= maxBlocks {
return nil
}
if s == nil {
return nil
}
if dv.Exceeded() {
return nil
}
inspectedBlocks++
err = s.SearchTagValues(ctx, tagName, dv.Collect, mc.Add, common.DefaultSearchOptions())
if err != nil && !errors.Is(err, util.ErrUnsupported) {
return fmt.Errorf("unexpected error searching tag values (%s): %w", tagName, err)
}
return nil
}
i.headBlockMtx.RLock()
err = search(i.headBlock, distinctValues)
i.headBlockMtx.RUnlock()
if err != nil {
return nil, fmt.Errorf("unexpected error searching head block (%s): %w", i.headBlock.BlockMeta().BlockID, err)
}
i.blocksMtx.RLock()
defer i.blocksMtx.RUnlock()
for _, b := range i.completingBlocks {
if err = search(b, distinctValues); err != nil {
return nil, fmt.Errorf("unexpected error searching completing block (%s): %w", b.BlockMeta().BlockID, err)
}
}
for _, b := range i.completeBlocks {
if err = search(b, distinctValues); err != nil {
return nil, fmt.Errorf("unexpected error searching complete block (%s): %w", b.BlockMeta().BlockID, err)
}
}
if distinctValues.Exceeded() {
level.Warn(log.Logger).Log("msg", "Search of tags exceeded limit, reduce cardinality or size of tags", "tag", tagName, "orgID", userID, "stopReason", distinctValues.StopReason())
}
return &tempopb.SearchTagValuesResponse{
TagValues: distinctValues.Strings(),
Metrics: &tempopb.MetadataMetrics{InspectedBytes: mc.TotalValue()},
}, nil
}
func (i *instance) SearchTagValuesV2(ctx context.Context, req *tempopb.SearchTagValuesRequest) (*tempopb.SearchTagValuesV2Response, error) {
userID, err := validation.ExtractValidTenantID(ctx)
if err != nil {
return nil, err
}
ctx, span := tracer.Start(ctx, "instance.SearchTagValuesV2")
defer span.End()
limit := i.limiter.Limits().MaxBytesPerTagValuesQuery(userID)
valueCollector := collector.NewDistinctValue(limit, req.MaxTagValues, req.StaleValueThreshold, func(v tempopb.TagValue) int { return len(v.Type) + len(v.Value) })
mc := collector.NewMetricsCollector() // to collect bytesRead metric
engine := traceql.NewEngine()
// we usually have 5-10 blocks on an ingester so cap of 20 is more than enough and usually more than the blocks
// we need to search, and this also acts as the limit on the amount of search load on the ingester.
wg := boundedwaitgroup.New(20)
var anyErr atomic.Error
var inspectedBlocks atomic.Int32
var maxBlocks int32
if limit := i.limiter.Limits().MaxBlocksPerTagValuesQuery(userID); limit > 0 {
maxBlocks = int32(limit)
}
tag, err := traceql.ParseIdentifier(req.TagName)
if err != nil {
return nil, err
}
if tag == traceql.IntrinsicLinkTraceIDAttribute ||
tag == traceql.IntrinsicLinkSpanIDAttribute ||
tag == traceql.IntrinsicSpanIDAttribute ||
tag == traceql.IntrinsicTraceIDAttribute ||
tag == traceql.IntrinsicParentIDAttribute {
// do not return tag values for IDs
return &tempopb.SearchTagValuesV2Response{}, nil
}
query := traceql.ExtractMatchers(req.Query)
// cacheKey will be same for all blocks in a request so only compute it once
// NOTE: cacheKey tag name and query, so if we start respecting start and end, add them to the cacheKey
cacheKey := searchTagValuesV2CacheKey(req, limit, "cache_search_tagvaluesv2")
// helper functions as closures, to access local variables
performSearch := func(ctx context.Context, s common.Searcher, collector *collector.DistinctValue[tempopb.TagValue]) error {
if traceql.IsEmptyQuery(query) {
return s.SearchTagValuesV2(ctx, tag, traceql.MakeCollectTagValueFunc(collector.Collect), mc.Add, common.DefaultSearchOptions())
}
// Otherwise, use the filtered search
fetcher := traceql.NewTagValuesFetcherWrapper(func(ctx context.Context, req traceql.FetchTagValuesRequest, cb traceql.FetchTagValuesCallback) error {
return s.FetchTagValues(ctx, req, cb, mc.Add, common.DefaultSearchOptions())
})
return engine.ExecuteTagValues(ctx, tag, query, traceql.MakeCollectTagValueFunc(collector.Collect), fetcher)
}
exitEarly := func() bool {
if anyErr.Load() != nil {
return true // Early exit if any error has occurred
}
if maxBlocks > 0 && inspectedBlocks.Inc() > maxBlocks {
return true
}
return false // Continue searching
}
searchBlock := func(ctx context.Context, s common.Searcher, spanName string) error {
ctx, span := tracer.Start(ctx, "instance.SearchTagValuesV2."+spanName)
defer span.End()
if exitEarly() {
return nil
}
return performSearch(ctx, s, valueCollector)
}
searchBlockWithCache := func(ctx context.Context, b *LocalBlock, spanName string) error {
ctx, span := tracer.Start(ctx, "instance.SearchTagValuesV2."+spanName)
defer span.End()
if exitEarly() {
return nil
}
// check the cache first
cacheData, err := b.GetDiskCache(ctx, cacheKey)
if err != nil {
// just log the error and move on...we will search the block
_ = level.Warn(log.Logger).Log("msg", "GetDiskCache failed", "err", err)
}
// we got data...unmarshall, and add values to central collector and add bytesRead
if len(cacheData) > 0 && err == nil {
resp := &tempopb.SearchTagValuesV2Response{}
err = proto.Unmarshal(cacheData, resp)
if err != nil {
return err
}
span.SetAttributes(attribute.Bool("cached", true))
// Instead of the reporting the InspectedBytes of the cached response.
// we report the size of cacheData as the Inspected bytes in case we hit disk cache.
// we do this because, because it's incorrect and misleading to report the metrics of cachedResponse
// we report the size of the cacheData as the amount of data was read to search this block.
// this can skew our metrics because this will be lower than the data read to search the block.
// we can remove this if this becomes an issue but leave it in for now to more accurate.
mc.Add(uint64(len(cacheData)))
for _, v := range resp.TagValues {
if valueCollector.Collect(*v) {
break // we have reached the limit, so stop
}
}
return nil
}
// cache miss, search the block. We will cache the results if we find any.
span.SetAttributes(attribute.Bool("cached", false))
// using local collector to collect values from the block and cache them.
localCol := collector.NewDistinctValue[tempopb.TagValue](limit, req.MaxTagValues, req.StaleValueThreshold, func(v tempopb.TagValue) int { return len(v.Type) + len(v.Value) })
localErr := performSearch(ctx, b, localCol)
if localErr != nil {
return localErr
}
// marshal the values local collector and set the cache
values := localCol.Values()
v2RespProto, err := valuesToTagValuesV2RespProto(values)
if err == nil && len(v2RespProto) > 0 {
err2 := b.SetDiskCache(ctx, cacheKey, v2RespProto)
if err2 != nil {
_ = level.Warn(log.Logger).Log("msg", "SetDiskCache failed", "err", err2)
}
}
// now add values to the central collector to make sure they are included in the response.
for _, v := range values {
if valueCollector.Collect(v) {
break // we have reached the limit, so stop
}
}
return nil
}
// head block
// A warning about deadlocks!! This area does a hard-acquire of both mutexes.
// To avoid deadlocks this function and all others must acquire them in
// the ** same_order ** or else!!! i.e. another function can't acquire blocksMtx
// then headblockMtx. Even if the likelihood is low it is a statistical certainly
// that eventually a deadlock will occur.
i.headBlockMtx.RLock()
span.AddEvent("acquired headblock mtx")
if i.headBlock != nil {
wg.Add(1)
go func() {
defer i.headBlockMtx.RUnlock()
defer wg.Done()
if err := searchBlock(ctx, i.headBlock, "headBlock"); err != nil {
anyErr.Store(fmt.Errorf("unexpected error searching head block (%s): %w", i.headBlock.BlockMeta().BlockID, err))
}
}()
} else {
i.headBlockMtx.RUnlock()
}
i.blocksMtx.RLock()
defer i.blocksMtx.RUnlock()
span.AddEvent("acquired blocks mtx")
// completed blocks
for _, b := range i.completeBlocks {
wg.Add(1)
go func(b *LocalBlock) {
defer wg.Done()
if err := searchBlockWithCache(ctx, b, "completeBlocks"); err != nil {
anyErr.Store(fmt.Errorf("unexpected error searching complete block (%s): %w", b.BlockMeta().BlockID, err))
}
}(b)
}
// completing blocks
for _, b := range i.completingBlocks {
wg.Add(1)
go func(b common.WALBlock) {
defer wg.Done()
if err := searchBlock(ctx, b, "completingBlocks"); err != nil {
anyErr.Store(fmt.Errorf("unexpected error searching completing block (%s): %w", b.BlockMeta().BlockID, err))
}
}(b)
}
wg.Wait()
if err := anyErr.Load(); err != nil {
return nil, err
}
if valueCollector.Exceeded() {
_ = level.Warn(log.Logger).Log("msg", "size of tag values exceeded limit, reduce cardinality or size of tags", "tag", req.TagName, "tenant", userID, "limit", limit, "size", valueCollector.Size())
}
resp := &tempopb.SearchTagValuesV2Response{
Metrics: &tempopb.MetadataMetrics{InspectedBytes: mc.TotalValue()}, // include metrics in response
}
for _, v := range valueCollector.Values() {
v2 := v
resp.TagValues = append(resp.TagValues, &v2)
}
return resp, nil
}
// includeBlock uses the provided time range to determine if the block should be included in the search.
func includeBlock(b *backend.BlockMeta, req *tempopb.SearchRequest) bool {
start := int64(req.Start)
end := int64(req.End)
if start == 0 || end == 0 {
return true
}
return b.StartTime.Unix() <= end && b.EndTime.Unix() >= start
}
// searchTagValuesV2CacheKey generates a cache key for the searchTagValuesV2 request
// cache key is used as the filename to store the protobuf data on disk
func searchTagValuesV2CacheKey(req *tempopb.SearchTagValuesRequest, limit int, prefix string) string {
var cacheKey string
if req.Query != "" {
q := traceql.ExtractMatchers(req.Query)
if ast, err := traceql.Parse(q); err == nil {
// forces the query into a canonical form
cacheKey = ast.String()
} else {
// In case of a bad TraceQL query, we ignore the query and return unfiltered results.
// if we fail to parse the query, we will assume query is empty and compute the cache key.
cacheKey = ""
}
}
// NOTE: we are not adding req.Start and req.End to the cache key because we don't respect the start and end
// please add them to cacheKey if we start respecting them
h := fnv1a.HashString64(req.TagName)
h = fnv1a.AddString64(h, cacheKey)
h = fnv1a.AddUint64(h, uint64(limit))
return fmt.Sprintf("%s_%v.buf", prefix, h)
}
// valuesToTagValuesV2RespProto converts TagValues to a protobuf marshalled bytes
// this is slightly modified version of valuesToV2Response from querier.go
func valuesToTagValuesV2RespProto(tagValues []tempopb.TagValue) ([]byte, error) {
// NOTE: we only cache TagValues and don't Marshal Metrics
resp := &tempopb.SearchTagValuesV2Response{}
resp.TagValues = make([]*tempopb.TagValue, 0, len(tagValues))
for _, v := range tagValues {
v2 := &v
resp.TagValues = append(resp.TagValues, v2)
}
data, err := proto.Marshal(resp)
if err != nil {
return nil, err
}
return data, nil
}