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

     1  package querier
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"connectrpc.com/connect"
    14  	"github.com/google/pprof/profile"
    15  	"github.com/prometheus/common/model"
    16  	"github.com/prometheus/prometheus/model/labels"
    17  	"github.com/prometheus/prometheus/promql/parser"
    18  	"golang.org/x/sync/errgroup"
    19  
    20  	profilev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    21  	querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
    22  	"github.com/grafana/pyroscope/api/gen/proto/go/querier/v1/querierv1connect"
    23  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    24  	"github.com/grafana/pyroscope/pkg/frontend/dot/graph"
    25  	"github.com/grafana/pyroscope/pkg/frontend/dot/report"
    26  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    27  	"github.com/grafana/pyroscope/pkg/og/structs/flamebearer"
    28  	"github.com/grafana/pyroscope/pkg/og/util/attime"
    29  	"github.com/grafana/pyroscope/pkg/querier/timeline"
    30  	httputil "github.com/grafana/pyroscope/pkg/util/http"
    31  )
    32  
    33  func NewHTTPHandlers(client querierv1connect.QuerierServiceClient) *QueryHandlers {
    34  	return &QueryHandlers{client}
    35  }
    36  
    37  type QueryHandlers struct {
    38  	client querierv1connect.QuerierServiceClient
    39  }
    40  
    41  // LabelValues only returns the label values for the given label name.
    42  // This is mostly for fulfilling the pyroscope API and won't be used in the future.
    43  // For example, /label-values?label=__name__ will return all the profile types.
    44  func (q *QueryHandlers) LabelValues(w http.ResponseWriter, req *http.Request) {
    45  	label := req.URL.Query().Get("label")
    46  	if label == "" {
    47  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, errors.New("label parameter is required")))
    48  		return
    49  	}
    50  	var res []string
    51  
    52  	if label == "__name__" {
    53  		response, err := q.client.ProfileTypes(req.Context(), connect.NewRequest(&querierv1.ProfileTypesRequest{}))
    54  		if err != nil {
    55  			httputil.Error(w, err)
    56  			return
    57  		}
    58  		for _, t := range response.Msg.ProfileTypes {
    59  			res = append(res, t.ID)
    60  		}
    61  	} else {
    62  		response, err := q.client.LabelValues(req.Context(), connect.NewRequest(&typesv1.LabelValuesRequest{}))
    63  		if err != nil {
    64  			httputil.Error(w, err)
    65  			return
    66  		}
    67  		res = response.Msg.Names
    68  	}
    69  
    70  	w.Header().Add("Content-Type", "application/json")
    71  	if err := json.NewEncoder(w).Encode(res); err != nil {
    72  		httputil.Error(w, err)
    73  		return
    74  	}
    75  }
    76  
    77  func (q *QueryHandlers) RenderDiff(w http.ResponseWriter, req *http.Request) {
    78  	if err := req.ParseForm(); err != nil {
    79  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, err))
    80  		return
    81  	}
    82  
    83  	// Left
    84  	leftSelectParams, leftProfileType, err := parseSelectProfilesRequest(renderRequestFieldNames{
    85  		query: "leftQuery",
    86  		from:  "leftFrom",
    87  		until: "leftUntil",
    88  	}, req)
    89  	if err != nil {
    90  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, err))
    91  		return
    92  	}
    93  
    94  	rightSelectParams, rightProfileType, err := parseSelectProfilesRequest(renderRequestFieldNames{
    95  		query: "rightQuery",
    96  		from:  "rightFrom",
    97  		until: "rightUntil",
    98  	}, req)
    99  	if err != nil {
   100  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, err))
   101  		return
   102  	}
   103  	// TODO: check profile types?
   104  	if leftProfileType.ID != rightProfileType.ID {
   105  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, errors.New("profile types must match")))
   106  		return
   107  	}
   108  
   109  	res, err := q.client.Diff(req.Context(), connect.NewRequest(&querierv1.DiffRequest{
   110  		Left:  leftSelectParams,
   111  		Right: rightSelectParams,
   112  	}))
   113  	if err != nil {
   114  		httputil.Error(w, err)
   115  		return
   116  	}
   117  
   118  	w.Header().Add("Content-Type", "application/json")
   119  	if err := json.NewEncoder(w).Encode(phlaremodel.ExportDiffToFlamebearer(res.Msg.Flamegraph, leftProfileType)); err != nil {
   120  		httputil.Error(w, err)
   121  		return
   122  	}
   123  }
   124  
   125  func (q *QueryHandlers) Render(w http.ResponseWriter, req *http.Request) {
   126  	if err := req.ParseForm(); err != nil {
   127  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, err))
   128  		return
   129  	}
   130  	selectParams, profileType, err := parseSelectProfilesRequest(renderRequestFieldNames{}, req)
   131  	if err != nil {
   132  		httputil.Error(w, connect.NewError(connect.CodeInvalidArgument, err))
   133  		return
   134  	}
   135  
   136  	groupBy := req.URL.Query()["groupBy"]
   137  	var aggregation typesv1.TimeSeriesAggregationType
   138  	if req.URL.Query().Has("aggregation") {
   139  		aggregationParam := req.URL.Query().Get("aggregation")
   140  		switch aggregationParam {
   141  		case "sum":
   142  			aggregation = typesv1.TimeSeriesAggregationType_TIME_SERIES_AGGREGATION_TYPE_SUM
   143  		case "avg":
   144  			aggregation = typesv1.TimeSeriesAggregationType_TIME_SERIES_AGGREGATION_TYPE_AVERAGE
   145  		}
   146  	}
   147  
   148  	format := req.URL.Query().Get("format")
   149  	if format == "dot" {
   150  		// We probably should distinguish max nodes of the source pprof
   151  		// profile and max nodes value for the output profile in dot format.
   152  		sourceProfileMaxNodes := int64(512)
   153  		dotProfileMaxNodes := int64(100)
   154  		if selectParams.MaxNodes != nil {
   155  			if v := *selectParams.MaxNodes; v > 0 {
   156  				dotProfileMaxNodes = v
   157  			}
   158  			if dotProfileMaxNodes > sourceProfileMaxNodes {
   159  				sourceProfileMaxNodes = dotProfileMaxNodes
   160  			}
   161  		}
   162  		resp, err := q.client.SelectMergeProfile(req.Context(), connect.NewRequest(&querierv1.SelectMergeProfileRequest{
   163  			Start:         selectParams.Start,
   164  			End:           selectParams.End,
   165  			ProfileTypeID: selectParams.ProfileTypeID,
   166  			LabelSelector: selectParams.LabelSelector,
   167  			MaxNodes:      &sourceProfileMaxNodes,
   168  		}))
   169  		if err != nil {
   170  			httputil.Error(w, err)
   171  			return
   172  		}
   173  		// Check if profile has any data - return empty string if no data
   174  		if resp.Msg == nil || len(resp.Msg.Sample) == 0 {
   175  			w.Header().Set("Content-Type", "text/plain")
   176  			w.WriteHeader(http.StatusOK)
   177  			return
   178  		}
   179  		if err = pprofToDotProfile(w, resp.Msg, int(dotProfileMaxNodes)); err != nil {
   180  			httputil.Error(w, err)
   181  		}
   182  		return
   183  	}
   184  
   185  	var resFlame *connect.Response[querierv1.SelectMergeStacktracesResponse]
   186  	g, gCtx := errgroup.WithContext(req.Context())
   187  	selectParamsClone := selectParams.CloneVT()
   188  	g.Go(func() error {
   189  		var err error
   190  		resFlame, err = q.client.SelectMergeStacktraces(gCtx, connect.NewRequest(selectParamsClone))
   191  		return err
   192  	})
   193  
   194  	timelineStep := timeline.CalcPointInterval(selectParams.Start, selectParams.End)
   195  	var resSeries *connect.Response[querierv1.SelectSeriesResponse]
   196  	g.Go(func() error {
   197  		var err error
   198  		resSeries, err = q.client.SelectSeries(req.Context(),
   199  			connect.NewRequest(&querierv1.SelectSeriesRequest{
   200  				ProfileTypeID: selectParams.ProfileTypeID,
   201  				LabelSelector: selectParams.LabelSelector,
   202  				Start:         selectParams.Start,
   203  				End:           selectParams.End,
   204  				Step:          timelineStep,
   205  				GroupBy:       groupBy,
   206  				Aggregation:   &aggregation,
   207  			}))
   208  
   209  		return err
   210  	})
   211  
   212  	err = g.Wait()
   213  	if err != nil {
   214  		httputil.Error(w, err)
   215  		return
   216  	}
   217  
   218  	seriesVal := &typesv1.Series{}
   219  	if len(resSeries.Msg.Series) == 1 {
   220  		seriesVal = resSeries.Msg.Series[0]
   221  	}
   222  
   223  	fb := phlaremodel.ExportToFlamebearer(resFlame.Msg.Flamegraph, profileType)
   224  	fb.Timeline = timeline.New(seriesVal, selectParams.Start, selectParams.End, int64(timelineStep))
   225  
   226  	if len(groupBy) > 0 {
   227  		fb.Groups = make(map[string]*flamebearer.FlamebearerTimelineV1)
   228  		for _, s := range resSeries.Msg.Series {
   229  			key := "*"
   230  			for _, l := range s.Labels {
   231  				// right now we only support one group by
   232  				if l.Name == groupBy[0] {
   233  					key = l.Value
   234  					break
   235  				}
   236  			}
   237  			fb.Groups[key] = timeline.New(s, selectParams.Start, selectParams.End, int64(timelineStep))
   238  		}
   239  	}
   240  
   241  	w.Header().Add("Content-Type", "application/json")
   242  	if err := json.NewEncoder(w).Encode(fb); err != nil {
   243  		httputil.Error(w, err)
   244  		return
   245  	}
   246  }
   247  
   248  func pprofToDotProfile(w io.Writer, p *profilev1.Profile, maxNodes int) error {
   249  	data, err := p.MarshalVT()
   250  	if err != nil {
   251  		return connect.NewError(connect.CodeInternal, err)
   252  	}
   253  	pr, err := profile.ParseData(data)
   254  	if err != nil {
   255  		return connect.NewError(connect.CodeInternal, err)
   256  	}
   257  	rpt := report.NewDefault(pr, report.Options{NodeCount: maxNodes})
   258  	gr, cfg := report.GetDOT(rpt)
   259  	graph.ComposeDot(w, gr, &graph.DotAttributes{}, cfg)
   260  	return nil
   261  }
   262  
   263  type renderRequestFieldNames struct {
   264  	query string
   265  	from  string
   266  	until string
   267  }
   268  
   269  // render/render?format=json&from=now-12h&until=now&query=pyroscope.server.cpu
   270  func parseSelectProfilesRequest(fieldNames renderRequestFieldNames, req *http.Request) (*querierv1.SelectMergeStacktracesRequest, *typesv1.ProfileType, error) {
   271  	if fieldNames == (renderRequestFieldNames{}) {
   272  		fieldNames = renderRequestFieldNames{
   273  			query: "query",
   274  			from:  "from",
   275  			until: "until",
   276  		}
   277  	}
   278  	selector, ptype, err := parseQuery(fieldNames.query, req)
   279  	if err != nil {
   280  		return nil, nil, err
   281  	}
   282  
   283  	v := req.URL.Query()
   284  
   285  	from := time.Now()
   286  	if f := v.Get(fieldNames.from); f != "" {
   287  		from = attime.Parse(f)
   288  	}
   289  	until := time.Now()
   290  	if u := v.Get(fieldNames.until); u != "" {
   291  		until = attime.Parse(u)
   292  	}
   293  
   294  	start := model.TimeFromUnixNano(from.UnixNano())
   295  	end := model.TimeFromUnixNano(until.UnixNano())
   296  
   297  	p := &querierv1.SelectMergeStacktracesRequest{
   298  		Start:         int64(start),
   299  		End:           int64(end),
   300  		LabelSelector: selector,
   301  		ProfileTypeID: ptype.ID,
   302  	}
   303  
   304  	var mn int64
   305  	if v, err := strconv.Atoi(v.Get("max-nodes")); err == nil && v != 0 {
   306  		mn = int64(v)
   307  	}
   308  	if v, err := strconv.Atoi(v.Get("maxNodes")); err == nil && v != 0 {
   309  		mn = int64(v)
   310  	}
   311  	p.MaxNodes = &mn
   312  
   313  	return p, ptype, nil
   314  }
   315  
   316  func parseQuery(fieldName string, req *http.Request) (string, *typesv1.ProfileType, error) {
   317  	q := req.Form.Get(fieldName)
   318  	if q == "" {
   319  		return "", nil, fmt.Errorf("%q is required", fieldName)
   320  	}
   321  
   322  	parsedSelector, err := parser.ParseMetricSelector(q)
   323  	if err != nil {
   324  		return "", nil, fmt.Errorf("failed to parse %q: %w", fieldName, err)
   325  	}
   326  
   327  	sel := make([]*labels.Matcher, 0, len(parsedSelector))
   328  	var nameLabel *labels.Matcher
   329  	for _, matcher := range parsedSelector {
   330  		if matcher.Name == labels.MetricName {
   331  			nameLabel = matcher
   332  		} else {
   333  			sel = append(sel, matcher)
   334  		}
   335  	}
   336  	if nameLabel == nil {
   337  		return "", nil, fmt.Errorf("%q must contain a profile-type selection", fieldName)
   338  	}
   339  
   340  	profileSelector, err := phlaremodel.ParseProfileTypeSelector(nameLabel.Value)
   341  	if err != nil {
   342  		return "", nil, fmt.Errorf("failed to parse %q", fieldName)
   343  	}
   344  	return convertMatchersToString(sel), profileSelector, nil
   345  }
   346  
   347  func convertMatchersToString(matchers []*labels.Matcher) string {
   348  	out := strings.Builder{}
   349  	out.WriteRune('{')
   350  
   351  	for idx, m := range matchers {
   352  		if idx > 0 {
   353  			out.WriteRune(',')
   354  		}
   355  
   356  		out.WriteString(m.String())
   357  	}
   358  
   359  	out.WriteRune('}')
   360  	return out.String()
   361  }