github.com/m3db/m3@v1.5.0/src/query/api/v1/middleware/rewrite_test.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 "io/ioutil" 26 "net/http" 27 "net/http/httptest" 28 "net/url" 29 "testing" 30 "time" 31 32 "github.com/go-kit/kit/log" 33 kitlogzap "github.com/go-kit/kit/log/zap" 34 "github.com/gorilla/mux" 35 "github.com/prometheus/prometheus/promql" 36 "github.com/stretchr/testify/require" 37 "go.uber.org/zap/zapcore" 38 39 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 40 "github.com/m3db/m3/src/query/storage/m3/storagemetadata" 41 "github.com/m3db/m3/src/query/storage/mock" 42 "github.com/m3db/m3/src/x/instrument" 43 ) 44 45 func TestPrometheusRangeRewrite(t *testing.T) { 46 // nolint:maligned 47 queryTests := []struct { 48 name string 49 attrs []storagemetadata.Attributes 50 enabled bool 51 mult int 52 query string 53 instant bool 54 lookback *time.Duration 55 56 expectedQuery string 57 expectedLookback *time.Duration 58 usePromEngine bool 59 }{ 60 { 61 name: "query with range to unagg", 62 attrs: unaggregatedAttrs(), 63 enabled: true, 64 mult: 2, 65 query: "rate(foo[1m])", 66 67 expectedQuery: "rate(foo[1m])", 68 }, 69 { 70 name: "query with no range", 71 attrs: unaggregatedAttrs(), 72 enabled: true, 73 mult: 2, 74 query: "foo", 75 76 expectedQuery: "foo", 77 }, 78 { 79 name: "query with rewriteable range", 80 attrs: aggregatedAttrs(5 * time.Minute), 81 enabled: true, 82 mult: 2, 83 query: "rate(foo[30s])", 84 85 expectedQuery: "rate(foo[10m])", 86 expectedLookback: durationPtr(10 * time.Minute), 87 }, 88 { 89 name: "query with range to agg; no rewrite", 90 attrs: aggregatedAttrs(1 * time.Minute), 91 enabled: true, 92 mult: 2, 93 query: "rate(foo[5m])", 94 95 expectedQuery: "rate(foo[5m])", 96 }, 97 { 98 name: "query with rewriteable range; disabled", 99 attrs: aggregatedAttrs(5 * time.Minute), 100 enabled: false, 101 mult: 2, 102 query: "rate(foo[30s])", 103 104 expectedQuery: "rate(foo[30s])", 105 }, 106 { 107 name: "query with rewriteable range; zero multiplier", 108 attrs: aggregatedAttrs(5 * time.Minute), 109 enabled: false, 110 mult: 0, 111 query: "rate(foo[30s])", 112 113 expectedQuery: "rate(foo[30s])", 114 }, 115 { 116 name: "instant query; no rewrite", 117 attrs: unaggregatedAttrs(), 118 enabled: true, 119 mult: 3, 120 instant: true, 121 query: "rate(foo[1m])", 122 123 expectedQuery: "rate(foo[1m])", 124 }, 125 { 126 name: "instant query; rewrite", 127 attrs: aggregatedAttrs(5 * time.Minute), 128 enabled: true, 129 mult: 3, 130 instant: true, 131 query: "rate(foo[30s])", 132 133 expectedQuery: "rate(foo[15m])", 134 expectedLookback: durationPtr(15 * time.Minute), 135 }, 136 { 137 name: "range with lookback not set; keep default lookback", 138 attrs: aggregatedAttrs(1 * time.Minute), 139 enabled: true, 140 mult: 2, 141 query: "foo", 142 143 expectedQuery: "foo", 144 }, 145 { 146 name: "instant with lookback not set; keep default lookback", 147 attrs: aggregatedAttrs(1 * time.Minute), 148 enabled: true, 149 mult: 2, 150 instant: true, 151 query: "foo", 152 153 expectedQuery: "foo", 154 }, 155 { 156 name: "range with lookback not set; rewrite lookback to higher", 157 attrs: aggregatedAttrs(3 * time.Minute), 158 enabled: true, 159 mult: 3, 160 query: "foo", 161 162 expectedQuery: "foo", 163 expectedLookback: durationPtr(9 * time.Minute), 164 }, 165 { 166 name: "instant with lookback not set; rewrite lookback to higher", 167 attrs: aggregatedAttrs(4 * time.Minute), 168 enabled: true, 169 mult: 3, 170 instant: true, 171 query: "foo", 172 173 expectedQuery: "foo", 174 expectedLookback: durationPtr(12 * time.Minute), 175 }, 176 { 177 name: "range with lookback already set; keep existing lookback", 178 attrs: aggregatedAttrs(5 * time.Minute), 179 enabled: true, 180 mult: 2, 181 query: "foo", 182 lookback: durationPtr(11 * time.Minute), 183 184 expectedQuery: "foo", 185 expectedLookback: durationPtr(11 * time.Minute), 186 }, 187 { 188 name: "instant with lookback already set; keep existing lookback", 189 attrs: aggregatedAttrs(4 * time.Minute), 190 enabled: true, 191 mult: 3, 192 instant: true, 193 query: "foo", 194 lookback: durationPtr(13 * time.Minute), 195 196 expectedQuery: "foo", 197 expectedLookback: durationPtr(13 * time.Minute), 198 }, 199 { 200 name: "range with lookback already set; rewrite lookback to higher", 201 attrs: aggregatedAttrs(5 * time.Minute), 202 enabled: true, 203 mult: 3, 204 query: "foo", 205 lookback: durationPtr(11 * time.Minute), 206 207 expectedQuery: "foo", 208 expectedLookback: durationPtr(15 * time.Minute), 209 }, 210 { 211 name: "instant with lookback already set; rewrite lookback to higher", 212 attrs: aggregatedAttrs(5 * time.Minute), 213 enabled: true, 214 mult: 3, 215 instant: true, 216 query: "foo", 217 lookback: durationPtr(13 * time.Minute), 218 219 expectedQuery: "foo", 220 expectedLookback: durationPtr(15 * time.Minute), 221 }, 222 { 223 name: "instant query; rewrite w/ prom engine", 224 attrs: aggregatedAttrs(5 * time.Minute), 225 enabled: true, 226 mult: 3, 227 instant: true, 228 usePromEngine: true, 229 query: "rate(foo[30s])", 230 lookback: durationPtr(15 * time.Minute), 231 232 expectedQuery: "rate(foo[15m])", 233 expectedLookback: durationPtr(15 * time.Minute), 234 }, 235 { 236 name: "instant query; rewrite w/ prom engine & offset", 237 // Just testing the parsing code paths since this is a fake storage 238 attrs: aggregatedAttrs(5 * time.Minute), 239 enabled: true, 240 usePromEngine: true, 241 mult: 3, 242 instant: true, 243 query: "rate(foo[30s] offset 1w)", 244 lookback: durationPtr(15 * time.Minute), 245 246 expectedQuery: "rate(foo[15m] offset 1w)", 247 expectedLookback: durationPtr(15 * time.Minute), 248 }, 249 { 250 name: "range query; rewrite w/ prom engine & offset", 251 // Just testing the parsing code paths since this is a fake storage 252 attrs: aggregatedAttrs(5 * time.Minute), 253 enabled: true, 254 usePromEngine: true, 255 mult: 3, 256 instant: false, 257 query: "rate(foo[30s] offset 1w)", 258 lookback: durationPtr(15 * time.Minute), 259 260 expectedQuery: "rate(foo[15m] offset 1w)", 261 expectedLookback: durationPtr(15 * time.Minute), 262 }, 263 } 264 for _, tt := range queryTests { 265 t.Run(tt.name, func(t *testing.T) { 266 r := mux.NewRouter() 267 268 opts := makeBaseOpts(t, r, tt.usePromEngine) 269 270 store := opts.PrometheusRangeRewrite.Storage.(mock.Storage) 271 store.SetQueryStorageMetadataAttributesResult(tt.attrs, nil) 272 273 opts.PrometheusRangeRewrite.Enabled = tt.enabled 274 opts.PrometheusRangeRewrite.ResolutionMultiplier = tt.mult 275 opts.PrometheusRangeRewrite.Instant = tt.instant 276 277 params := url.Values{} 278 params.Add("step", (time.Duration(3600) * time.Second).String()) 279 if tt.instant { 280 params.Add("now", "1600000000") 281 } else { 282 params.Add(startParam, "1600000000") 283 params.Add(endParam, "1600000001") 284 } 285 params.Add(queryParam, tt.query) 286 if tt.lookback != nil { 287 params.Add(lookbackParam, tt.lookback.String()) 288 } 289 encodedParams := params.Encode() 290 291 h := PrometheusRangeRewrite(opts).Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 292 require.Equal(t, r.FormValue(queryParam), tt.expectedQuery) 293 if tt.expectedLookback != nil { 294 require.Equal(t, r.FormValue(lookbackParam), tt.expectedLookback.String()) 295 } 296 297 enabled := tt.enabled && tt.mult > 0 298 if enabled && r.Method == "POST" { 299 params.Set("query", tt.expectedQuery) 300 if tt.expectedLookback != nil { 301 params.Set(lookbackParam, tt.expectedLookback.String()) 302 } 303 304 body, err := ioutil.ReadAll(r.Body) 305 require.NoError(t, err) 306 // request body should be exactly the same with the exception of an updated 307 // query, potentially. 308 require.Equal(t, params.Encode(), string(body)) 309 } 310 311 if r.Method == "GET" { 312 require.Equal(t, http.NoBody, r.Body) 313 } 314 })) 315 path := "/query_range" 316 if tt.instant { 317 path = "/query" 318 } 319 opts.Route.Path(path).Handler(h) 320 321 server := httptest.NewServer(r) 322 defer server.Close() 323 324 var ( 325 resp *http.Response 326 err error 327 ) 328 329 // Validate as GET 330 // nolint: noctx 331 resp, err = server.Client().Get( 332 server.URL + path + "?" + encodedParams, 333 ) 334 require.NoError(t, err) 335 require.NoError(t, resp.Body.Close()) 336 require.Equal(t, 200, resp.StatusCode) 337 338 // Validate as POST 339 // nolint: noctx 340 resp, err = server.Client().Post( 341 server.URL+path, 342 "application/x-www-form-urlencoded", 343 bytes.NewReader([]byte(encodedParams)), 344 ) 345 require.NoError(t, err) 346 require.NoError(t, resp.Body.Close()) 347 require.Equal(t, 200, resp.StatusCode) 348 }) 349 } 350 } 351 352 func durationMilliseconds(d time.Duration) int64 { 353 return int64(d / (time.Millisecond / time.Nanosecond)) 354 } 355 356 func makeBaseOpts(t *testing.T, r *mux.Router, addPromEngine bool) Options { 357 var ( 358 instrumentOpts = instrument.NewOptions() 359 kitLogger = kitlogzap.NewZapSugarLogger(instrumentOpts.Logger(), zapcore.InfoLevel) 360 engineOpts = promql.EngineOpts{ 361 Logger: log.With(kitLogger, "component", "query engine"), 362 MaxSamples: 100, 363 Timeout: 1 * time.Minute, 364 NoStepSubqueryIntervalFn: func(rangeMillis int64) int64 { 365 return durationMilliseconds(1 * time.Minute) 366 }, 367 } 368 ) 369 engine := promql.NewEngine(engineOpts) 370 route := r.NewRoute() 371 372 mockStorage := mock.NewMockStorage() 373 374 fetchOptsBuilderCfg := handleroptions.FetchOptionsBuilderOptions{ 375 Timeout: 15 * time.Second, 376 } 377 fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(fetchOptsBuilderCfg) 378 require.NoError(t, err) 379 380 opts := Options{ 381 InstrumentOpts: instrument.NewOptions(), 382 Route: route, 383 PrometheusRangeRewrite: PrometheusRangeRewriteOptions{ 384 Enabled: true, 385 FetchOptionsBuilder: fetchOptsBuilder, 386 ResolutionMultiplier: 2, 387 DefaultLookback: 5 * time.Minute, 388 Storage: mockStorage, 389 }, 390 } 391 if addPromEngine { 392 opts.PrometheusRangeRewrite.PrometheusEngineFn = func(duration time.Duration) (*promql.Engine, error) { 393 return engine, nil 394 } 395 } 396 return opts 397 } 398 399 func unaggregatedAttrs() []storagemetadata.Attributes { 400 return []storagemetadata.Attributes{ 401 { 402 MetricsType: storagemetadata.UnaggregatedMetricsType, 403 }, 404 } 405 } 406 407 func aggregatedAttrs(resolution time.Duration) []storagemetadata.Attributes { 408 return []storagemetadata.Attributes{ 409 { 410 MetricsType: storagemetadata.AggregatedMetricsType, 411 Resolution: resolution, 412 }, 413 } 414 } 415 416 func durationPtr(duration time.Duration) *time.Duration { 417 return &duration 418 }