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