github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prom/read_test.go (about)

     1  // Copyright (c) 2020 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 prom
    22  
    23  import (
    24  	"context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"net/url"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/prometheus/prometheus/pkg/labels"
    34  	"github.com/prometheus/prometheus/promql"
    35  	promstorage "github.com/prometheus/prometheus/storage"
    36  	"github.com/stretchr/testify/require"
    37  	"go.uber.org/zap"
    38  
    39  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    40  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/native"
    41  	"github.com/m3db/m3/src/query/api/v1/options"
    42  	"github.com/m3db/m3/src/query/executor"
    43  	"github.com/m3db/m3/src/query/storage"
    44  	"github.com/m3db/m3/src/query/storage/prometheus"
    45  	xerrors "github.com/m3db/m3/src/x/errors"
    46  	"github.com/m3db/m3/src/x/instrument"
    47  )
    48  
    49  const promQuery = `http_requests_total{job="prometheus",group="canary"}`
    50  
    51  const (
    52  	queryParam = "query"
    53  	startParam = "start"
    54  	endParam   = "end"
    55  )
    56  
    57  var testPromQLEngineFn = func(_ time.Duration) (*promql.Engine, error) {
    58  	return newMockPromQLEngine(), nil
    59  }
    60  
    61  type testHandlers struct {
    62  	queryable          *mockQueryable
    63  	readHandler        http.Handler
    64  	readInstantHandler http.Handler
    65  }
    66  
    67  func setupTest(t *testing.T) testHandlers {
    68  	fetchOptsBuilderCfg := handleroptions.FetchOptionsBuilderOptions{
    69  		Timeout: 15 * time.Second,
    70  	}
    71  	fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(fetchOptsBuilderCfg)
    72  	require.NoError(t, err)
    73  	instrumentOpts := instrument.NewOptions()
    74  	engineOpts := executor.NewEngineOptions().
    75  		SetLookbackDuration(time.Minute).
    76  		SetInstrumentOptions(instrumentOpts)
    77  	engine := executor.NewEngine(engineOpts)
    78  	hOpts := options.EmptyHandlerOptions().
    79  		SetFetchOptionsBuilder(fetchOptsBuilder).
    80  		SetEngine(engine)
    81  
    82  	queryable := &mockQueryable{}
    83  	readHandler, err := newReadHandler(hOpts, opts{
    84  		queryable:  queryable,
    85  		instant:    false,
    86  		newQueryFn: newRangeQueryFn(testPromQLEngineFn, queryable),
    87  	})
    88  	require.NoError(t, err)
    89  	readInstantHandler, err := newReadHandler(hOpts, opts{
    90  		queryable:  queryable,
    91  		instant:    true,
    92  		newQueryFn: newInstantQueryFn(testPromQLEngineFn, queryable),
    93  	})
    94  	require.NoError(t, err)
    95  	return testHandlers{
    96  		queryable:          queryable,
    97  		readHandler:        readHandler,
    98  		readInstantHandler: readInstantHandler,
    99  	}
   100  }
   101  
   102  func defaultParams() url.Values {
   103  	vals := url.Values{}
   104  	now := time.Now()
   105  	vals.Add(queryParam, promQuery)
   106  	vals.Add(startParam, now.Format(time.RFC3339))
   107  	vals.Add(endParam, now.Add(time.Hour).Format(time.RFC3339))
   108  	vals.Add(handleroptions.StepParam, (time.Duration(10) * time.Second).String())
   109  	return vals
   110  }
   111  
   112  func defaultParamsWithoutQuery() url.Values {
   113  	vals := url.Values{}
   114  	now := time.Now()
   115  	vals.Add(startParam, now.Format(time.RFC3339))
   116  	vals.Add(endParam, now.Add(time.Hour).Format(time.RFC3339))
   117  	vals.Add(handleroptions.StepParam, (time.Duration(10) * time.Second).String())
   118  	return vals
   119  }
   120  
   121  func TestPromReadHandler(t *testing.T) {
   122  	setup := setupTest(t)
   123  
   124  	req, _ := http.NewRequest("GET", native.PromReadURL, nil)
   125  	req.URL.RawQuery = defaultParams().Encode()
   126  
   127  	recorder := httptest.NewRecorder()
   128  	setup.readHandler.ServeHTTP(recorder, req)
   129  
   130  	var resp response
   131  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   132  	require.Equal(t, statusSuccess, resp.Status)
   133  }
   134  
   135  func TestPromReadHandlerInvalidQuery(t *testing.T) {
   136  	setup := setupTest(t)
   137  
   138  	req, _ := http.NewRequest("GET", native.PromReadURL, nil)
   139  	req.URL.RawQuery = defaultParamsWithoutQuery().Encode()
   140  
   141  	recorder := httptest.NewRecorder()
   142  	setup.readHandler.ServeHTTP(recorder, req)
   143  
   144  	var resp response
   145  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   146  	require.Equal(t, statusError, resp.Status)
   147  	require.Equal(t, http.StatusBadRequest, recorder.Code)
   148  }
   149  
   150  func TestPromReadHandlerErrors(t *testing.T) {
   151  	testCases := []struct {
   152  		name     string
   153  		err      error
   154  		httpCode int
   155  	}{
   156  		{
   157  			name:     "prom error",
   158  			err:      fmt.Errorf("prom error"),
   159  			httpCode: http.StatusBadRequest,
   160  		},
   161  		{
   162  			name:     "prom timeout",
   163  			err:      promql.ErrQueryTimeout("timeout"),
   164  			httpCode: http.StatusGatewayTimeout,
   165  		},
   166  		{
   167  			name:     "prom cancel",
   168  			err:      promql.ErrQueryCanceled("cancel"),
   169  			httpCode: 499,
   170  		},
   171  		{
   172  			name:     "storage 500",
   173  			err:      prometheus.NewStorageErr(fmt.Errorf("500 storage error")),
   174  			httpCode: http.StatusInternalServerError,
   175  		},
   176  		{
   177  			name:     "storage 400",
   178  			err:      prometheus.NewStorageErr(xerrors.NewInvalidParamsError(fmt.Errorf("400 storage error"))),
   179  			httpCode: http.StatusBadRequest,
   180  		},
   181  	}
   182  
   183  	for _, tc := range testCases {
   184  		tc := tc
   185  		t.Run(tc.name, func(t *testing.T) {
   186  			setup := setupTest(t)
   187  			setup.queryable.selectFn = func(
   188  				sortSeries bool,
   189  				hints *promstorage.SelectHints,
   190  				labelMatchers ...*labels.Matcher,
   191  			) promstorage.SeriesSet {
   192  				return promstorage.ErrSeriesSet(tc.err)
   193  			}
   194  
   195  			req, _ := http.NewRequestWithContext(context.Background(), "GET", native.PromReadURL, nil)
   196  			req.URL.RawQuery = defaultParams().Encode()
   197  
   198  			recorder := httptest.NewRecorder()
   199  			setup.readHandler.ServeHTTP(recorder, req)
   200  
   201  			var resp response
   202  			require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   203  			require.Equal(t, statusError, resp.Status)
   204  			require.Equal(t, tc.httpCode, recorder.Code)
   205  		})
   206  	}
   207  }
   208  
   209  func TestPromReadInstantHandler(t *testing.T) {
   210  	setup := setupTest(t)
   211  
   212  	req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil)
   213  	req.URL.RawQuery = defaultParams().Encode()
   214  
   215  	recorder := httptest.NewRecorder()
   216  	setup.readInstantHandler.ServeHTTP(recorder, req)
   217  
   218  	var resp response
   219  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   220  	require.Equal(t, statusSuccess, resp.Status)
   221  }
   222  
   223  func TestPromReadInstantHandlerInvalidQuery(t *testing.T) {
   224  	setup := setupTest(t)
   225  
   226  	req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil)
   227  	req.URL.RawQuery = defaultParamsWithoutQuery().Encode()
   228  
   229  	recorder := httptest.NewRecorder()
   230  	setup.readInstantHandler.ServeHTTP(recorder, req)
   231  
   232  	var resp response
   233  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   234  	require.Equal(t, statusError, resp.Status)
   235  }
   236  
   237  func TestPromReadInstantHandlerParseMinTime(t *testing.T) {
   238  	setup := setupTest(t)
   239  
   240  	var (
   241  		query   *promstorage.SelectHints
   242  		selects int
   243  	)
   244  	setup.queryable.selectFn = func(
   245  		sortSeries bool,
   246  		hints *promstorage.SelectHints,
   247  		labelMatchers ...*labels.Matcher,
   248  	) promstorage.SeriesSet {
   249  		selects++
   250  		query = hints
   251  		return &mockSeriesSet{}
   252  	}
   253  
   254  	req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil)
   255  	params := defaultParams()
   256  	params.Set("time", minTimeFormatted)
   257  	req.URL.RawQuery = params.Encode()
   258  
   259  	var resp response
   260  	recorder := httptest.NewRecorder()
   261  
   262  	setup.readInstantHandler.ServeHTTP(recorder, req)
   263  
   264  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   265  	require.Equal(t, statusSuccess, resp.Status)
   266  
   267  	require.Equal(t, 1, selects)
   268  
   269  	fudge := 5 * time.Minute // Need to account for lookback
   270  	expected := time.Unix(0, 0)
   271  	actual := millisTime(query.Start)
   272  	require.True(t, abs(expected.Sub(actual)) <= fudge,
   273  		fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v",
   274  			expected, actual, fudge, expected.Sub(actual)))
   275  
   276  	fudge = 5 * time.Minute // Need to account for lookback
   277  	expected = time.Unix(0, 0)
   278  	actual = millisTime(query.Start)
   279  	require.True(t, abs(expected.Sub(actual)) <= fudge,
   280  		fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v",
   281  			expected, actual, fudge, expected.Sub(actual)))
   282  }
   283  
   284  func TestPromReadInstantHandlerParseMaxTime(t *testing.T) {
   285  	setup := setupTest(t)
   286  
   287  	var (
   288  		query   *promstorage.SelectHints
   289  		selects int
   290  	)
   291  	setup.queryable.selectFn = func(
   292  		sortSeries bool,
   293  		hints *promstorage.SelectHints,
   294  		labelMatchers ...*labels.Matcher,
   295  	) promstorage.SeriesSet {
   296  		selects++
   297  		query = hints
   298  		return &mockSeriesSet{}
   299  	}
   300  
   301  	req, _ := http.NewRequest("GET", native.PromReadInstantURL, nil)
   302  	params := defaultParams()
   303  	params.Set("time", maxTimeFormatted)
   304  	req.URL.RawQuery = params.Encode()
   305  
   306  	var resp response
   307  	recorder := httptest.NewRecorder()
   308  
   309  	setup.readInstantHandler.ServeHTTP(recorder, req)
   310  
   311  	require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &resp))
   312  	require.Equal(t, statusSuccess, resp.Status)
   313  
   314  	require.Equal(t, 1, selects)
   315  
   316  	fudge := 6 * time.Minute // Need to account for lookback + time.Now() skew
   317  	expected := time.Now()
   318  	actual := millisTime(query.Start)
   319  	require.True(t, abs(expected.Sub(actual)) <= fudge,
   320  		fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v",
   321  			expected, actual, fudge, expected.Sub(actual)))
   322  
   323  	fudge = 6 * time.Minute // Need to account for lookback + time.Now() skew
   324  	expected = time.Now()
   325  	actual = millisTime(query.Start)
   326  	require.True(t, abs(expected.Sub(actual)) <= fudge,
   327  		fmt.Sprintf("expected=%v, actual=%v, fudge=%v, delta=%v",
   328  			expected, actual, fudge, expected.Sub(actual)))
   329  }
   330  
   331  func TestLimitedReturnedDataVector(t *testing.T) {
   332  	handler := &readHandler{
   333  		logger: zap.NewNop(),
   334  	}
   335  
   336  	r := &promql.Result{
   337  		Value: promql.Vector{
   338  			{Point: promql.Point{T: 1, V: 1.0}},
   339  			{Point: promql.Point{T: 2, V: 2.0}},
   340  			{Point: promql.Point{T: 3, V: 3.0}},
   341  		},
   342  	}
   343  
   344  	tests := []struct {
   345  		name                string
   346  		maxSeries           int
   347  		maxDatapoints       int
   348  		expectedSeries      int
   349  		expectedTotalSeries int
   350  		expectedDatapoints  int
   351  		expectedLimited     bool
   352  	}{
   353  		{
   354  			name:            "Omit limits",
   355  			expectedLimited: false,
   356  		},
   357  		{
   358  			name:            "Series below max",
   359  			maxSeries:       4,
   360  			expectedLimited: false,
   361  		},
   362  		{
   363  			name:            "Series at max",
   364  			maxSeries:       3,
   365  			expectedLimited: false,
   366  		},
   367  		{
   368  			name:                "Series above max",
   369  			maxSeries:           2,
   370  			expectedLimited:     true,
   371  			expectedSeries:      2,
   372  			expectedTotalSeries: 3,
   373  			expectedDatapoints:  2,
   374  		},
   375  		{
   376  			name:            "Datapoints below max",
   377  			maxDatapoints:   4,
   378  			expectedLimited: false,
   379  		},
   380  		{
   381  			name:            "Datapoints at max",
   382  			maxDatapoints:   3,
   383  			expectedLimited: false,
   384  		},
   385  		{
   386  			name:                "Datapoints above max",
   387  			maxDatapoints:       2,
   388  			expectedLimited:     true,
   389  			expectedSeries:      2,
   390  			expectedTotalSeries: 3,
   391  			expectedDatapoints:  2,
   392  		},
   393  		{
   394  			name:                "Series and datapoints limit (former lower)",
   395  			maxSeries:           1,
   396  			maxDatapoints:       2,
   397  			expectedLimited:     true,
   398  			expectedSeries:      1,
   399  			expectedTotalSeries: 3,
   400  			expectedDatapoints:  1,
   401  		},
   402  		{
   403  			name:                "Series and datapoints limit (former higher)",
   404  			maxSeries:           2,
   405  			maxDatapoints:       1,
   406  			expectedLimited:     true,
   407  			expectedSeries:      1,
   408  			expectedTotalSeries: 3,
   409  			expectedDatapoints:  1,
   410  		},
   411  		{
   412  			name:                "Series and datapoints limit (only one under limit)",
   413  			maxSeries:           1,
   414  			maxDatapoints:       10,
   415  			expectedLimited:     true,
   416  			expectedSeries:      1,
   417  			expectedTotalSeries: 3,
   418  			expectedDatapoints:  1,
   419  		},
   420  	}
   421  
   422  	for _, test := range tests {
   423  		t.Run(test.name, func(t *testing.T) {
   424  			result := *r
   425  			limited := handler.limitReturnedData("", &result, &storage.FetchOptions{
   426  				ReturnedSeriesLimit:     test.maxSeries,
   427  				ReturnedDatapointsLimit: test.maxDatapoints,
   428  			})
   429  			require.Equal(t, test.expectedLimited, limited.Limited)
   430  
   431  			seriesCount := len(result.Value.(promql.Vector))
   432  
   433  			if limited.Limited {
   434  				require.Equal(t, test.expectedSeries, seriesCount)
   435  				require.Equal(t, test.expectedSeries, limited.Series)
   436  				require.Equal(t, test.expectedTotalSeries, limited.TotalSeries)
   437  				require.Equal(t, test.expectedDatapoints, limited.Datapoints)
   438  			} else {
   439  				// Full results
   440  				require.Equal(t, 3, seriesCount)
   441  			}
   442  		})
   443  	}
   444  }
   445  
   446  func TestLimitedReturnedDataMatrix(t *testing.T) {
   447  	handler := &readHandler{
   448  		logger: zap.NewNop(),
   449  	}
   450  
   451  	r := &promql.Result{
   452  		Value: promql.Matrix{
   453  			{Points: []promql.Point{
   454  				{T: 1, V: 1.0},
   455  			}},
   456  			{Points: []promql.Point{
   457  				{T: 1, V: 1.0},
   458  				{T: 2, V: 2.0},
   459  			}},
   460  			{Points: []promql.Point{
   461  				{T: 1, V: 1.0},
   462  				{T: 2, V: 2.0},
   463  				{T: 3, V: 3.0},
   464  			}},
   465  		},
   466  	}
   467  
   468  	tests := []struct {
   469  		name                string
   470  		maxSeries           int
   471  		maxDatapoints       int
   472  		expectedSeries      int
   473  		expectedTotalSeries int
   474  		expectedDatapoints  int
   475  		expectedLimited     bool
   476  	}{
   477  		{
   478  			name:            "Omit limits",
   479  			expectedLimited: false,
   480  		},
   481  		{
   482  			name:            "Series below max",
   483  			maxSeries:       4,
   484  			expectedLimited: false,
   485  		},
   486  		{
   487  			name:            "Series at max",
   488  			maxSeries:       3,
   489  			expectedLimited: false,
   490  		},
   491  		{
   492  			name:                "Series above max",
   493  			maxSeries:           2,
   494  			expectedLimited:     true,
   495  			expectedSeries:      2,
   496  			expectedTotalSeries: 3,
   497  			expectedDatapoints:  3,
   498  		},
   499  		{
   500  			name:            "Datapoints below max",
   501  			maxDatapoints:   7,
   502  			expectedLimited: false,
   503  		},
   504  		{
   505  			name:            "Datapoints at max",
   506  			maxDatapoints:   6,
   507  			expectedLimited: false,
   508  		},
   509  		{
   510  			name:                "Datapoints above max - 2 series left - A",
   511  			maxDatapoints:       5,
   512  			expectedLimited:     true,
   513  			expectedSeries:      2,
   514  			expectedTotalSeries: 3,
   515  			expectedDatapoints:  3,
   516  		},
   517  		{
   518  			name:                "Datapoints above max - 2 series left - B",
   519  			maxDatapoints:       3,
   520  			expectedLimited:     true,
   521  			expectedSeries:      2,
   522  			expectedTotalSeries: 3,
   523  			expectedDatapoints:  3,
   524  		},
   525  		{
   526  			name:                "Datapoints above max - 1 series left - A",
   527  			maxDatapoints:       2,
   528  			expectedLimited:     true,
   529  			expectedSeries:      1,
   530  			expectedTotalSeries: 3,
   531  			expectedDatapoints:  1,
   532  		},
   533  		{
   534  			name:                "Datapoints above max - 1 series left - B",
   535  			maxDatapoints:       1,
   536  			expectedLimited:     true,
   537  			expectedSeries:      1,
   538  			expectedTotalSeries: 3,
   539  			expectedDatapoints:  1,
   540  		},
   541  		{
   542  			name:                "Series and datapoints limit (former lower)",
   543  			maxSeries:           1,
   544  			maxDatapoints:       6,
   545  			expectedLimited:     true,
   546  			expectedSeries:      1,
   547  			expectedTotalSeries: 3,
   548  			expectedDatapoints:  1,
   549  		},
   550  		{
   551  			name:                "Series and datapoints limit (former higher)",
   552  			maxSeries:           6,
   553  			maxDatapoints:       1,
   554  			expectedLimited:     true,
   555  			expectedSeries:      1,
   556  			expectedTotalSeries: 3,
   557  			expectedDatapoints:  1,
   558  		},
   559  		{
   560  			name:                "Series and datapoints limit (only one under limit)",
   561  			maxSeries:           1,
   562  			maxDatapoints:       10,
   563  			expectedLimited:     true,
   564  			expectedSeries:      1,
   565  			expectedTotalSeries: 3,
   566  			expectedDatapoints:  1,
   567  		},
   568  	}
   569  
   570  	for _, test := range tests {
   571  		t.Run(test.name, func(t *testing.T) {
   572  			result := *r
   573  			limited := handler.limitReturnedData("", &result, &storage.FetchOptions{
   574  				ReturnedSeriesLimit:     test.maxSeries,
   575  				ReturnedDatapointsLimit: test.maxDatapoints,
   576  			})
   577  			require.Equal(t, test.expectedLimited, limited.Limited)
   578  
   579  			m := result.Value.(promql.Matrix)
   580  			seriesCount := len(m)
   581  			datapointCount := 0
   582  			for _, d := range m {
   583  				datapointCount += len(d.Points)
   584  			}
   585  
   586  			if limited.Limited {
   587  				require.Equal(t, test.expectedSeries, seriesCount, "series count")
   588  				require.Equal(t, test.expectedDatapoints, datapointCount, "datapoint count")
   589  				require.Equal(t, test.expectedSeries, limited.Series, "series")
   590  				require.Equal(t, test.expectedTotalSeries, limited.TotalSeries, "total series")
   591  				require.Equal(t, test.expectedDatapoints, limited.Datapoints, "datapoints")
   592  			} else {
   593  				// Full results
   594  				require.Equal(t, 3, seriesCount, "expect full series")
   595  				require.Equal(t, 6, datapointCount, "expect full datapoints")
   596  			}
   597  		})
   598  	}
   599  }
   600  
   601  func abs(v time.Duration) time.Duration {
   602  	if v < 0 {
   603  		return v * -1
   604  	}
   605  	return v
   606  }
   607  
   608  func millisTime(timestampMilliseconds int64) time.Time {
   609  	return time.Unix(0, timestampMilliseconds*int64(time.Millisecond))
   610  }