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 }