github.com/m3db/m3@v1.5.0/src/query/api/v1/httpd/handler_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 httpd
    22  
    23  import (
    24  	"encoding/json"
    25  	"fmt"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/golang/mock/gomock"
    32  	"github.com/prometheus/prometheus/promql"
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  
    36  	handleroptions3 "github.com/m3db/m3/src/cluster/placementhandler/handleroptions"
    37  	"github.com/m3db/m3/src/cmd/services/m3coordinator/ingest"
    38  	"github.com/m3db/m3/src/cmd/services/m3query/config"
    39  	"github.com/m3db/m3/src/dbnode/encoding"
    40  	"github.com/m3db/m3/src/query/api/v1/handler/graphite"
    41  	"github.com/m3db/m3/src/query/api/v1/handler/influxdb"
    42  	m3json "github.com/m3db/m3/src/query/api/v1/handler/json"
    43  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    44  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/native"
    45  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/remote"
    46  	"github.com/m3db/m3/src/query/api/v1/middleware"
    47  	"github.com/m3db/m3/src/query/api/v1/options"
    48  	"github.com/m3db/m3/src/query/executor"
    49  	graphiteStorage "github.com/m3db/m3/src/query/graphite/storage"
    50  	"github.com/m3db/m3/src/query/models"
    51  	"github.com/m3db/m3/src/query/storage"
    52  	m3storage "github.com/m3db/m3/src/query/storage/m3"
    53  	"github.com/m3db/m3/src/query/test/m3"
    54  	"github.com/m3db/m3/src/x/instrument"
    55  	xsync "github.com/m3db/m3/src/x/sync"
    56  )
    57  
    58  var (
    59  	// Created by init().
    60  	testWorkerPool            xsync.PooledWorkerPool
    61  	testM3DBOpts              = m3storage.NewOptions(encoding.NewOptions())
    62  	defaultLookbackDuration   = time.Minute
    63  	defaultCPUProfileduration = 5 * time.Second
    64  	svcDefaultOptions         = []handleroptions3.ServiceOptionsDefault{
    65  		func(o handleroptions3.ServiceOptions) handleroptions3.ServiceOptions {
    66  			return o
    67  		},
    68  	}
    69  )
    70  
    71  func makeTagOptions() models.TagOptions {
    72  	return models.NewTagOptions().SetMetricName([]byte("some_name"))
    73  }
    74  
    75  func newEngine(
    76  	s storage.Storage,
    77  	lookbackDuration time.Duration,
    78  	instrumentOpts instrument.Options,
    79  ) executor.Engine {
    80  	engineOpts := executor.NewEngineOptions().
    81  		SetStore(s).
    82  		SetLookbackDuration(lookbackDuration).
    83  		SetInstrumentOptions(instrumentOpts)
    84  
    85  	return executor.NewEngine(engineOpts)
    86  }
    87  
    88  func setupHandler(
    89  	store storage.Storage,
    90  	customHandlers ...options.CustomHandler,
    91  ) (*Handler, error) {
    92  	instrumentOpts := instrument.NewOptions()
    93  	downsamplerAndWriter := ingest.NewDownsamplerAndWriter(store, nil, testWorkerPool, instrument.NewOptions())
    94  	engine := newEngine(store, time.Minute, instrumentOpts)
    95  	fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(
    96  		handleroptions.FetchOptionsBuilderOptions{
    97  			Timeout: 15 * time.Second,
    98  		})
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	promEngineFn := func(_ time.Duration) (*promql.Engine, error) {
   103  		return newPromEngine(), nil
   104  	}
   105  	opts, err := options.NewHandlerOptions(
   106  		downsamplerAndWriter,
   107  		makeTagOptions(),
   108  		engine,
   109  		promEngineFn,
   110  		nil,
   111  		nil,
   112  		config.Configuration{LookbackDuration: &defaultLookbackDuration},
   113  		nil,
   114  		fetchOptsBuilder,
   115  		fetchOptsBuilder,
   116  		fetchOptsBuilder,
   117  		models.QueryContextOptions{},
   118  		instrumentOpts,
   119  		defaultCPUProfileduration,
   120  		svcDefaultOptions,
   121  		NewQueryRouter(),
   122  		NewQueryRouter(),
   123  		graphiteStorage.M3WrappedStorageOptions{},
   124  		testM3DBOpts,
   125  		NewGraphiteRenderRouter(),
   126  		NewGraphiteFindRouter(),
   127  		defaultLookbackDuration,
   128  	)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	return NewHandler(opts, config.MiddlewareConfiguration{}, customHandlers...), nil
   134  }
   135  
   136  func newPromEngine() *promql.Engine {
   137  	return promql.NewEngine(promql.EngineOpts{
   138  		MaxSamples: 10000,
   139  		Timeout:    100 * time.Second,
   140  		NoStepSubqueryIntervalFn: func(rangeMillis int64) int64 {
   141  			return durationMilliseconds(1 * time.Minute)
   142  		},
   143  	})
   144  }
   145  
   146  func durationMilliseconds(d time.Duration) int64 {
   147  	return int64(d / (time.Millisecond / time.Nanosecond))
   148  }
   149  
   150  func TestPromRemoteReadGet(t *testing.T) {
   151  	req := httptest.NewRequest("GET", remote.PromReadURL, nil)
   152  	res := httptest.NewRecorder()
   153  	ctrl := gomock.NewController(t)
   154  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   155  
   156  	h, err := setupHandler(storage)
   157  	require.NoError(t, err, "unable to setup handler")
   158  	err = h.RegisterRoutes()
   159  	require.NoError(t, err, "unable to register routes")
   160  	h.Router().ServeHTTP(res, req)
   161  	require.Equal(t, http.StatusBadRequest, res.Code)
   162  }
   163  
   164  func TestPromRemoteReadPost(t *testing.T) {
   165  	req := httptest.NewRequest("POST", remote.PromReadURL, nil)
   166  	res := httptest.NewRecorder()
   167  	ctrl := gomock.NewController(t)
   168  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   169  
   170  	h, err := setupHandler(storage)
   171  	require.NoError(t, err, "unable to setup handler")
   172  	err = h.RegisterRoutes()
   173  	require.NoError(t, err, "unable to register routes")
   174  	h.Router().ServeHTTP(res, req)
   175  	require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   176  }
   177  
   178  func TestPromNativeReadGet(t *testing.T) {
   179  	tests := []struct {
   180  		routePrefix string
   181  	}{
   182  		{""},
   183  		{"/prometheus"},
   184  		{"/m3query"},
   185  	}
   186  
   187  	for _, tt := range tests {
   188  		url := tt.routePrefix + native.PromReadURL
   189  		t.Run("Testing endpoint GET "+url, func(t *testing.T) {
   190  			req := httptest.NewRequest("GET", url, nil)
   191  			res := httptest.NewRecorder()
   192  			ctrl := gomock.NewController(t)
   193  			storage, _ := m3.NewStorageAndSession(t, ctrl)
   194  
   195  			h, err := setupHandler(storage)
   196  			require.NoError(t, err, "unable to setup handler")
   197  			h.RegisterRoutes()
   198  			h.Router().ServeHTTP(res, req)
   199  			require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   200  		})
   201  	}
   202  }
   203  
   204  func TestPromNativeReadPost(t *testing.T) {
   205  	tests := []struct {
   206  		routePrefix string
   207  	}{
   208  		{""},
   209  		{"/prometheus"},
   210  		{"/m3query"},
   211  	}
   212  
   213  	for _, tt := range tests {
   214  		url := tt.routePrefix + native.PromReadURL
   215  		t.Run("Testing endpoint GET "+url, func(t *testing.T) {
   216  			req := httptest.NewRequest("POST", url, nil)
   217  			res := httptest.NewRecorder()
   218  			ctrl := gomock.NewController(t)
   219  			storage, _ := m3.NewStorageAndSession(t, ctrl)
   220  
   221  			h, err := setupHandler(storage)
   222  			require.NoError(t, err, "unable to setup handler")
   223  			h.RegisterRoutes()
   224  			h.Router().ServeHTTP(res, req)
   225  			require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   226  		})
   227  	}
   228  }
   229  
   230  func TestJSONWritePost(t *testing.T) {
   231  	req := httptest.NewRequest("POST", m3json.WriteJSONURL, nil)
   232  	res := httptest.NewRecorder()
   233  	ctrl := gomock.NewController(t)
   234  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   235  
   236  	h, err := setupHandler(storage)
   237  	require.NoError(t, err, "unable to setup handler")
   238  	h.RegisterRoutes()
   239  	h.Router().ServeHTTP(res, req)
   240  	require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   241  }
   242  
   243  func TestInfluxDBWritePost(t *testing.T) {
   244  	req := httptest.NewRequest(influxdb.InfluxWriteHTTPMethod, influxdb.InfluxWriteURL, nil)
   245  	res := httptest.NewRecorder()
   246  	ctrl := gomock.NewController(t)
   247  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   248  
   249  	h, err := setupHandler(storage)
   250  	require.NoError(t, err, "unable to setup handler")
   251  	err = h.RegisterRoutes()
   252  	require.NoError(t, err)
   253  	h.Router().ServeHTTP(res, req)
   254  	require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   255  }
   256  
   257  func TestRoutesGet(t *testing.T) {
   258  	req := httptest.NewRequest("GET", routesURL, nil)
   259  	res := httptest.NewRecorder()
   260  	ctrl := gomock.NewController(t)
   261  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   262  
   263  	h, err := setupHandler(storage)
   264  	require.NoError(t, err, "unable to setup handler")
   265  	h.RegisterRoutes()
   266  	h.Router().ServeHTTP(res, req)
   267  
   268  	require.Equal(t, res.Code, http.StatusOK)
   269  
   270  	response := &struct {
   271  		Routes []string `json:"routes"`
   272  	}{}
   273  
   274  	err = json.NewDecoder(res.Body).Decode(response)
   275  	require.NoError(t, err)
   276  
   277  	foundRoutesURL := false
   278  	for _, route := range response.Routes {
   279  		if route == routesURL {
   280  			foundRoutesURL = true
   281  			break
   282  		}
   283  	}
   284  	assert.True(t, foundRoutesURL, "routes URL not served by routes endpoint")
   285  }
   286  
   287  func TestHealthGet(t *testing.T) {
   288  	req := httptest.NewRequest("GET", healthURL, nil)
   289  	res := httptest.NewRecorder()
   290  	ctrl := gomock.NewController(t)
   291  	storage, _ := m3.NewStorageAndSession(t, ctrl)
   292  
   293  	h, err := setupHandler(storage)
   294  	require.NoError(t, err, "unable to setup handler")
   295  	h.RegisterRoutes()
   296  
   297  	h.Router().ServeHTTP(res, req)
   298  
   299  	require.Equal(t, res.Code, http.StatusOK)
   300  
   301  	response := &struct {
   302  		Uptime string `json:"uptime"`
   303  	}{}
   304  
   305  	err = json.NewDecoder(res.Body).Decode(response)
   306  	require.NoError(t, err)
   307  
   308  	result, err := time.ParseDuration(response.Uptime)
   309  	require.NoError(t, err)
   310  
   311  	assert.True(t, result > 0)
   312  }
   313  
   314  func TestGraphite(t *testing.T) {
   315  	tests := []struct {
   316  		url    string
   317  		target string
   318  	}{
   319  		{graphite.ReadURL, "GET"},
   320  		{graphite.ReadURL, "POST"},
   321  		{graphite.FindURL, "GET"},
   322  		{graphite.FindURL, "POST"},
   323  	}
   324  
   325  	for _, tt := range tests {
   326  		url := graphite.ReadURL
   327  		t.Run(tt.url+"_"+tt.target, func(t *testing.T) {
   328  			req := httptest.NewRequest(tt.target, url, nil)
   329  			res := httptest.NewRecorder()
   330  			ctrl := gomock.NewController(t)
   331  			storage, _ := m3.NewStorageAndSession(t, ctrl)
   332  
   333  			h, err := setupHandler(storage)
   334  			require.NoError(t, err, "unable to setup handler")
   335  			err = h.RegisterRoutes()
   336  			require.NoError(t, err)
   337  			h.Router().ServeHTTP(res, req)
   338  			require.Equal(t, http.StatusBadRequest, res.Code, "Empty request")
   339  		})
   340  	}
   341  }
   342  
   343  func init() {
   344  	var err error
   345  	testWorkerPool, err = xsync.NewPooledWorkerPool(
   346  		16,
   347  		xsync.NewPooledWorkerPoolOptions().
   348  			SetGrowOnDemand(true),
   349  	)
   350  
   351  	if err != nil {
   352  		panic(fmt.Sprintf("unable to create pooled worker pool: %v", err))
   353  	}
   354  
   355  	testWorkerPool.Init()
   356  }
   357  
   358  type assertFn func(t *testing.T, prev http.Handler, r *http.Request)
   359  
   360  type customHandler struct {
   361  	t          *testing.T
   362  	routeName  string
   363  	methods    []string
   364  	assertFn   assertFn
   365  	middleware middleware.OverrideOptions
   366  }
   367  
   368  func (h *customHandler) Route() string     { return h.routeName }
   369  func (h *customHandler) Methods() []string { return h.methods }
   370  func (h *customHandler) Handler(
   371  	opts options.HandlerOptions,
   372  	prev http.Handler,
   373  ) (http.Handler, error) {
   374  	assert.Equal(h.t, "z", string(opts.TagOptions().MetricName()))
   375  	fn := func(w http.ResponseWriter, r *http.Request) {
   376  		h.assertFn(h.t, prev, r)
   377  		_, err := w.Write([]byte("success!"))
   378  		require.NoError(h.t, err)
   379  	}
   380  
   381  	return http.HandlerFunc(fn), nil
   382  }
   383  func (h *customHandler) MiddlewareOverride() middleware.OverrideOptions {
   384  	return h.middleware
   385  }
   386  
   387  func TestCustomRoutes(t *testing.T) {
   388  	ctrl := gomock.NewController(t)
   389  	store, _ := m3.NewStorageAndSession(t, ctrl)
   390  	instrumentOpts := instrument.NewOptions()
   391  	downsamplerAndWriter := ingest.NewDownsamplerAndWriter(store, nil, testWorkerPool, instrument.NewOptions())
   392  	engine := newEngine(store, time.Minute, instrumentOpts)
   393  	fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(
   394  		handleroptions.FetchOptionsBuilderOptions{
   395  			Timeout: 15 * time.Second,
   396  		})
   397  	require.NoError(t, err)
   398  	promEngineFn := func(_ time.Duration) (*promql.Engine, error) {
   399  		return newPromEngine(), nil
   400  	}
   401  	opts, err := options.NewHandlerOptions(
   402  		downsamplerAndWriter, makeTagOptions().SetMetricName([]byte("z")),
   403  		engine, promEngineFn, nil, nil,
   404  		config.Configuration{LookbackDuration: &defaultLookbackDuration}, nil,
   405  		fetchOptsBuilder, fetchOptsBuilder, fetchOptsBuilder,
   406  		models.QueryContextOptions{}, instrumentOpts, defaultCPUProfileduration,
   407  		svcDefaultOptions, NewQueryRouter(), NewQueryRouter(),
   408  		graphiteStorage.M3WrappedStorageOptions{}, testM3DBOpts, NewGraphiteRenderRouter(), NewGraphiteFindRouter(),
   409  		defaultLookbackDuration,
   410  	)
   411  	require.NoError(t, err)
   412  	custom := &customHandler{
   413  		t:         t,
   414  		routeName: "/custom",
   415  		methods:   []string{http.MethodGet, http.MethodHead},
   416  		assertFn: func(t *testing.T, prev http.Handler, r *http.Request) {
   417  			assert.Nil(t, prev, "Should not shadow already existing handler")
   418  		},
   419  	}
   420  	customShadowGet := &customHandler{
   421  		t:         t,
   422  		routeName: "/custom",
   423  		methods:   []string{http.MethodGet},
   424  		assertFn: func(t *testing.T, prev http.Handler, r *http.Request) {
   425  			assert.NotNil(t, prev, "Should shadow already existing handler")
   426  		},
   427  	}
   428  	customShadowHead := &customHandler{
   429  		t:         t,
   430  		routeName: "/custom",
   431  		methods:   []string{http.MethodHead},
   432  		assertFn: func(t *testing.T, prev http.Handler, r *http.Request) {
   433  			assert.NotNil(t, prev, "Should shadow already existing handler")
   434  		},
   435  	}
   436  	customNew := &customHandler{
   437  		t:         t,
   438  		routeName: "/custom/new",
   439  		methods:   []string{http.MethodGet, http.MethodHead},
   440  		assertFn: func(t *testing.T, prev http.Handler, r *http.Request) {
   441  			assert.Nil(t, prev, "Should not shadow already existing handler")
   442  		},
   443  	}
   444  	handler := NewHandler(opts, config.MiddlewareConfiguration{},
   445  		custom, customShadowGet, customShadowHead, customNew)
   446  	require.NoError(t, err, "unable to setup handler")
   447  	err = handler.RegisterRoutes()
   448  	require.NoError(t, err, "unable to register routes")
   449  
   450  	for _, method := range custom.methods {
   451  		assertRoute(t, custom.routeName, method, handler, http.StatusOK)
   452  	}
   453  
   454  	for _, method := range customNew.methods {
   455  		assertRoute(t, customNew.routeName, method, handler, http.StatusOK)
   456  	}
   457  
   458  	assertRoute(t, customNew.routeName, http.MethodPost, handler, http.StatusMethodNotAllowed)
   459  	assertRoute(t, "/unknown", http.MethodGet, handler, http.StatusNotFound)
   460  }
   461  
   462  func assertRoute(t *testing.T, routeName string, method string, handler *Handler, expectedStatusCode int) {
   463  	req := httptest.NewRequest(method, routeName, nil)
   464  	res := httptest.NewRecorder()
   465  	handler.Router().ServeHTTP(res, req)
   466  	require.Equal(t, expectedStatusCode, res.Code)
   467  }