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  }