github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/api/v1/handler/graphite/find.go (about)

     1  // Copyright (c) 2019 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package graphite
    22  
    23  import (
    24  	"errors"
    25  	"net/http"
    26  	"sort"
    27  	"sync"
    28  
    29  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    30  	"github.com/m3db/m3/src/query/api/v1/options"
    31  	"github.com/m3db/m3/src/query/api/v1/route"
    32  	"github.com/m3db/m3/src/query/graphite/graphite"
    33  	graphitestorage "github.com/m3db/m3/src/query/graphite/storage"
    34  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    35  	"github.com/m3db/m3/src/query/util/logging"
    36  	xerrors "github.com/m3db/m3/src/x/errors"
    37  	"github.com/m3db/m3/src/x/instrument"
    38  	xhttp "github.com/m3db/m3/src/x/net/http"
    39  
    40  	"go.uber.org/zap"
    41  )
    42  
    43  const (
    44  	// FindURL is the url for finding graphite metrics.
    45  	FindURL = route.Prefix + "/graphite/metrics/find"
    46  )
    47  
    48  // FindHTTPMethods are the HTTP methods for this handler.
    49  var FindHTTPMethods = []string{http.MethodGet, http.MethodPost}
    50  
    51  type grahiteFindHandler struct {
    52  	storage             graphitestorage.Storage
    53  	graphiteStorageOpts graphitestorage.M3WrappedStorageOptions
    54  	fetchOptionsBuilder handleroptions.FetchOptionsBuilder
    55  	instrumentOpts      instrument.Options
    56  }
    57  
    58  // NewFindHandler returns a new instance of handler.
    59  func NewFindHandler(opts options.HandlerOptions) http.Handler {
    60  	wrappedStore := graphitestorage.NewM3WrappedStorage(opts.Storage(),
    61  		opts.M3DBOptions(), opts.InstrumentOpts(), opts.GraphiteStorageOptions())
    62  	return &grahiteFindHandler{
    63  		storage:             wrappedStore,
    64  		graphiteStorageOpts: opts.GraphiteStorageOptions(),
    65  		fetchOptionsBuilder: opts.GraphiteFindFetchOptionsBuilder(),
    66  		instrumentOpts:      opts.InstrumentOpts(),
    67  	}
    68  }
    69  
    70  func mergeTags(
    71  	terminatedResult *consolidators.CompleteTagsResult,
    72  	childResult *consolidators.CompleteTagsResult,
    73  ) (map[string]nodeDescriptor, error) {
    74  	var (
    75  		terminatedResultTags = 0
    76  		childResultTags      = 0
    77  	)
    78  
    79  	// Sanity check the queries aren't complete name only queries.
    80  	if terminatedResult != nil {
    81  		terminatedResultTags = len(terminatedResult.CompletedTags)
    82  		if terminatedResult.CompleteNameOnly {
    83  			return nil, errors.New("terminated result is completing name only")
    84  		}
    85  	}
    86  
    87  	if childResult != nil {
    88  		childResultTags = len(childResult.CompletedTags)
    89  		if childResult.CompleteNameOnly {
    90  			return nil, errors.New("child result is completing name only")
    91  		}
    92  	}
    93  
    94  	size := terminatedResultTags + childResultTags
    95  	tagMap := make(map[string]nodeDescriptor, size)
    96  	if terminatedResult != nil {
    97  		for _, tag := range terminatedResult.CompletedTags {
    98  			for _, value := range tag.Values {
    99  				descriptor := tagMap[string(value)]
   100  				descriptor.isLeaf = true
   101  				tagMap[string(value)] = descriptor
   102  			}
   103  		}
   104  	}
   105  
   106  	if childResult != nil {
   107  		for _, tag := range childResult.CompletedTags {
   108  			for _, value := range tag.Values {
   109  				descriptor := tagMap[string(value)]
   110  				descriptor.hasChildren = true
   111  				tagMap[string(value)] = descriptor
   112  			}
   113  		}
   114  	}
   115  
   116  	return tagMap, nil
   117  }
   118  
   119  func findResultsSorted(prefix string, tagMap map[string]nodeDescriptor) []findResult {
   120  	results := make([]findResult, 0, len(tagMap))
   121  	for name, node := range tagMap {
   122  		results = append(results, findResult{
   123  			id:   prefix + name,
   124  			name: name,
   125  			node: node,
   126  		})
   127  	}
   128  	sort.Slice(results, func(i, j int) bool {
   129  		if results[i].id != results[j].id {
   130  			return results[i].id < results[j].id
   131  		}
   132  		if results[i].node.hasChildren && !results[j].node.hasChildren {
   133  			return true
   134  		}
   135  		if results[i].node.isLeaf && !results[j].node.isLeaf {
   136  			return true
   137  		}
   138  		return false
   139  	})
   140  	return results
   141  }
   142  
   143  func (h *grahiteFindHandler) ServeHTTP(
   144  	w http.ResponseWriter,
   145  	r *http.Request,
   146  ) {
   147  	ctx, opts, err := h.fetchOptionsBuilder.NewFetchOptions(r.Context(), r)
   148  	if err != nil {
   149  		xhttp.WriteError(w, err)
   150  		return
   151  	}
   152  
   153  	logger := logging.WithContext(ctx, h.instrumentOpts)
   154  	w.Header().Set(xhttp.HeaderContentType, xhttp.ContentTypeJSON)
   155  
   156  	// NB: need to run two separate queries, one of which will match only the
   157  	// provided matchers, and one which will match the provided matchers with at
   158  	// least one more child node. For further information, refer to the comment
   159  	// for parseFindParamsToQueries.
   160  	terminatedQuery, childQuery, raw, err := parseFindParamsToQueries(r)
   161  	if err != nil {
   162  		xhttp.WriteError(w, err)
   163  		return
   164  	}
   165  
   166  	var (
   167  		terminatedResult *consolidators.CompleteTagsResult
   168  		tErr             error
   169  		childResult      *consolidators.CompleteTagsResult
   170  		cErr             error
   171  		wg               sync.WaitGroup
   172  	)
   173  	if terminatedQuery != nil {
   174  		// Sometimes we only perform the child query, so only perform
   175  		// terminated query if not nil.
   176  		wg.Add(1)
   177  		go func() {
   178  			terminatedResult, tErr = h.storage.CompleteTags(ctx, terminatedQuery, opts)
   179  			wg.Done()
   180  		}()
   181  	}
   182  	// Always perform child query.
   183  	wg.Add(1)
   184  	go func() {
   185  		childResult, cErr = h.storage.CompleteTags(ctx, childQuery, opts)
   186  		wg.Done()
   187  	}()
   188  
   189  	wg.Wait()
   190  
   191  	if err := xerrors.FirstError(tErr, cErr); err != nil {
   192  		logger.Error("unable to find search", zap.Error(err))
   193  		xhttp.WriteError(w, err)
   194  		return
   195  	}
   196  
   197  	meta := childResult.Metadata
   198  	if terminatedResult != nil {
   199  		meta = terminatedResult.Metadata.CombineMetadata(childResult.Metadata)
   200  	}
   201  
   202  	// NB: merge results from both queries to specify which series have children
   203  	seenMap, err := mergeTags(terminatedResult, childResult)
   204  	if err != nil {
   205  		logger.Error("unable to find merge", zap.Error(err))
   206  		xhttp.WriteError(w, err)
   207  		return
   208  	}
   209  
   210  	prefix := graphite.DropLastMetricPart(raw)
   211  	if len(prefix) > 0 {
   212  		prefix += "."
   213  	}
   214  
   215  	results := findResultsSorted(prefix, seenMap)
   216  
   217  	err = handleroptions.AddDBResultResponseHeaders(w, meta, opts)
   218  	if err != nil {
   219  		logger.Error("unable to render find header", zap.Error(err))
   220  		xhttp.WriteError(w, err)
   221  		return
   222  	}
   223  
   224  	// TODO: Support multiple result types
   225  	resultOpts := findResultsOptions{
   226  		includeBothExpandableAndLeaf: h.graphiteStorageOpts.FindResultsIncludeBothExpandableAndLeaf,
   227  	}
   228  	if err := findResultsJSON(w, results, resultOpts); err != nil {
   229  		logger.Error("unable to render find results", zap.Error(err))
   230  	}
   231  }