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, &params); 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, &params.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, &params.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  }