github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prometheus/remote/read_test.go (about) 1 // Copyright (c) 2018 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 remote 22 23 import ( 24 "bytes" 25 "context" 26 "fmt" 27 "io" 28 "net/http" 29 "net/http/httptest" 30 "net/url" 31 "strings" 32 "testing" 33 "time" 34 35 "github.com/m3db/m3/src/cmd/services/m3query/config" 36 "github.com/m3db/m3/src/dbnode/client" 37 xmetrics "github.com/m3db/m3/src/dbnode/x/metrics" 38 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 39 "github.com/m3db/m3/src/query/api/v1/options" 40 "github.com/m3db/m3/src/query/block" 41 "github.com/m3db/m3/src/query/executor" 42 "github.com/m3db/m3/src/query/generated/proto/prompb" 43 "github.com/m3db/m3/src/query/models" 44 xpromql "github.com/m3db/m3/src/query/parser/promql" 45 "github.com/m3db/m3/src/query/storage" 46 "github.com/m3db/m3/src/query/test" 47 "github.com/m3db/m3/src/query/test/m3" 48 xclock "github.com/m3db/m3/src/x/clock" 49 "github.com/m3db/m3/src/x/instrument" 50 xhttp "github.com/m3db/m3/src/x/net/http" 51 xtest "github.com/m3db/m3/src/x/test" 52 xtime "github.com/m3db/m3/src/x/time" 53 54 "github.com/golang/mock/gomock" 55 "github.com/stretchr/testify/assert" 56 "github.com/stretchr/testify/require" 57 "github.com/uber-go/tally" 58 ) 59 60 var ( 61 promReadTestMetrics = newPromReadMetrics(tally.NewTestScope("", nil)) 62 defaultLookbackDuration = time.Minute 63 ) 64 65 func buildBody(query string, start time.Time) io.Reader { 66 vals := url.Values{} 67 vals.Add("query", query) 68 vals.Add("start", start.Format(time.RFC3339)) 69 vals.Add("end", start.Add(time.Hour).Format(time.RFC3339)) 70 qs := vals.Encode() 71 return bytes.NewBuffer([]byte(qs)) 72 } 73 74 func TestParseExpr(t *testing.T) { 75 query := "" + 76 `up{a="b"} + 7 - sum(rate(down{c!="d"}[2m])) + ` + 77 `left{e=~"f"} offset 30m and right{g!~"h"} + ` + ` 78 max_over_time(foo[1m] offset 1h)` 79 80 start := time.Now().Truncate(time.Hour) 81 req := httptest.NewRequest(http.MethodPost, "/", buildBody(query, start)) 82 req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeFormURLEncoded) 83 readReq, err := ParseExpr(req, xpromql.NewParseOptions()) 84 require.NoError(t, err) 85 86 q := func(start, end time.Time, matchers []*prompb.LabelMatcher) *prompb.Query { 87 return &prompb.Query{ 88 StartTimestampMs: start.Unix() * 1000, 89 EndTimestampMs: end.Unix() * 1000, 90 Matchers: matchers, 91 } 92 } 93 94 b := func(s string) []byte { return []byte(s) } 95 expected := []*prompb.Query{ 96 q(start, start.Add(time.Hour), 97 []*prompb.LabelMatcher{ 98 {Name: b("a"), Value: b("b"), Type: prompb.LabelMatcher_EQ}, 99 {Name: b("__name__"), Value: b("up"), Type: prompb.LabelMatcher_EQ}}), 100 q(start.Add(time.Minute*-2), start.Add(time.Hour), 101 []*prompb.LabelMatcher{ 102 {Name: b("c"), Value: b("d"), Type: prompb.LabelMatcher_NEQ}, 103 {Name: b("__name__"), Value: b("down"), Type: prompb.LabelMatcher_EQ}}), 104 q(start.Add(time.Minute*-30), start.Add(time.Minute*30), 105 []*prompb.LabelMatcher{ 106 {Name: b("e"), Value: b("f"), Type: prompb.LabelMatcher_RE}, 107 {Name: b("__name__"), Value: b("left"), Type: prompb.LabelMatcher_EQ}}), 108 q(start, start.Add(time.Hour), 109 []*prompb.LabelMatcher{ 110 {Name: b("g"), Value: b("h"), Type: prompb.LabelMatcher_NRE}, 111 {Name: b("__name__"), Value: b("right"), Type: prompb.LabelMatcher_EQ}}), 112 q(start.Add(time.Minute*-61), start, 113 []*prompb.LabelMatcher{ 114 {Name: b("__name__"), Value: b("foo"), Type: prompb.LabelMatcher_EQ}}), 115 } 116 117 assert.Equal(t, expected, readReq.Queries) 118 } 119 120 func newEngine( 121 s storage.Storage, 122 lookbackDuration time.Duration, 123 instrumentOpts instrument.Options, 124 ) executor.Engine { 125 engineOpts := executor.NewEngineOptions(). 126 SetStore(s). 127 SetLookbackDuration(lookbackDuration). 128 SetInstrumentOptions(instrumentOpts) 129 130 return executor.NewEngine(engineOpts) 131 } 132 133 func setupServer(t *testing.T) *httptest.Server { 134 ctrl := xtest.NewController(t) 135 defer ctrl.Finish() 136 137 lstore, session := m3.NewStorageAndSession(t, ctrl) 138 session.EXPECT(). 139 FetchTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 140 Return(nil, client.FetchResponseMetadata{Exhaustive: false}, 141 fmt.Errorf("not initialized")).MaxTimes(1) 142 storage := test.NewSlowStorage(lstore, 10*time.Millisecond) 143 promRead := readHandler(t, storage) 144 server := httptest.NewServer(test.NewSlowHandler(promRead, 10*time.Millisecond)) 145 return server 146 } 147 148 func readHandler(t *testing.T, store storage.Storage) http.Handler { 149 fetchOpts := handleroptions.FetchOptionsBuilderOptions{ 150 Limits: handleroptions.FetchOptionsBuilderLimitsOptions{ 151 SeriesLimit: 100, 152 }, 153 Timeout: 15 * time.Second, 154 } 155 fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(fetchOpts) 156 require.NoError(t, err) 157 iOpts := instrument.NewOptions() 158 engine := newEngine(store, defaultLookbackDuration, iOpts) 159 opts := options.EmptyHandlerOptions(). 160 SetEngine(engine). 161 SetInstrumentOpts(iOpts). 162 SetFetchOptionsBuilder(fetchOptsBuilder) 163 164 return NewPromReadHandler(opts) 165 } 166 167 func TestPromReadParsing(t *testing.T) { 168 ctrl := xtest.NewController(t) 169 storage, _ := m3.NewStorageAndSession(t, ctrl) 170 builderOpts := handleroptions.FetchOptionsBuilderOptions{ 171 Limits: handleroptions.FetchOptionsBuilderLimitsOptions{ 172 SeriesLimit: 100, 173 }, 174 Timeout: 15 * time.Second, 175 } 176 fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(builderOpts) 177 require.NoError(t, err) 178 engine := newEngine(storage, defaultLookbackDuration, 179 instrument.NewOptions()) 180 181 opts := options.EmptyHandlerOptions(). 182 SetEngine(engine). 183 SetFetchOptionsBuilder(fetchOptsBuilder) 184 185 req := httptest.NewRequest("POST", PromReadURL, test.GeneratePromReadBody(t)) 186 _, r, fetchOpts, err := ParseRequest(context.Background(), req, opts) 187 require.Nil(t, err, "unable to parse request") 188 require.Equal(t, len(r.Queries), 1) 189 fmt.Println(fetchOpts) 190 } 191 192 func TestPromReadParsingBad(t *testing.T) { 193 req := httptest.NewRequest("POST", PromReadURL, strings.NewReader("bad body")) 194 _, _, _, err := ParseRequest(context.Background(), req, options.EmptyHandlerOptions()) 195 require.NotNil(t, err, "unable to parse request") 196 } 197 198 func TestPromReadStorageWithFetchError(t *testing.T) { 199 ctrl := xtest.NewController(t) 200 readRequest := &prompb.ReadRequest{ 201 Queries: []*prompb.Query{ 202 {}, 203 }, 204 } 205 206 fetchOpts := &storage.FetchOptions{} 207 result := storage.PromResult{ 208 PromResult: &prompb.QueryResult{ 209 Timeseries: []*prompb.TimeSeries{}, 210 }, 211 } 212 engine := executor.NewMockEngine(ctrl) 213 engine.EXPECT(). 214 ExecuteProm(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 215 Return(result, fmt.Errorf("expr err")) 216 217 opts := options.EmptyHandlerOptions().SetEngine(engine) 218 res, err := Read(context.TODO(), readRequest, fetchOpts, opts) 219 require.Error(t, err, "unable to read from storage") 220 221 meta := res.Meta 222 assert.True(t, meta.Exhaustive) 223 assert.True(t, meta.LocalOnly) 224 assert.Equal(t, 0, len(meta.Warnings)) 225 } 226 227 func TestQueryMatchMustBeEqual(t *testing.T) { 228 req := test.GeneratePromReadRequest() 229 matchers, err := storage.PromMatchersToM3(req.Queries[0].Matchers) 230 require.NoError(t, err) 231 232 _, err = matchers.ToTags(models.NewTagOptions()) 233 assert.NoError(t, err) 234 } 235 236 func TestQueryKillOnClientDisconnect(t *testing.T) { 237 server := setupServer(t) 238 defer server.Close() 239 240 c := &http.Client{ 241 Timeout: 1 * time.Millisecond, 242 } 243 244 _, err := c.Post(server.URL, xhttp.ContentTypeProtobuf, test.GeneratePromReadBody(t)) 245 assert.Error(t, err) 246 } 247 248 func TestQueryKillOnTimeout(t *testing.T) { 249 server := setupServer(t) 250 defer server.Close() 251 252 req, _ := http.NewRequest("POST", server.URL, test.GeneratePromReadBody(t)) 253 req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeProtobuf) 254 req.Header.Add("timeout", "1ms") 255 resp, err := http.DefaultClient.Do(req) 256 require.NoError(t, err) 257 defer resp.Body.Close() 258 require.NotNil(t, resp) 259 assert.Equal(t, resp.StatusCode, 500, "Status code not 500") 260 } 261 262 func TestReadErrorMetricsCount(t *testing.T) { 263 ctrl := xtest.NewController(t) 264 defer ctrl.Finish() 265 266 storage, session := m3.NewStorageAndSession(t, ctrl) 267 session.EXPECT().FetchTagged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 268 Return(nil, client.FetchResponseMetadata{Exhaustive: true}, fmt.Errorf("unable to get data")) 269 270 reporter := xmetrics.NewTestStatsReporter(xmetrics.NewTestStatsReporterOptions()) 271 scope, closer := tally.NewRootScope(tally.ScopeOptions{Reporter: reporter}, time.Millisecond) 272 defer closer.Close() 273 readMetrics := newPromReadMetrics(scope) 274 buildOpts := handleroptions.FetchOptionsBuilderOptions{ 275 Limits: handleroptions.FetchOptionsBuilderLimitsOptions{ 276 SeriesLimit: 100, 277 }, 278 Timeout: 15 * time.Second, 279 } 280 fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(buildOpts) 281 require.NoError(t, err) 282 engine := newEngine(storage, defaultLookbackDuration, 283 instrument.NewOptions()) 284 opts := options.EmptyHandlerOptions(). 285 SetEngine(engine). 286 SetFetchOptionsBuilder(fetchOptsBuilder) 287 promRead := &promReadHandler{ 288 promReadMetrics: readMetrics, 289 opts: opts, 290 } 291 292 req := httptest.NewRequest("POST", PromReadURL, test.GeneratePromReadBody(t)) 293 promRead.ServeHTTP(httptest.NewRecorder(), req) 294 foundMetric := xclock.WaitUntil(func() bool { 295 found := reporter.Counters()["fetch.errors"] 296 return found == 1 297 }, 5*time.Second) 298 require.True(t, foundMetric) 299 } 300 301 func TestMultipleRead(t *testing.T) { 302 ctrl := xtest.NewController(t) 303 defer ctrl.Finish() 304 305 now := xtime.Now() 306 promNow := storage.TimeToPromTimestamp(now) 307 308 r := storage.PromResult{ 309 PromResult: &prompb.QueryResult{ 310 Timeseries: []*prompb.TimeSeries{ 311 { 312 Samples: []prompb.Sample{{Value: 1, Timestamp: promNow}}, 313 Labels: []prompb.Label{{Name: []byte("a"), Value: []byte("b")}}, 314 }, 315 }, 316 }, 317 Metadata: block.ResultMetadata{ 318 Exhaustive: true, 319 LocalOnly: true, 320 Warnings: []block.Warning{{Name: "foo", Message: "bar"}}, 321 }, 322 } 323 324 rTwo := storage.PromResult{ 325 PromResult: &prompb.QueryResult{ 326 Timeseries: []*prompb.TimeSeries{ 327 { 328 Samples: []prompb.Sample{{Value: 2, Timestamp: promNow}}, 329 Labels: []prompb.Label{{Name: []byte("c"), Value: []byte("d")}}, 330 }, 331 }, 332 }, 333 Metadata: block.ResultMetadata{ 334 Exhaustive: false, 335 LocalOnly: true, 336 Warnings: []block.Warning{}, 337 }, 338 } 339 340 req := &prompb.ReadRequest{ 341 Queries: []*prompb.Query{ 342 {StartTimestampMs: 10, EndTimestampMs: 100}, 343 {StartTimestampMs: 20, EndTimestampMs: 200}, 344 }, 345 } 346 347 q, err := storage.PromReadQueryToM3(req.Queries[0]) 348 require.NoError(t, err) 349 qTwo, err := storage.PromReadQueryToM3(req.Queries[1]) 350 require.NoError(t, err) 351 352 engine := executor.NewMockEngine(ctrl) 353 engine.EXPECT(). 354 ExecuteProm(gomock.Any(), q, gomock.Any(), gomock.Any()). 355 Return(r, nil) 356 engine.EXPECT(). 357 ExecuteProm(gomock.Any(), qTwo, gomock.Any(), gomock.Any()). 358 Return(rTwo, nil) 359 360 handlerOpts := options.EmptyHandlerOptions().SetEngine(engine). 361 SetConfig(config.Configuration{ 362 ResultOptions: config.ResultOptions{ 363 KeepNaNs: true, 364 }, 365 }) 366 367 fetchOpts := &storage.FetchOptions{} 368 res, err := Read(context.TODO(), req, fetchOpts, handlerOpts) 369 require.NoError(t, err) 370 expected := &prompb.QueryResult{ 371 Timeseries: []*prompb.TimeSeries{ 372 { 373 Labels: []prompb.Label{{Name: []byte("a"), Value: []byte("b")}}, 374 Samples: []prompb.Sample{{Timestamp: promNow, Value: 1}}, 375 }, 376 { 377 Labels: []prompb.Label{{Name: []byte("c"), Value: []byte("d")}}, 378 Samples: []prompb.Sample{{Timestamp: promNow, Value: 2}}, 379 }, 380 }, 381 } 382 383 result := res.Result 384 assert.Equal(t, expected.Timeseries[0], result[0].Timeseries[0]) 385 assert.Equal(t, expected.Timeseries[1], result[1].Timeseries[0]) 386 387 meta := res.Meta 388 assert.False(t, meta.Exhaustive) 389 assert.True(t, meta.LocalOnly) 390 require.Equal(t, 1, len(meta.Warnings)) 391 assert.Equal(t, "foo_bar", meta.Warnings[0].Header()) 392 } 393 394 func TestReadWithOptions(t *testing.T) { 395 ctrl := xtest.NewController(t) 396 defer ctrl.Finish() 397 398 now := xtime.Now() 399 promNow := storage.TimeToPromTimestamp(now) 400 401 r := storage.PromResult{ 402 PromResult: &prompb.QueryResult{ 403 Timeseries: []*prompb.TimeSeries{ 404 { 405 Samples: []prompb.Sample{{Value: 1, Timestamp: promNow}}, 406 Labels: []prompb.Label{ 407 {Name: []byte("a"), Value: []byte("b")}, 408 {Name: []byte("remove"), Value: []byte("c")}, 409 }, 410 }, 411 }, 412 }, 413 } 414 415 req := &prompb.ReadRequest{ 416 Queries: []*prompb.Query{{StartTimestampMs: 10, EndTimestampMs: 100}}, 417 } 418 419 q, err := storage.PromReadQueryToM3(req.Queries[0]) 420 require.NoError(t, err) 421 422 engine := executor.NewMockEngine(ctrl) 423 engine.EXPECT(). 424 ExecuteProm(gomock.Any(), q, gomock.Any(), gomock.Any()). 425 Return(r, nil) 426 427 fetchOpts := storage.NewFetchOptions() 428 fetchOpts.RestrictQueryOptions = &storage.RestrictQueryOptions{ 429 RestrictByTag: &storage.RestrictByTag{ 430 Strip: [][]byte{[]byte("remove")}, 431 }, 432 } 433 434 handlerOpts := options.EmptyHandlerOptions().SetEngine(engine). 435 SetConfig(config.Configuration{ 436 ResultOptions: config.ResultOptions{ 437 KeepNaNs: true, 438 }, 439 }) 440 441 res, err := Read(context.TODO(), req, fetchOpts, handlerOpts) 442 require.NoError(t, err) 443 expected := &prompb.QueryResult{ 444 Timeseries: []*prompb.TimeSeries{ 445 { 446 Labels: []prompb.Label{{Name: []byte("a"), Value: []byte("b")}}, 447 Samples: []prompb.Sample{{Timestamp: promNow, Value: 1}}, 448 }, 449 }, 450 } 451 452 result := res.Result 453 assert.Equal(t, expected.Timeseries[0], result[0].Timeseries[0]) 454 }