github.com/m3db/m3@v1.5.0/src/query/api/v1/middleware/request_classification.go (about)

     1  // Copyright (c) 2021 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 middleware
    22  
    23  import (
    24  	"net/http"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/prometheus/prometheus/promql/parser"
    30  	"github.com/uber-go/tally"
    31  	"go.uber.org/zap"
    32  
    33  	"github.com/m3db/m3/src/cmd/services/m3query/config"
    34  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus"
    35  	"github.com/m3db/m3/src/query/api/v1/route"
    36  	"github.com/m3db/m3/src/x/headers"
    37  )
    38  
    39  const (
    40  	// promDefaultLookback is the default lookback in Prometheus, that affects
    41  	// the actual range it fetches datapoints from.
    42  	promDefaultLookback = time.Minute * 5
    43  )
    44  
    45  type classificationMetrics struct {
    46  	classifiedResult   tally.Counter
    47  	classifiedDuration tally.Counter
    48  	notClassifiable    tally.Counter
    49  	badParams          tally.Counter
    50  	error              tally.Counter
    51  }
    52  
    53  func newClassificationMetrics(scope tally.Scope) *classificationMetrics {
    54  	buildCounter := func(status string) tally.Counter {
    55  		return scope.Tagged(map[string]string{"status": status}).Counter("count")
    56  	}
    57  
    58  	return &classificationMetrics{
    59  		classifiedResult:   buildCounter("classified_result"),
    60  		classifiedDuration: buildCounter("classified_duration"),
    61  		notClassifiable:    buildCounter("not_classifiable"),
    62  		badParams:          buildCounter("bad_params"),
    63  		error:              buildCounter("error"),
    64  	}
    65  }
    66  
    67  func retrieveQueryRange(expr parser.Node, rangeSoFar time.Duration) time.Duration {
    68  	var (
    69  		queryRange  = rangeSoFar
    70  		useLookback = rangeSoFar == 0
    71  	)
    72  
    73  	parser.Inspect(expr, func(node parser.Node, path []parser.Node) error {
    74  		switch n := node.(type) {
    75  		case *parser.SubqueryExpr:
    76  			useLookback = false
    77  			d := retrieveQueryRange(n.Expr, n.Range+n.OriginalOffset+rangeSoFar)
    78  			if d > queryRange {
    79  				queryRange = d
    80  			}
    81  		case *parser.MatrixSelector:
    82  			useLookback = false
    83  			// nolint
    84  			v := n.VectorSelector.(*parser.VectorSelector)
    85  			if d := v.OriginalOffset + n.Range + rangeSoFar; d > queryRange {
    86  				queryRange = d
    87  			}
    88  		case *parser.VectorSelector:
    89  			if d := n.OriginalOffset + rangeSoFar; d > queryRange {
    90  				queryRange = d
    91  			}
    92  		}
    93  
    94  		return nil
    95  	})
    96  
    97  	if useLookback {
    98  		// NB: no range provided in any query selectors; use lookback time.
    99  		queryRange += promDefaultLookback
   100  	}
   101  
   102  	return queryRange
   103  }
   104  
   105  type classificationTags map[string]string
   106  
   107  const (
   108  	resultsClassification  = "results_bucket"
   109  	durationClassification = "duration_bucket"
   110  	unclassified           = "unclassified"
   111  )
   112  
   113  func newClassificationTags() classificationTags {
   114  	return map[string]string{
   115  		resultsClassification:  unclassified,
   116  		durationClassification: unclassified,
   117  	}
   118  }
   119  
   120  // classifyRequest determines the bucket(s) that important request metrics
   121  // will fall into. Supported request metrics at this time are the number of results
   122  // fetched and the time range of the request. The number of buckets and their values
   123  // can be configured by the client. Currently, only query (i.e. query and query_range)
   124  // and label (i.e. label names and label values) endpoints are supported.
   125  func classifyRequest(
   126  	w http.ResponseWriter,
   127  	r *http.Request,
   128  	metrics *classificationMetrics,
   129  	opts Options,
   130  	requestStart time.Time,
   131  	path string,
   132  ) classificationTags {
   133  	var (
   134  		cfg  = opts.Metrics.Config
   135  		tags = newClassificationTags()
   136  	)
   137  
   138  	// NB(nate): have to check for /label/*/values this way since the URL is templated
   139  	labelValues := strings.Contains(path, "/label/") && strings.Contains(path, "/values")
   140  	if path == route.QueryRangeURL || path == route.QueryURL {
   141  		if opts.Metrics.ParseQueryParams == nil {
   142  			metrics.notClassifiable.Inc(1)
   143  			return tags
   144  		}
   145  
   146  		params, err := opts.Metrics.ParseQueryParams(r, requestStart)
   147  		if err != nil || params.Query == "" {
   148  			metrics.badParams.Inc(1)
   149  			return tags
   150  		}
   151  
   152  		if tags, err = classifyForQueryEndpoints(
   153  			cfg.QueryEndpointsClassification, params, w, metrics,
   154  		); err != nil {
   155  			opts.InstrumentOpts.Logger().Error(
   156  				"failed to classify query endpoint request", zap.Error(err),
   157  			)
   158  			metrics.error.Inc(1)
   159  			return tags
   160  		}
   161  	} else if path == route.LabelNamesURL || labelValues {
   162  		start, end, err := prometheus.ParseStartAndEnd(r, opts.Metrics.ParseOptions)
   163  		if err != nil {
   164  			metrics.badParams.Inc(1)
   165  			return tags
   166  		}
   167  		if tags, err = classifyForLabelEndpoints(
   168  			cfg.LabelEndpointsClassification, start, end, w, metrics,
   169  		); err != nil {
   170  			opts.InstrumentOpts.Logger().Error(
   171  				"failed to classify label endpoint request", zap.Error(err),
   172  			)
   173  			metrics.error.Inc(1)
   174  			return tags
   175  		}
   176  	}
   177  
   178  	return tags
   179  }
   180  
   181  func classifyForQueryEndpoints(
   182  	cfg config.QueryClassificationConfig,
   183  	params QueryParams,
   184  	w http.ResponseWriter,
   185  	metrics *classificationMetrics,
   186  ) (classificationTags, error) {
   187  	resultsBuckets := cfg.ResultsBuckets
   188  	tags := newClassificationTags()
   189  	if len(resultsBuckets) > 0 {
   190  		fetchedCount := w.Header().Get(headers.FetchedSeriesCount)
   191  		if fetchedCount == "" {
   192  			fetchedCount = "0"
   193  		}
   194  		fetched, err := strconv.Atoi(fetchedCount)
   195  		if err != nil {
   196  			return newClassificationTags(), err
   197  		}
   198  
   199  		tags[resultsClassification] = strconv.Itoa(resultsBuckets[0])
   200  		for _, bucket := range resultsBuckets {
   201  			if fetched >= bucket {
   202  				tags[resultsClassification] = strconv.Itoa(bucket)
   203  			}
   204  		}
   205  		metrics.classifiedResult.Inc(1)
   206  	}
   207  
   208  	durationBuckets := cfg.DurationBuckets
   209  	if len(durationBuckets) > 0 {
   210  		expr, err := parser.ParseExpr(params.Query)
   211  		if err != nil {
   212  			return newClassificationTags(), err
   213  		}
   214  
   215  		queryRange := retrieveQueryRange(expr, 0)
   216  		duration := params.Range()
   217  		totalDuration := duration + queryRange
   218  
   219  		tags[durationClassification] = durationBuckets[0].String()
   220  		for _, bucket := range durationBuckets {
   221  			if totalDuration >= bucket {
   222  				tags[durationClassification] = bucket.String()
   223  			}
   224  		}
   225  		metrics.classifiedDuration.Inc(1)
   226  	}
   227  
   228  	return tags, nil
   229  }
   230  
   231  func classifyForLabelEndpoints(
   232  	cfg config.QueryClassificationConfig,
   233  	start time.Time,
   234  	end time.Time,
   235  	w http.ResponseWriter,
   236  	metrics *classificationMetrics,
   237  ) (classificationTags, error) {
   238  	resultsBuckets := cfg.ResultsBuckets
   239  	tags := newClassificationTags()
   240  	if len(resultsBuckets) > 0 {
   241  		fetchedCount := w.Header().Get(headers.FetchedMetadataCount)
   242  		if fetchedCount == "" {
   243  			fetchedCount = "0"
   244  		}
   245  		fetched, err := strconv.Atoi(fetchedCount)
   246  		if err != nil {
   247  			return newClassificationTags(), err
   248  		}
   249  
   250  		tags[resultsClassification] = strconv.Itoa(resultsBuckets[0])
   251  		for _, bucket := range resultsBuckets {
   252  			if fetched >= bucket {
   253  				tags[resultsClassification] = strconv.Itoa(bucket)
   254  			}
   255  		}
   256  		metrics.classifiedResult.Inc(1)
   257  	}
   258  
   259  	durationBuckets := cfg.DurationBuckets
   260  	if len(durationBuckets) > 0 {
   261  		duration := end.Sub(start)
   262  		tags[durationClassification] = durationBuckets[0].String()
   263  		for _, bucket := range durationBuckets {
   264  			if duration >= bucket {
   265  				tags[durationClassification] = bucket.String()
   266  			}
   267  		}
   268  		metrics.classifiedDuration.Inc(1)
   269  	}
   270  
   271  	return tags, nil
   272  }