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 }