github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/api/v1/handler/graphite/find_parser.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  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/m3db/m3/src/query/errors"
    31  	"github.com/m3db/m3/src/query/graphite/graphite"
    32  	graphitestorage "github.com/m3db/m3/src/query/graphite/storage"
    33  	"github.com/m3db/m3/src/query/models"
    34  	"github.com/m3db/m3/src/query/storage"
    35  	"github.com/m3db/m3/src/query/util/json"
    36  	xerrors "github.com/m3db/m3/src/x/errors"
    37  	xtime "github.com/m3db/m3/src/x/time"
    38  )
    39  
    40  // parseFindParamsToQueries parses an incoming request to two find queries,
    41  // which are then combined to give the final result.
    42  // It returns, in order:
    43  //
    44  //	the given query; this will return all values for exactly that tag which have
    45  //
    46  // _terminatedQuery, which adds an explicit terminator after the last term in
    47  //
    48  //	no child nodes
    49  //
    50  // _childQuery, which adds an explicit match all after the last term in the
    51  // given query; this will return all values for exactly that tag which have at
    52  // least one child node.
    53  // _rawQueryString, which is the initial query request (bar final
    54  // matcher), which  is used to reconstruct the return values.
    55  // _err, any error encountered during parsing.
    56  //
    57  // As an example, given the query `a.b*`, and metrics `a.bar.c` and `a.biz`,
    58  // terminatedQuery will return only [biz], and childQuery will return only
    59  // [bar].
    60  func parseFindParamsToQueries(r *http.Request) (
    61  	_terminatedQuery *storage.CompleteTagsQuery,
    62  	_childQuery *storage.CompleteTagsQuery,
    63  	_rawQueryString string,
    64  	_err error,
    65  ) {
    66  	query := r.FormValue("query")
    67  	if query == "" {
    68  		return nil, nil, "",
    69  			xerrors.NewInvalidParamsError(errors.ErrNoQueryFound)
    70  	}
    71  
    72  	now := time.Now()
    73  	fromString, untilString := r.FormValue("from"), r.FormValue("until")
    74  	if len(fromString) == 0 {
    75  		fromString = "0"
    76  	}
    77  
    78  	if len(untilString) == 0 {
    79  		untilString = "now"
    80  	}
    81  
    82  	from, err := graphite.ParseTime(
    83  		fromString,
    84  		now,
    85  		tzOffsetForAbsoluteTime,
    86  	)
    87  	if err != nil {
    88  		return nil, nil, "",
    89  			xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'from': %s", fromString))
    90  	}
    91  
    92  	until, err := graphite.ParseTime(
    93  		untilString,
    94  		now,
    95  		tzOffsetForAbsoluteTime,
    96  	)
    97  	if err != nil {
    98  		return nil, nil, "",
    99  			xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'until': %s", untilString))
   100  	}
   101  
   102  	matchers, queryType, err := graphitestorage.TranslateQueryToMatchersWithTerminator(query)
   103  	if err != nil {
   104  		return nil, nil, "",
   105  			xerrors.NewInvalidParamsError(fmt.Errorf("invalid 'query': %s", query))
   106  	}
   107  
   108  	switch queryType {
   109  	case graphitestorage.StarStarUnterminatedTranslatedQuery:
   110  		// Translated query for "**" has unterminated search for children
   111  		// terms, we are only going to do a single search and assume all
   112  		// results that come back have also children in the graphite path
   113  		// tree (since it's very expensive to check if each result that comes
   114  		// back is a child or leaf node and "**" in a find query is typically
   115  		// only used for template variables rather than searching for metric
   116  		// results, which is the only use case isLeaf/hasChildren is useful).
   117  		// Note: Filter to all graphite tags that appears at the last node
   118  		// or greater than that (we use 100 as an arbitrary upper bound).
   119  		maxPathIndexes := 100
   120  		filter := make([][]byte, 0, maxPathIndexes)
   121  		parts := 1 + strings.Count(query, ".")
   122  		firstPathIndex := parts - 1
   123  		for i := firstPathIndex; i < firstPathIndex+maxPathIndexes; i++ {
   124  			filter = append(filter, graphite.TagName(i))
   125  		}
   126  		childQuery := &storage.CompleteTagsQuery{
   127  			CompleteNameOnly: false,
   128  			FilterNameTags:   filter,
   129  			TagMatchers:      matchers,
   130  			Start:            xtime.ToUnixNano(from),
   131  			End:              xtime.ToUnixNano(until),
   132  		}
   133  		return nil, childQuery, query, nil
   134  	case graphitestorage.TerminatedTranslatedQuery:
   135  		// Default type of translated query, explicitly craft queries for
   136  		// a terminated part of the query and a child part of the query.
   137  		break
   138  	default:
   139  		return nil, nil, "", fmt.Errorf("unknown query type: %v", queryType)
   140  	}
   141  
   142  	// NB: Filter will always be the second last term in the matchers, and the
   143  	// matchers should always have a length of at least 2 (term + terminator)
   144  	// so this is a sanity check and unexpected in actual execution.
   145  	if len(matchers) < 2 {
   146  		return nil, nil, "",
   147  			xerrors.NewInvalidParamsError(fmt.Errorf("unable to parse 'query': %s", query))
   148  	}
   149  
   150  	filter := [][]byte{matchers[len(matchers)-2].Name}
   151  	terminatedQuery := &storage.CompleteTagsQuery{
   152  		CompleteNameOnly: false,
   153  		FilterNameTags:   filter,
   154  		TagMatchers:      matchers,
   155  		Start:            xtime.ToUnixNano(from),
   156  		End:              xtime.ToUnixNano(until),
   157  	}
   158  
   159  	clonedMatchers := make([]models.Matcher, len(matchers))
   160  	copy(clonedMatchers, matchers)
   161  	// NB: change terminator from `MatchNotField` to `MatchField` to ensure
   162  	// segments with children are matched.
   163  	clonedMatchers[len(clonedMatchers)-1].Type = models.MatchField
   164  	childQuery := &storage.CompleteTagsQuery{
   165  		CompleteNameOnly: false,
   166  		FilterNameTags:   filter,
   167  		TagMatchers:      clonedMatchers,
   168  		Start:            xtime.ToUnixNano(from),
   169  		End:              xtime.ToUnixNano(until),
   170  	}
   171  
   172  	return terminatedQuery, childQuery, query, nil
   173  }
   174  
   175  type findResultsOptions struct {
   176  	includeBothExpandableAndLeaf bool
   177  }
   178  
   179  func findResultsJSON(
   180  	w io.Writer,
   181  	results []findResult,
   182  	opts findResultsOptions,
   183  ) error {
   184  	jw := json.NewWriter(w)
   185  	jw.BeginArray()
   186  
   187  	for _, result := range results {
   188  		writeFindNodeResultJSON(jw, result, opts)
   189  	}
   190  
   191  	jw.EndArray()
   192  	return jw.Close()
   193  }
   194  
   195  type findResult struct {
   196  	id   string
   197  	name string
   198  	node nodeDescriptor
   199  }
   200  
   201  type nodeDescriptor struct {
   202  	hasChildren bool
   203  	isLeaf      bool
   204  }
   205  
   206  func writeFindNodeResultJSON(
   207  	jw json.Writer,
   208  	result findResult,
   209  	opts findResultsOptions,
   210  ) {
   211  	descriptor := result.node
   212  
   213  	// Include the leaf node only if no leaf was specified or
   214  	// if config optionally sets that both should come back.
   215  	// The default behavior matches graphite web.
   216  	includeLeafNode := (descriptor.isLeaf && !descriptor.hasChildren) ||
   217  		(descriptor.isLeaf &&
   218  			descriptor.hasChildren &&
   219  			opts.includeBothExpandableAndLeaf)
   220  	if includeLeafNode {
   221  		writeFindResultJSON(jw, result.id, result.name, false)
   222  	}
   223  
   224  	if descriptor.hasChildren {
   225  		writeFindResultJSON(jw, result.id, result.name, true)
   226  	}
   227  }
   228  
   229  func writeFindResultJSON(
   230  	jw json.Writer,
   231  	id string,
   232  	value string,
   233  	hasChildren bool,
   234  ) {
   235  	leaf := 1
   236  	if hasChildren {
   237  		leaf = 0
   238  	}
   239  
   240  	jw.BeginObject()
   241  
   242  	jw.BeginObjectField("id")
   243  	jw.WriteString(id)
   244  
   245  	jw.BeginObjectField("text")
   246  	jw.WriteString(value)
   247  
   248  	jw.BeginObjectField("leaf")
   249  	jw.WriteInt(leaf)
   250  
   251  	jw.BeginObjectField("expandable")
   252  	jw.WriteInt(1 - leaf)
   253  
   254  	jw.BeginObjectField("allowChildren")
   255  	jw.WriteInt(1 - leaf)
   256  
   257  	jw.EndObject()
   258  }