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 }