github.com/thanos-io/thanos@v0.32.5/pkg/queryfrontend/roundtrip.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package queryfrontend
     5  
     6  import (
     7  	"net/http"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/thanos-io/thanos/pkg/querysharding"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/pkg/errors"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/client_golang/prometheus/promauto"
    18  
    19  	"github.com/thanos-io/thanos/internal/cortex/querier/queryrange"
    20  	"github.com/thanos-io/thanos/internal/cortex/util/validation"
    21  )
    22  
    23  const (
    24  	// labels used in metrics.
    25  	rangeQueryOp   = "query_range"
    26  	instantQueryOp = "query"
    27  	labelNamesOp   = "label_names"
    28  	labelValuesOp  = "label_values"
    29  	seriesOp       = "series"
    30  )
    31  
    32  var labelValuesPattern = regexp.MustCompile("/api/v1/label/.+/values$")
    33  
    34  // NewTripperware returns a Tripperware which sends requests to different sub tripperwares based on the query type.
    35  func NewTripperware(config Config, reg prometheus.Registerer, logger log.Logger) (queryrange.Tripperware, error) {
    36  	var (
    37  		queryRangeLimits, labelsLimits queryrange.Limits
    38  		err                            error
    39  	)
    40  	if config.QueryRangeConfig.Limits != nil {
    41  		queryRangeLimits, err = validation.NewOverrides(*config.QueryRangeConfig.Limits, nil)
    42  		if err != nil {
    43  			return nil, errors.Wrap(err, "initialize query range limits")
    44  		}
    45  	}
    46  
    47  	if config.LabelsConfig.Limits != nil {
    48  		labelsLimits, err = validation.NewOverrides(*config.LabelsConfig.Limits, nil)
    49  		if err != nil {
    50  			return nil, errors.Wrap(err, "initialize labels limits")
    51  		}
    52  	}
    53  
    54  	queryRangeCodec := NewThanosQueryRangeCodec(config.QueryRangeConfig.PartialResponseStrategy)
    55  	labelsCodec := NewThanosLabelsCodec(config.LabelsConfig.PartialResponseStrategy, config.DefaultTimeRange)
    56  	queryInstantCodec := NewThanosQueryInstantCodec(config.QueryRangeConfig.PartialResponseStrategy)
    57  
    58  	queryRangeTripperware, err := newQueryRangeTripperware(
    59  		config.QueryRangeConfig,
    60  		queryRangeLimits,
    61  		queryRangeCodec,
    62  		config.NumShards,
    63  		prometheus.WrapRegistererWith(prometheus.Labels{"tripperware": "query_range"}, reg), logger, config.ForwardHeaders)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	labelsTripperware, err := newLabelsTripperware(config.LabelsConfig, labelsLimits, labelsCodec,
    69  		prometheus.WrapRegistererWith(prometheus.Labels{"tripperware": "labels"}, reg), logger, config.ForwardHeaders)
    70  	if err != nil {
    71  		return nil, err
    72  	}
    73  	queryInstantTripperware := newInstantQueryTripperware(
    74  		config.NumShards,
    75  		queryRangeLimits,
    76  		queryInstantCodec,
    77  		prometheus.WrapRegistererWith(prometheus.Labels{"tripperware": "query_instant"}, reg),
    78  		config.ForwardHeaders,
    79  	)
    80  	return func(next http.RoundTripper) http.RoundTripper {
    81  		return newRoundTripper(next, queryRangeTripperware(next), labelsTripperware(next), queryInstantTripperware(next), reg)
    82  	}, nil
    83  }
    84  
    85  type roundTripper struct {
    86  	next, queryInstant, queryRange, labels http.RoundTripper
    87  
    88  	queriesCount *prometheus.CounterVec
    89  }
    90  
    91  func newRoundTripper(next, queryRange, metadata, queryInstant http.RoundTripper, reg prometheus.Registerer) roundTripper {
    92  	r := roundTripper{
    93  		next:         next,
    94  		queryInstant: queryInstant,
    95  		queryRange:   queryRange,
    96  		labels:       metadata,
    97  		queriesCount: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{
    98  			Name: "thanos_query_frontend_queries_total",
    99  			Help: "Total queries passing through query frontend",
   100  		}, []string{"op"}),
   101  	}
   102  
   103  	r.queriesCount.WithLabelValues(instantQueryOp)
   104  	r.queriesCount.WithLabelValues(rangeQueryOp)
   105  	r.queriesCount.WithLabelValues(labelNamesOp)
   106  	r.queriesCount.WithLabelValues(labelValuesOp)
   107  	r.queriesCount.WithLabelValues(seriesOp)
   108  	return r
   109  }
   110  
   111  func (r roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   112  	switch op := getOperation(req); op {
   113  	case instantQueryOp:
   114  		r.queriesCount.WithLabelValues(instantQueryOp).Inc()
   115  		return r.queryInstant.RoundTrip(req)
   116  	case rangeQueryOp:
   117  		r.queriesCount.WithLabelValues(rangeQueryOp).Inc()
   118  		return r.queryRange.RoundTrip(req)
   119  	case labelNamesOp, labelValuesOp, seriesOp:
   120  		r.queriesCount.WithLabelValues(op).Inc()
   121  		return r.labels.RoundTrip(req)
   122  	default:
   123  	}
   124  
   125  	return r.next.RoundTrip(req)
   126  }
   127  
   128  func getOperation(r *http.Request) string {
   129  	if r.Method == http.MethodGet || r.Method == http.MethodPost {
   130  		switch {
   131  		case strings.HasSuffix(r.URL.Path, "/api/v1/query"):
   132  			return instantQueryOp
   133  		case strings.HasSuffix(r.URL.Path, "/api/v1/query_range"):
   134  			return rangeQueryOp
   135  		case strings.HasSuffix(r.URL.Path, "/api/v1/labels"):
   136  			return labelNamesOp
   137  		case strings.HasSuffix(r.URL.Path, "/api/v1/series"):
   138  			return seriesOp
   139  		default:
   140  			if labelValuesPattern.MatchString(r.URL.Path) {
   141  				return labelValuesOp
   142  			}
   143  		}
   144  	}
   145  
   146  	return ""
   147  }
   148  
   149  // newQueryRangeTripperware returns a Tripperware for range queries configured with middlewares of
   150  // limit, step align, downsampled, split by interval, cache requests and retry.
   151  func newQueryRangeTripperware(
   152  	config QueryRangeConfig,
   153  	limits queryrange.Limits,
   154  	codec *queryRangeCodec,
   155  	numShards int,
   156  	reg prometheus.Registerer,
   157  	logger log.Logger,
   158  	forwardHeaders []string,
   159  ) (queryrange.Tripperware, error) {
   160  	queryRangeMiddleware := []queryrange.Middleware{queryrange.NewLimitsMiddleware(limits)}
   161  	m := queryrange.NewInstrumentMiddlewareMetrics(reg)
   162  
   163  	// step align middleware.
   164  	if config.AlignRangeWithStep {
   165  		queryRangeMiddleware = append(
   166  			queryRangeMiddleware,
   167  			queryrange.InstrumentMiddleware("step_align", m),
   168  			queryrange.StepAlignMiddleware,
   169  		)
   170  	}
   171  
   172  	if config.RequestDownsampled {
   173  		queryRangeMiddleware = append(
   174  			queryRangeMiddleware,
   175  			queryrange.InstrumentMiddleware("downsampled", m),
   176  			DownsampledMiddleware(codec, reg),
   177  		)
   178  	}
   179  
   180  	if config.SplitQueriesByInterval != 0 || config.MinQuerySplitInterval != 0 {
   181  		queryIntervalFn := dynamicIntervalFn(config)
   182  
   183  		queryRangeMiddleware = append(
   184  			queryRangeMiddleware,
   185  			queryrange.InstrumentMiddleware("split_by_interval", m),
   186  			SplitByIntervalMiddleware(queryIntervalFn, limits, codec, reg),
   187  		)
   188  	}
   189  
   190  	if numShards > 0 {
   191  		analyzer := querysharding.NewQueryAnalyzer()
   192  		queryRangeMiddleware = append(
   193  			queryRangeMiddleware,
   194  			PromQLShardingMiddleware(analyzer, numShards, limits, codec, reg),
   195  		)
   196  	}
   197  
   198  	if config.ResultsCacheConfig != nil {
   199  		queryCacheMiddleware, _, err := queryrange.NewResultsCacheMiddleware(
   200  			logger,
   201  			*config.ResultsCacheConfig,
   202  			newThanosCacheKeyGenerator(dynamicIntervalFn(config)),
   203  			limits,
   204  			codec,
   205  			queryrange.PrometheusResponseExtractor{},
   206  			nil,
   207  			shouldCache,
   208  			reg,
   209  		)
   210  		if err != nil {
   211  			return nil, errors.Wrap(err, "create results cache middleware")
   212  		}
   213  
   214  		queryRangeMiddleware = append(
   215  			queryRangeMiddleware,
   216  			queryrange.InstrumentMiddleware("results_cache", m),
   217  			queryCacheMiddleware,
   218  		)
   219  	}
   220  
   221  	if config.MaxRetries > 0 {
   222  		queryRangeMiddleware = append(
   223  			queryRangeMiddleware,
   224  			queryrange.InstrumentMiddleware("retry", m),
   225  			queryrange.NewRetryMiddleware(logger, config.MaxRetries, queryrange.NewRetryMiddlewareMetrics(reg)),
   226  		)
   227  	}
   228  
   229  	return func(next http.RoundTripper) http.RoundTripper {
   230  		rt := queryrange.NewRoundTripper(next, codec, forwardHeaders, queryRangeMiddleware...)
   231  		return queryrange.RoundTripFunc(func(r *http.Request) (*http.Response, error) {
   232  			return rt.RoundTrip(r)
   233  		})
   234  	}, nil
   235  }
   236  
   237  func dynamicIntervalFn(config QueryRangeConfig) queryrange.IntervalFn {
   238  	return func(r queryrange.Request) time.Duration {
   239  		// Use static interval, by default.
   240  		if config.SplitQueriesByInterval != 0 {
   241  			return config.SplitQueriesByInterval
   242  		}
   243  
   244  		queryInterval := time.Duration(r.GetEnd()-r.GetStart()) * time.Millisecond
   245  		// If the query is multiple of max interval, we use the max interval to split.
   246  		if queryInterval/config.MaxQuerySplitInterval >= 2 {
   247  			return config.MaxQuerySplitInterval
   248  		}
   249  
   250  		if queryInterval > config.MinQuerySplitInterval {
   251  			// If the query duration is less than max interval, we split it equally in HorizontalShards.
   252  			return time.Duration(queryInterval.Milliseconds()/config.HorizontalShards) * time.Millisecond
   253  		}
   254  
   255  		return config.MinQuerySplitInterval
   256  	}
   257  }
   258  
   259  // newLabelsTripperware returns a Tripperware for labels and series requests
   260  // configured with middlewares of split by interval and retry.
   261  func newLabelsTripperware(
   262  	config LabelsConfig,
   263  	limits queryrange.Limits,
   264  	codec *labelsCodec,
   265  	reg prometheus.Registerer,
   266  	logger log.Logger,
   267  	forwardHeaders []string,
   268  ) (queryrange.Tripperware, error) {
   269  	labelsMiddleware := []queryrange.Middleware{}
   270  	m := queryrange.NewInstrumentMiddlewareMetrics(reg)
   271  
   272  	queryIntervalFn := func(_ queryrange.Request) time.Duration {
   273  		return config.SplitQueriesByInterval
   274  	}
   275  
   276  	if config.SplitQueriesByInterval != 0 {
   277  		labelsMiddleware = append(
   278  			labelsMiddleware,
   279  			queryrange.InstrumentMiddleware("split_interval", m),
   280  			SplitByIntervalMiddleware(queryIntervalFn, limits, codec, reg),
   281  		)
   282  	}
   283  
   284  	if config.ResultsCacheConfig != nil {
   285  		staticIntervalFn := func(_ queryrange.Request) time.Duration { return config.SplitQueriesByInterval }
   286  		queryCacheMiddleware, _, err := queryrange.NewResultsCacheMiddleware(
   287  			logger,
   288  			*config.ResultsCacheConfig,
   289  			newThanosCacheKeyGenerator(staticIntervalFn),
   290  			limits,
   291  			codec,
   292  			ThanosResponseExtractor{},
   293  			nil,
   294  			shouldCache,
   295  			reg,
   296  		)
   297  		if err != nil {
   298  			return nil, errors.Wrap(err, "create results cache middleware")
   299  		}
   300  
   301  		labelsMiddleware = append(
   302  			labelsMiddleware,
   303  			queryrange.InstrumentMiddleware("results_cache", m),
   304  			queryCacheMiddleware,
   305  		)
   306  	}
   307  
   308  	if config.MaxRetries > 0 {
   309  		labelsMiddleware = append(
   310  			labelsMiddleware,
   311  			queryrange.InstrumentMiddleware("retry", m),
   312  			queryrange.NewRetryMiddleware(logger, config.MaxRetries, queryrange.NewRetryMiddlewareMetrics(reg)),
   313  		)
   314  	}
   315  	return func(next http.RoundTripper) http.RoundTripper {
   316  		rt := queryrange.NewRoundTripper(next, codec, forwardHeaders, labelsMiddleware...)
   317  		return queryrange.RoundTripFunc(func(r *http.Request) (*http.Response, error) {
   318  			return rt.RoundTrip(r)
   319  		})
   320  	}, nil
   321  }
   322  
   323  func newInstantQueryTripperware(
   324  	numShards int,
   325  	limits queryrange.Limits,
   326  	codec queryrange.Codec,
   327  	reg prometheus.Registerer,
   328  	forwardHeaders []string,
   329  ) queryrange.Tripperware {
   330  	instantQueryMiddlewares := []queryrange.Middleware{}
   331  	m := queryrange.NewInstrumentMiddlewareMetrics(reg)
   332  	if numShards > 0 {
   333  		analyzer := querysharding.NewQueryAnalyzer()
   334  		instantQueryMiddlewares = append(
   335  			instantQueryMiddlewares,
   336  			queryrange.InstrumentMiddleware("sharding", m),
   337  			PromQLShardingMiddleware(analyzer, numShards, limits, codec, reg),
   338  		)
   339  	}
   340  
   341  	return func(next http.RoundTripper) http.RoundTripper {
   342  		rt := queryrange.NewRoundTripper(next, codec, forwardHeaders, instantQueryMiddlewares...)
   343  		return queryrange.RoundTripFunc(func(r *http.Request) (*http.Response, error) {
   344  			return rt.RoundTrip(r)
   345  		})
   346  	}
   347  }
   348  
   349  // shouldCache controls what kind of Thanos request should be cached.
   350  // For more information about requests that skip caching logic, please visit
   351  // the query-frontend documentation.
   352  func shouldCache(r queryrange.Request) bool {
   353  	if thanosReqStoreMatcherGettable, ok := r.(ThanosRequestStoreMatcherGetter); ok {
   354  		if len(thanosReqStoreMatcherGettable.GetStoreMatchers()) > 0 {
   355  			return false
   356  		}
   357  	}
   358  
   359  	if thanosReqDedup, ok := r.(ThanosRequestDedup); ok {
   360  		if !thanosReqDedup.IsDedupEnabled() {
   361  			return false
   362  		}
   363  	}
   364  
   365  	return !r.GetCachingOptions().Disabled
   366  }