github.com/go-graphite/carbonapi@v0.17.0/cmd/carbonapi/http/expand_handler.go (about) 1 package http 2 3 import ( 4 "encoding/json" 5 "html" 6 "net/http" 7 "sort" 8 "time" 9 10 "github.com/ansel1/merry" 11 pbv3 "github.com/go-graphite/protocol/carbonapi_v3_pb" 12 "github.com/lomik/zapwriter" 13 uuid "github.com/satori/go.uuid" 14 15 "github.com/go-graphite/carbonapi/carbonapipb" 16 "github.com/go-graphite/carbonapi/cmd/carbonapi/config" 17 "github.com/go-graphite/carbonapi/date" 18 utilctx "github.com/go-graphite/carbonapi/util/ctx" 19 ) 20 21 func expandHandler(w http.ResponseWriter, r *http.Request) { 22 t0 := time.Now() 23 uid := uuid.NewV4() 24 // TODO: Migrate to context.WithTimeout 25 // ctx, _ := context.WithTimeout(context.TODO(), config.Config.ZipperTimeout) 26 ctx := utilctx.SetUUID(r.Context(), uid.String()) 27 username, _, _ := r.BasicAuth() 28 requestHeaders := utilctx.GetLogHeaders(ctx) 29 30 format, ok, formatRaw := getFormat(r, treejsonFormat) 31 jsonp := r.FormValue("jsonp") 32 groupByExpr := r.FormValue("groupByExpr") 33 leavesOnly := r.FormValue("leavesOnly") 34 35 qtz := r.FormValue("tz") 36 from := r.FormValue("from") 37 until := r.FormValue("until") 38 from64 := date.DateParamToEpoch(from, qtz, timeNow().Add(-time.Hour).Unix(), config.Config.DefaultTimeZone) 39 until64 := date.DateParamToEpoch(until, qtz, timeNow().Unix(), config.Config.DefaultTimeZone) 40 41 srcIP, srcPort := splitRemoteAddr(r.RemoteAddr) 42 43 accessLogger := zapwriter.Logger("access") 44 var accessLogDetails = carbonapipb.AccessLogDetails{ 45 Handler: "expand", 46 Username: username, 47 CarbonapiUUID: uid.String(), 48 URL: r.URL.RequestURI(), 49 PeerIP: srcIP, 50 PeerPort: srcPort, 51 Host: r.Host, 52 Referer: r.Referer(), 53 URI: r.RequestURI, 54 Format: formatRaw, 55 RequestHeaders: requestHeaders, 56 } 57 58 logAsError := false 59 defer func() { 60 deferredAccessLogging(accessLogger, &accessLogDetails, t0, logAsError) 61 }() 62 63 err := r.ParseForm() 64 if err != nil { 65 setError(w, &accessLogDetails, err.Error(), http.StatusBadRequest, uid.String()) 66 logAsError = true 67 return 68 } 69 query := r.Form["query"] 70 71 if !ok || !format.ValidExpandFormat() { 72 http.Error(w, "unsupported format: "+html.EscapeString(formatRaw), http.StatusBadRequest) 73 accessLogDetails.HTTPCode = http.StatusBadRequest 74 accessLogDetails.Reason = "unsupported format: " + formatRaw 75 logAsError = true 76 return 77 } 78 79 if queryLengthLimitExceeded(query, config.Config.MaxQueryLength) { 80 setError(w, &accessLogDetails, "query length limit exceeded", http.StatusBadRequest, uid.String()) 81 logAsError = true 82 return 83 } 84 85 var pv3Request pbv3.MultiGlobRequest 86 pv3Request.Metrics = query 87 pv3Request.StartTime = from64 88 pv3Request.StopTime = until64 89 90 multiGlobs, stats, err := config.Config.ZipperInstance.Find(ctx, pv3Request) 91 if stats != nil { 92 accessLogDetails.ZipperRequests = stats.ZipperRequests 93 accessLogDetails.TotalMetricsCount += stats.TotalMetricsCount 94 } 95 if err != nil { 96 returnCode := merry.HTTPCode(err) 97 if returnCode != http.StatusOK || multiGlobs == nil { 98 // Allow override status code for 404-not-found replies. 99 if returnCode == http.StatusNotFound { 100 returnCode = config.Config.NotFoundStatusCode 101 } 102 103 if returnCode < 300 { 104 multiGlobs = &pbv3.MultiGlobResponse{Metrics: []pbv3.GlobResponse{}} 105 } else { 106 http.Error(w, http.StatusText(returnCode), returnCode) 107 accessLogDetails.HTTPCode = int32(returnCode) 108 accessLogDetails.Reason = err.Error() 109 // We don't want to log this as an error if it's something normal 110 // Normal is everything that is >= 500. So if config.Config.NotFoundStatusCode is 500 - this will be 111 // logged as error 112 113 if returnCode >= 500 { 114 logAsError = true 115 } 116 return 117 } 118 } 119 } 120 121 var b []byte 122 var err2 error 123 b, err2 = expandEncoder(multiGlobs, leavesOnly, groupByExpr) 124 err = merry.Wrap(err2) 125 if err != nil { 126 http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 127 accessLogDetails.HTTPCode = http.StatusInternalServerError 128 accessLogDetails.Reason = err.Error() 129 logAsError = true 130 return 131 } 132 133 writeResponse(w, http.StatusOK, b, jsonFormat, jsonp, uid.String()) 134 } 135 136 func expandEncoder(multiGlobs *pbv3.MultiGlobResponse, leavesOnly string, groupByExpr string) ([]byte, error) { 137 var b []byte 138 var err error 139 groups := make(map[string][]string) 140 seen := make(map[string]bool) 141 for _, globs := range multiGlobs.Metrics { 142 paths := make([]string, 0, len(globs.Matches)) 143 for _, g := range globs.Matches { 144 if leavesOnly == "1" && !g.IsLeaf { 145 continue 146 } 147 if _, ok := seen[g.Path]; ok { 148 continue 149 } 150 seen[g.Path] = true 151 paths = append(paths, g.Path) 152 } 153 sort.Strings(paths) 154 groups[globs.Name] = paths 155 } 156 if groupByExpr != "1" { 157 // results are just []string otherwise 158 // so, flatting map 159 flatData := make([]string, 0) 160 for _, group := range groups { 161 flatData = append(flatData, group...) 162 } 163 // sorting flat list one more to mimic graphite-web 164 sort.Strings(flatData) 165 data := map[string][]string{ 166 "results": flatData, 167 } 168 b, err = json.Marshal(data) 169 } else { 170 // results are map[string][]string 171 data := map[string]map[string][]string{ 172 "results": groups, 173 } 174 b, err = json.Marshal(data) 175 } 176 return b, err 177 }