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(¶ms.From) 88 queryCmd.Flag("to", "End of the query.").Default("now").StringVar(¶ms.To) 89 queryCmd.Flag("query", "Label selector to query.").Default("{}").StringVar(¶ms.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(¶ms.ProfileType) 104 queryCmd.Flag("stacktrace-selector", "Only query locations with those symbols. Provide multiple times starting with the root").StringsVar(¶ms.StacktraceSelector) 105 queryCmd.Flag("max-nodes", "Maximum number of nodes to return in the profile").Int64Var(¶ms.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 = ¶ms.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(¶ms.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(¶ms.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(¶ms.LabelNames) 207 queryCmd.Flag("api-type", "Which API type to query (querier, ingester or store-gateway).").Default("querier").StringVar(¶ms.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(¶ms.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 }