Skip to content
This repository was archived by the owner on Jul 19, 2023. It is now read-only.

Commit 2f03659

Browse files
authored
Add query subcommand to profilecli for downloading pprof from phlare (#475)
* Add query subcommand to download pprof from phlare Usage: ``` $ profilecli query merge --from now-5m --output pprof=my.pprof $ profilecli query merge --from now-5m --profile-type memory:alloc_space:bytes:space:bytes &googlev1.Profile{ SampleType: []*googlev1.ValueType{ &googlev1.ValueType{ Type: 1, Unit: 2, }, }, [...] DropFrames: 0, KeepFrames: 0, TimeNanos: 1673453999823000000, DurationNanos: 300000000000, PeriodType: &googlev1.ValueType{ Type: 434, Unit: 2, }, Period: 524288, Comment: []int64(nil), DefaultSampleType: 1, } ``` * Use parsed profile output for console by default Previous output is available with --output=raw * Implement gzip compression for pprof file This will also change the behaviour, when create a pprof file, it will fail when the file already exists.
1 parent 43309e4 commit 2f03659

File tree

4 files changed

+206
-1
lines changed

4 files changed

+206
-1
lines changed

cmd/profilecli/main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ func main() {
4848
parquetInspectCmd := parquetCmd.Command("inspect", "Inspect a parquet file's structure.")
4949
parquetInspectFiles := parquetInspectCmd.Arg("file", "parquet file path").Required().ExistingFiles()
5050

51+
queryCmd := app.Command("query", "Query profile store.")
52+
queryParams := addQueryParams(queryCmd)
53+
queryOutput := queryCmd.Flag("output", "How to output the result, examples: console, raw, pprof=./my.pprof").Default("console").String()
54+
queryMergeCmd := queryCmd.Command("merge", "Request merged profile.")
55+
56+
// parse command line arguments
5157
parsedCmd := kingpin.MustParse(app.Parse(os.Args[1:]))
5258

59+
// enable verbose logging if requested
5360
if !cfg.verbose {
5461
logger = level.NewFilter(logger, level.AllowWarn())
5562
}
@@ -63,7 +70,14 @@ func main() {
6370
os.Exit(checkError(err))
6471
}
6572
}
73+
case queryMergeCmd.FullCommand():
74+
if err := queryMerge(ctx, queryParams, *queryOutput); err != nil {
75+
os.Exit(checkError(err))
76+
}
77+
default:
78+
level.Error(logger).Log("msg", "unknown command", "cmd", parsedCmd)
6679
}
80+
6781
}
6882

