1// Copyright 2016 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package web 15 16import ( 17 "context" 18 "encoding/json" 19 "fmt" 20 "io" 21 "io/ioutil" 22 "net" 23 "net/http" 24 "net/http/httptest" 25 "net/url" 26 "os" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "sync" 31 "testing" 32 "time" 33 34 "github.com/prometheus/client_golang/prometheus" 35 prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" 36 "github.com/stretchr/testify/require" 37 38 "github.com/prometheus/prometheus/config" 39 "github.com/prometheus/prometheus/notifier" 40 "github.com/prometheus/prometheus/rules" 41 "github.com/prometheus/prometheus/scrape" 42 "github.com/prometheus/prometheus/tsdb" 43) 44 45func TestMain(m *testing.M) { 46 // On linux with a global proxy the tests will fail as the go client(http,grpc) tries to connect through the proxy. 47 os.Setenv("no_proxy", "localhost,127.0.0.1,0.0.0.0,:") 48 os.Exit(m.Run()) 49} 50 51func TestGlobalURL(t *testing.T) { 52 opts := &Options{ 53 ListenAddress: ":9090", 54 ExternalURL: &url.URL{ 55 Scheme: "https", 56 Host: "externalhost:80", 57 Path: "/path/prefix", 58 }, 59 } 60 61 tests := []struct { 62 inURL string 63 outURL string 64 }{ 65 { 66 // Nothing should change if the input URL is not on localhost, even if the port is our listening port. 67 inURL: "http://somehost:9090/metrics", 68 outURL: "http://somehost:9090/metrics", 69 }, 70 { 71 // Port and host should change if target is on localhost and port is our listening port. 72 inURL: "http://localhost:9090/metrics", 73 outURL: "https://externalhost:80/metrics", 74 }, 75 { 76 // Only the host should change if the port is not our listening port, but the host is localhost. 77 inURL: "http://localhost:8000/metrics", 78 outURL: "http://externalhost:8000/metrics", 79 }, 80 { 81 // Alternative localhost representations should also work. 82 inURL: "http://127.0.0.1:9090/metrics", 83 outURL: "https://externalhost:80/metrics", 84 }, 85 } 86 87 for _, test := range tests { 88 inURL, err := url.Parse(test.inURL) 89 90 require.NoError(t, err) 91 92 globalURL := tmplFuncs("", opts)["globalURL"].(func(u *url.URL) *url.URL) 93 outURL := globalURL(inURL) 94 95 require.Equal(t, test.outURL, outURL.String()) 96 } 97} 98 99type dbAdapter struct { 100 *tsdb.DB 101} 102 103func (a *dbAdapter) Stats(statsByLabelName string) (*tsdb.Stats, error) { 104 return a.Head().Stats(statsByLabelName), nil 105} 106 107func (a *dbAdapter) WALReplayStatus() (tsdb.WALReplayStatus, error) { 108 return tsdb.WALReplayStatus{}, nil 109} 110 111func TestReadyAndHealthy(t *testing.T) { 112 t.Parallel() 113 114 dbDir, err := ioutil.TempDir("", "tsdb-ready") 115 require.NoError(t, err) 116 defer func() { require.NoError(t, os.RemoveAll(dbDir)) }() 117 118 db, err := tsdb.Open(dbDir, nil, nil, nil, nil) 119 require.NoError(t, err) 120 121 opts := &Options{ 122 ListenAddress: ":9090", 123 ReadTimeout: 30 * time.Second, 124 MaxConnections: 512, 125 Context: nil, 126 Storage: nil, 127 LocalStorage: &dbAdapter{db}, 128 TSDBDir: dbDir, 129 QueryEngine: nil, 130 ScrapeManager: &scrape.Manager{}, 131 RuleManager: &rules.Manager{}, 132 Notifier: nil, 133 RoutePrefix: "/", 134 EnableAdminAPI: true, 135 ExternalURL: &url.URL{ 136 Scheme: "http", 137 Host: "localhost:9090", 138 Path: "/", 139 }, 140 Version: &PrometheusVersion{}, 141 Gatherer: prometheus.DefaultGatherer, 142 } 143 144 opts.Flags = map[string]string{} 145 146 webHandler := New(nil, opts) 147 148 webHandler.config = &config.Config{} 149 webHandler.notifier = ¬ifier.Manager{} 150 l, err := webHandler.Listener() 151 if err != nil { 152 panic(fmt.Sprintf("Unable to start web listener: %s", err)) 153 } 154 155 ctx, cancel := context.WithCancel(context.Background()) 156 defer cancel() 157 go func() { 158 err := webHandler.Run(ctx, l, "") 159 if err != nil { 160 panic(fmt.Sprintf("Can't start web handler:%s", err)) 161 } 162 }() 163 164 // Give some time for the web goroutine to run since we need the server 165 // to be up before starting tests. 166 time.Sleep(5 * time.Second) 167 168 resp, err := http.Get("http://localhost:9090/-/healthy") 169 require.NoError(t, err) 170 require.Equal(t, http.StatusOK, resp.StatusCode) 171 cleanupTestResponse(t, resp) 172 173 for _, u := range []string{ 174 "http://localhost:9090/-/ready", 175 "http://localhost:9090/classic/graph", 176 "http://localhost:9090/classic/flags", 177 "http://localhost:9090/classic/rules", 178 "http://localhost:9090/classic/service-discovery", 179 "http://localhost:9090/classic/targets", 180 "http://localhost:9090/classic/status", 181 "http://localhost:9090/classic/config", 182 } { 183 resp, err = http.Get(u) 184 require.NoError(t, err) 185 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 186 cleanupTestResponse(t, resp) 187 } 188 189 resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/snapshot", "", strings.NewReader("")) 190 require.NoError(t, err) 191 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 192 cleanupTestResponse(t, resp) 193 194 resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}")) 195 require.NoError(t, err) 196 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 197 cleanupTestResponse(t, resp) 198 199 // Set to ready. 200 webHandler.Ready() 201 202 for _, u := range []string{ 203 "http://localhost:9090/-/healthy", 204 "http://localhost:9090/-/ready", 205 "http://localhost:9090/classic/graph", 206 "http://localhost:9090/classic/flags", 207 "http://localhost:9090/classic/rules", 208 "http://localhost:9090/classic/service-discovery", 209 "http://localhost:9090/classic/targets", 210 "http://localhost:9090/classic/status", 211 "http://localhost:9090/classic/config", 212 } { 213 resp, err = http.Get(u) 214 require.NoError(t, err) 215 require.Equal(t, http.StatusOK, resp.StatusCode) 216 cleanupTestResponse(t, resp) 217 } 218 219 resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/snapshot", "", strings.NewReader("")) 220 require.NoError(t, err) 221 require.Equal(t, http.StatusOK, resp.StatusCode) 222 cleanupSnapshot(t, dbDir, resp) 223 cleanupTestResponse(t, resp) 224 225 resp, err = http.Post("http://localhost:9090/api/v1/admin/tsdb/delete_series?match[]=up", "", nil) 226 require.NoError(t, err) 227 require.Equal(t, http.StatusNoContent, resp.StatusCode) 228 cleanupTestResponse(t, resp) 229} 230 231func TestRoutePrefix(t *testing.T) { 232 t.Parallel() 233 dbDir, err := ioutil.TempDir("", "tsdb-ready") 234 require.NoError(t, err) 235 defer func() { require.NoError(t, os.RemoveAll(dbDir)) }() 236 237 db, err := tsdb.Open(dbDir, nil, nil, nil, nil) 238 require.NoError(t, err) 239 240 opts := &Options{ 241 ListenAddress: ":9091", 242 ReadTimeout: 30 * time.Second, 243 MaxConnections: 512, 244 Context: nil, 245 TSDBDir: dbDir, 246 LocalStorage: &dbAdapter{db}, 247 Storage: nil, 248 QueryEngine: nil, 249 ScrapeManager: nil, 250 RuleManager: nil, 251 Notifier: nil, 252 RoutePrefix: "/prometheus", 253 EnableAdminAPI: true, 254 ExternalURL: &url.URL{ 255 Host: "localhost.localdomain:9090", 256 Scheme: "http", 257 }, 258 } 259 260 opts.Flags = map[string]string{} 261 262 webHandler := New(nil, opts) 263 l, err := webHandler.Listener() 264 if err != nil { 265 panic(fmt.Sprintf("Unable to start web listener: %s", err)) 266 } 267 ctx, cancel := context.WithCancel(context.Background()) 268 defer cancel() 269 go func() { 270 err := webHandler.Run(ctx, l, "") 271 if err != nil { 272 panic(fmt.Sprintf("Can't start web handler:%s", err)) 273 } 274 }() 275 276 // Give some time for the web goroutine to run since we need the server 277 // to be up before starting tests. 278 time.Sleep(5 * time.Second) 279 280 resp, err := http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/healthy") 281 require.NoError(t, err) 282 require.Equal(t, http.StatusOK, resp.StatusCode) 283 cleanupTestResponse(t, resp) 284 285 resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/ready") 286 require.NoError(t, err) 287 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 288 cleanupTestResponse(t, resp) 289 290 resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader("")) 291 require.NoError(t, err) 292 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 293 cleanupTestResponse(t, resp) 294 295 resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}")) 296 require.NoError(t, err) 297 require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) 298 cleanupTestResponse(t, resp) 299 300 // Set to ready. 301 webHandler.Ready() 302 303 resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/healthy") 304 require.NoError(t, err) 305 require.Equal(t, http.StatusOK, resp.StatusCode) 306 cleanupTestResponse(t, resp) 307 308 resp, err = http.Get("http://localhost:9091" + opts.RoutePrefix + "/-/ready") 309 require.NoError(t, err) 310 require.Equal(t, http.StatusOK, resp.StatusCode) 311 cleanupTestResponse(t, resp) 312 313 resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader("")) 314 require.NoError(t, err) 315 require.Equal(t, http.StatusOK, resp.StatusCode) 316 cleanupSnapshot(t, dbDir, resp) 317 cleanupTestResponse(t, resp) 318 319 resp, err = http.Post("http://localhost:9091"+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series?match[]=up", "", nil) 320 require.NoError(t, err) 321 require.Equal(t, http.StatusNoContent, resp.StatusCode) 322 cleanupTestResponse(t, resp) 323} 324 325func TestDebugHandler(t *testing.T) { 326 for _, tc := range []struct { 327 prefix, url string 328 code int 329 }{ 330 {"/", "/debug/pprof/cmdline", 200}, 331 {"/foo", "/foo/debug/pprof/cmdline", 200}, 332 333 {"/", "/debug/pprof/goroutine", 200}, 334 {"/foo", "/foo/debug/pprof/goroutine", 200}, 335 336 {"/", "/debug/pprof/foo", 404}, 337 {"/foo", "/bar/debug/pprof/goroutine", 404}, 338 } { 339 opts := &Options{ 340 RoutePrefix: tc.prefix, 341 ListenAddress: "somehost:9090", 342 ExternalURL: &url.URL{ 343 Host: "localhost.localdomain:9090", 344 Scheme: "http", 345 }, 346 } 347 handler := New(nil, opts) 348 handler.Ready() 349 350 w := httptest.NewRecorder() 351 352 req, err := http.NewRequest("GET", tc.url, nil) 353 354 require.NoError(t, err) 355 356 handler.router.ServeHTTP(w, req) 357 358 require.Equal(t, tc.code, w.Code) 359 } 360} 361 362func TestHTTPMetrics(t *testing.T) { 363 t.Parallel() 364 handler := New(nil, &Options{ 365 RoutePrefix: "/", 366 ListenAddress: "somehost:9090", 367 ExternalURL: &url.URL{ 368 Host: "localhost.localdomain:9090", 369 Scheme: "http", 370 }, 371 }) 372 getReady := func() int { 373 t.Helper() 374 w := httptest.NewRecorder() 375 376 req, err := http.NewRequest("GET", "/-/ready", nil) 377 require.NoError(t, err) 378 379 handler.router.ServeHTTP(w, req) 380 return w.Code 381 } 382 383 code := getReady() 384 require.Equal(t, http.StatusServiceUnavailable, code) 385 counter := handler.metrics.requestCounter 386 require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable))))) 387 388 handler.Ready() 389 for range [2]int{} { 390 code = getReady() 391 require.Equal(t, http.StatusOK, code) 392 } 393 require.Equal(t, 2, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusOK))))) 394 require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable))))) 395} 396 397func TestShutdownWithStaleConnection(t *testing.T) { 398 dbDir, err := ioutil.TempDir("", "tsdb-ready") 399 require.NoError(t, err) 400 defer func() { require.NoError(t, os.RemoveAll(dbDir)) }() 401 402 db, err := tsdb.Open(dbDir, nil, nil, nil, nil) 403 require.NoError(t, err) 404 405 timeout := 10 * time.Second 406 407 opts := &Options{ 408 ListenAddress: ":9090", 409 ReadTimeout: timeout, 410 MaxConnections: 512, 411 Context: nil, 412 Storage: nil, 413 LocalStorage: &dbAdapter{db}, 414 TSDBDir: dbDir, 415 QueryEngine: nil, 416 ScrapeManager: &scrape.Manager{}, 417 RuleManager: &rules.Manager{}, 418 Notifier: nil, 419 RoutePrefix: "/", 420 ExternalURL: &url.URL{ 421 Scheme: "http", 422 Host: "localhost:9090", 423 Path: "/", 424 }, 425 Version: &PrometheusVersion{}, 426 Gatherer: prometheus.DefaultGatherer, 427 } 428 429 opts.Flags = map[string]string{} 430 431 webHandler := New(nil, opts) 432 433 webHandler.config = &config.Config{} 434 webHandler.notifier = ¬ifier.Manager{} 435 l, err := webHandler.Listener() 436 if err != nil { 437 panic(fmt.Sprintf("Unable to start web listener: %s", err)) 438 } 439 440 closed := make(chan struct{}) 441 442 ctx, cancel := context.WithCancel(context.Background()) 443 go func() { 444 err := webHandler.Run(ctx, l, "") 445 if err != nil { 446 panic(fmt.Sprintf("Can't start web handler:%s", err)) 447 } 448 close(closed) 449 }() 450 451 // Give some time for the web goroutine to run since we need the server 452 // to be up before starting tests. 453 time.Sleep(5 * time.Second) 454 455 // Open a socket, and don't use it. This connection should then be closed 456 // after the ReadTimeout. 457 c, err := net.Dial("tcp", "localhost:9090") 458 require.NoError(t, err) 459 t.Cleanup(func() { require.NoError(t, c.Close()) }) 460 461 // Stop the web handler. 462 cancel() 463 464 select { 465 case <-closed: 466 case <-time.After(timeout + 5*time.Second): 467 t.Fatalf("Server still running after read timeout.") 468 } 469} 470 471func TestHandleMultipleQuitRequests(t *testing.T) { 472 opts := &Options{ 473 ListenAddress: ":9090", 474 MaxConnections: 512, 475 EnableLifecycle: true, 476 RoutePrefix: "/", 477 ExternalURL: &url.URL{ 478 Scheme: "http", 479 Host: "localhost:9090", 480 Path: "/", 481 }, 482 } 483 webHandler := New(nil, opts) 484 webHandler.config = &config.Config{} 485 webHandler.notifier = ¬ifier.Manager{} 486 l, err := webHandler.Listener() 487 if err != nil { 488 panic(fmt.Sprintf("Unable to start web listener: %s", err)) 489 } 490 ctx, cancel := context.WithCancel(context.Background()) 491 closed := make(chan struct{}) 492 go func() { 493 err := webHandler.Run(ctx, l, "") 494 if err != nil { 495 panic(fmt.Sprintf("Can't start web handler:%s", err)) 496 } 497 close(closed) 498 }() 499 500 // Give some time for the web goroutine to run since we need the server 501 // to be up before starting tests. 502 time.Sleep(5 * time.Second) 503 504 start := make(chan struct{}) 505 var wg sync.WaitGroup 506 for i := 0; i < 3; i++ { 507 wg.Add(1) 508 go func() { 509 defer wg.Done() 510 <-start 511 resp, err := http.Post("http://localhost:9090/-/quit", "", strings.NewReader("")) 512 require.NoError(t, err) 513 require.Equal(t, http.StatusOK, resp.StatusCode) 514 }() 515 } 516 close(start) 517 wg.Wait() 518 519 // Stop the web handler. 520 cancel() 521 522 select { 523 case <-closed: 524 case <-time.After(5 * time.Second): 525 t.Fatalf("Server still running after 5 seconds.") 526 } 527} 528 529func cleanupTestResponse(t *testing.T, resp *http.Response) { 530 _, err := io.Copy(ioutil.Discard, resp.Body) 531 require.NoError(t, err) 532 require.NoError(t, resp.Body.Close()) 533} 534 535func cleanupSnapshot(t *testing.T, dbDir string, resp *http.Response) { 536 snapshot := &struct { 537 Data struct { 538 Name string `json:"name"` 539 } `json:"data"` 540 }{} 541 b, err := ioutil.ReadAll(resp.Body) 542 require.NoError(t, err) 543 require.NoError(t, json.Unmarshal(b, snapshot)) 544 require.NotZero(t, snapshot.Data.Name, "snapshot directory not returned") 545 require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots", snapshot.Data.Name))) 546 require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots"))) 547} 548