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 }