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 }