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  }