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

     1  package querybackend
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/go-kit/log"
     9  	"github.com/go-kit/log/level"
    10  	"github.com/grafana/dskit/multierror"
    11  	"github.com/grafana/dskit/tracing"
    12  	"github.com/opentracing/opentracing-go"
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	"github.com/prometheus/common/model"
    15  	"github.com/prometheus/prometheus/model/labels"
    16  	"github.com/prometheus/prometheus/promql/parser"
    17  	"golang.org/x/sync/errgroup"
    18  	"google.golang.org/grpc/codes"
    19  	"google.golang.org/grpc/status"
    20  
    21  	metastorev1 "github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1"
    22  	queryv1 "github.com/grafana/pyroscope/api/gen/proto/go/query/v1"
    23  	"github.com/grafana/pyroscope/pkg/block"
    24  	"github.com/grafana/pyroscope/pkg/objstore"
    25  	"github.com/grafana/pyroscope/pkg/util"
    26  )
    27  
    28  // BlockReader reads blocks from object storage. Each block is represented by
    29  // a single object, which consists of datasets – regions within the object
    30  // that contain tenant data.
    31  //
    32  // A single Invoke request may span multiple blocks (objects).
    33  // Querying an object could involve processing multiple datasets in parallel.
    34  // Multiple parallel queries can be executed on the same tenant dataset.
    35  //
    36  // object-a    dataset-a   query-a
    37  //                         query-b
    38  //             dataset-b   query-a
    39  //                         query-b
    40  // object-b    dataset-a   query-a
    41  //                         query-b
    42  //             dataset-b   query-a
    43  //                         query-b
    44  //
    45  
    46  type BlockReader struct {
    47  	log     log.Logger
    48  	storage objstore.Bucket
    49  
    50  	metrics *metrics
    51  
    52  	// TODO:
    53  	//  - Use a worker pool instead of the errgroup.
    54  	//  - Reusable query context.
    55  	//  - Query pipelining: currently, queries share the same context,
    56  	//    and reuse resources, but the data is processed independently.
    57  	//    Instead, they should share the processing pipeline, if possible.
    58  }
    59  
    60  func NewBlockReader(logger log.Logger, storage objstore.Bucket, reg prometheus.Registerer) *BlockReader {
    61  	return &BlockReader{
    62  		log:     logger,
    63  		storage: storage,
    64  		metrics: newMetrics(reg),
    65  	}
    66  }
    67  
    68  func (b *BlockReader) Invoke(
    69  	ctx context.Context,
    70  	req *queryv1.InvokeRequest,
    71  ) (*queryv1.InvokeResponse, error) {
    72  	span, ctx := opentracing.StartSpanFromContext(ctx, "BlockReader.Invoke")
    73  	defer span.Finish()
    74  	r, err := validateRequest(req)
    75  	if err != nil {
    76  		return nil, status.Errorf(codes.InvalidArgument, "request validation failed: %v", err)
    77  	}
    78  	r.setTraceTags(span)
    79  
    80  	g, ctx := errgroup.WithContext(ctx)
    81  	agg := newAggregator(req)
    82  
    83  	tenantMap := make(map[string]struct{})
    84  	for _, tenant := range req.Tenant {
    85  		tenantMap[tenant] = struct{}{}
    86  	}
    87  
    88  	for _, md := range req.QueryPlan.Root.Blocks {
    89  		md.Datasets, err = filterNotOwnedDatasets(md, tenantMap)
    90  		if err != nil {
    91  			b.metrics.datasetTenantIsolationFailure.Inc()
    92  			traceId, _ := tracing.ExtractTraceID(ctx)
    93  			level.Error(b.log).Log("msg", "trying to query datasets of other tenants", "valid-tenant", strings.Join(req.Tenant, ","), "block", md.Id, "err", err, "traceId", traceId)
    94  		}
    95  		if len(md.Datasets) == 0 {
    96  			continue
    97  		}
    98  		obj := block.NewObject(b.storage, md)
    99  		g.Go(util.RecoverPanic((&blockContext{
   100  			ctx: ctx,
   101  			log: b.log,
   102  			req: r,
   103  			agg: agg,
   104  			obj: obj,
   105  			grp: g,
   106  		}).execute))
   107  	}
   108  
   109  	if err = g.Wait(); err != nil {
   110  		return nil, err
   111  	}
   112  	return agg.response()
   113  }
   114  
   115  type request struct {
   116  	src       *queryv1.InvokeRequest
   117  	matchers  []*labels.Matcher
   118  	startTime int64 // Unix nano.
   119  	endTime   int64 // Unix nano.
   120  }
   121  
   122  func (r *request) setTraceTags(span opentracing.Span) {
   123  	if r.src == nil {
   124  		return
   125  	}
   126  	span.SetTag("start_time", model.Time(r.src.StartTime).Time().String())
   127  	span.SetTag("end_time", model.Time(r.src.EndTime).Time().String())
   128  	span.SetTag("matchers", r.src.LabelSelector)
   129  
   130  	if len(r.src.Query) > 0 {
   131  		queryTypes := make([]string, 0, len(r.src.Query))
   132  		for _, q := range r.src.Query {
   133  			queryTypes = append(queryTypes, q.QueryType.String())
   134  		}
   135  		span.SetTag("query_types", queryTypes)
   136  	}
   137  }
   138  
   139  func validateRequest(req *queryv1.InvokeRequest) (*request, error) {
   140  	if len(req.Query) == 0 {
   141  		return nil, fmt.Errorf("no query provided")
   142  	}
   143  	if req.QueryPlan == nil || len(req.QueryPlan.Root.Blocks) == 0 {
   144  		return nil, fmt.Errorf("no blocks to query")
   145  	}
   146  	if len(req.Tenant) == 0 {
   147  		return nil, fmt.Errorf("no tenant provided")
   148  	}
   149  	matchers, err := parser.ParseMetricSelector(req.LabelSelector)
   150  	if err != nil {
   151  		return nil, fmt.Errorf("label selection is invalid: %w", err)
   152  	}
   153  	r := request{
   154  		src:       req,
   155  		matchers:  matchers,
   156  		startTime: model.Time(req.StartTime).UnixNano(),
   157  		endTime:   model.Time(req.EndTime).UnixNano(),
   158  	}
   159  	return &r, nil
   160  }
   161  
   162  // While the metastore is expected to already filter datasets of other tenants, we do an additional check to avoid
   163  // processing blocks or datasets belonging to the wrong tenant.
   164  func filterNotOwnedDatasets(b *metastorev1.BlockMeta, tenantMap map[string]struct{}) ([]*metastorev1.Dataset, error) {
   165  	errs := multierror.New()
   166  	datasets := make([]*metastorev1.Dataset, 0)
   167  	for _, dataset := range b.Datasets {
   168  		datasetTenant := b.StringTable[dataset.Tenant]
   169  		_, ok := tenantMap[datasetTenant]
   170  		if ok {
   171  			datasets = append(datasets, dataset)
   172  		} else {
   173  			errs.Add(fmt.Errorf(`dataset "%s" belongs to tenant "%s"`, b.StringTable[dataset.Name], datasetTenant))
   174  		}
   175  	}
   176  	return datasets, errs.Err()
   177  }