github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/exemplars_merge.go (about)

     1  package server
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math"
     7  	"net/http"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/pyroscope-io/pyroscope/pkg/history"
    12  	"github.com/pyroscope-io/pyroscope/pkg/storage"
    13  	"github.com/pyroscope-io/pyroscope/pkg/storage/heatmap"
    14  	"github.com/pyroscope-io/pyroscope/pkg/structs/flamebearer"
    15  	"github.com/pyroscope-io/pyroscope/pkg/util/attime"
    16  )
    17  
    18  type mergeExemplarsRequest struct {
    19  	QueryID  history.QueryID
    20  	AppName  string   `json:"appName"`
    21  	Profiles []string `json:"profiles"`
    22  	MaxNodes int      `json:"maxNodes"`
    23  
    24  	StartTime           string `json:"startTime"`
    25  	EndTime             string `json:"endTime"`
    26  	MinValue            uint64 `json:"minValue"`
    27  	MaxValue            uint64 `json:"maxValue"`
    28  	HeatmapTimeBuckets  int64  `json:"heatmapTimeBuckets"`
    29  	HeatmapValueBuckets int64  `json:"heatmapValueBuckets"`
    30  
    31  	SelectionStartTime string `json:"selectionStartTime"`
    32  	SelectionEndTime   string `json:"selectionEndTime"`
    33  	SelectionMinValue  uint64 `json:"selectionMinValue"`
    34  	SelectionMaxValue  uint64 `json:"selectionMaxValue"`
    35  
    36  	// For consistency with render handler: `startTime` and `endTime` take precedence.
    37  	From  string `json:"from"`
    38  	Until string `json:"until"`
    39  }
    40  
    41  type mergeExemplarsResponse struct {
    42  	flamebearer.FlamebearerProfile
    43  	Metadata *mergeExemplarsMetadata `json:"mergeMetadata"`
    44  	QueryID  history.QueryID         `json:"queryID"`
    45  }
    46  
    47  type mergeExemplarsMetadata struct {
    48  	flamebearer.FlamebearerMetadataV1
    49  	AppName        string `json:"appName"`
    50  	StartTime      string `json:"startTime"`
    51  	EndTime        string `json:"endTime"`
    52  	MaxNodes       int    `json:"maxNodes"`
    53  	ProfilesLength int    `json:"profilesLength"`
    54  }
    55  
    56  func (h ExemplarsHandler) MergeExemplars(w http.ResponseWriter, r *http.Request) {
    57  	req := h.mergeExemplarsRequest(w, r)
    58  	if req == nil {
    59  		return
    60  	}
    61  
    62  	maxNodes := h.MaxNodesDefault
    63  	if newMaxNodes, ok := MaxNodesFromContext(r.Context()); ok {
    64  		maxNodes = newMaxNodes
    65  	}
    66  	if req.MaxNodes != 0 {
    67  		maxNodes = req.MaxNodes
    68  	}
    69  
    70  	input := mergeExemplarsInputFromMergeExemplarsRequest(req)
    71  	out, err := h.ExemplarsMerger.MergeExemplars(r.Context(), input)
    72  	if err != nil {
    73  		h.HTTPUtils.WriteInternalServerError(r, w, err, "failed to retrieve data")
    74  		return
    75  	}
    76  
    77  	queryID := req.QueryID
    78  	if queryID == "" {
    79  		e := &history.Entry{
    80  			Type:      history.EntryTypeMerge, //EntryType
    81  			Timestamp: time.Now(),             //time.Time
    82  
    83  			AppName:   input.AppName,
    84  			Profiles:  input.ProfileIDs, //[]string
    85  			StartTime: input.StartTime,
    86  			EndTime:   input.EndTime,
    87  		}
    88  		e.PopulateFromRequest(r)
    89  		queryID, err = h.HistoryManager.Add(r.Context(), e)
    90  		if err != nil {
    91  			h.HTTPUtils.WriteInternalServerError(r, w, err, "failed to save query")
    92  			return
    93  		}
    94  	}
    95  
    96  	flame := flamebearer.NewProfile(flamebearer.ProfileConfig{
    97  		MaxNodes:  maxNodes,
    98  		Metadata:  out.Metadata,
    99  		Tree:      out.Tree,
   100  		Heatmap:   h.HeatmapBuilder.BuildFromSketch(out.HeatmapSketch),
   101  		Telemetry: out.Telemetry,
   102  	})
   103  
   104  	md := &mergeExemplarsMetadata{
   105  		FlamebearerMetadataV1: flame.Metadata,
   106  		AppName:               input.AppName,
   107  		StartTime:             strconv.Itoa(int(input.StartTime.Unix())),
   108  		EndTime:               strconv.Itoa(int(input.EndTime.Unix())),
   109  		MaxNodes:              req.MaxNodes,
   110  		ProfilesLength:        len(req.Profiles),
   111  	}
   112  
   113  	h.StatsReceiver.StatsInc("merge")
   114  	h.HTTPUtils.WriteResponseJSON(r, w, mergeExemplarsResponse{
   115  		QueryID:            queryID,
   116  		FlamebearerProfile: flame,
   117  		Metadata:           md,
   118  	})
   119  }
   120  
   121  func (h *ExemplarsHandler) mergeExemplarsRequestFromQueryID(w http.ResponseWriter, r *http.Request, qid string) *mergeExemplarsRequest {
   122  	var req mergeExemplarsRequest
   123  	if qid == "" {
   124  		res, err := h.HistoryManager.Get(r.Context(), history.QueryID(qid))
   125  		if err != nil {
   126  			h.HTTPUtils.WriteInvalidParameterError(r, w, fmt.Errorf("error getting query: %v", err))
   127  			return nil
   128  		}
   129  		if res == nil {
   130  			h.HTTPUtils.WriteInvalidParameterError(r, w, fmt.Errorf("queryID \"%v\" not found", qid))
   131  			return nil
   132  		}
   133  		req.QueryID = history.QueryID(qid)
   134  		req.AppName = res.AppName
   135  		req.StartTime = strconv.Itoa(int(res.StartTime.Unix()))
   136  		req.EndTime = strconv.Itoa(int(res.EndTime.Unix()))
   137  		req.Profiles = res.Profiles
   138  		// TODO: handle separately
   139  		// req.MaxNodes = res.MaxNodes
   140  	}
   141  	return &req
   142  }
   143  
   144  func (h *ExemplarsHandler) mergeExemplarsRequestFromJSONBody(w http.ResponseWriter, r *http.Request) *mergeExemplarsRequest {
   145  	var req mergeExemplarsRequest
   146  	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
   147  		h.HTTPUtils.WriteInvalidParameterError(r, w, err)
   148  		return nil
   149  	}
   150  
   151  	if req.AppName == "" {
   152  		h.HTTPUtils.WriteInvalidParameterError(r, w, fmt.Errorf("application name required"))
   153  		return nil
   154  	}
   155  	if len(req.Profiles) == 0 {
   156  		h.HTTPUtils.WriteInvalidParameterError(r, w, fmt.Errorf("at least one profile ID must be specified"))
   157  		return nil
   158  	}
   159  	return &req
   160  }
   161  
   162  func (h *ExemplarsHandler) mergeExemplarsRequest(w http.ResponseWriter, r *http.Request) *mergeExemplarsRequest {
   163  	qid := r.URL.Query().Get("queryID")
   164  	if qid != "" {
   165  		return h.mergeExemplarsRequestFromQueryID(w, r, qid)
   166  	}
   167  	return h.mergeExemplarsRequestFromJSONBody(w, r)
   168  }
   169  
   170  func mergeExemplarsInputFromMergeExemplarsRequest(req *mergeExemplarsRequest) storage.MergeExemplarsInput {
   171  	startTime := parseTimeFallback(req.StartTime, req.From)
   172  	endTime := parseTimeFallback(req.EndTime, req.Until)
   173  	return storage.MergeExemplarsInput{
   174  		AppName:    req.AppName,
   175  		ProfileIDs: req.Profiles,
   176  		StartTime:  startTime,
   177  		EndTime:    endTime,
   178  		ExemplarsSelection: storage.ExemplarsSelection{
   179  			StartTime: parseTime(req.SelectionStartTime),
   180  			EndTime:   parseTime(req.SelectionEndTime),
   181  			MinValue:  req.MinValue,
   182  			MaxValue:  req.MaxValue,
   183  		},
   184  		HeatmapParams: heatmap.HeatmapParams{
   185  			StartTime:    startTime,
   186  			EndTime:      endTime,
   187  			MinValue:     req.MinValue,
   188  			MaxValue:     req.MaxValue,
   189  			TimeBuckets:  req.HeatmapTimeBuckets,
   190  			ValueBuckets: req.HeatmapValueBuckets,
   191  		},
   192  	}
   193  }
   194  
   195  func parseTimeFallback(primary, fallback string) time.Time {
   196  	if primary != "" {
   197  		return attime.Parse(primary)
   198  	}
   199  	if fallback != "" {
   200  		return attime.Parse(fallback)
   201  	}
   202  	return time.Unix(0, 0)
   203  }
   204  
   205  func parseTime(t string) time.Time {
   206  	if t == "" {
   207  		return time.Unix(0, 0)
   208  	}
   209  	return attime.Parse(t)
   210  }
   211  
   212  func parseNumber(n string, ceil bool) (uint64, error) {
   213  	if n == "" {
   214  		return 0, nil
   215  	}
   216  	x, err := strconv.ParseUint(n, 10, 64)
   217  	if err == nil {
   218  		return x, nil
   219  	}
   220  	f, err := strconv.ParseFloat(n, 64)
   221  	if err == nil {
   222  		if ceil {
   223  			return uint64(math.Ceil(f)), nil
   224  		}
   225  		return uint64(f), nil
   226  	}
   227  	return 0, fmt.Errorf("invalid value: expected uint or float: %q", n)
   228  }