github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/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 handleroptions3 "github.com/m3db/m3/src/cluster/placementhandler/handleroptions" 32 "github.com/m3db/m3/src/cmd/services/m3coordinator/ingest" 33 "github.com/m3db/m3/src/cmd/services/m3query/config" 34 "github.com/m3db/m3/src/dbnode/encoding" 35 "github.com/m3db/m3/src/query/api/v1/handler/graphite" 36 "github.com/m3db/m3/src/query/api/v1/handler/influxdb" 37 m3json "github.com/m3db/m3/src/query/api/v1/handler/json" 38 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions" 39 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/native" 40 "github.com/m3db/m3/src/query/api/v1/handler/prometheus/remote" 41 "github.com/m3db/m3/src/query/api/v1/middleware" 42 "github.com/m3db/m3/src/query/api/v1/options" 43 "github.com/m3db/m3/src/query/executor" 44 graphiteStorage "github.com/m3db/m3/src/query/graphite/storage" 45 "github.com/m3db/m3/src/query/models" 46 "github.com/m3db/m3/src/query/storage" 47 m3storage "github.com/m3db/m3/src/query/storage/m3" 48 "github.com/m3db/m3/src/query/test/m3" 49 "github.com/m3db/m3/src/x/instrument" 50 xsync "github.com/m3db/m3/src/x/sync" 51 52 "github.com/golang/mock/gomock" 53 "github.com/prometheus/prometheus/promql" 54 "github.com/stretchr/testify/assert" 55 "github.com/stretchr/testify/require" 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 }