github.com/go-graphite/carbonapi@v0.17.0/cmd/carbonapi/http/find_handlers.go (about) 1 package http 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/ansel1/merry" 14 pbv2 "github.com/go-graphite/protocol/carbonapi_v2_pb" 15 pbv3 "github.com/go-graphite/protocol/carbonapi_v3_pb" 16 pickle "github.com/lomik/og-rek" 17 "github.com/lomik/zapwriter" 18 "github.com/maruel/natural" 19 uuid "github.com/satori/go.uuid" 20 21 "github.com/go-graphite/carbonapi/carbonapipb" 22 "github.com/go-graphite/carbonapi/cmd/carbonapi/config" 23 "github.com/go-graphite/carbonapi/date" 24 "github.com/go-graphite/carbonapi/intervalset" 25 utilctx "github.com/go-graphite/carbonapi/util/ctx" 26 "github.com/go-graphite/carbonapi/zipper/helper" 27 ) 28 29 // Find handler and it's helper functions 30 type treejson struct { 31 AllowChildren int `json:"allowChildren"` 32 Expandable int `json:"expandable"` 33 Leaf int `json:"leaf"` 34 ID string `json:"id"` 35 Text string `json:"text"` 36 Context map[string]int `json:"context"` // unused 37 } 38 39 var treejsonContext = make(map[string]int) 40 41 func findTreejson(multiGlobs *pbv3.MultiGlobResponse) ([]byte, error) { 42 var b bytes.Buffer 43 44 var tree = make([]treejson, 0) 45 46 seen := make(map[string]struct{}) 47 48 for _, globs := range multiGlobs.Metrics { 49 basepath := globs.Name 50 51 if i := strings.LastIndex(basepath, "."); i != -1 { 52 basepath = basepath[:i+1] 53 } else { 54 basepath = "" 55 } 56 57 for _, g := range globs.Matches { 58 if strings.HasPrefix(g.Path, "_tag") { 59 continue 60 } 61 62 name := g.Path 63 64 if i := strings.LastIndex(name, "."); i != -1 { 65 name = name[i+1:] 66 } 67 68 if _, ok := seen[name]; ok { 69 continue 70 } 71 seen[name] = struct{}{} 72 73 t := treejson{ 74 ID: basepath + name, 75 Context: treejsonContext, 76 Text: name, 77 } 78 79 if g.IsLeaf { 80 t.Leaf = 1 81 } else { 82 t.AllowChildren = 1 83 t.Expandable = 1 84 } 85 86 tree = append(tree, t) 87 } 88 } 89 90 sort.Slice(tree, func(i, j int) bool { 91 if tree[i].Leaf < tree[j].Leaf { 92 return true 93 } 94 if tree[i].Leaf > tree[j].Leaf { 95 return false 96 } 97 return natural.Less(tree[i].Text, tree[j].Text) 98 }) 99 100 err := json.NewEncoder(&b).Encode(tree) 101 return b.Bytes(), err 102 } 103 104 type completer struct { 105 Path string `json:"path"` 106 Name string `json:"name"` 107 IsLeaf string `json:"is_leaf"` 108 } 109 110 func findCompleter(multiGlobs *pbv3.MultiGlobResponse) ([]byte, error) { 111 var b bytes.Buffer 112 113 var complete = make([]completer, 0) 114 115 for _, globs := range multiGlobs.Metrics { 116 for _, g := range globs.Matches { 117 if strings.HasPrefix(g.Path, "_tag") { 118 continue 119 } 120 path := g.Path 121 if !g.IsLeaf && path[len(path)-1:] != "." { 122 path = g.Path + "." 123 } 124 c := completer{ 125 Path: path, 126 } 127 128 if g.IsLeaf { 129 c.IsLeaf = "1" 130 } else { 131 c.IsLeaf = "0" 132 } 133 134 i := strings.LastIndex(c.Path, ".") 135 136 if i != -1 { 137 c.Name = c.Path[i+1:] 138 } else { 139 c.Name = g.Path 140 } 141 142 complete = append(complete, c) 143 } 144 } 145 146 err := json.NewEncoder(&b).Encode(struct { 147 Metrics []completer `json:"metrics"` 148 }{ 149 Metrics: complete}, 150 ) 151 return b.Bytes(), err 152 } 153 154 func findList(multiGlobs *pbv3.MultiGlobResponse) ([]byte, error) { 155 var b bytes.Buffer 156 157 for _, globs := range multiGlobs.Metrics { 158 for _, g := range globs.Matches { 159 if strings.HasPrefix(g.Path, "_tag") { 160 continue 161 } 162 163 var dot string 164 // make sure non-leaves end in one dot 165 if !g.IsLeaf && !strings.HasSuffix(g.Path, ".") { 166 dot = "." 167 } 168 169 fmt.Fprintln(&b, g.Path+dot) 170 } 171 } 172 173 return b.Bytes(), nil 174 } 175 176 func findHandler(w http.ResponseWriter, r *http.Request) { 177 t0 := time.Now() 178 uid := uuid.NewV4() 179 // TODO: Migrate to context.WithTimeout 180 // ctx, _ := context.WithTimeout(context.TODO(), config.Config.ZipperTimeout) 181 ctx := utilctx.SetUUID(r.Context(), uid.String()) 182 username, _, _ := r.BasicAuth() 183 requestHeaders := utilctx.GetLogHeaders(ctx) 184 185 format, ok, formatRaw := getFormat(r, treejsonFormat) 186 jsonp := r.FormValue("jsonp") 187 188 qtz := r.FormValue("tz") 189 from := r.FormValue("from") 190 until := r.FormValue("until") 191 from64 := date.DateParamToEpoch(from, qtz, timeNow().Add(-time.Hour).Unix(), config.Config.DefaultTimeZone) 192 until64 := date.DateParamToEpoch(until, qtz, timeNow().Unix(), config.Config.DefaultTimeZone) 193 194 query := r.Form["query"] 195 srcIP, srcPort := splitRemoteAddr(r.RemoteAddr) 196 197 accessLogger := zapwriter.Logger("access") 198 var accessLogDetails = carbonapipb.AccessLogDetails{ 199 Handler: "find", 200 Username: username, 201 CarbonapiUUID: uid.String(), 202 URL: r.URL.RequestURI(), 203 PeerIP: srcIP, 204 PeerPort: srcPort, 205 Host: r.Host, 206 Referer: r.Referer(), 207 URI: r.RequestURI, 208 Format: formatRaw, 209 RequestHeaders: requestHeaders, 210 } 211 212 logAsError := false 213 defer func() { 214 deferredAccessLogging(accessLogger, &accessLogDetails, t0, logAsError) 215 }() 216 217 if !ok || !format.ValidFindFormat() { 218 setError(w, &accessLogDetails, "unsupported format: "+formatRaw, http.StatusBadRequest, uid.String()) 219 logAsError = true 220 return 221 } 222 223 if queryLengthLimitExceeded(query, config.Config.MaxQueryLength) { 224 setError(w, &accessLogDetails, "query length limit exceeded", http.StatusBadRequest, uid.String()) 225 logAsError = true 226 return 227 } 228 229 if format == completerFormat { 230 var replacer = strings.NewReplacer("/", ".") 231 for i := range query { 232 query[i] = replacer.Replace(query[i]) 233 if query[i] == "" || query[i] == "/" || query[i] == "." { 234 query[i] = ".*" 235 } else { 236 query[i] += "*" 237 } 238 } 239 } 240 241 var pv3Request pbv3.MultiGlobRequest 242 243 if format == protoV3Format { 244 body, err := io.ReadAll(r.Body) 245 if err != nil { 246 setError(w, &accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String()) 247 logAsError = true 248 return 249 } 250 251 err = pv3Request.Unmarshal(body) 252 if err != nil { 253 setError(w, &accessLogDetails, "failed to parse message body: "+err.Error(), http.StatusBadRequest, uid.String()) 254 logAsError = true 255 return 256 } 257 } else { 258 pv3Request.Metrics = query 259 pv3Request.StartTime = from64 260 pv3Request.StopTime = until64 261 } 262 263 if len(pv3Request.Metrics) == 0 { 264 setError(w, &accessLogDetails, "missing parameter `query`", http.StatusBadRequest, uid.String()) 265 logAsError = true 266 return 267 } 268 269 accessLogDetails.Metrics = pv3Request.Metrics 270 271 multiGlobs, stats, err := config.Config.ZipperInstance.Find(ctx, pv3Request) 272 if stats != nil { 273 accessLogDetails.ZipperRequests = stats.ZipperRequests 274 accessLogDetails.TotalMetricsCount += stats.TotalMetricsCount 275 } 276 if err != nil { 277 returnCode := merry.HTTPCode(err) 278 if returnCode != http.StatusOK || multiGlobs == nil { 279 // Allow override status code for 404-not-found replies. 280 if returnCode == http.StatusNotFound { 281 returnCode = config.Config.NotFoundStatusCode 282 } 283 284 if returnCode < 300 { 285 multiGlobs = &pbv3.MultiGlobResponse{Metrics: []pbv3.GlobResponse{}} 286 } else { 287 setError(w, &accessLogDetails, helper.MerryRootError(err), returnCode, uid.String()) 288 // We don't want to log this as an error if it's something normal 289 // Normal is everything that is >= 500. So if config.Config.NotFoundStatusCode is 500 - this will be 290 // logged as error 291 292 if returnCode >= 500 { 293 logAsError = true 294 } 295 return 296 } 297 } 298 } 299 var b []byte 300 var err2 error 301 switch format { 302 case treejsonFormat, jsonFormat: 303 b, err2 = findTreejson(multiGlobs) 304 err = merry.Wrap(err2) 305 format = jsonFormat 306 case completerFormat: 307 b, err2 = findCompleter(multiGlobs) 308 err = merry.Wrap(err2) 309 format = jsonFormat 310 case rawFormat: 311 b, err2 = findList(multiGlobs) 312 err = merry.Wrap(err2) 313 format = rawFormat 314 case protoV2Format: 315 r := pbv2.GlobResponse{ 316 Name: multiGlobs.Metrics[0].Name, 317 Matches: make([]pbv2.GlobMatch, 0, len(multiGlobs.Metrics)), 318 } 319 320 for i := range multiGlobs.Metrics { 321 for _, m := range multiGlobs.Metrics[i].Matches { 322 r.Matches = append(r.Matches, pbv2.GlobMatch{IsLeaf: m.IsLeaf, Path: m.Path}) 323 } 324 } 325 b, err2 = r.Marshal() 326 err = merry.Wrap(err2) 327 case protoV3Format: 328 b, err2 = multiGlobs.Marshal() 329 err = merry.Wrap(err2) 330 case pickleFormat: 331 var result []map[string]interface{} 332 now := int32(time.Now().Unix() + 60) 333 for _, globs := range multiGlobs.Metrics { 334 for _, metric := range globs.Matches { 335 if strings.HasPrefix(metric.Path, "_tag") { 336 continue 337 } 338 // Tell graphite-web that we have everything 339 var mm map[string]interface{} 340 if config.Config.GraphiteWeb09Compatibility { 341 // graphite-web 0.9.x 342 mm = map[string]interface{}{ 343 // graphite-web 0.9.x 344 "metric_path": metric.Path, 345 "isLeaf": metric.IsLeaf, 346 } 347 } else { 348 // graphite-web 1.0 349 interval := &intervalset.IntervalSet{Start: 0, End: now} 350 mm = map[string]interface{}{ 351 "is_leaf": metric.IsLeaf, 352 "path": metric.Path, 353 "intervals": interval, 354 } 355 } 356 result = append(result, mm) 357 } 358 } 359 360 p := bytes.NewBuffer(b) 361 pEnc := pickle.NewEncoder(p) 362 err = merry.Wrap(pEnc.Encode(result)) 363 b = p.Bytes() 364 } 365 366 if err != nil { 367 setError(w, &accessLogDetails, err.Error(), http.StatusInternalServerError, uid.String()) 368 logAsError = true 369 return 370 } 371 372 writeResponse(w, http.StatusOK, b, format, jsonp, uid.String()) 373 }