github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/api/v1/middleware/rewrite.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  	"bytes"
    25  	"context"
    26  	"errors"
    27  	"io/ioutil"
    28  	"net/http"
    29  	"net/url"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus"
    33  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    34  	"github.com/m3db/m3/src/query/storage"
    35  	xhttp "github.com/m3db/m3/src/x/net/http"
    36  	xtime "github.com/m3db/m3/src/x/time"
    37  
    38  	"github.com/gorilla/mux"
    39  	"github.com/prometheus/prometheus/promql"
    40  	"github.com/prometheus/prometheus/promql/parser"
    41  	promstorage "github.com/prometheus/prometheus/storage"
    42  	"go.uber.org/zap"
    43  )
    44  
    45  var errIgnorableQuerierError = errors.New("ignorable error")
    46  
    47  // PrometheusRangeRewriteOptions are the options for the prometheus range rewriting middleware.
    48  type PrometheusRangeRewriteOptions struct { // nolint:maligned
    49  	Enabled              bool
    50  	FetchOptionsBuilder  handleroptions.FetchOptionsBuilder
    51  	Instant              bool
    52  	ResolutionMultiplier int
    53  	DefaultLookback      time.Duration
    54  	Storage              storage.Storage
    55  
    56  	// TODO(marcus): There's a conversation with Prometheus about supporting dynamic lookback.
    57  	//  We can replace this with a single engine reference if that work is ever completed.
    58  	//   https://groups.google.com/g/prometheus-developers/c/9wzuobfLMV8
    59  	PrometheusEngineFn func(time.Duration) (*promql.Engine, error)
    60  }
    61  
    62  // PrometheusRangeRewrite is middleware that, when enabled, will rewrite the query parameter
    63  // on the request (url and body, if present) if it's determined that the query contains a
    64  // range duration that is less than the resolution that will be used to serve the request.
    65  // With this middleware disabled, queries like this will return no data. When enabled, the
    66  // range is updated to be the namespace resolution * a configurable multiple which should allow
    67  // for data to be returned.
    68  func PrometheusRangeRewrite(opts Options) mux.MiddlewareFunc {
    69  	return func(base http.Handler) http.Handler {
    70  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    71  			var (
    72  				mwOpts   = opts.PrometheusRangeRewrite
    73  				mult     = mwOpts.ResolutionMultiplier
    74  				disabled = !mwOpts.Enabled
    75  			)
    76  			if disabled || mult == 0 {
    77  				base.ServeHTTP(w, r)
    78  				return
    79  			}
    80  
    81  			logger := opts.InstrumentOpts.Logger()
    82  			if err := RewriteRangeDuration(r, mwOpts, logger); err != nil {
    83  				logger.Error("could not rewrite range", zap.Error(err))
    84  				xhttp.WriteError(w, err)
    85  				return
    86  			}
    87  			base.ServeHTTP(w, r)
    88  		})
    89  	}
    90  }
    91  
    92  const (
    93  	queryParam    = "query"
    94  	startParam    = "start"
    95  	endParam      = "end"
    96  	lookbackParam = handleroptions.LookbackParam
    97  )
    98  
    99  // RewriteRangeDuration is the driver function for the PrometheusRangeRewrite middleware
   100  func RewriteRangeDuration(
   101  	r *http.Request,
   102  	opts PrometheusRangeRewriteOptions,
   103  	logger *zap.Logger,
   104  ) error {
   105  	// Extract relevant query params
   106  	params, err := extractParams(r, opts.Instant)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	defer func() {
   111  		// Reset the body on the request for any handlers that may want access to the raw body.
   112  		if opts.Instant {
   113  			// NB(nate): shared time param parsing logic modifies the form on the request to set these
   114  			// to the "now" string to aid in parsing. Remove these before resetting the body.
   115  			r.Form.Del(startParam)
   116  			r.Form.Del(endParam)
   117  		}
   118  
   119  		if r.Method == "GET" {
   120  			return
   121  		}
   122  
   123  		body := r.Form.Encode()
   124  		r.Body = ioutil.NopCloser(bytes.NewBufferString(body))
   125  	}()
   126  
   127  	// Query for namespace metadata of namespaces used to service the request
   128  	store := opts.Storage
   129  	ctx, fetchOpts, err := opts.FetchOptionsBuilder.NewFetchOptions(r.Context(), r)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	// Get the appropriate time range before updating the lookback
   135  	// This is necessary to cover things like the offset and `@` modifiers.
   136  	startTime, endTime := getQueryBounds(opts, params, fetchOpts, logger)
   137  	res, err := findLargestQueryResolution(ctx, store, fetchOpts, startTime, endTime)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	// Largest resolution is 0 which means we're routing to the unaggregated namespace.
   142  	// Unaggregated namespace can service all requests, so return.
   143  	if res == 0 {
   144  		return nil
   145  	}
   146  
   147  	updatedLookback, updateLookback := maybeUpdateLookback(params, res, opts)
   148  	originalLookback := params.lookback
   149  
   150  	// We use the lookback as a part of bounds calculation
   151  	// If the lookback had changed, we need to recalculate the bounds
   152  	if updateLookback {
   153  		params.lookback = updatedLookback
   154  		startTime, endTime = getQueryBounds(opts, params, fetchOpts, logger)
   155  		res, err = findLargestQueryResolution(ctx, store, fetchOpts, startTime, endTime)
   156  		if err != nil {
   157  			return err
   158  		}
   159  	}
   160  
   161  	// parse the query so that we can manipulate it
   162  	expr, err := parser.ParseExpr(params.query)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	updatedQuery, updateQuery := maybeRewriteRangeInQuery(params.query, expr, res, opts.ResolutionMultiplier)
   167  
   168  	if !updateQuery && !updateLookback {
   169  		return nil
   170  	}
   171  
   172  	// Update query and lookback params in URL, if present and needed.
   173  	urlQueryValues, err := url.ParseQuery(r.URL.RawQuery)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	if urlQueryValues.Get(queryParam) != "" {
   178  		if updateQuery {
   179  			urlQueryValues.Set(queryParam, updatedQuery)
   180  		}
   181  		if updateLookback {
   182  			urlQueryValues.Set(lookbackParam, updatedLookback.String())
   183  		}
   184  	}
   185  	updatedURL, err := url.Parse(r.URL.String())
   186  	if err != nil {
   187  		return err
   188  	}
   189  	updatedURL.RawQuery = urlQueryValues.Encode()
   190  	r.URL = updatedURL
   191  
   192  	// Update query and lookback params in the request body, if present and needed.
   193  	if r.Form.Get(queryParam) != "" {
   194  		if updateQuery {
   195  			r.Form.Set(queryParam, updatedQuery)
   196  		}
   197  		if updateLookback {
   198  			r.Form.Set(lookbackParam, updatedLookback.String())
   199  		}
   200  	}
   201  
   202  	logger.Debug("rewrote duration values in request",
   203  		zap.String("originalQuery", params.query),
   204  		zap.String("updatedQuery", updatedQuery),
   205  		zap.Duration("originalLookback", originalLookback),
   206  		zap.Duration("updatedLookback", updatedLookback))
   207  
   208  	return nil
   209  }
   210  
   211  func findLargestQueryResolution(ctx context.Context,
   212  	store storage.Storage,
   213  	fetchOpts *storage.FetchOptions,
   214  	startTime time.Time,
   215  	endTime time.Time,
   216  ) (time.Duration, error) {
   217  	attrs, err := store.QueryStorageMetadataAttributes(ctx, startTime, endTime, fetchOpts)
   218  	if err != nil {
   219  		return 0, err
   220  	}
   221  
   222  	// Find the largest resolution
   223  	var res time.Duration
   224  	for _, attr := range attrs {
   225  		if attr.Resolution > res {
   226  			res = attr.Resolution
   227  		}
   228  	}
   229  	return res, nil
   230  }
   231  
   232  // Using the prometheus engine in this way should be considered
   233  // optional and best effort. Fall back to the frequently accurate logic
   234  // of using the start and end time in the request
   235  func getQueryBounds(
   236  	opts PrometheusRangeRewriteOptions,
   237  	params params,
   238  	fetchOpts *storage.FetchOptions,
   239  	logger *zap.Logger,
   240  ) (start time.Time, end time.Time) {
   241  	start = params.start
   242  	end = params.end
   243  	if opts.PrometheusEngineFn == nil {
   244  		return start, end
   245  	}
   246  
   247  	lookback := opts.DefaultLookback
   248  	if params.isLookbackSet {
   249  		lookback = params.lookback
   250  	}
   251  	engine, err := opts.PrometheusEngineFn(lookback)
   252  	if err != nil {
   253  		logger.Debug("Found an error when getting a Prom engine to "+
   254  			"calculate start/end time for query rewriting. Falling back to request start/end time",
   255  			zap.String("originalQuery", params.query),
   256  			zap.Duration("lookbackDuration", lookback))
   257  		return start, end
   258  	}
   259  
   260  	queryable := fakeQueryable{
   261  		engine:  engine,
   262  		instant: opts.Instant,
   263  	}
   264  	err = queryable.calculateQueryBounds(params.query, params.start, params.end, fetchOpts.Step)
   265  	if err != nil {
   266  		logger.Debug("Found an error when using the Prom engine to "+
   267  			"calculate start/end time for query rewriting. Falling back to request start/end time",
   268  			zap.String("originalQuery", params.query))
   269  		return start, end
   270  	}
   271  	// calculates the query boundaries in roughly the same way as prometheus
   272  	start, end = queryable.getQueryBounds()
   273  	return start, end
   274  }
   275  
   276  type params struct {
   277  	query         string
   278  	start, end    time.Time
   279  	lookback      time.Duration
   280  	isLookbackSet bool
   281  }
   282  
   283  func extractParams(r *http.Request, instant bool) (params, error) {
   284  	if err := r.ParseForm(); err != nil {
   285  		return params{}, err
   286  	}
   287  
   288  	query := r.FormValue(queryParam)
   289  
   290  	if instant {
   291  		prometheus.SetDefaultStartEndParamsForInstant(r)
   292  	}
   293  
   294  	timeParams, err := prometheus.ParseTimeParams(r)
   295  	if err != nil {
   296  		return params{}, err
   297  	}
   298  
   299  	lookback, isLookbackSet, err := handleroptions.ParseLookbackDuration(r)
   300  	if err != nil {
   301  		return params{}, err
   302  	}
   303  
   304  	return params{
   305  		query:         query,
   306  		start:         timeParams.Start,
   307  		end:           timeParams.End,
   308  		lookback:      lookback,
   309  		isLookbackSet: isLookbackSet,
   310  	}, nil
   311  }
   312  
   313  func maybeRewriteRangeInQuery(query string, expr parser.Node, res time.Duration, multiplier int) (string, bool) {
   314  	updated := false // nolint: ifshort
   315  	parser.Inspect(expr, func(node parser.Node, path []parser.Node) error {
   316  		// nolint:gocritic
   317  		switch n := node.(type) {
   318  		case *parser.MatrixSelector:
   319  			if n.Range <= res {
   320  				n.Range = res * time.Duration(multiplier)
   321  				updated = true
   322  			}
   323  		}
   324  		return nil
   325  	})
   326  
   327  	if updated {
   328  		return expr.String(), true
   329  	}
   330  	return query, false
   331  }
   332  
   333  func maybeUpdateLookback(
   334  	params params,
   335  	maxResolution time.Duration,
   336  	opts PrometheusRangeRewriteOptions,
   337  ) (time.Duration, bool) {
   338  	var (
   339  		lookback                = params.lookback
   340  		resolutionBasedLookback = maxResolution * time.Duration(opts.ResolutionMultiplier) // nolint: durationcheck
   341  	)
   342  	if !params.isLookbackSet {
   343  		lookback = opts.DefaultLookback
   344  	}
   345  	if lookback < resolutionBasedLookback {
   346  		return resolutionBasedLookback, true
   347  	}
   348  	return lookback, false
   349  }
   350  
   351  type fakeQueryable struct {
   352  	engine              *promql.Engine
   353  	instant             bool
   354  	calculatedStartTime time.Time
   355  	calculatedEndTime   time.Time
   356  }
   357  
   358  func (f *fakeQueryable) Querier(ctx context.Context, mint, maxt int64) (promstorage.Querier, error) {
   359  	f.calculatedStartTime = xtime.FromUnixMillis(mint)
   360  	f.calculatedEndTime = xtime.FromUnixMillis(maxt)
   361  	// fail here to cause prometheus to give up on query execution
   362  	return nil, errIgnorableQuerierError
   363  }
   364  
   365  func (f *fakeQueryable) calculateQueryBounds(
   366  	q string,
   367  	start time.Time,
   368  	end time.Time,
   369  	step time.Duration,
   370  ) (err error) {
   371  	var query promql.Query
   372  	if f.instant {
   373  		// startTime and endTime are the same for instant queries
   374  		query, err = f.engine.NewInstantQuery(f, q, start)
   375  	} else {
   376  		query, err = f.engine.NewRangeQuery(f, q, start, end, step)
   377  	}
   378  	if err != nil {
   379  		return err
   380  	}
   381  	// The result returned by Exec will be an error, but that's expected
   382  	if res := query.Exec(context.Background()); !errors.Is(res.Err, errIgnorableQuerierError) {
   383  		return err
   384  	}
   385  	return nil
   386  }
   387  
   388  func (f *fakeQueryable) getQueryBounds() (startTime time.Time, endTime time.Time) {
   389  	return f.calculatedStartTime, f.calculatedEndTime
   390  }