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  }