6983
func checkError(err error) int {

cmd/profilecli/query.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"strings"
11+
"time"
12+
13+
"github.com/bufbuild/connect-go"
14+
"github.com/go-kit/log/level"
15+
gprofile "github.com/google/pprof/profile"
16+
"github.com/grafana/dskit/runutil"
17+
"github.com/k0kubun/pp/v3"
18+
"github.com/klauspost/compress/gzip"
19+
"github.com/mattn/go-isatty"
20+
"github.com/pkg/errors"
21+
"github.com/prometheus/common/model"
22+
"gopkg.in/alecthomas/kingpin.v2"
23+
24+
querierv1 "github.com/grafana/phlare/api/gen/proto/go/querier/v1"
25+
"github.com/grafana/phlare/api/gen/proto/go/querier/v1/querierv1connect"
26+
)
27+
28+
const (
29+
outputConsole = "console"
30+
outputRaw = "raw"
31+
outputPprof = "pprof="
32+
)
33+
34+
func parseTime(s string) (time.Time, error) {
35+
if s == "" {
36+
return time.Time{}, fmt.Errorf("empty time")
37+
}
38+
t, err := time.Parse(time.RFC3339, s)
39+
if err == nil {
40+
return t, nil
41+
}
42+
43+
// try if it is a relative time
44+
d, rerr := parseRelativeTime(s)
45+
if rerr == nil {
46+
return time.Now().Add(-d), nil
47+
}
48+
49+
// if not return first error
50+
return time.Time{}, err
51+
52+
}
53+
54+
func parseRelativeTime(s string) (time.Duration, error) {
55+
s = strings.TrimSpace(s)
56+
if s == "now" {
57+
return 0, nil
58+
}
59+
s = strings.TrimPrefix(s, "now-")
60+
61+
d, err := model.ParseDuration(s)
62+
if err != nil {
63+
return 0, err
64+
}
65+
return time.Duration(d), nil
66+
}
67+
68+
type queryParams struct {
69+
URL string
70+
From string
71+
To string
72+
ProfileType string
73+
Query string
74+
}
75+
76+
func (p *queryParams) parseFromTo() (from time.Time, to time.Time, err error) {
77+
from, err = parseTime(p.From)
78+
if err != nil {
79+
return time.Time{}, time.Time{}, errors.Wrap(err, "failed to parse from")
80+
}
81+
to, err = parseTime(p.To)
82+
if err != nil {
83+
return time.Time{}, time.Time{}, errors.Wrap(err, "failed to parse to")
84+
}
85+
86+
if to.Before(from) {
87+
return time.Time{}, time.Time{}, errors.Wrap(err, "from cannot be after")
88+
}
89+
90+
return from, to, nil
91+
}
92+
93+
func (p *queryParams) client() querierv1connect.QuerierServiceClient {
94+
return querierv1connect.NewQuerierServiceClient(
95+
http.DefaultClient,
96+
p.URL,
97+
)
98+
}
99+
100+
type flagger interface {
101+
Flag(name, help string) *kingpin.FlagClause
102+
}
103+
104+
func addQueryParams(queryCmd flagger) *queryParams {
105+
params := &queryParams{}
106+
queryCmd.Flag("url", "URL of the profile store.").Default("http://localhost:4100").StringVar(&params.URL)
107+
queryCmd.Flag("from", "Beginning of the query.").Default("now-1h").StringVar(&params.From)
108+
queryCmd.Flag("to", "End of the query.").Default("now").StringVar(&params.To)
109+
queryCmd.Flag("profile-type", "Profile type to query.").Default("process_cpu:cpu:nanoseconds:cpu:nanoseconds").StringVar(&params.ProfileType)
110+
queryCmd.Flag("query", "Label selector to query.").Default("{}").StringVar(&params.Query)
111+
return params
112+
}
113+
114+
func queryMerge(ctx context.Context, params *queryParams, outputFlag string) (err error) {
115+
from, to, err := params.parseFromTo()
116+
if err != nil {
117+
return err
118+
}
119+
120+
level.Info(logger).Log("msg", "query aggregated profile from profile store", "url", params.URL, "from", from, "to", to, "query", params.Query, "type", params.ProfileType)
121+
122+
qc := params.client()
123+
124+
resp, err := qc.SelectMergeProfile(ctx, connect.NewRequest(&querierv1.SelectMergeProfileRequest{
125+
ProfileTypeID: params.ProfileType,
126+
Start: from.UnixMilli(),
127+
End: to.UnixMilli(),
128+
LabelSelector: params.Query,
129+
}))
130+
131+
if err != nil {
132+
return errors.Wrap(err, "failed to query")
133+
}
134+
135+
mypp := pp.New()
136+
mypp.SetColoringEnabled(isatty.IsTerminal(os.Stdout.Fd()))
137+
mypp.SetExportedOnly(true)
138+
139+
if outputFlag == outputConsole {
140+
buf, err := resp.Msg.MarshalVT()
141+
if err != nil {
142+
return errors.Wrap(err, "failed to marshal protobuf")
143+
}
144+
145+
p, err := gprofile.Parse(bytes.NewReader(buf))
146+
if err != nil {
147+
return errors.Wrap(err, "failed to parse profile")
148+
}
149+
150+
fmt.Fprintln(output(ctx), p.String())
151+
return nil
152+
153+
}
154+
155+
if outputFlag == outputRaw {
156+
mypp.Print(resp.Msg)
157+
return nil
158+
}
159+
160+
if strings.HasPrefix(outputFlag, outputPprof) {
161+
filePath := strings.TrimPrefix(outputFlag, outputPprof)
162+
if filePath == "" {
163+
return errors.New("no file path specified after pprof=")
164+
}
165+
buf, err := resp.Msg.MarshalVT()
166+
if err != nil {
167+
return errors.Wrap(err, "failed to marshal protobuf")
168+
}
169+
170+
// open new file, fail when the file already exists
171+
f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
172+
if err != nil {
173+
return errors.Wrap(err, "failed to create pprof file")
174+
}
175+
defer runutil.CloseWithErrCapture(&err, f, "failed to close pprof file")
176+
177+
gzipWriter := gzip.NewWriter(f)
178+
defer runutil.CloseWithErrCapture(&err, gzipWriter, "failed to close pprof gzip writer")
179+
180+
if _, err := io.Copy(gzipWriter, bytes.NewReader(buf)); err != nil {
181+
return errors.Wrap(err, "failed to write pprof")
182+
}
183+
184+
return nil
185+
}
186+
187+
return errors.Errorf("unknown output %s", outputFlag)
188+
}

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ require (
2323
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
2424
github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.0
2525
github.com/json-iterator/go v1.1.12
26+
github.com/k0kubun/pp/v3 v3.2.0
2627
github.com/klauspost/compress v1.15.13
28+
github.com/mattn/go-isatty v0.0.16
2729
github.com/minio/minio-go/v7 v7.0.45
2830
github.com/mitchellh/go-wordwrap v1.0.1
2931
github.com/oklog/ulid v1.3.1
@@ -177,7 +179,6 @@ require (
177179
github.com/linode/linodego v1.9.3 // indirect
178180
github.com/mailru/easyjson v0.7.7 // indirect
179181
github.com/mattn/go-colorable v0.1.13 // indirect
180-
github.com/mattn/go-isatty v0.0.16 // indirect
181182
github.com/mattn/go-runewidth v0.0.14 // indirect
182183
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
183184
github.com/miekg/dns v1.1.50 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
584584
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
585585
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
586586
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
587+
github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs=
588+
github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA=
587589
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
588590
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
589591
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=

0 commit comments

Comments
 (0)