github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/render.go (about) 1 package server 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "strconv" 8 "time" 9 10 "github.com/sirupsen/logrus" 11 "google.golang.org/protobuf/proto" 12 13 "github.com/pyroscope-io/pyroscope/pkg/api" 14 "github.com/pyroscope-io/pyroscope/pkg/flameql" 15 "github.com/pyroscope-io/pyroscope/pkg/history" 16 "github.com/pyroscope-io/pyroscope/pkg/model" 17 "github.com/pyroscope-io/pyroscope/pkg/server/httputils" 18 "github.com/pyroscope-io/pyroscope/pkg/storage" 19 "github.com/pyroscope-io/pyroscope/pkg/storage/metadata" 20 "github.com/pyroscope-io/pyroscope/pkg/storage/segment" 21 "github.com/pyroscope-io/pyroscope/pkg/storage/tree" 22 "github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer" 23 "github.com/pyroscope-io/pyroscope/pkg/util/attime" 24 ) 25 26 var ( 27 errUnknownFormat = errors.New("unknown format") 28 errLabelIsRequired = errors.New("label parameter is required") 29 errNoData = errors.New("no data") 30 errTimeParamsAreRequired = errors.New("leftFrom,leftUntil,rightFrom,rightUntil are required") 31 ) 32 33 type renderParams struct { 34 format string 35 maxNodes int 36 gi *storage.GetInput 37 38 leftStartTime time.Time 39 leftEndTime time.Time 40 rghtStartTime time.Time 41 rghtEndTime time.Time 42 } 43 44 type renderMetadataResponse struct { 45 flamebearer.FlamebearerMetadataV1 46 AppName string `json:"appName"` 47 StartTime int64 `json:"startTime"` 48 EndTime int64 `json:"endTime"` 49 Query string `json:"query"` 50 MaxNodes int `json:"maxNodes"` 51 } 52 53 type annotationsResponse struct { 54 Content string `json:"content"` 55 Timestamp int64 `json:"timestamp"` 56 } 57 type renderResponse struct { 58 flamebearer.FlamebearerProfile 59 Metadata renderMetadataResponse `json:"metadata"` 60 Annotations []annotationsResponse `json:"annotations"` 61 } 62 63 type RenderHandler struct { 64 log *logrus.Logger 65 storage storage.Getter 66 dir http.FileSystem 67 stats StatsReceiver 68 maxNodesDefault int 69 httpUtils httputils.Utils 70 historyMgr history.Manager 71 annotationsService api.AnnotationsService 72 } 73 74 func (ctrl *Controller) renderHandler() http.HandlerFunc { 75 return NewRenderHandler(ctrl.log, ctrl.storage, ctrl.dir, ctrl, ctrl.config.MaxNodesRender, ctrl.httpUtils, ctrl.historyMgr, ctrl.annotationsService).ServeHTTP 76 } 77 78 //revive:disable:argument-limit TODO(petethepig): we will refactor this later 79 func NewRenderHandler( 80 l *logrus.Logger, 81 s storage.Getter, 82 dir http.FileSystem, 83 stats StatsReceiver, 84 maxNodesDefault int, 85 httpUtils httputils.Utils, 86 historyMgr history.Manager, 87 annotationsService api.AnnotationsService, 88 ) *RenderHandler { 89 return &RenderHandler{ 90 log: l, 91 storage: s, 92 dir: dir, 93 stats: stats, 94 maxNodesDefault: maxNodesDefault, 95 httpUtils: httpUtils, 96 historyMgr: historyMgr, 97 annotationsService: annotationsService, 98 } 99 } 100 101 func (rh *RenderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 102 var p renderParams 103 if err := rh.renderParametersFromRequest(r, &p); err != nil { 104 rh.httpUtils.WriteInvalidParameterError(r, w, err) 105 return 106 } 107 108 out, err := rh.storage.Get(r.Context(), p.gi) 109 var appName string 110 if p.gi.Key != nil { 111 appName = p.gi.Key.AppName() 112 } else if p.gi.Query != nil { 113 appName = p.gi.Query.AppName 114 } 115 filename := fmt.Sprintf("%v %v", appName, p.gi.StartTime.UTC().Format(time.RFC3339)) 116 rh.stats.StatsInc("render") 117 if err != nil { 118 rh.httpUtils.WriteInternalServerError(r, w, err, "failed to retrieve data") 119 return 120 } 121 if out == nil { 122 out = &storage.GetOutput{ 123 Tree: tree.New(), 124 Timeline: segment.GenerateTimeline(p.gi.StartTime, p.gi.EndTime), 125 } 126 } 127 128 switch p.format { 129 case "json": 130 flame := flamebearer.NewProfile(flamebearer.ProfileConfig{ 131 Name: filename, 132 MaxNodes: p.maxNodes, 133 Tree: out.Tree, 134 Timeline: out.Timeline, 135 Groups: out.Groups, 136 Telemetry: out.Telemetry, 137 Metadata: metadata.Metadata{ 138 SpyName: out.SpyName, 139 SampleRate: out.SampleRate, 140 Units: out.Units, 141 AggregationType: out.AggregationType, 142 }, 143 }) 144 145 // Look up annotations 146 annotations, err := rh.annotationsService.FindAnnotationsByTimeRange(r.Context(), appName, p.gi.StartTime, p.gi.EndTime) 147 if err != nil { 148 rh.log.Error(err) 149 // it's better to not show any annotations than falling the entire request 150 annotations = []model.Annotation{} 151 } 152 153 res := rh.mountRenderResponse(flame, appName, p.gi, p.maxNodes, annotations) 154 rh.httpUtils.WriteResponseJSON(r, w, res) 155 case "pprof": 156 pprof := out.Tree.Pprof(&tree.PprofMetadata{ 157 // TODO(petethepig): not sure if this conversion is right 158 Unit: string(out.Units), 159 StartTime: p.gi.StartTime, 160 }) 161 out, err := proto.Marshal(pprof) 162 if err == nil { 163 rh.httpUtils.WriteResponseFile(r, w, fmt.Sprintf("%v.pprof", filename), out) 164 } else { 165 rh.httpUtils.WriteInternalServerError(r, w, err, "failed to serialize data") 166 } 167 case "collapsed": 168 collapsed := out.Tree.Collapsed() 169 rh.httpUtils.WriteResponseFile(r, w, fmt.Sprintf("%v.collapsed.txt", filename), []byte(collapsed)) 170 case "html": 171 res := flamebearer.NewProfile(flamebearer.ProfileConfig{ 172 Name: filename, 173 MaxNodes: p.maxNodes, 174 Tree: out.Tree, 175 Timeline: out.Timeline, 176 Groups: out.Groups, 177 Telemetry: out.Telemetry, 178 Metadata: metadata.Metadata{ 179 SpyName: out.SpyName, 180 SampleRate: out.SampleRate, 181 Units: out.Units, 182 AggregationType: out.AggregationType, 183 }, 184 }) 185 w.Header().Add("Content-Type", "text/html") 186 if err := flamebearer.FlamebearerToStandaloneHTML(&res, rh.dir, w); err != nil { 187 rh.httpUtils.WriteJSONEncodeError(r, w, err) 188 return 189 } 190 } 191 } 192 193 // Enhance the flamebearer with a few additional fields the UI requires 194 func (*RenderHandler) mountRenderResponse(flame flamebearer.FlamebearerProfile, appName string, gi *storage.GetInput, maxNodes int, annotations []model.Annotation) renderResponse { 195 md := renderMetadataResponse{ 196 FlamebearerMetadataV1: flame.Metadata, 197 AppName: appName, 198 StartTime: gi.StartTime.Unix(), 199 EndTime: gi.EndTime.Unix(), 200 Query: gi.Query.String(), 201 MaxNodes: maxNodes, 202 } 203 204 annotationsResp := make([]annotationsResponse, len(annotations)) 205 for i, an := range annotations { 206 annotationsResp[i] = annotationsResponse{ 207 Content: an.Content, 208 Timestamp: an.Timestamp.Unix(), 209 } 210 } 211 212 return renderResponse{ 213 FlamebearerProfile: flame, 214 Metadata: md, 215 Annotations: annotationsResp, 216 } 217 } 218 219 func (rh *RenderHandler) renderParametersFromRequest(r *http.Request, p *renderParams) error { 220 v := r.URL.Query() 221 p.gi = new(storage.GetInput) 222 223 k := v.Get("name") 224 q := v.Get("query") 225 p.gi.GroupBy = v.Get("groupBy") 226 227 switch { 228 case k == "" && q == "": 229 return fmt.Errorf("'query' or 'name' parameter is required") 230 case k != "": 231 sk, err := segment.ParseKey(k) 232 if err != nil { 233 return fmt.Errorf("name: parsing storage key: %w", err) 234 } 235 p.gi.Key = sk 236 case q != "": 237 qry, err := flameql.ParseQuery(q) 238 if err != nil { 239 return fmt.Errorf("query: %w", err) 240 } 241 p.gi.Query = qry 242 } 243 244 p.maxNodes = rh.maxNodesDefault 245 if newMaxNodes, ok := MaxNodesFromContext(r.Context()); ok { 246 p.maxNodes = newMaxNodes 247 } 248 if mn, err := strconv.Atoi(v.Get("max-nodes")); err == nil && mn != 0 { 249 p.maxNodes = mn 250 } 251 if mn, err := strconv.Atoi(v.Get("maxNodes")); err == nil && mn != 0 { 252 p.maxNodes = mn 253 } 254 255 p.gi.StartTime = attime.Parse(v.Get("from")) 256 p.gi.EndTime = attime.Parse(v.Get("until")) 257 p.format = v.Get("format") 258 259 return expectFormats(p.format) 260 }