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 }