github.com/grafana/pyroscope@v1.18.0/pkg/querier/querier.go (about)

     1  package querier
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"fmt"
     7  	"math"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"connectrpc.com/connect"
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/grafana/dskit/ring"
    17  	ring_client "github.com/grafana/dskit/ring/client"
    18  	"github.com/grafana/dskit/services"
    19  	"github.com/grafana/dskit/tenant"
    20  	"github.com/opentracing/opentracing-go"
    21  	otlog "github.com/opentracing/opentracing-go/log"
    22  	"github.com/pkg/errors"
    23  	"github.com/prometheus/client_golang/prometheus"
    24  	"github.com/prometheus/client_golang/prometheus/promauto"
    25  	"github.com/prometheus/common/model"
    26  	"github.com/prometheus/prometheus/promql/parser"
    27  	"github.com/samber/lo"
    28  	"golang.org/x/sync/errgroup"
    29  
    30  	"github.com/grafana/pyroscope/pkg/featureflags"
    31  
    32  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    33  	ingestv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1"
    34  	querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
    35  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    36  	connectapi "github.com/grafana/pyroscope/pkg/api/connect"
    37  	"github.com/grafana/pyroscope/pkg/clientpool"
    38  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    39  	phlareobj "github.com/grafana/pyroscope/pkg/objstore"
    40  	"github.com/grafana/pyroscope/pkg/phlaredb/bucketindex"
    41  	"github.com/grafana/pyroscope/pkg/pprof"
    42  	"github.com/grafana/pyroscope/pkg/storegateway"
    43  	"github.com/grafana/pyroscope/pkg/util/spanlogger"
    44  	"github.com/grafana/pyroscope/pkg/validation"
    45  )
    46  
    47  type Config struct {
    48  	PoolConfig      clientpool.PoolConfig `yaml:"pool_config,omitempty"`
    49  	QueryStoreAfter time.Duration         `yaml:"query_store_after" category:"advanced"`
    50  }
    51  
    52  // RegisterFlags registers distributor-related flags.
    53  func (cfg *Config) RegisterFlags(fs *flag.FlagSet) {
    54  	cfg.PoolConfig.RegisterFlagsWithPrefix("querier", fs)
    55  	fs.DurationVar(&cfg.QueryStoreAfter, "querier.query-store-after", 4*time.Hour, "The time after which a metric should be queried from storage and not just ingesters. 0 means all queries are sent to store. If this option is enabled, the time range of the query sent to the store-gateway will be manipulated to ensure the query end is not more recent than 'now - query-store-after'.")
    56  }
    57  
    58  type Limits interface {
    59  	QueryAnalysisSeriesEnabled(string) bool
    60  }
    61  
    62  type Querier struct {
    63  	services.Service
    64  	subservices        *services.Manager
    65  	subservicesWatcher *services.FailureWatcher
    66  
    67  	cfg    Config
    68  	logger log.Logger
    69  
    70  	ingesterQuerier     *IngesterQuerier
    71  	storeGatewayQuerier *StoreGatewayQuerier
    72  
    73  	storageBucket        phlareobj.Bucket
    74  	tenantConfigProvider phlareobj.TenantConfigProvider
    75  
    76  	limits Limits
    77  }
    78  
    79  // TODO(kolesnikovae): For backwards compatibility.
    80  // Should be removed in the next release.
    81  //
    82  // The default value should never be used in practice:
    83  // querier frontend sets the limit.
    84  const maxNodesDefault = int64(2048)
    85  
    86  type NewQuerierParams struct {
    87  	Cfg             Config
    88  	StoreGatewayCfg storegateway.Config
    89  	Overrides       *validation.Overrides
    90  	StorageBucket   phlareobj.Bucket
    91  	CfgProvider     phlareobj.TenantConfigProvider
    92  	IngestersRing   ring.ReadRing
    93  	PoolFactory     ring_client.PoolFactory
    94  	Reg             prometheus.Registerer
    95  	Logger          log.Logger
    96  	ClientOptions   []connect.ClientOption
    97  }
    98  
    99  func New(params *NewQuerierParams) (*Querier, error) {
   100  	params.ClientOptions = append(connectapi.DefaultClientOptions(), params.ClientOptions...)
   101  
   102  	// disable gzip compression for querier-ingester communication as most of payload are not benefit from it.
   103  	clientsMetrics := promauto.With(params.Reg).NewGauge(prometheus.GaugeOpts{
   104  		Namespace: "pyroscope",
   105  		Name:      "querier_ingester_clients",
   106  		Help:      "The current number of ingester clients.",
   107  	})
   108  
   109  	// if a storage bucket is configured we need to create a store gateway querier
   110  	var storeGatewayQuerier *StoreGatewayQuerier
   111  	var err error
   112  	if params.StorageBucket != nil {
   113  		storeGatewayQuerier, err = newStoreGatewayQuerier(
   114  			params.StoreGatewayCfg,
   115  			params.PoolFactory,
   116  			params.Overrides,
   117  			log.With(params.Logger, "component", "store-gateway-querier"),
   118  			params.Reg,
   119  			params.ClientOptions...)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  	}
   124  
   125  	q := &Querier{
   126  		cfg:    params.Cfg,
   127  		logger: params.Logger,
   128  		ingesterQuerier: NewIngesterQuerier(
   129  			clientpool.NewIngesterPool(params.Cfg.PoolConfig, params.IngestersRing, params.PoolFactory, clientsMetrics, params.Logger, params.ClientOptions...),
   130  			params.IngestersRing,
   131  		),
   132  		storeGatewayQuerier:  storeGatewayQuerier,
   133  		storageBucket:        params.StorageBucket,
   134  		tenantConfigProvider: params.CfgProvider,
   135  		limits:               params.Overrides,
   136  	}
   137  
   138  	svcs := []services.Service{q.ingesterQuerier.pool}
   139  	if storeGatewayQuerier != nil {
   140  		svcs = append(svcs, storeGatewayQuerier)
   141  	}
   142  	// should we watch for the ring module status ?
   143  	q.subservices, err = services.NewManager(svcs...)
   144  	if err != nil {
   145  		return nil, errors.Wrap(err, "services manager")
   146  	}
   147  	q.subservicesWatcher = services.NewFailureWatcher()
   148  	q.subservicesWatcher.WatchManager(q.subservices)
   149  	q.Service = services.NewBasicService(q.starting, q.running, q.stopping)
   150  	return q, nil
   151  }
   152  
   153  func (q *Querier) starting(ctx context.Context) error {
   154  	return services.StartManagerAndAwaitHealthy(ctx, q.subservices)
   155  }
   156  
   157  func (q *Querier) running(ctx context.Context) error {
   158  	select {
   159  	case <-ctx.Done():
   160  		return nil
   161  	case err := <-q.subservicesWatcher.Chan():
   162  		return errors.Wrap(err, "querier subservice failed")
   163  	}
   164  }
   165  
   166  func (q *Querier) stopping(_ error) error {
   167  	return services.StopManagerAndAwaitStopped(context.Background(), q.subservices)
   168  }
   169  
   170  func (q *Querier) ProfileTypes(ctx context.Context, req *connect.Request[querierv1.ProfileTypesRequest]) (*connect.Response[querierv1.ProfileTypesResponse], error) {
   171  	sp, ctx := opentracing.StartSpanFromContext(ctx, "ProfileTypes")
   172  	defer sp.Finish()
   173  
   174  	lblReq := connect.NewRequest(&typesv1.LabelValuesRequest{
   175  		Start:    req.Msg.Start,
   176  		End:      req.Msg.End,
   177  		Matchers: []string{"{}"},
   178  		Name:     phlaremodel.LabelNameProfileType,
   179  	})
   180  
   181  	lblRes, err := q.LabelValues(ctx, lblReq)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	var profileTypes []*typesv1.ProfileType
   187  
   188  	for _, profileTypeStr := range lblRes.Msg.Names {
   189  		profileType, err := phlaremodel.ParseProfileTypeSelector(profileTypeStr)
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		profileTypes = append(profileTypes, profileType)
   194  	}
   195  
   196  	sort.Slice(profileTypes, func(i, j int) bool {
   197  		return profileTypes[i].ID < profileTypes[j].ID
   198  	})
   199  
   200  	return connect.NewResponse(&querierv1.ProfileTypesResponse{
   201  		ProfileTypes: profileTypes,
   202  	}), nil
   203  }
   204  
   205  func (q *Querier) LabelValues(ctx context.Context, req *connect.Request[typesv1.LabelValuesRequest]) (*connect.Response[typesv1.LabelValuesResponse], error) {
   206  	sp, ctx := opentracing.StartSpanFromContext(ctx, "LabelValues")
   207  	defer sp.Finish()
   208  
   209  	_, hasTimeRange := phlaremodel.GetTimeRange(req.Msg)
   210  	sp.LogFields(
   211  		otlog.Bool("legacy_request", !hasTimeRange),
   212  		otlog.String("name", req.Msg.Name),
   213  		otlog.String("matchers", strings.Join(req.Msg.Matchers, ",")),
   214  		otlog.Int64("start", req.Msg.Start),
   215  		otlog.Int64("end", req.Msg.End),
   216  	)
   217  
   218  	if req.Msg.Name == "" {
   219  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("name is required"))
   220  	}
   221  
   222  	if q.storeGatewayQuerier == nil || !hasTimeRange {
   223  		responses, err := q.labelValuesFromIngesters(ctx, req.Msg)
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		return connect.NewResponse(&typesv1.LabelValuesResponse{
   228  			Names: uniqueSortedStrings(responses),
   229  		}), nil
   230  	}
   231  
   232  	storeQueries := splitQueryToStores(model.Time(req.Msg.Start), model.Time(req.Msg.End), model.Now(), q.cfg.QueryStoreAfter, nil)
   233  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
   234  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
   235  	}
   236  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
   237  
   238  	var responses []ResponseFromReplica[[]string]
   239  	var lock sync.Mutex
   240  	group, gCtx := errgroup.WithContext(ctx)
   241  
   242  	if storeQueries.ingester.shouldQuery {
   243  		group.Go(func() error {
   244  			ir, err := q.labelValuesFromIngesters(gCtx, storeQueries.ingester.LabelValuesRequest(req.Msg))
   245  			if err != nil {
   246  				return err
   247  			}
   248  
   249  			lock.Lock()
   250  			responses = append(responses, ir...)
   251  			lock.Unlock()
   252  			return nil
   253  		})
   254  	}
   255  
   256  	if storeQueries.storeGateway.shouldQuery {
   257  		group.Go(func() error {
   258  			ir, err := q.labelValuesFromStoreGateway(gCtx, storeQueries.storeGateway.LabelValuesRequest(req.Msg))
   259  			if err != nil {
   260  				return err
   261  			}
   262  
   263  			lock.Lock()
   264  			responses = append(responses, ir...)
   265  			lock.Unlock()
   266  			return nil
   267  		})
   268  	}
   269  
   270  	err := group.Wait()
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	return connect.NewResponse(&typesv1.LabelValuesResponse{
   276  		Names: uniqueSortedStrings(responses),
   277  	}), nil
   278  }
   279  
   280  func filterLabelNames(labelNames []string) []string {
   281  	filtered := make([]string, 0, len(labelNames))
   282  	// Filter out label names not passing legacy validation if utf8 label names not enabled
   283  	for _, labelName := range labelNames {
   284  		if _, _, ok := validation.SanitizeLegacyLabelName(labelName); !ok {
   285  			continue
   286  		}
   287  		filtered = append(filtered, labelName)
   288  	}
   289  	return filtered
   290  }
   291  
   292  func (q *Querier) LabelNames(ctx context.Context, req *connect.Request[typesv1.LabelNamesRequest]) (*connect.Response[typesv1.LabelNamesResponse], error) {
   293  	sp, ctx := opentracing.StartSpanFromContext(ctx, "LabelNames")
   294  	defer sp.Finish()
   295  
   296  	_, hasTimeRange := phlaremodel.GetTimeRange(req.Msg)
   297  	sp.LogFields(
   298  		otlog.Bool("legacy_request", !hasTimeRange),
   299  		otlog.String("matchers", strings.Join(req.Msg.Matchers, ",")),
   300  		otlog.Int64("start", req.Msg.Start),
   301  		otlog.Int64("end", req.Msg.End),
   302  	)
   303  
   304  	if q.storeGatewayQuerier == nil || !hasTimeRange {
   305  		responses, err := q.labelNamesFromIngesters(ctx, req.Msg)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  
   310  		labelNames := uniqueSortedStrings(responses)
   311  		if capabilities, ok := featureflags.GetClientCapabilities(ctx); !ok || !capabilities.AllowUtf8LabelNames {
   312  			level.Debug(q.logger).Log("msg", "filtering out non-valid labels")
   313  			labelNames = filterLabelNames(labelNames)
   314  		}
   315  
   316  		return connect.NewResponse(&typesv1.LabelNamesResponse{
   317  			Names: labelNames,
   318  		}), nil
   319  	}
   320  
   321  	storeQueries := splitQueryToStores(model.Time(req.Msg.Start), model.Time(req.Msg.End), model.Now(), q.cfg.QueryStoreAfter, nil)
   322  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
   323  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
   324  	}
   325  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
   326  
   327  	var responses []ResponseFromReplica[[]string]
   328  	var lock sync.Mutex
   329  	group, gCtx := errgroup.WithContext(ctx)
   330  
   331  	if storeQueries.ingester.shouldQuery {
   332  		group.Go(func() error {
   333  			ir, err := q.labelNamesFromIngesters(gCtx, storeQueries.ingester.LabelNamesRequest(req.Msg))
   334  			if err != nil {
   335  				return err
   336  			}
   337  
   338  			lock.Lock()
   339  			responses = append(responses, ir...)
   340  			lock.Unlock()
   341  			return nil
   342  		})
   343  	}
   344  
   345  	if storeQueries.storeGateway.shouldQuery {
   346  		group.Go(func() error {
   347  			ir, err := q.labelNamesFromStoreGateway(gCtx, storeQueries.storeGateway.LabelNamesRequest(req.Msg))
   348  			if err != nil {
   349  				return err
   350  			}
   351  
   352  			lock.Lock()
   353  			responses = append(responses, ir...)
   354  			lock.Unlock()
   355  			return nil
   356  		})
   357  	}
   358  
   359  	err := group.Wait()
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	labelNames := uniqueSortedStrings(responses)
   365  	if capabilities, ok := featureflags.GetClientCapabilities(ctx); !ok || !capabilities.AllowUtf8LabelNames {
   366  		level.Debug(q.logger).Log("msg", "filtering out non-valid labels")
   367  		labelNames = filterLabelNames(labelNames)
   368  	}
   369  
   370  	return connect.NewResponse(&typesv1.LabelNamesResponse{
   371  		Names: labelNames,
   372  	}), nil
   373  }
   374  
   375  func (q *Querier) blockSelect(ctx context.Context, start, end model.Time) (blockPlan, error) {
   376  	sp, ctx := opentracing.StartSpanFromContext(ctx, "blockSelect")
   377  	defer sp.Finish()
   378  
   379  	sp.LogFields(
   380  		otlog.String("start", start.Time().String()),
   381  		otlog.String("end", end.Time().String()),
   382  	)
   383  
   384  	ingesterReq := &ingestv1.BlockMetadataRequest{
   385  		Start: int64(start),
   386  		End:   int64(end),
   387  	}
   388  
   389  	results := newReplicasPerBlockID(q.logger)
   390  
   391  	// get first all blocks from store gateways, as they should be querier with a priority and also are the only ones containing duplicated blocks because of replication
   392  	if q.storeGatewayQuerier != nil {
   393  		res, err := q.blockSelectFromStoreGateway(ctx, ingesterReq)
   394  		if err != nil {
   395  			return nil, err
   396  		}
   397  
   398  		results.add(res, storeGatewayInstance)
   399  	}
   400  
   401  	if q.ingesterQuerier != nil {
   402  		res, err := q.blockSelectFromIngesters(ctx, ingesterReq)
   403  		if err != nil {
   404  			return nil, err
   405  		}
   406  		results.add(res, ingesterInstance)
   407  	}
   408  
   409  	return results.blockPlan(ctx), nil
   410  }
   411  
   412  // filterLabelNames filters out non-legacy (see validation.SanitizeLegacyLabelName)
   413  // label names by default. If no label names are passed in the req, all label names
   414  // are fetched and then filtered. Otherwise, the label names in the req are filtered.
   415  // If the `AllowUtf8LabelNames` client capability is enabled, this function is a no-op.
   416  func (q *Querier) filterLabelNames(
   417  	ctx context.Context,
   418  	req *connect.Request[querierv1.SeriesRequest],
   419  ) ([]string, error) {
   420  	if capabilities, ok := featureflags.GetClientCapabilities(ctx); ok && capabilities.AllowUtf8LabelNames {
   421  		return req.Msg.LabelNames, nil
   422  	}
   423  
   424  	if len(req.Msg.LabelNames) == 0 {
   425  		// Querying for all label names; must retrieve all label names to then filter out
   426  		response, err := q.LabelNames(ctx, connect.NewRequest(&typesv1.LabelNamesRequest{
   427  			Matchers: req.Msg.Matchers,
   428  			Start:    req.Msg.Start,
   429  			End:      req.Msg.End,
   430  		}))
   431  		if err != nil {
   432  			return nil, err
   433  		}
   434  		return response.Msg.Names, nil
   435  	}
   436  
   437  	// Filter out label names in request if not passing legacy validation
   438  	filtered := make([]string, 0, len(req.Msg.LabelNames))
   439  	for _, name := range req.Msg.LabelNames {
   440  		if _, _, ok := validation.SanitizeLegacyLabelName(name); !ok {
   441  			level.Debug(q.logger).Log("msg", "filtering out label", "label_name", name)
   442  			continue
   443  		}
   444  		filtered = append(filtered, name)
   445  	}
   446  	return filtered, nil
   447  }
   448  
   449  func (q *Querier) Series(ctx context.Context, req *connect.Request[querierv1.SeriesRequest]) (*connect.Response[querierv1.SeriesResponse], error) {
   450  	sp, ctx := opentracing.StartSpanFromContext(ctx, "Series")
   451  	defer sp.Finish()
   452  
   453  	_, hasTimeRange := phlaremodel.GetTimeRange(req.Msg)
   454  	sp.LogFields(
   455  		otlog.Bool("legacy_request", !hasTimeRange),
   456  		otlog.String("matchers", strings.Join(req.Msg.Matchers, ",")),
   457  		otlog.String("label_names", strings.Join(req.Msg.LabelNames, ",")),
   458  		otlog.Int64("start", req.Msg.Start),
   459  		otlog.Int64("end", req.Msg.End),
   460  	)
   461  
   462  	// Update LabelNames
   463  	filteredLabelNames, err := q.filterLabelNames(ctx, req)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  	req.Msg.LabelNames = filteredLabelNames
   468  
   469  	// no store gateways configured so just query the ingesters
   470  	if q.storeGatewayQuerier == nil || !hasTimeRange {
   471  		responses, err := q.seriesFromIngesters(ctx, &ingestv1.SeriesRequest{
   472  			Matchers:   req.Msg.Matchers,
   473  			LabelNames: req.Msg.LabelNames,
   474  			Start:      req.Msg.Start,
   475  			End:        req.Msg.End,
   476  		})
   477  		if err != nil {
   478  			return nil, err
   479  		}
   480  
   481  		return connect.NewResponse(&querierv1.SeriesResponse{
   482  			LabelsSet: lo.UniqBy(
   483  				lo.FlatMap(responses, func(r ResponseFromReplica[[]*typesv1.Labels], _ int) []*typesv1.Labels {
   484  					return r.response
   485  				}),
   486  				func(t *typesv1.Labels) uint64 {
   487  					return phlaremodel.Labels(t.Labels).Hash()
   488  				}),
   489  		}), nil
   490  	}
   491  
   492  	storeQueries := splitQueryToStores(model.Time(req.Msg.Start), model.Time(req.Msg.End), model.Now(), q.cfg.QueryStoreAfter, nil)
   493  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
   494  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
   495  	}
   496  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
   497  
   498  	var responses []ResponseFromReplica[[]*typesv1.Labels]
   499  	var lock sync.Mutex
   500  	group, gCtx := errgroup.WithContext(ctx)
   501  
   502  	if storeQueries.ingester.shouldQuery {
   503  		group.Go(func() error {
   504  			ir, err := q.seriesFromIngesters(gCtx, storeQueries.ingester.SeriesRequest(req.Msg))
   505  			if err != nil {
   506  				return err
   507  			}
   508  
   509  			lock.Lock()
   510  			responses = append(responses, ir...)
   511  			lock.Unlock()
   512  			return nil
   513  		})
   514  	}
   515  
   516  	if storeQueries.storeGateway.shouldQuery {
   517  		group.Go(func() error {
   518  			ir, err := q.seriesFromStoreGateway(gCtx, storeQueries.storeGateway.SeriesRequest(req.Msg))
   519  			if err != nil {
   520  				return err
   521  			}
   522  
   523  			lock.Lock()
   524  			responses = append(responses, ir...)
   525  			lock.Unlock()
   526  			return nil
   527  		})
   528  	}
   529  
   530  	err = group.Wait()
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  
   535  	return connect.NewResponse(&querierv1.SeriesResponse{
   536  		LabelsSet: lo.UniqBy(
   537  			lo.FlatMap(responses, func(r ResponseFromReplica[[]*typesv1.Labels], _ int) []*typesv1.Labels {
   538  				return r.response
   539  			}),
   540  			func(t *typesv1.Labels) uint64 {
   541  				return phlaremodel.Labels(t.Labels).Hash()
   542  			},
   543  		),
   544  	}), nil
   545  }
   546  
   547  // FIXME(kolesnikovae): The method is never used and should be removed.
   548  func (q *Querier) Diff(ctx context.Context, req *connect.Request[querierv1.DiffRequest]) (*connect.Response[querierv1.DiffResponse], error) {
   549  	sp, ctx := opentracing.StartSpanFromContext(ctx, "Diff")
   550  	defer func() {
   551  		sp.LogFields(
   552  			otlog.String("leftStart", model.Time(req.Msg.Left.Start).Time().String()),
   553  			otlog.String("leftEnd", model.Time(req.Msg.Left.End).Time().String()),
   554  			// Assume are the same
   555  			otlog.String("selector", req.Msg.Left.LabelSelector),
   556  			otlog.String("profile_id", req.Msg.Left.ProfileTypeID),
   557  		)
   558  		sp.Finish()
   559  	}()
   560  
   561  	var leftTree, rightTree *phlaremodel.Tree
   562  	g, gCtx := errgroup.WithContext(ctx)
   563  
   564  	g.Go(func() error {
   565  		res, err := q.selectTree(gCtx, req.Msg.Left)
   566  		if err != nil {
   567  			return err
   568  		}
   569  
   570  		leftTree = res
   571  		return nil
   572  	})
   573  
   574  	g.Go(func() error {
   575  		res, err := q.selectTree(gCtx, req.Msg.Right)
   576  		if err != nil {
   577  			return err
   578  		}
   579  		rightTree = res
   580  		return nil
   581  	})
   582  
   583  	if err := g.Wait(); err != nil {
   584  		return nil, err
   585  	}
   586  
   587  	fd, err := phlaremodel.NewFlamegraphDiff(leftTree, rightTree, maxNodesDefault)
   588  	if err != nil {
   589  		return nil, connect.NewError(connect.CodeInvalidArgument, err)
   590  	}
   591  
   592  	return connect.NewResponse(&querierv1.DiffResponse{
   593  		Flamegraph: fd,
   594  	}), nil
   595  }
   596  
   597  func (q *Querier) GetProfileStats(ctx context.Context, req *connect.Request[typesv1.GetProfileStatsRequest]) (*connect.Response[typesv1.GetProfileStatsResponse], error) {
   598  	sp, ctx := opentracing.StartSpanFromContext(ctx, "GetProfileStats")
   599  	defer sp.Finish()
   600  
   601  	responses, err := forAllIngesters(ctx, q.ingesterQuerier, func(childCtx context.Context, ic IngesterQueryClient) (*typesv1.GetProfileStatsResponse, error) {
   602  		response, err := ic.GetProfileStats(childCtx, connect.NewRequest(&typesv1.GetProfileStatsRequest{}))
   603  		if err != nil {
   604  			return nil, err
   605  		}
   606  		return response.Msg, nil
   607  	})
   608  	if err != nil {
   609  		return nil, err
   610  	}
   611  
   612  	response := &typesv1.GetProfileStatsResponse{
   613  		DataIngested:      false,
   614  		OldestProfileTime: math.MaxInt64,
   615  		NewestProfileTime: math.MinInt64,
   616  	}
   617  	for _, r := range responses {
   618  		response.DataIngested = response.DataIngested || r.response.DataIngested
   619  		if r.response.OldestProfileTime < response.OldestProfileTime {
   620  			response.OldestProfileTime = r.response.OldestProfileTime
   621  		}
   622  		if r.response.NewestProfileTime > response.NewestProfileTime {
   623  			response.NewestProfileTime = r.response.NewestProfileTime
   624  		}
   625  	}
   626  
   627  	if q.storageBucket != nil {
   628  		tenantId, err := tenant.TenantID(ctx)
   629  		if err != nil {
   630  			return nil, err
   631  		}
   632  		index, err := bucketindex.ReadIndex(ctx, q.storageBucket, tenantId, q.tenantConfigProvider, q.logger)
   633  		if err != nil && !errors.Is(err, bucketindex.ErrIndexNotFound) {
   634  			return nil, err
   635  		}
   636  		if index != nil && len(index.Blocks) > 0 {
   637  			// assuming blocks are ordered by time in ascending order
   638  			// ignoring deleted blocks as we only need the overall time range of blocks
   639  			minTime := index.Blocks[0].MinTime.Time().UnixMilli()
   640  			if minTime < response.OldestProfileTime {
   641  				response.OldestProfileTime = minTime
   642  			}
   643  			maxTime := index.Blocks[len(index.Blocks)-1].MaxTime.Time().UnixMilli()
   644  			if maxTime > response.NewestProfileTime {
   645  				response.NewestProfileTime = maxTime
   646  			}
   647  			response.DataIngested = true
   648  		}
   649  	}
   650  
   651  	return connect.NewResponse(response), nil
   652  }
   653  
   654  func (q *Querier) SelectMergeStacktraces(ctx context.Context, req *connect.Request[querierv1.SelectMergeStacktracesRequest]) (*connect.Response[querierv1.SelectMergeStacktracesResponse], error) {
   655  	sp, ctx := opentracing.StartSpanFromContext(ctx, "SelectMergeStacktraces")
   656  	level.Info(spanlogger.FromContext(ctx, q.logger)).Log(
   657  		"start", model.Time(req.Msg.Start).Time().String(),
   658  		"end", model.Time(req.Msg.End).Time().String(),
   659  		"selector", req.Msg.LabelSelector,
   660  		"profile_id", req.Msg.ProfileTypeID,
   661  	)
   662  	defer func() {
   663  		sp.Finish()
   664  	}()
   665  
   666  	if req.Msg.MaxNodes == nil || *req.Msg.MaxNodes == 0 {
   667  		mn := maxNodesDefault
   668  		req.Msg.MaxNodes = &mn
   669  	}
   670  
   671  	if len(req.Msg.ProfileIdSelector) > 0 {
   672  		return nil, connect.NewError(connect.CodeUnimplemented, errors.New("profile_id_selector is only supported with the v2 query backend"))
   673  	}
   674  
   675  	t, err := q.selectTree(ctx, req.Msg)
   676  	if err != nil {
   677  		return nil, err
   678  	}
   679  
   680  	var resp querierv1.SelectMergeStacktracesResponse
   681  	switch req.Msg.Format {
   682  	default:
   683  		resp.Flamegraph = phlaremodel.NewFlameGraph(t, req.Msg.GetMaxNodes())
   684  	case querierv1.ProfileFormat_PROFILE_FORMAT_TREE:
   685  		resp.Tree = t.Bytes(req.Msg.GetMaxNodes())
   686  	}
   687  	return connect.NewResponse(&resp), nil
   688  }
   689  
   690  func (q *Querier) SelectMergeSpanProfile(ctx context.Context, req *connect.Request[querierv1.SelectMergeSpanProfileRequest]) (*connect.Response[querierv1.SelectMergeSpanProfileResponse], error) {
   691  	sp, ctx := opentracing.StartSpanFromContext(ctx, "SelectMergeSpanProfile")
   692  	level.Info(spanlogger.FromContext(ctx, q.logger)).Log(
   693  		"start", model.Time(req.Msg.Start).Time().String(),
   694  		"end", model.Time(req.Msg.End).Time().String(),
   695  		"selector", req.Msg.LabelSelector,
   696  		"profile_id", req.Msg.ProfileTypeID,
   697  	)
   698  	defer func() {
   699  		sp.Finish()
   700  	}()
   701  
   702  	if req.Msg.MaxNodes == nil || *req.Msg.MaxNodes == 0 {
   703  		mn := maxNodesDefault
   704  		req.Msg.MaxNodes = &mn
   705  	}
   706  
   707  	t, err := q.selectSpanProfile(ctx, req.Msg)
   708  	if err != nil {
   709  		return nil, err
   710  	}
   711  
   712  	var resp querierv1.SelectMergeSpanProfileResponse
   713  	switch req.Msg.Format {
   714  	default:
   715  		resp.Flamegraph = phlaremodel.NewFlameGraph(t, req.Msg.GetMaxNodes())
   716  	case querierv1.ProfileFormat_PROFILE_FORMAT_TREE:
   717  		resp.Tree = t.Bytes(req.Msg.GetMaxNodes())
   718  	}
   719  	return connect.NewResponse(&resp), nil
   720  }
   721  
   722  func isEndpointNotExistingErr(err error) bool {
   723  	if err == nil {
   724  		return false
   725  	}
   726  
   727  	var cerr *connect.Error
   728  	// unwrap all intermediate connect errors
   729  	for errors.As(err, &cerr) {
   730  		err = cerr.Unwrap()
   731  	}
   732  	return err.Error() == "405 Method Not Allowed"
   733  }
   734  
   735  func (q *Querier) selectTree(ctx context.Context, req *querierv1.SelectMergeStacktracesRequest) (*phlaremodel.Tree, error) {
   736  	// determine the block hints
   737  	plan, err := q.blockSelect(ctx, model.Time(req.Start), model.Time(req.End))
   738  	if isEndpointNotExistingErr(err) {
   739  		level.Warn(spanlogger.FromContext(ctx, q.logger)).Log(
   740  			"msg", "block select not supported on at least one component, fallback to use full dataset",
   741  			"err", err,
   742  		)
   743  		plan = nil
   744  	} else if err != nil {
   745  		return nil, fmt.Errorf("error during block select: %w", err)
   746  	}
   747  
   748  	// no store gateways configured so just query the ingesters
   749  	if q.storeGatewayQuerier == nil {
   750  		return q.selectTreeFromIngesters(ctx, req, plan)
   751  	}
   752  
   753  	storeQueries := splitQueryToStores(model.Time(req.Start), model.Time(req.End), model.Now(), q.cfg.QueryStoreAfter, plan)
   754  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
   755  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
   756  	}
   757  
   758  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
   759  
   760  	if plan == nil && !storeQueries.ingester.shouldQuery {
   761  		return q.selectTreeFromStoreGateway(ctx, storeQueries.storeGateway.MergeStacktracesRequest(req), plan)
   762  	}
   763  	if plan == nil && !storeQueries.storeGateway.shouldQuery {
   764  		return q.selectTreeFromIngesters(ctx, storeQueries.ingester.MergeStacktracesRequest(req), plan)
   765  	}
   766  
   767  	g, gCtx := errgroup.WithContext(ctx)
   768  	var ingesterTree, storegatewayTree *phlaremodel.Tree
   769  	g.Go(func() error {
   770  		var err error
   771  		ingesterTree, err = q.selectTreeFromIngesters(gCtx, storeQueries.ingester.MergeStacktracesRequest(req), plan)
   772  		if err != nil {
   773  			return err
   774  		}
   775  		return nil
   776  	})
   777  	g.Go(func() error {
   778  		var err error
   779  		storegatewayTree, err = q.selectTreeFromStoreGateway(ctx, storeQueries.storeGateway.MergeStacktracesRequest(req), plan)
   780  		if err != nil {
   781  			return err
   782  		}
   783  		return nil
   784  	})
   785  	if err := g.Wait(); err != nil {
   786  		return nil, err
   787  	}
   788  	storegatewayTree.Merge(ingesterTree)
   789  	return storegatewayTree, nil
   790  }
   791  
   792  type storeQuery struct {
   793  	start, end  model.Time
   794  	shouldQuery bool
   795  }
   796  
   797  func (sq storeQuery) MergeStacktracesRequest(req *querierv1.SelectMergeStacktracesRequest) *querierv1.SelectMergeStacktracesRequest {
   798  	return &querierv1.SelectMergeStacktracesRequest{
   799  		Start:         int64(sq.start),
   800  		End:           int64(sq.end),
   801  		LabelSelector: req.LabelSelector,
   802  		ProfileTypeID: req.ProfileTypeID,
   803  		MaxNodes:      req.MaxNodes,
   804  		Format:        req.Format,
   805  	}
   806  }
   807  
   808  func (sq storeQuery) MergeSeriesRequest(req *querierv1.SelectSeriesRequest, profileType *typesv1.ProfileType) *ingestv1.MergeProfilesLabelsRequest {
   809  	return &ingestv1.MergeProfilesLabelsRequest{
   810  		Request: &ingestv1.SelectProfilesRequest{
   811  			Type:          profileType,
   812  			LabelSelector: req.LabelSelector,
   813  			Start:         int64(sq.start),
   814  			End:           int64(sq.end),
   815  			Aggregation:   req.Aggregation,
   816  		},
   817  		By:                 req.GroupBy,
   818  		StackTraceSelector: req.StackTraceSelector,
   819  	}
   820  }
   821  
   822  func (sq storeQuery) MergeSpanProfileRequest(req *querierv1.SelectMergeSpanProfileRequest) *querierv1.SelectMergeSpanProfileRequest {
   823  	return &querierv1.SelectMergeSpanProfileRequest{
   824  		Start:         int64(sq.start),
   825  		End:           int64(sq.end),
   826  		ProfileTypeID: req.ProfileTypeID,
   827  		LabelSelector: req.LabelSelector,
   828  		SpanSelector:  req.SpanSelector,
   829  		MaxNodes:      req.MaxNodes,
   830  		Format:        req.Format,
   831  	}
   832  }
   833  
   834  func (sq storeQuery) MergeProfileRequest(req *querierv1.SelectMergeProfileRequest) *querierv1.SelectMergeProfileRequest {
   835  	return &querierv1.SelectMergeProfileRequest{
   836  		ProfileTypeID:      req.ProfileTypeID,
   837  		LabelSelector:      req.LabelSelector,
   838  		Start:              int64(sq.start),
   839  		End:                int64(sq.end),
   840  		MaxNodes:           req.MaxNodes,
   841  		StackTraceSelector: req.StackTraceSelector,
   842  	}
   843  }
   844  
   845  func (sq storeQuery) SeriesRequest(req *querierv1.SeriesRequest) *ingestv1.SeriesRequest {
   846  	return &ingestv1.SeriesRequest{
   847  		Start:      int64(sq.start),
   848  		End:        int64(sq.end),
   849  		Matchers:   req.Matchers,
   850  		LabelNames: req.LabelNames,
   851  	}
   852  }
   853  
   854  func (sq storeQuery) LabelNamesRequest(req *typesv1.LabelNamesRequest) *typesv1.LabelNamesRequest {
   855  	return &typesv1.LabelNamesRequest{
   856  		Matchers: req.Matchers,
   857  		Start:    int64(sq.start),
   858  		End:      int64(sq.end),
   859  	}
   860  }
   861  
   862  func (sq storeQuery) LabelValuesRequest(req *typesv1.LabelValuesRequest) *typesv1.LabelValuesRequest {
   863  	return &typesv1.LabelValuesRequest{
   864  		Name:     req.Name,
   865  		Matchers: req.Matchers,
   866  		Start:    int64(sq.start),
   867  		End:      int64(sq.end),
   868  	}
   869  }
   870  
   871  func (sq storeQuery) ProfileTypesRequest(req *querierv1.ProfileTypesRequest) *ingestv1.ProfileTypesRequest {
   872  	return &ingestv1.ProfileTypesRequest{
   873  		Start: int64(sq.start),
   874  		End:   int64(sq.end),
   875  	}
   876  }
   877  
   878  type storeQueries struct {
   879  	ingester, storeGateway storeQuery
   880  	queryStoreAfter        time.Duration
   881  }
   882  
   883  func (sq storeQueries) Log(logger log.Logger) {
   884  	logger.Log(
   885  		"msg", "storeQueries",
   886  		"queryStoreAfter", sq.queryStoreAfter.String(),
   887  		"ingester", sq.ingester.shouldQuery,
   888  		"ingester.start", sq.ingester.start.Time().Format(time.RFC3339Nano), "ingester.end", sq.ingester.end.Time().Format(time.RFC3339Nano),
   889  		"store-gateway", sq.storeGateway.shouldQuery,
   890  		"store-gateway.start", sq.storeGateway.start.Time().Format(time.RFC3339Nano), "store-gateway.end", sq.storeGateway.end.Time().Format(time.RFC3339Nano),
   891  	)
   892  }
   893  
   894  // splitQueryToStores splits the query into ingester and store gateway queries using the given cut off time.
   895  // todo(ctovena): Later we should try to deduplicate blocks between ingesters and store gateways (prefer) and simply query both
   896  func splitQueryToStores(start, end model.Time, now model.Time, queryStoreAfter time.Duration, plan blockPlan) (queries storeQueries) {
   897  	if plan != nil {
   898  		// if we have a plan we can use it to split the query, we retain the original start and end time as we want to query the full range for those particular blocks selected.
   899  		queries.queryStoreAfter = 0
   900  		queries.ingester = storeQuery{shouldQuery: true, start: start, end: end}
   901  		queries.storeGateway = storeQuery{shouldQuery: true, start: start, end: end}
   902  		return queries
   903  	}
   904  
   905  	queries.queryStoreAfter = queryStoreAfter
   906  	cutOff := now.Add(-queryStoreAfter)
   907  	if start.Before(cutOff) {
   908  		queries.storeGateway = storeQuery{shouldQuery: true, start: start, end: min(cutOff, end)}
   909  	}
   910  	if end.After(cutOff) {
   911  		queries.ingester = storeQuery{shouldQuery: true, start: max(cutOff, start), end: end}
   912  		// Note that the ranges must not overlap.
   913  		if queries.storeGateway.shouldQuery {
   914  			queries.ingester.start++
   915  		}
   916  	}
   917  	return queries
   918  }
   919  
   920  func (q *Querier) SelectMergeProfile(ctx context.Context, req *connect.Request[querierv1.SelectMergeProfileRequest]) (*connect.Response[googlev1.Profile], error) {
   921  	sp, ctx := opentracing.StartSpanFromContext(ctx, "SelectMergeProfile")
   922  	sp.SetTag("start", model.Time(req.Msg.Start).Time().String()).
   923  		SetTag("end", model.Time(req.Msg.End).Time().String()).
   924  		SetTag("selector", req.Msg.LabelSelector).
   925  		SetTag("max_nodes", req.Msg.GetMaxNodes()).
   926  		SetTag("profile_type", req.Msg.ProfileTypeID)
   927  	defer sp.Finish()
   928  
   929  	profile, err := q.selectProfile(ctx, req.Msg)
   930  	if err != nil {
   931  		return nil, err
   932  	}
   933  	profile.DurationNanos = model.Time(req.Msg.End).UnixNano() - model.Time(req.Msg.Start).UnixNano()
   934  	profile.TimeNanos = model.Time(req.Msg.End).UnixNano()
   935  	return connect.NewResponse(profile), nil
   936  }
   937  
   938  func (q *Querier) selectProfile(ctx context.Context, req *querierv1.SelectMergeProfileRequest) (*googlev1.Profile, error) {
   939  	// determine the block hints
   940  	plan, err := q.blockSelect(ctx, model.Time(req.Start), model.Time(req.End))
   941  	if isEndpointNotExistingErr(err) {
   942  		level.Warn(spanlogger.FromContext(ctx, q.logger)).Log(
   943  			"msg", "block select not supported on at least one component, fallback to use full dataset",
   944  			"err", err,
   945  		)
   946  		plan = nil
   947  	} else if err != nil {
   948  		return nil, fmt.Errorf("error during block select: %w", err)
   949  	}
   950  
   951  	// no store gateways configured so just query the ingesters
   952  	if q.storeGatewayQuerier == nil {
   953  		return q.selectProfileFromIngesters(ctx, req, plan)
   954  	}
   955  
   956  	storeQueries := splitQueryToStores(model.Time(req.Start), model.Time(req.End), model.Now(), q.cfg.QueryStoreAfter, plan)
   957  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
   958  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
   959  	}
   960  
   961  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
   962  
   963  	if plan == nil && !storeQueries.ingester.shouldQuery {
   964  		return q.selectProfileFromStoreGateway(ctx, storeQueries.storeGateway.MergeProfileRequest(req), plan)
   965  	}
   966  	if plan == nil && !storeQueries.storeGateway.shouldQuery {
   967  		return q.selectProfileFromIngesters(ctx, storeQueries.ingester.MergeProfileRequest(req), plan)
   968  	}
   969  
   970  	g, gCtx := errgroup.WithContext(ctx)
   971  	var merge pprof.ProfileMerge
   972  	g.Go(func() error {
   973  		ingesterProfile, err := q.selectProfileFromIngesters(gCtx, storeQueries.ingester.MergeProfileRequest(req), plan)
   974  		if err != nil {
   975  			return err
   976  		}
   977  		return merge.Merge(ingesterProfile, true)
   978  	})
   979  	g.Go(func() error {
   980  		storegatewayProfile, err := q.selectProfileFromStoreGateway(gCtx, storeQueries.storeGateway.MergeProfileRequest(req), plan)
   981  		if err != nil {
   982  			return err
   983  		}
   984  		return merge.Merge(storegatewayProfile, true)
   985  	})
   986  	if err := g.Wait(); err != nil {
   987  		return nil, err
   988  	}
   989  
   990  	return merge.Profile(), nil
   991  }
   992  
   993  func (q *Querier) SelectSeries(ctx context.Context, req *connect.Request[querierv1.SelectSeriesRequest]) (*connect.Response[querierv1.SelectSeriesResponse], error) {
   994  	sp, ctx := opentracing.StartSpanFromContext(ctx, "SelectSeries")
   995  	defer func() {
   996  		sp.LogFields(
   997  			otlog.String("start", model.Time(req.Msg.Start).Time().String()),
   998  			otlog.String("end", model.Time(req.Msg.End).Time().String()),
   999  			otlog.String("selector", req.Msg.LabelSelector),
  1000  			otlog.String("profile_id", req.Msg.ProfileTypeID),
  1001  			otlog.String("group_by", strings.Join(req.Msg.GroupBy, ",")),
  1002  			otlog.Float64("step", req.Msg.Step),
  1003  		)
  1004  		sp.Finish()
  1005  	}()
  1006  
  1007  	_, err := parser.ParseMetricSelector(req.Msg.LabelSelector)
  1008  	if err != nil {
  1009  		return nil, connect.NewError(connect.CodeInvalidArgument, err)
  1010  	}
  1011  
  1012  	if req.Msg.Start > req.Msg.End {
  1013  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start must be before end"))
  1014  	}
  1015  
  1016  	if req.Msg.Step == 0 {
  1017  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("step must be non-zero"))
  1018  	}
  1019  
  1020  	// SelectSeries (v1 API) does not support exemplars
  1021  	if req.Msg.ExemplarType == typesv1.ExemplarType_EXEMPLAR_TYPE_INDIVIDUAL ||
  1022  		req.Msg.ExemplarType == typesv1.ExemplarType_EXEMPLAR_TYPE_SPAN {
  1023  		return nil, connect.NewError(connect.CodeUnimplemented, errors.New("exemplars are not supported in SelectSeries API, use the v2 query API instead"))
  1024  	}
  1025  
  1026  	stepMs := time.Duration(req.Msg.Step * float64(time.Second)).Milliseconds()
  1027  	ctx, cancel := context.WithCancel(ctx)
  1028  	defer cancel()
  1029  
  1030  	// determine the block hints
  1031  	plan, err := q.blockSelect(ctx, model.Time(req.Msg.Start), model.Time(req.Msg.End))
  1032  	if isEndpointNotExistingErr(err) {
  1033  		level.Warn(spanlogger.FromContext(ctx, q.logger)).Log(
  1034  			"msg", "block select not supported on at least one component, fallback to use full dataset",
  1035  			"err", err,
  1036  		)
  1037  		plan = nil
  1038  	} else if err != nil {
  1039  		return nil, fmt.Errorf("error during block select: %w", err)
  1040  	}
  1041  
  1042  	responses, err := q.selectSeries(ctx, req, plan)
  1043  	if err != nil {
  1044  		return nil, err
  1045  	}
  1046  
  1047  	it, err := selectMergeSeries(ctx, req.Msg.Aggregation, responses)
  1048  	if err != nil {
  1049  		return nil, connect.NewError(connect.CodeInternal, err)
  1050  	}
  1051  
  1052  	result := phlaremodel.RangeSeries(it, req.Msg.Start, req.Msg.End, stepMs, req.Msg.Aggregation)
  1053  	if it.Err() != nil {
  1054  		return nil, connect.NewError(connect.CodeInternal, it.Err())
  1055  	}
  1056  
  1057  	return connect.NewResponse(&querierv1.SelectSeriesResponse{
  1058  		Series: result,
  1059  	}), nil
  1060  }
  1061  
  1062  func (q *Querier) selectSeries(ctx context.Context, req *connect.Request[querierv1.SelectSeriesRequest], plan map[string]*blockPlanEntry) ([]ResponseFromReplica[clientpool.BidiClientMergeProfilesLabels], error) {
  1063  	stepMs := time.Duration(req.Msg.Step * float64(time.Second)).Milliseconds()
  1064  	sort.Strings(req.Msg.GroupBy)
  1065  
  1066  	// we need to request profile from start - step to end since start is inclusive.
  1067  	// The first step starts at start-step to start.
  1068  	start := req.Msg.Start - stepMs
  1069  
  1070  	profileType, err := phlaremodel.ParseProfileTypeSelector(req.Msg.ProfileTypeID)
  1071  	if err != nil {
  1072  		return nil, connect.NewError(connect.CodeInvalidArgument, err)
  1073  	}
  1074  
  1075  	if q.storeGatewayQuerier == nil {
  1076  		return q.selectSeriesFromIngesters(ctx, &ingestv1.MergeProfilesLabelsRequest{
  1077  			Request: &ingestv1.SelectProfilesRequest{
  1078  				LabelSelector: req.Msg.LabelSelector,
  1079  				Start:         start,
  1080  				End:           req.Msg.End,
  1081  				Type:          profileType,
  1082  				Aggregation:   req.Msg.Aggregation,
  1083  			},
  1084  			By:                 req.Msg.GroupBy,
  1085  			StackTraceSelector: req.Msg.StackTraceSelector,
  1086  		}, plan)
  1087  	}
  1088  
  1089  	storeQueries := splitQueryToStores(model.Time(start), model.Time(req.Msg.End), model.Now(), q.cfg.QueryStoreAfter, plan)
  1090  
  1091  	var responses []ResponseFromReplica[clientpool.BidiClientMergeProfilesLabels]
  1092  
  1093  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
  1094  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
  1095  	}
  1096  
  1097  	// todo in parallel
  1098  
  1099  	if storeQueries.ingester.shouldQuery {
  1100  		ir, err := q.selectSeriesFromIngesters(ctx, storeQueries.ingester.MergeSeriesRequest(req.Msg, profileType), plan)
  1101  		if err != nil {
  1102  			return nil, err
  1103  		}
  1104  		responses = append(responses, ir...)
  1105  	}
  1106  
  1107  	if storeQueries.storeGateway.shouldQuery {
  1108  		ir, err := q.selectSeriesFromStoreGateway(ctx, storeQueries.storeGateway.MergeSeriesRequest(req.Msg, profileType), plan)
  1109  		if err != nil {
  1110  			return nil, err
  1111  		}
  1112  		responses = append(responses, ir...)
  1113  	}
  1114  	return responses, nil
  1115  }
  1116  
  1117  func uniqueSortedStrings(responses []ResponseFromReplica[[]string]) []string {
  1118  	total := 0
  1119  	for _, r := range responses {
  1120  		total += len(r.response)
  1121  	}
  1122  	unique := make(map[string]struct{}, total)
  1123  	result := make([]string, 0, total)
  1124  	for _, r := range responses {
  1125  		for _, elem := range r.response {
  1126  			if _, ok := unique[elem]; !ok {
  1127  				unique[elem] = struct{}{}
  1128  				result = append(result, elem)
  1129  			}
  1130  		}
  1131  	}
  1132  	sort.Strings(result)
  1133  	return result
  1134  }
  1135  
  1136  func (q *Querier) selectSpanProfile(ctx context.Context, req *querierv1.SelectMergeSpanProfileRequest) (*phlaremodel.Tree, error) {
  1137  	// determine the block hints
  1138  	plan, err := q.blockSelect(ctx, model.Time(req.Start), model.Time(req.End))
  1139  	if isEndpointNotExistingErr(err) {
  1140  		level.Warn(spanlogger.FromContext(ctx, q.logger)).Log(
  1141  			"msg", "block select not supported on at least one component, fallback to use full dataset",
  1142  			"err", err,
  1143  		)
  1144  		plan = nil
  1145  	} else if err != nil {
  1146  		return nil, fmt.Errorf("error during block select: %w", err)
  1147  	}
  1148  
  1149  	// no store gateways configured so just query the ingesters
  1150  	if q.storeGatewayQuerier == nil {
  1151  		return q.selectSpanProfileFromIngesters(ctx, req, plan)
  1152  	}
  1153  
  1154  	storeQueries := splitQueryToStores(model.Time(req.Start), model.Time(req.End), model.Now(), q.cfg.QueryStoreAfter, plan)
  1155  	if !storeQueries.ingester.shouldQuery && !storeQueries.storeGateway.shouldQuery {
  1156  		return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("start and end time are outside of the ingester and store gateway retention"))
  1157  	}
  1158  
  1159  	storeQueries.Log(level.Debug(spanlogger.FromContext(ctx, q.logger)))
  1160  
  1161  	if plan == nil && !storeQueries.ingester.shouldQuery {
  1162  		return q.selectSpanProfileFromStoreGateway(ctx, storeQueries.storeGateway.MergeSpanProfileRequest(req), plan)
  1163  	}
  1164  	if plan == nil && !storeQueries.storeGateway.shouldQuery {
  1165  		return q.selectSpanProfileFromIngesters(ctx, storeQueries.ingester.MergeSpanProfileRequest(req), plan)
  1166  	}
  1167  
  1168  	g, gCtx := errgroup.WithContext(ctx)
  1169  	var ingesterTree, storegatewayTree *phlaremodel.Tree
  1170  	g.Go(func() error {
  1171  		var err error
  1172  		ingesterTree, err = q.selectSpanProfileFromIngesters(gCtx, storeQueries.ingester.MergeSpanProfileRequest(req), plan)
  1173  		if err != nil {
  1174  			return err
  1175  		}
  1176  		return nil
  1177  	})
  1178  	g.Go(func() error {
  1179  		var err error
  1180  		storegatewayTree, err = q.selectSpanProfileFromStoreGateway(gCtx, storeQueries.storeGateway.MergeSpanProfileRequest(req), plan)
  1181  		if err != nil {
  1182  			return err
  1183  		}
  1184  		return nil
  1185  	})
  1186  	if err := g.Wait(); err != nil {
  1187  		return nil, err
  1188  	}
  1189  	storegatewayTree.Merge(ingesterTree)
  1190  	return storegatewayTree, nil
  1191  }