github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/render_diff.go (about) 1 package server 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "runtime/debug" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/pyroscope-io/pyroscope/pkg/flameql" 14 "github.com/pyroscope-io/pyroscope/pkg/history" 15 "github.com/pyroscope-io/pyroscope/pkg/server/httputils" 16 "github.com/pyroscope-io/pyroscope/pkg/storage" 17 "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" 18 "github.com/pyroscope-io/pyroscope/pkg/storage/tree" 19 "github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer" 20 "github.com/pyroscope-io/pyroscope/pkg/util/attime" 21 "github.com/sirupsen/logrus" 22 ) 23 24 // RenderDiffParams refers to the params accepted by the renderDiffHandler 25 type RenderDiffParams struct { 26 LeftQuery string `json:"leftQuery"` 27 LeftFrom string `json:"leftFrom"` 28 LeftUntil string `json:"leftUntil"` 29 30 RightQuery string `json:"rightQuery"` 31 RightFrom string `json:"rightFrom"` 32 RightUntil string `json:"rightUntil"` 33 34 Format string `json:"format"` 35 MaxNodes *int `json:"maxNodes,omitempty"` 36 } 37 38 // RenderDiffResponse refers to the response of the renderDiffHandler 39 type RenderDiffResponse struct { 40 *flamebearer.FlamebearerProfile 41 Metadata renderMetadataResponse `json:"metadata"` 42 } 43 44 type diffParams struct { 45 Left storage.GetInput 46 Right storage.GetInput 47 48 Format string 49 MaxNodes int 50 } 51 52 // parseDiffQueryParams parses query params into a diffParams 53 func (rh *RenderDiffHandler) parseDiffQueryParams(r *http.Request, p *diffParams) (err error) { 54 parseDiffQueryParams := func(r *http.Request, prefix string) (gi storage.GetInput, err error) { 55 v := r.URL.Query() 56 getWithPrefix := func(param string) string { 57 return v.Get(prefix + strings.Title(param)) 58 } 59 60 // Parse query 61 qry, err := flameql.ParseQuery(getWithPrefix("query")) 62 if err != nil { 63 return gi, fmt.Errorf("%q: %+w", "Error parsing query", err) 64 } 65 gi.Query = qry 66 67 gi.StartTime = attime.Parse(getWithPrefix("from")) 68 gi.EndTime = attime.Parse(getWithPrefix("until")) 69 70 return gi, nil 71 } 72 73 p.Left, err = parseDiffQueryParams(r, "left") 74 if err != nil { 75 return fmt.Errorf("%q: %+w", "Could not parse 'left' side", err) 76 } 77 78 p.Right, err = parseDiffQueryParams(r, "right") 79 if err != nil { 80 return fmt.Errorf("%q: %+w", "Could not parse 'right' side", err) 81 } 82 83 // Parse the common fields 84 v := r.URL.Query() 85 p.MaxNodes = rh.maxNodesDefault 86 if mn, err := strconv.Atoi(v.Get("max-nodes")); err == nil && mn != 0 { 87 p.MaxNodes = mn 88 } 89 90 p.Format = v.Get("format") 91 return expectFormats(p.Format) 92 } 93 94 func (ctrl *Controller) renderDiffHandler() http.HandlerFunc { 95 return NewRenderDiffHandler(ctrl.log, ctrl.storage, ctrl.dir, ctrl, ctrl.config.MaxNodesRender, ctrl.httpUtils, ctrl.historyMgr).ServeHTTP 96 } 97 98 type RenderDiffHandler struct { 99 log *logrus.Logger 100 storage storage.Getter 101 dir http.FileSystem 102 stats StatsReceiver 103 maxNodesDefault int 104 httpUtils httputils.Utils 105 historyMgr history.Manager 106 } 107 108 //revive:disable:argument-limit TODO(petethepig): we will refactor this later 109 func NewRenderDiffHandler( 110 l *logrus.Logger, 111 s storage.Getter, 112 dir http.FileSystem, 113 stats StatsReceiver, 114 maxNodesDefault int, 115 httpUtils httputils.Utils, 116 historyMgr history.Manager, 117 ) *RenderDiffHandler { 118 return &RenderDiffHandler{ 119 log: l, 120 storage: s, 121 dir: dir, 122 stats: stats, 123 maxNodesDefault: maxNodesDefault, 124 httpUtils: httpUtils, 125 historyMgr: historyMgr, 126 } 127 } 128 129 func (rh *RenderDiffHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 130 var params diffParams 131 ctx := r.Context() 132 133 switch r.Method { 134 case http.MethodGet: 135 if err := rh.parseDiffQueryParams(r, ¶ms); err != nil { 136 rh.httpUtils.WriteInvalidParameterError(r, w, err) 137 return 138 } 139 default: 140 rh.httpUtils.WriteInvalidMethodError(r, w) 141 return 142 } 143 144 // Load Both trees 145 // TODO: do this concurrently 146 leftOut, err := rh.loadTree(ctx, ¶ms.Left, params.Left.StartTime, params.Left.EndTime) 147 if err != nil { 148 rh.httpUtils.WriteInvalidParameterError(r, w, fmt.Errorf("%q: %+w", "could not load 'left' tree", err)) 149 return 150 } 151 152 rightOut, err := rh.loadTree(ctx, ¶ms.Right, params.Right.StartTime, params.Right.EndTime) 153 if err != nil { 154 rh.httpUtils.WriteInvalidParameterError(r, w, fmt.Errorf("%q: %+w", "could not load 'right' tree", err)) 155 return 156 } 157 158 leftProfile := flamebearer.ProfileConfig{ 159 Name: "diff", 160 MaxNodes: params.MaxNodes, 161 Metadata: metadata.Metadata{ 162 SpyName: leftOut.SpyName, 163 SampleRate: leftOut.SampleRate, 164 Units: leftOut.Units, 165 }, 166 Tree: leftOut.Tree, 167 Timeline: leftOut.Timeline, 168 Groups: leftOut.Groups, 169 Telemetry: leftOut.Telemetry, 170 } 171 172 rightProfile := flamebearer.ProfileConfig{ 173 Name: "diff", 174 MaxNodes: params.MaxNodes, 175 Metadata: metadata.Metadata{ 176 SpyName: rightOut.SpyName, 177 SampleRate: rightOut.SampleRate, 178 Units: rightOut.Units, 179 }, 180 Tree: rightOut.Tree, 181 Timeline: rightOut.Timeline, 182 Groups: rightOut.Groups, 183 Telemetry: rightOut.Telemetry, 184 } 185 186 combined, err := flamebearer.NewCombinedProfile(leftProfile, rightProfile) 187 if err != nil { 188 rh.httpUtils.WriteInvalidParameterError(r, w, err) 189 return 190 } 191 192 switch params.Format { 193 case "html": 194 w.Header().Add("Content-Type", "text/html") 195 if err := flamebearer.FlamebearerToStandaloneHTML(&combined, rh.dir, w); err != nil { 196 rh.httpUtils.WriteJSONEncodeError(r, w, err) 197 return 198 } 199 200 case "json": 201 // fallthrough to default, to maintain existing behaviour 202 fallthrough 203 default: 204 md := renderMetadataResponse{FlamebearerMetadataV1: combined.Metadata} 205 rh.enhanceWithCustomFields(&md, params) 206 207 res := RenderDiffResponse{ 208 FlamebearerProfile: &combined, 209 Metadata: md, 210 } 211 212 rh.httpUtils.WriteResponseJSON(r, w, res) 213 } 214 } 215 216 //revive:disable-next-line:argument-limit 7 parameters here is fine 217 func (rh *RenderDiffHandler) loadTreeConcurrently( 218 ctx context.Context, 219 gi *storage.GetInput, 220 treeStartTime, treeEndTime time.Time, 221 leftStartTime, leftEndTime time.Time, 222 rghtStartTime, rghtEndTime time.Time, 223 ) (treeOut, leftOut, rghtOut *storage.GetOutput, _ error) { 224 var treeErr, leftErr, rghtErr error 225 var wg sync.WaitGroup 226 wg.Add(3) 227 go func() { defer wg.Done(); treeOut, treeErr = rh.loadTree(ctx, gi, treeStartTime, treeEndTime) }() 228 go func() { defer wg.Done(); leftOut, leftErr = rh.loadTree(ctx, gi, leftStartTime, leftEndTime) }() 229 go func() { defer wg.Done(); rghtOut, rghtErr = rh.loadTree(ctx, gi, rghtStartTime, rghtEndTime) }() 230 wg.Wait() 231 232 for _, err := range []error{treeErr, leftErr, rghtErr} { 233 if err != nil { 234 return nil, nil, nil, err 235 } 236 } 237 return treeOut, leftOut, rghtOut, nil 238 } 239 240 func (rh *RenderDiffHandler) loadTree(ctx context.Context, gi *storage.GetInput, startTime, endTime time.Time) (_ *storage.GetOutput, _err error) { 241 defer func() { 242 rerr := recover() 243 if rerr != nil { 244 _err = fmt.Errorf("panic: %v", rerr) 245 rh.log.WithFields(logrus.Fields{ 246 "recover": rerr, 247 "stack": string(debug.Stack()), 248 }).Error("loadTree: recovered from panic") 249 } 250 }() 251 252 _gi := *gi // clone the struct 253 _gi.StartTime, _gi.EndTime = startTime, endTime 254 out, err := rh.storage.Get(ctx, &_gi) 255 if err != nil { 256 return nil, err 257 } 258 if out == nil { 259 // TODO: handle properly 260 return &storage.GetOutput{Tree: tree.New()}, nil 261 } 262 return out, nil 263 } 264 265 // add custom fields to renderMetadataResponse 266 // original motivation is to add custom {start,end}Time calculated dynamically 267 func (rh *RenderDiffHandler) enhanceWithCustomFields(md *renderMetadataResponse, params diffParams) { 268 var diffAppName string 269 270 if params.Left.Query.AppName == params.Right.Query.AppName { 271 diffAppName = fmt.Sprintf("diff_%s_%s", params.Left.Query.AppName, params.Right.Query.AppName) 272 } else { 273 diffAppName = fmt.Sprintf("diff_%s", params.Left.Query.AppName) 274 } 275 276 startTime, endTime := rh.findStartEndTime(params.Left, params.Right) 277 278 md.AppName = diffAppName 279 md.StartTime = startTime.Unix() 280 md.EndTime = endTime.Unix() 281 // TODO: add missing fields 282 } 283 284 func (*RenderDiffHandler) findStartEndTime(left storage.GetInput, right storage.GetInput) (time.Time, time.Time) { 285 startTime := left.StartTime 286 if right.StartTime.Before(left.StartTime) { 287 startTime = right.StartTime 288 } 289 290 endTime := left.EndTime 291 if right.EndTime.After(right.EndTime) { 292 endTime = right.EndTime 293 } 294 295 return startTime, endTime 296 }