github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/tools/querytee/proxy_test.go (about)

     1  package querytee
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"strconv"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/gorilla/mux"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  var testReadRoutes = []Route{
    21  	{Path: "/api/v1/query", RouteName: "api_v1_query", Methods: []string{"GET"}, ResponseComparator: &testComparator{}},
    22  }
    23  
    24  var testWriteRoutes = []Route{}
    25  
    26  type testComparator struct{}
    27  
    28  func (testComparator) Compare(expected, actual []byte) error { return nil }
    29  
    30  func Test_NewProxy(t *testing.T) {
    31  	cfg := ProxyConfig{}
    32  
    33  	p, err := NewProxy(cfg, log.NewNopLogger(), testReadRoutes, testWriteRoutes, nil)
    34  	assert.Equal(t, errMinBackends, err)
    35  	assert.Nil(t, p)
    36  }
    37  
    38  func Test_Proxy_RequestsForwarding(t *testing.T) {
    39  	const (
    40  		querySingleMetric1 = `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"cortex_build_info"},"value":[1583320883,"1"]}]}}`
    41  		querySingleMetric2 = `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"cortex_build_info"},"value":[1583320883,"2"]}]}}`
    42  	)
    43  
    44  	type mockedBackend struct {
    45  		pathPrefix string
    46  		handler    http.HandlerFunc
    47  	}
    48  
    49  	tests := map[string]struct {
    50  		backends            []mockedBackend
    51  		preferredBackendIdx int
    52  		expectedStatus      int
    53  		expectedRes         string
    54  	}{
    55  		"one backend returning 2xx": {
    56  			backends: []mockedBackend{
    57  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
    58  			},
    59  			expectedStatus: 200,
    60  			expectedRes:    querySingleMetric1,
    61  		},
    62  		"one backend returning 5xx": {
    63  			backends: []mockedBackend{
    64  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
    65  			},
    66  			expectedStatus: 500,
    67  			expectedRes:    "",
    68  		},
    69  		"two backends without path prefix": {
    70  			backends: []mockedBackend{
    71  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
    72  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric2)},
    73  			},
    74  			preferredBackendIdx: 0,
    75  			expectedStatus:      200,
    76  			expectedRes:         querySingleMetric1,
    77  		},
    78  		"two backends with the same path prefix": {
    79  			backends: []mockedBackend{
    80  				{
    81  					pathPrefix: "/api/prom",
    82  					handler:    mockQueryResponse("/api/prom/api/v1/query", 200, querySingleMetric1),
    83  				},
    84  				{
    85  					pathPrefix: "/api/prom",
    86  					handler:    mockQueryResponse("/api/prom/api/v1/query", 200, querySingleMetric2),
    87  				},
    88  			},
    89  			preferredBackendIdx: 0,
    90  			expectedStatus:      200,
    91  			expectedRes:         querySingleMetric1,
    92  		},
    93  		"two backends with different path prefix": {
    94  			backends: []mockedBackend{
    95  				{
    96  					pathPrefix: "/prefix-1",
    97  					handler:    mockQueryResponse("/prefix-1/api/v1/query", 200, querySingleMetric1),
    98  				},
    99  				{
   100  					pathPrefix: "/prefix-2",
   101  					handler:    mockQueryResponse("/prefix-2/api/v1/query", 200, querySingleMetric2),
   102  				},
   103  			},
   104  			preferredBackendIdx: 0,
   105  			expectedStatus:      200,
   106  			expectedRes:         querySingleMetric1,
   107  		},
   108  		"preferred backend returns 4xx": {
   109  			backends: []mockedBackend{
   110  				{handler: mockQueryResponse("/api/v1/query", 400, "")},
   111  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   112  			},
   113  			preferredBackendIdx: 0,
   114  			expectedStatus:      400,
   115  			expectedRes:         "",
   116  		},
   117  		"preferred backend returns 5xx": {
   118  			backends: []mockedBackend{
   119  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   120  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   121  			},
   122  			preferredBackendIdx: 0,
   123  			expectedStatus:      200,
   124  			expectedRes:         querySingleMetric1,
   125  		},
   126  		"non-preferred backend returns 5xx": {
   127  			backends: []mockedBackend{
   128  				{handler: mockQueryResponse("/api/v1/query", 200, querySingleMetric1)},
   129  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   130  			},
   131  			preferredBackendIdx: 0,
   132  			expectedStatus:      200,
   133  			expectedRes:         querySingleMetric1,
   134  		},
   135  		"all backends returns 5xx": {
   136  			backends: []mockedBackend{
   137  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   138  				{handler: mockQueryResponse("/api/v1/query", 500, "")},
   139  			},
   140  			preferredBackendIdx: 0,
   141  			expectedStatus:      500,
   142  			expectedRes:         "",
   143  		},
   144  	}
   145  
   146  	for testName, testData := range tests {
   147  		t.Run(testName, func(t *testing.T) {
   148  			backendURLs := []string{}
   149  
   150  			// Start backend servers.
   151  			for _, b := range testData.backends {
   152  				s := httptest.NewServer(b.handler)
   153  				defer s.Close()
   154  
   155  				backendURLs = append(backendURLs, s.URL+b.pathPrefix)
   156  			}
   157  
   158  			// Start the proxy.
   159  			cfg := ProxyConfig{
   160  				BackendEndpoints:   strings.Join(backendURLs, ","),
   161  				PreferredBackend:   strconv.Itoa(testData.preferredBackendIdx),
   162  				ServerServicePort:  0,
   163  				BackendReadTimeout: time.Second,
   164  			}
   165  
   166  			if len(backendURLs) == 2 {
   167  				cfg.CompareResponses = true
   168  			}
   169  
   170  			p, err := NewProxy(cfg, log.NewNopLogger(), testReadRoutes, testWriteRoutes, nil)
   171  			require.NoError(t, err)
   172  			require.NotNil(t, p)
   173  			defer p.Stop() //nolint:errcheck
   174  
   175  			require.NoError(t, p.Start())
   176  
   177  			// Send a query request to the proxy.
   178  			res, err := http.Get(fmt.Sprintf("http://%s/api/v1/query", p.Endpoint()))
   179  			require.NoError(t, err)
   180  
   181  			defer res.Body.Close()
   182  			body, err := ioutil.ReadAll(res.Body)
   183  			require.NoError(t, err)
   184  
   185  			assert.Equal(t, testData.expectedStatus, res.StatusCode)
   186  			assert.Equal(t, testData.expectedRes, string(body))
   187  		})
   188  	}
   189  }
   190  
   191  func TestProxy_Passthrough(t *testing.T) {
   192  	type route struct {
   193  		path, response string
   194  	}
   195  
   196  	type mockedBackend struct {
   197  		routes []route
   198  	}
   199  
   200  	type query struct {
   201  		path               string
   202  		expectedRes        string
   203  		expectedStatusCode int
   204  	}
   205  
   206  	const (
   207  		pathCommon = "/common" // common path implemented by both backends
   208  
   209  		pathZero = "/zero" // only implemented by backend at index 0
   210  		pathOne  = "/one"  // only implemented by backend at index 1
   211  
   212  		// responses by backend at index 0
   213  		responseCommon0 = "common-0"
   214  		responseZero    = "zero"
   215  
   216  		// responses by backend at index 1
   217  		responseCommon1 = "common-1"
   218  		responseOne     = "one"
   219  	)
   220  
   221  	backends := []mockedBackend{
   222  		{
   223  			routes: []route{
   224  				{
   225  					path:     pathCommon,
   226  					response: responseCommon0,
   227  				},
   228  				{
   229  					path:     pathZero,
   230  					response: responseZero,
   231  				},
   232  			},
   233  		},
   234  		{
   235  			routes: []route{
   236  				{
   237  					path:     pathCommon,
   238  					response: responseCommon1,
   239  				},
   240  				{
   241  					path:     pathOne,
   242  					response: responseOne,
   243  				},
   244  			},
   245  		},
   246  	}
   247  
   248  	tests := map[string]struct {
   249  		preferredBackendIdx int
   250  		queries             []query
   251  	}{
   252  		"first backend preferred": {
   253  			preferredBackendIdx: 0,
   254  			queries: []query{
   255  				{
   256  					path:               pathCommon,
   257  					expectedRes:        responseCommon0,
   258  					expectedStatusCode: 200,
   259  				},
   260  				{
   261  					path:               pathZero,
   262  					expectedRes:        responseZero,
   263  					expectedStatusCode: 200,
   264  				},
   265  				{
   266  					path:               pathOne,
   267  					expectedRes:        "404 page not found\n",
   268  					expectedStatusCode: 404,
   269  				},
   270  			},
   271  		},
   272  		"second backend preferred": {
   273  			preferredBackendIdx: 1,
   274  			queries: []query{
   275  				{
   276  					path:               pathCommon,
   277  					expectedRes:        responseCommon1,
   278  					expectedStatusCode: 200,
   279  				},
   280  				{
   281  					path:               pathOne,
   282  					expectedRes:        responseOne,
   283  					expectedStatusCode: 200,
   284  				},
   285  				{
   286  					path:               pathZero,
   287  					expectedRes:        "404 page not found\n",
   288  					expectedStatusCode: 404,
   289  				},
   290  			},
   291  		},
   292  	}
   293  
   294  	for testName, testData := range tests {
   295  		t.Run(testName, func(t *testing.T) {
   296  			backendURLs := []string{}
   297  
   298  			// Start backend servers.
   299  			for _, b := range backends {
   300  				router := mux.NewRouter()
   301  				for _, route := range b.routes {
   302  					router.Handle(route.path, mockQueryResponse(route.path, 200, route.response))
   303  				}
   304  				s := httptest.NewServer(router)
   305  				defer s.Close()
   306  
   307  				backendURLs = append(backendURLs, s.URL)
   308  			}
   309  
   310  			// Start the proxy.
   311  			cfg := ProxyConfig{
   312  				BackendEndpoints:               strings.Join(backendURLs, ","),
   313  				PreferredBackend:               strconv.Itoa(testData.preferredBackendIdx),
   314  				ServerServicePort:              0,
   315  				BackendReadTimeout:             time.Second,
   316  				PassThroughNonRegisteredRoutes: true,
   317  			}
   318  
   319  			p, err := NewProxy(cfg, log.NewNopLogger(), testReadRoutes, testWriteRoutes, nil)
   320  			require.NoError(t, err)
   321  			require.NotNil(t, p)
   322  			defer p.Stop() //nolint:errcheck
   323  
   324  			require.NoError(t, p.Start())
   325  
   326  			for _, query := range testData.queries {
   327  
   328  				// Send a query request to the proxy.
   329  				res, err := http.Get(fmt.Sprintf("http://%s%s", p.Endpoint(), query.path))
   330  				require.NoError(t, err)
   331  
   332  				defer res.Body.Close()
   333  				body, err := ioutil.ReadAll(res.Body)
   334  				require.NoError(t, err)
   335  
   336  				assert.Equal(t, query.expectedStatusCode, res.StatusCode)
   337  				assert.Equal(t, query.expectedRes, string(body))
   338  			}
   339  		})
   340  	}
   341  }
   342  
   343  func mockQueryResponse(path string, status int, res string) http.HandlerFunc {
   344  	return func(w http.ResponseWriter, r *http.Request) {
   345  		// Ensure the path is the expected one.
   346  		if r.URL.Path != path {
   347  			w.WriteHeader(http.StatusNotFound)
   348  			return
   349  		}
   350  
   351  		// Send back the mocked response.
   352  		w.WriteHeader(status)
   353  		if status == http.StatusOK {
   354  			_, _ = w.Write([]byte(res))
   355  		}
   356  	}
   357  }
   358  
   359  func TestFilterReadDisabledBackend(t *testing.T) {
   360  	urlMustParse := func(urlStr string) *url.URL {
   361  		u, err := url.Parse(urlStr)
   362  		require.NoError(t, err)
   363  		return u
   364  	}
   365  
   366  	backends := []*ProxyBackend{
   367  		NewProxyBackend("test1", urlMustParse("http:/test1"), time.Second, true),
   368  		NewProxyBackend("test2", urlMustParse("http:/test2"), time.Second, false),
   369  		NewProxyBackend("test3", urlMustParse("http:/test3"), time.Second, false),
   370  		NewProxyBackend("test4", urlMustParse("http:/test4"), time.Second, false),
   371  	}
   372  	for name, tc := range map[string]struct {
   373  		disableReadProxyCfg string
   374  		expectedBackends    []*ProxyBackend
   375  	}{
   376  		"nothing disabled": {
   377  			expectedBackends: backends,
   378  		},
   379  		"test2 disabled": {
   380  			disableReadProxyCfg: "test2",
   381  			expectedBackends:    []*ProxyBackend{backends[0], backends[2], backends[3]},
   382  		},
   383  		"test2 and test4 disabled": {
   384  			disableReadProxyCfg: "test2, test4",
   385  			expectedBackends:    []*ProxyBackend{backends[0], backends[2]},
   386  		},
   387  		"all secondary disabled": {
   388  			disableReadProxyCfg: "test2, test4,test3",
   389  			expectedBackends:    []*ProxyBackend{backends[0]},
   390  		},
   391  		"disabling primary should not be filtered out": {
   392  			disableReadProxyCfg: "test1",
   393  			expectedBackends:    backends,
   394  		},
   395  	} {
   396  		t.Run(name, func(t *testing.T) {
   397  			filteredBackends := filterReadDisabledBackends(backends, tc.disableReadProxyCfg)
   398  			require.Equal(t, tc.expectedBackends, filteredBackends)
   399  		})
   400  	}
   401  }