github.com/grafana/pyroscope@v1.18.0/cmd/profilecli/query.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"time"
     8  
     9  	"connectrpc.com/connect"
    10  	"github.com/dustin/go-humanize"
    11  	"github.com/go-kit/log/level"
    12  	"github.com/olekukonko/tablewriter"
    13  	"github.com/pkg/errors"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	ingestv1 "github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1"
    17  	"github.com/grafana/pyroscope/api/gen/proto/go/ingester/v1/ingesterv1connect"
    18  	querierv1 "github.com/grafana/pyroscope/api/gen/proto/go/querier/v1"
    19  	"github.com/grafana/pyroscope/api/gen/proto/go/querier/v1/querierv1connect"
    20  	"github.com/grafana/pyroscope/api/gen/proto/go/storegateway/v1/storegatewayv1connect"
    21  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    22  	connectapi "github.com/grafana/pyroscope/pkg/api/connect"
    23  	"github.com/grafana/pyroscope/pkg/operations"
    24  )
    25  
    26  func (c *phlareClient) queryClient() querierv1connect.QuerierServiceClient {
    27  	return querierv1connect.NewQuerierServiceClient(
    28  		c.httpClient(),
    29  		c.URL,
    30  		append(
    31  			connectapi.DefaultClientOptions(),
    32  			c.protocolOption(),
    33  		)...,
    34  	)
    35  }
    36  
    37  func (c *phlareClient) storeGatewayClient() storegatewayv1connect.StoreGatewayServiceClient {
    38  	return storegatewayv1connect.NewStoreGatewayServiceClient(
    39  		c.httpClient(),
    40  		c.URL,
    41  		append(
    42  			connectapi.DefaultClientOptions(),
    43  			c.protocolOption(),
    44  		)...,
    45  	)
    46  }
    47  
    48  func (c *phlareClient) ingesterClient() ingesterv1connect.IngesterServiceClient {
    49  	return ingesterv1connect.NewIngesterServiceClient(
    50  		c.httpClient(),
    51  		c.URL,
    52  		append(
    53  			connectapi.DefaultClientOptions(),
    54  			c.protocolOption(),
    55  		)...,
    56  	)
    57  }
    58  
    59  type queryParams struct {
    60  	*phlareClient
    61  	From  string
    62  	To    string
    63  	Query string
    64  }
    65  
    66  func (p *queryParams) parseFromTo() (from time.Time, to time.Time, err error) {
    67  	from, err = operations.ParseTime(p.From)
    68  	if err != nil {
    69  		return time.Time{}, time.Time{}, errors.Wrap(err, "failed to parse from")
    70  	}
    71  	to, err = operations.ParseTime(p.To)
    72  	if err != nil {
    73  		return time.Time{}, time.Time{}, errors.Wrap(err, "failed to parse to")
    74  	}
    75  
    76  	if to.Before(from) {
    77  		return time.Time{}, time.Time{}, errors.Wrap(err, "from cannot be after")
    78  	}
    79  
    80  	return from, to, nil
    81  }
    82  
    83  func addQueryParams(queryCmd commander) *queryParams {
    84  	params := new(queryParams)
    85  	params.phlareClient = addPhlareClient(queryCmd)
    86  
    87  	queryCmd.Flag("from", "Beginning of the query.").Default("now-1h").StringVar(&params.From)
    88  	queryCmd.Flag("to", "End of the query.").Default("now").StringVar(&params.To)
    89  	queryCmd.Flag("query", "Label selector to query.").Default("{}").StringVar(&params.Query)
    90  	return params
    91  }
    92  
    93  type queryProfileParams struct {
    94  	*queryParams
    95  	ProfileType        string
    96  	StacktraceSelector []string
    97  	MaxNodes           int64
    98  }
    99  
   100  func addQueryProfileParams(queryCmd commander) *queryProfileParams {
   101  	params := new(queryProfileParams)
   102  	params.queryParams = addQueryParams(queryCmd)
   103  	queryCmd.Flag("profile-type", "Profile type to query.").Default("process_cpu:cpu:nanoseconds:cpu:nanoseconds").StringVar(&params.ProfileType)
   104  	queryCmd.Flag("stacktrace-selector", "Only query locations with those symbols. Provide multiple times starting with the root").StringsVar(&params.StacktraceSelector)
   105  	queryCmd.Flag("max-nodes", "Maximum number of nodes to return in the profile").Int64Var(&params.MaxNodes)
   106  	return params
   107  }
   108  
   109  func queryProfile(ctx context.Context, params *queryProfileParams, outputFlag string) (err error) {
   110  	from, to, err := params.parseFromTo()
   111  	if err != nil {
   112  		return err
   113  	}
   114  	level.Info(logger).Log("msg", "query aggregated profile from profile store", "url", params.URL, "from", from, "to", to, "query", params.Query, "type", params.ProfileType)
   115  
   116  	req := &querierv1.SelectMergeProfileRequest{
   117  		ProfileTypeID: params.ProfileType,
   118  		Start:         from.UnixMilli(),
   119  		End:           to.UnixMilli(),
   120  		LabelSelector: params.Query,
   121  	}
   122  
   123  	if params.MaxNodes > 0 {
   124  		req.MaxNodes = &params.MaxNodes
   125  	}
   126  
   127  	if len(params.StacktraceSelector) > 0 {
   128  		locations := make([]*typesv1.Location, 0, len(params.StacktraceSelector))
   129  		for _, cs := range params.StacktraceSelector {
   130  			locations = append(locations, &typesv1.Location{
   131  				Name: cs,
   132  			})
   133  		}
   134  		req.StackTraceSelector = &typesv1.StackTraceSelector{
   135  			CallSite: locations,
   136  		}
   137  		level.Info(logger).Log("msg", "selecting with stackstrace selector", "call-site", fmt.Sprintf("%#+v", params.StacktraceSelector))
   138  	}
   139  
   140  	return selectMergeProfile(ctx, params.phlareClient, outputFlag, req)
   141  }
   142  
   143  func selectMergeProfile(ctx context.Context, client *phlareClient, outputFlag string, req *querierv1.SelectMergeProfileRequest) error {
   144  	qc := client.queryClient()
   145  	resp, err := qc.SelectMergeProfile(ctx, connect.NewRequest(req))
   146  	if err != nil {
   147  		return errors.Wrap(err, "failed to query")
   148  	}
   149  
   150  	return outputMergeProfile(ctx, outputFlag, resp.Msg)
   151  }
   152  
   153  type queryGoPGOParams struct {
   154  	*queryProfileParams
   155  	KeepLocations    uint32
   156  	AggregateCallees bool
   157  }
   158  
   159  func addQueryGoPGOParams(queryCmd commander) *queryGoPGOParams {
   160  	params := new(queryGoPGOParams)
   161  	params.queryProfileParams = addQueryProfileParams(queryCmd)
   162  	queryCmd.Flag("keep-locations", "Number of leaf locations to keep.").Default("5").Uint32Var(&params.KeepLocations)
   163  	queryCmd.Flag("aggregate-callees", "Default: true. Aggregate samples for the same callee by ignoring the line numbers in the leaf locations. Use --aggregate-callees to enable or --no-aggregate-callees to disable.").Default("true").BoolVar(&params.AggregateCallees)
   164  	return params
   165  }
   166  
   167  func queryGoPGO(ctx context.Context, params *queryGoPGOParams, outputFlag string) (err error) {
   168  	from, to, err := params.parseFromTo()
   169  	if err != nil {
   170  		return err
   171  	}
   172  	level.Info(logger).Log("msg", "querying pprof profile for Go PGO",
   173  		"url", params.URL,
   174  		"query", params.Query,
   175  		"from", from,
   176  		"to", to,
   177  		"type", params.ProfileType,
   178  		"output", outputFlag,
   179  		"keep-locations", params.KeepLocations,
   180  		"aggregate-callees", params.AggregateCallees,
   181  	)
   182  	return selectMergeProfile(ctx, params.phlareClient, outputFlag,
   183  		&querierv1.SelectMergeProfileRequest{
   184  			ProfileTypeID: params.ProfileType,
   185  			Start:         from.UnixMilli(),
   186  			End:           to.UnixMilli(),
   187  			LabelSelector: params.Query,
   188  			StackTraceSelector: &typesv1.StackTraceSelector{
   189  				GoPgo: &typesv1.GoPGO{
   190  					KeepLocations:    params.KeepLocations,
   191  					AggregateCallees: params.AggregateCallees,
   192  				},
   193  			},
   194  		})
   195  }
   196  
   197  type querySeriesParams struct {
   198  	*queryParams
   199  	LabelNames []string
   200  	APIType    string
   201  }
   202  
   203  func addQuerySeriesParams(queryCmd commander) *querySeriesParams {
   204  	params := new(querySeriesParams)
   205  	params.queryParams = addQueryParams(queryCmd)
   206  	queryCmd.Flag("label-names", "Filter returned labels to the supplied label names. Without any filter all labels are returned.").StringsVar(&params.LabelNames)
   207  	queryCmd.Flag("api-type", "Which API type to query (querier, ingester or store-gateway).").Default("querier").StringVar(&params.APIType)
   208  	return params
   209  }
   210  
   211  func querySeries(ctx context.Context, params *querySeriesParams) (err error) {
   212  	from, to, err := params.parseFromTo()
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	level.Info(logger).Log("msg", fmt.Sprintf("query series from %s", params.APIType), "url", params.URL, "from", from, "to", to, "labelNames", fmt.Sprintf("%q", params.LabelNames))
   218  
   219  	var result []*typesv1.Labels
   220  	switch params.APIType {
   221  	case "querier":
   222  		qc := params.phlareClient.queryClient()
   223  		resp, err := qc.Series(ctx, connect.NewRequest(&querierv1.SeriesRequest{
   224  			Start:      from.UnixMilli(),
   225  			End:        to.UnixMilli(),
   226  			Matchers:   []string{params.Query},
   227  			LabelNames: params.LabelNames,
   228  		}))
   229  		if err != nil {
   230  			return errors.Wrap(err, "failed to query")
   231  		}
   232  		result = resp.Msg.LabelsSet
   233  	case "ingester":
   234  		ic := params.phlareClient.ingesterClient()
   235  		resp, err := ic.Series(ctx, connect.NewRequest(&ingestv1.SeriesRequest{
   236  			Start:      from.UnixMilli(),
   237  			End:        to.UnixMilli(),
   238  			Matchers:   []string{params.Query},
   239  			LabelNames: params.LabelNames,
   240  		}))
   241  		if err != nil {
   242  			return errors.Wrap(err, "failed to query")
   243  		}
   244  		result = resp.Msg.LabelsSet
   245  	case "store-gateway":
   246  		sc := params.phlareClient.storeGatewayClient()
   247  		resp, err := sc.Series(ctx, connect.NewRequest(&ingestv1.SeriesRequest{
   248  			Start:      from.UnixMilli(),
   249  			End:        to.UnixMilli(),
   250  			Matchers:   []string{params.Query},
   251  			LabelNames: params.LabelNames,
   252  		}))
   253  		if err != nil {
   254  			return errors.Wrap(err, "failed to query")
   255  		}
   256  		result = resp.Msg.LabelsSet
   257  	default:
   258  		return errors.Errorf("unknown api type %s", params.APIType)
   259  	}
   260  
   261  	err = outputSeries(result)
   262  	return err
   263  }
   264  
   265  type queryLabelValuesCardinalityParams struct {
   266  	*queryParams
   267  	TopN uint64
   268  }
   269  
   270  func addQueryLabelValuesCardinalityParams(queryCmd commander) *queryLabelValuesCardinalityParams {
   271  	params := new(queryLabelValuesCardinalityParams)
   272  	params.queryParams = addQueryParams(queryCmd)
   273  	queryCmd.Flag("top-n", "Show the top N high cardinality label values").Default("20").Uint64Var(&params.TopN)
   274  	return params
   275  }
   276  
   277  func queryLabelValuesCardinality(ctx context.Context, params *queryLabelValuesCardinalityParams) (err error) {
   278  	from, to, err := params.parseFromTo()
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	level.Info(logger).Log("msg", "query label names", "url", params.URL, "from", from, "to", to)
   284  
   285  	qc := params.phlareClient.queryClient()
   286  	resp, err := qc.LabelNames(ctx, connect.NewRequest(&typesv1.LabelNamesRequest{
   287  		Start:    from.UnixMilli(),
   288  		End:      to.UnixMilli(),
   289  		Matchers: []string{params.Query},
   290  	}))
   291  	if err != nil {
   292  		return errors.Wrap(err, "failed to query")
   293  	}
   294  
   295  	level.Info(logger).Log("msg", fmt.Sprintf("received %d label names", len(resp.Msg.Names)))
   296  
   297  	g, gctx := errgroup.WithContext(ctx)
   298  	g.SetLimit(8)
   299  	result := make([]struct {
   300  		count int
   301  		name  string
   302  	}, len(resp.Msg.Names))
   303  
   304  	for idx := range resp.Msg.Names {
   305  		idx := idx
   306  		g.Go(func() error {
   307  			name := resp.Msg.Names[idx]
   308  			resp, err := qc.LabelValues(gctx, connect.NewRequest(&typesv1.LabelValuesRequest{
   309  				Name:     name,
   310  				Start:    from.UnixMilli(),
   311  				End:      to.UnixMilli(),
   312  				Matchers: []string{params.Query},
   313  			}))
   314  			if err != nil {
   315  				return fmt.Errorf("failed to query label values for %s: %w", name, err)
   316  			}
   317  
   318  			result[idx].name = name
   319  			result[idx].count = len(resp.Msg.Names)
   320  
   321  			return nil
   322  		})
   323  	}
   324  	if err := g.Wait(); err != nil {
   325  		return err
   326  	}
   327  
   328  	// sort the result
   329  	sort.Slice(result, func(i, j int) bool {
   330  		return result[i].count > result[j].count
   331  	})
   332  
   333  	table := tablewriter.NewWriter(output(ctx))
   334  	table.SetHeader([]string{"LabelName", "Value count"})
   335  	if len(result) > int(params.TopN) {
   336  		result = result[:params.TopN]
   337  	}
   338  	for _, r := range result {
   339  		table.Append([]string{r.name, humanize.FormatInteger("#,###.", r.count)})
   340  	}
   341  	table.Render()
   342  
   343  	return nil
   344  }