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  }