github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prom/read.go (about)

     1  // Copyright (c) 2020 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 prom provides custom handlers that support the prometheus
    22  // query endpoints.
    23  package prom
    24  
    25  import (
    26  	"context"
    27  	"errors"
    28  	"net/http"
    29  	"sync"
    30  
    31  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    32  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/native"
    33  	"github.com/m3db/m3/src/query/api/v1/options"
    34  	"github.com/m3db/m3/src/query/block"
    35  	queryerrors "github.com/m3db/m3/src/query/errors"
    36  	"github.com/m3db/m3/src/query/models"
    37  	"github.com/m3db/m3/src/query/storage"
    38  	"github.com/m3db/m3/src/query/storage/prometheus"
    39  	xerrors "github.com/m3db/m3/src/x/errors"
    40  	xhttp "github.com/m3db/m3/src/x/net/http"
    41  
    42  	errs "github.com/pkg/errors"
    43  	"github.com/prometheus/prometheus/promql"
    44  	"github.com/prometheus/prometheus/promql/parser"
    45  	promstorage "github.com/prometheus/prometheus/storage"
    46  	"github.com/uber-go/tally"
    47  	"go.uber.org/zap"
    48  )
    49  
    50  // NewQueryFn creates a new promql Query.
    51  type NewQueryFn func(params models.RequestParams) (promql.Query, error)
    52  
    53  var (
    54  	newRangeQueryFn = func(
    55  		engineFn options.PromQLEngineFn,
    56  		queryable promstorage.Queryable,
    57  	) NewQueryFn {
    58  		return func(params models.RequestParams) (promql.Query, error) {
    59  			engine, err := engineFn(params.LookbackDuration)
    60  			if err != nil {
    61  				return nil, err
    62  			}
    63  			return engine.NewRangeQuery(
    64  				queryable,
    65  				params.Query,
    66  				params.Start.ToTime(),
    67  				params.End.ToTime(),
    68  				params.Step)
    69  		}
    70  	}
    71  
    72  	newInstantQueryFn = func(
    73  		engineFn options.PromQLEngineFn,
    74  		queryable promstorage.Queryable,
    75  	) NewQueryFn {
    76  		return func(params models.RequestParams) (promql.Query, error) {
    77  			engine, err := engineFn(params.LookbackDuration)
    78  			if err != nil {
    79  				return nil, err
    80  			}
    81  			return engine.NewInstantQuery(
    82  				queryable,
    83  				params.Query,
    84  				params.Now)
    85  		}
    86  	}
    87  )
    88  
    89  type readHandler struct {
    90  	hOpts               options.HandlerOptions
    91  	scope               tally.Scope
    92  	logger              *zap.Logger
    93  	opts                opts
    94  	returnedDataMetrics native.PromReadReturnedDataMetrics
    95  }
    96  
    97  func newReadHandler(
    98  	hOpts options.HandlerOptions,
    99  	options opts,
   100  ) (http.Handler, error) {
   101  	scope := hOpts.InstrumentOpts().MetricsScope().Tagged(
   102  		map[string]string{"handler": "prometheus-read"},
   103  	)
   104  	return &readHandler{
   105  		hOpts:               hOpts,
   106  		opts:                options,
   107  		scope:               scope,
   108  		logger:              hOpts.InstrumentOpts().Logger(),
   109  		returnedDataMetrics: native.NewPromReadReturnedDataMetrics(scope),
   110  	}, nil
   111  }
   112  
   113  func (h *readHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   114  	ctx := r.Context()
   115  	ctx, request, err := native.ParseRequest(ctx, r, h.opts.instant, h.hOpts)
   116  	if err != nil {
   117  		xhttp.WriteError(w, err)
   118  		return
   119  	}
   120  
   121  	params := request.Params
   122  	fetchOptions := request.FetchOpts
   123  
   124  	// NB (@shreyas): We put the FetchOptions in context so it can be
   125  	// retrieved in the queryable object as there is no other way to pass
   126  	// that through.
   127  	//
   128  	// We also put a function into the context that allows callers to safely
   129  	// pass back result metadata concurrently so that they can be combined
   130  	// for later reporting.
   131  	var resultMetadataMutex sync.Mutex
   132  	resultMetadata := block.NewResultMetadata()
   133  	resultMetadataReceiveFn := func(m block.ResultMetadata) {
   134  		resultMetadataMutex.Lock()
   135  		defer resultMetadataMutex.Unlock()
   136  		resultMetadata = resultMetadata.CombineMetadata(m)
   137  	}
   138  	ctx = context.WithValue(ctx, prometheus.FetchOptionsContextKey, fetchOptions)
   139  	ctx = context.WithValue(ctx, prometheus.BlockResultMetadataFnKey, resultMetadataReceiveFn)
   140  
   141  	qry, err := h.opts.newQueryFn(params)
   142  	if err != nil {
   143  		h.logger.Error("error creating query",
   144  			zap.Error(err), zap.String("query", params.Query),
   145  			zap.Bool("instant", h.opts.instant))
   146  		xhttp.WriteError(w, xerrors.NewInvalidParamsError(err))
   147  		return
   148  	}
   149  	defer qry.Close()
   150  
   151  	res := qry.Exec(ctx)
   152  	if res.Err != nil {
   153  		h.logger.Error("error executing query",
   154  			zap.Error(res.Err), zap.String("query", params.Query),
   155  			zap.Bool("instant", h.opts.instant))
   156  		var sErr *prometheus.StorageErr
   157  		if errors.As(res.Err, &sErr) {
   158  			// If the error happened in the m3 storage layer, propagate the causing error as is.
   159  			err := sErr.Unwrap()
   160  			if queryerrors.IsTimeout(err) {
   161  				xhttp.WriteError(w, queryerrors.NewErrQueryTimeout(err))
   162  			} else {
   163  				xhttp.WriteError(w, err)
   164  			}
   165  		} else {
   166  			promErr := errs.Cause(res.Err)
   167  			switch promErr.(type) { //nolint:errorlint
   168  			case promql.ErrQueryTimeout:
   169  				promErr = queryerrors.NewErrQueryTimeout(promErr)
   170  			case promql.ErrQueryCanceled:
   171  			default:
   172  				// Assume any prometheus library error is a 4xx, since there are no remote calls.
   173  				promErr = xerrors.NewInvalidParamsError(res.Err)
   174  			}
   175  			xhttp.WriteError(w, promErr)
   176  		}
   177  		return
   178  	}
   179  
   180  	for _, warn := range resultMetadata.Warnings {
   181  		res.Warnings = append(res.Warnings, errors.New(warn.Message))
   182  	}
   183  
   184  	query := params.Query
   185  	err = ApplyRangeWarnings(query, &resultMetadata)
   186  	if err != nil {
   187  		h.logger.Warn("error applying range warnings",
   188  			zap.Error(err), zap.String("query", query),
   189  			zap.Bool("instant", h.opts.instant))
   190  	}
   191  
   192  	err = handleroptions.AddDBResultResponseHeaders(w, resultMetadata, fetchOptions)
   193  	if err != nil {
   194  		h.logger.Error("error writing database limit headers", zap.Error(err))
   195  		xhttp.WriteError(w, err)
   196  		return
   197  	}
   198  
   199  	returnedDataLimited := h.limitReturnedData(query, res, fetchOptions)
   200  	h.returnedDataMetrics.FetchDatapoints.RecordValue(float64(returnedDataLimited.Datapoints))
   201  	h.returnedDataMetrics.FetchSeries.RecordValue(float64(returnedDataLimited.Series))
   202  
   203  	limited := &handleroptions.ReturnedDataLimited{
   204  		Limited:     returnedDataLimited.Limited,
   205  		Series:      returnedDataLimited.Series,
   206  		TotalSeries: returnedDataLimited.TotalSeries,
   207  		Datapoints:  returnedDataLimited.Datapoints,
   208  	}
   209  	err = handleroptions.AddReturnedLimitResponseHeaders(w, limited, nil)
   210  	if err != nil {
   211  		h.logger.Error("error writing response headers",
   212  			zap.Error(err), zap.String("query", query),
   213  			zap.Bool("instant", h.opts.instant))
   214  		xhttp.WriteError(w, err)
   215  		return
   216  	}
   217  
   218  	if err := Respond(w, &QueryData{
   219  		Result:     res.Value,
   220  		ResultType: res.Value.Type(),
   221  	}, res.Warnings); err != nil {
   222  		h.logger.Error("error writing prom response",
   223  			zap.Error(err),
   224  			zap.String("query", params.Query),
   225  			zap.Bool("instant", h.opts.instant))
   226  	}
   227  }
   228  
   229  func (h *readHandler) limitReturnedData(query string,
   230  	res *promql.Result,
   231  	fetchOpts *storage.FetchOptions,
   232  ) native.ReturnedDataLimited {
   233  	var (
   234  		seriesLimit     = fetchOpts.ReturnedSeriesLimit
   235  		datapointsLimit = fetchOpts.ReturnedDatapointsLimit
   236  
   237  		limited     = false
   238  		series      int
   239  		datapoints  int
   240  		seriesTotal int
   241  	)
   242  	switch res.Value.Type() {
   243  	case parser.ValueTypeVector:
   244  		v, err := res.Vector()
   245  		if err != nil {
   246  			h.logger.Error("error parsing vector for returned data limits",
   247  				zap.Error(err), zap.String("query", query),
   248  				zap.Bool("instant", h.opts.instant))
   249  			break
   250  		}
   251  
   252  		// Determine maxSeries based on either series or datapoints limit. Vector has one datapoint per
   253  		// series and so the datapoint limit behaves the same way as the series one.
   254  		switch {
   255  		case seriesLimit > 0 && datapointsLimit == 0:
   256  			series = seriesLimit
   257  		case seriesLimit == 0 && datapointsLimit > 0:
   258  			series = datapointsLimit
   259  		case seriesLimit == 0 && datapointsLimit == 0:
   260  			// Set max to the actual size if no limits.
   261  			series = len(v)
   262  		default:
   263  			// Take the min of the two limits if both present.
   264  			series = seriesLimit
   265  			if seriesLimit > datapointsLimit {
   266  				series = datapointsLimit
   267  			}
   268  		}
   269  
   270  		seriesTotal = len(v)
   271  		limited = series < seriesTotal
   272  
   273  		if limited {
   274  			limitedSeries := v[:series]
   275  			res.Value = limitedSeries
   276  			datapoints = len(limitedSeries)
   277  		} else {
   278  			series = seriesTotal
   279  			datapoints = seriesTotal
   280  		}
   281  	case parser.ValueTypeMatrix:
   282  		m, err := res.Matrix()
   283  		if err != nil {
   284  			h.logger.Error("error parsing vector for returned data limits",
   285  				zap.Error(err), zap.String("query", query),
   286  				zap.Bool("instant", h.opts.instant))
   287  			break
   288  		}
   289  
   290  		for _, d := range m {
   291  			datapointCount := len(d.Points)
   292  			if fetchOpts.ReturnedSeriesLimit > 0 && series+1 > fetchOpts.ReturnedSeriesLimit {
   293  				limited = true
   294  				break
   295  			}
   296  			if fetchOpts.ReturnedDatapointsLimit > 0 && datapoints+datapointCount > fetchOpts.ReturnedDatapointsLimit {
   297  				limited = true
   298  				break
   299  			}
   300  			series++
   301  			datapoints += datapointCount
   302  		}
   303  		seriesTotal = len(m)
   304  
   305  		if series < seriesTotal {
   306  			res.Value = m[:series]
   307  		}
   308  	default:
   309  	}
   310  
   311  	return native.ReturnedDataLimited{
   312  		Limited:     limited,
   313  		Series:      series,
   314  		Datapoints:  datapoints,
   315  		TotalSeries: seriesTotal,
   316  	}
   317  }