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 }