prometheus
630 строк · 17.5 Кб
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"net"
22"net/http"
23"net/http/httptest"
24"net/url"
25"os"
26"path/filepath"
27"strconv"
28"strings"
29"sync"
30"testing"
31"time"
32
33"github.com/prometheus/client_golang/prometheus"
34prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
35"github.com/stretchr/testify/require"
36
37"github.com/prometheus/prometheus/config"
38"github.com/prometheus/prometheus/notifier"
39"github.com/prometheus/prometheus/rules"
40"github.com/prometheus/prometheus/scrape"
41"github.com/prometheus/prometheus/tsdb"
42"github.com/prometheus/prometheus/util/testutil"
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.
47os.Setenv("no_proxy", "localhost,127.0.0.1,0.0.0.0,:")
48os.Exit(m.Run())
49}
50
51type dbAdapter struct {
52*tsdb.DB
53}
54
55func (a *dbAdapter) Stats(statsByLabelName string, limit int) (*tsdb.Stats, error) {
56return a.Head().Stats(statsByLabelName, limit), nil
57}
58
59func (a *dbAdapter) WALReplayStatus() (tsdb.WALReplayStatus, error) {
60return tsdb.WALReplayStatus{}, nil
61}
62
63func TestReadyAndHealthy(t *testing.T) {
64t.Parallel()
65
66dbDir := t.TempDir()
67
68db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
69require.NoError(t, err)
70t.Cleanup(func() {
71require.NoError(t, db.Close())
72})
73port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
74
75opts := &Options{
76ListenAddress: port,
77ReadTimeout: 30 * time.Second,
78MaxConnections: 512,
79Context: nil,
80Storage: nil,
81LocalStorage: &dbAdapter{db},
82TSDBDir: dbDir,
83QueryEngine: nil,
84ScrapeManager: &scrape.Manager{},
85RuleManager: &rules.Manager{},
86Notifier: nil,
87RoutePrefix: "/",
88EnableAdminAPI: true,
89ExternalURL: &url.URL{
90Scheme: "http",
91Host: "localhost" + port,
92Path: "/",
93},
94Version: &PrometheusVersion{},
95Gatherer: prometheus.DefaultGatherer,
96}
97
98opts.Flags = map[string]string{}
99
100webHandler := New(nil, opts)
101
102webHandler.config = &config.Config{}
103webHandler.notifier = ¬ifier.Manager{}
104l, err := webHandler.Listener()
105if err != nil {
106panic(fmt.Sprintf("Unable to start web listener: %s", err))
107}
108
109ctx, cancel := context.WithCancel(context.Background())
110defer cancel()
111go func() {
112err := webHandler.Run(ctx, l, "")
113if err != nil {
114panic(fmt.Sprintf("Can't start web handler:%s", err))
115}
116}()
117
118// Give some time for the web goroutine to run since we need the server
119// to be up before starting tests.
120time.Sleep(5 * time.Second)
121
122baseURL := "http://localhost" + port
123
124resp, err := http.Get(baseURL + "/-/healthy")
125require.NoError(t, err)
126require.Equal(t, http.StatusOK, resp.StatusCode)
127cleanupTestResponse(t, resp)
128
129resp, err = http.Head(baseURL + "/-/healthy")
130require.NoError(t, err)
131require.Equal(t, http.StatusOK, resp.StatusCode)
132cleanupTestResponse(t, resp)
133
134for _, u := range []string{
135baseURL + "/-/ready",
136} {
137resp, err = http.Get(u)
138require.NoError(t, err)
139require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
140cleanupTestResponse(t, resp)
141
142resp, err = http.Head(u)
143require.NoError(t, err)
144require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
145cleanupTestResponse(t, resp)
146}
147
148resp, err = http.Post(baseURL+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
149require.NoError(t, err)
150require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
151cleanupTestResponse(t, resp)
152
153resp, err = http.Post(baseURL+"/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}"))
154require.NoError(t, err)
155require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
156cleanupTestResponse(t, resp)
157
158// Set to ready.
159webHandler.SetReady(true)
160
161for _, u := range []string{
162baseURL + "/-/healthy",
163baseURL + "/-/ready",
164} {
165resp, err = http.Get(u)
166require.NoError(t, err)
167require.Equal(t, http.StatusOK, resp.StatusCode)
168cleanupTestResponse(t, resp)
169
170resp, err = http.Head(u)
171require.NoError(t, err)
172require.Equal(t, http.StatusOK, resp.StatusCode)
173cleanupTestResponse(t, resp)
174}
175
176resp, err = http.Post(baseURL+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
177require.NoError(t, err)
178require.Equal(t, http.StatusOK, resp.StatusCode)
179cleanupSnapshot(t, dbDir, resp)
180cleanupTestResponse(t, resp)
181
182resp, err = http.Post(baseURL+"/api/v1/admin/tsdb/delete_series?match[]=up", "", nil)
183require.NoError(t, err)
184require.Equal(t, http.StatusNoContent, resp.StatusCode)
185cleanupTestResponse(t, resp)
186}
187
188func TestRoutePrefix(t *testing.T) {
189t.Parallel()
190dbDir := t.TempDir()
191
192db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
193require.NoError(t, err)
194t.Cleanup(func() {
195require.NoError(t, db.Close())
196})
197
198port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
199
200opts := &Options{
201ListenAddress: port,
202ReadTimeout: 30 * time.Second,
203MaxConnections: 512,
204Context: nil,
205TSDBDir: dbDir,
206LocalStorage: &dbAdapter{db},
207Storage: nil,
208QueryEngine: nil,
209ScrapeManager: nil,
210RuleManager: nil,
211Notifier: nil,
212RoutePrefix: "/prometheus",
213EnableAdminAPI: true,
214ExternalURL: &url.URL{
215Host: "localhost.localdomain" + port,
216Scheme: "http",
217},
218}
219
220opts.Flags = map[string]string{}
221
222webHandler := New(nil, opts)
223l, err := webHandler.Listener()
224if err != nil {
225panic(fmt.Sprintf("Unable to start web listener: %s", err))
226}
227ctx, cancel := context.WithCancel(context.Background())
228defer cancel()
229go func() {
230err := webHandler.Run(ctx, l, "")
231if err != nil {
232panic(fmt.Sprintf("Can't start web handler:%s", err))
233}
234}()
235
236// Give some time for the web goroutine to run since we need the server
237// to be up before starting tests.
238time.Sleep(5 * time.Second)
239
240baseURL := "http://localhost" + port
241
242resp, err := http.Get(baseURL + opts.RoutePrefix + "/-/healthy")
243require.NoError(t, err)
244require.Equal(t, http.StatusOK, resp.StatusCode)
245cleanupTestResponse(t, resp)
246
247resp, err = http.Get(baseURL + opts.RoutePrefix + "/-/ready")
248require.NoError(t, err)
249require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
250cleanupTestResponse(t, resp)
251
252resp, err = http.Post(baseURL+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
253require.NoError(t, err)
254require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
255cleanupTestResponse(t, resp)
256
257resp, err = http.Post(baseURL+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series", "", strings.NewReader("{}"))
258require.NoError(t, err)
259require.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
260cleanupTestResponse(t, resp)
261
262// Set to ready.
263webHandler.SetReady(true)
264
265resp, err = http.Get(baseURL + opts.RoutePrefix + "/-/healthy")
266require.NoError(t, err)
267require.Equal(t, http.StatusOK, resp.StatusCode)
268cleanupTestResponse(t, resp)
269
270resp, err = http.Get(baseURL + opts.RoutePrefix + "/-/ready")
271require.NoError(t, err)
272require.Equal(t, http.StatusOK, resp.StatusCode)
273cleanupTestResponse(t, resp)
274
275resp, err = http.Post(baseURL+opts.RoutePrefix+"/api/v1/admin/tsdb/snapshot", "", strings.NewReader(""))
276require.NoError(t, err)
277require.Equal(t, http.StatusOK, resp.StatusCode)
278cleanupSnapshot(t, dbDir, resp)
279cleanupTestResponse(t, resp)
280
281resp, err = http.Post(baseURL+opts.RoutePrefix+"/api/v1/admin/tsdb/delete_series?match[]=up", "", nil)
282require.NoError(t, err)
283require.Equal(t, http.StatusNoContent, resp.StatusCode)
284cleanupTestResponse(t, resp)
285}
286
287func TestDebugHandler(t *testing.T) {
288for _, tc := range []struct {
289prefix, url string
290code int
291}{
292{"/", "/debug/pprof/cmdline", 200},
293{"/foo", "/foo/debug/pprof/cmdline", 200},
294
295{"/", "/debug/pprof/goroutine", 200},
296{"/foo", "/foo/debug/pprof/goroutine", 200},
297
298{"/", "/debug/pprof/foo", 404},
299{"/foo", "/bar/debug/pprof/goroutine", 404},
300} {
301opts := &Options{
302RoutePrefix: tc.prefix,
303ListenAddress: "somehost:9090",
304ExternalURL: &url.URL{
305Host: "localhost.localdomain:9090",
306Scheme: "http",
307},
308}
309handler := New(nil, opts)
310handler.SetReady(true)
311
312w := httptest.NewRecorder()
313
314req, err := http.NewRequest(http.MethodGet, tc.url, nil)
315
316require.NoError(t, err)
317
318handler.router.ServeHTTP(w, req)
319
320require.Equal(t, tc.code, w.Code)
321}
322}
323
324func TestHTTPMetrics(t *testing.T) {
325t.Parallel()
326handler := New(nil, &Options{
327RoutePrefix: "/",
328ListenAddress: "somehost:9090",
329ExternalURL: &url.URL{
330Host: "localhost.localdomain:9090",
331Scheme: "http",
332},
333})
334getReady := func() int {
335t.Helper()
336w := httptest.NewRecorder()
337
338req, err := http.NewRequest(http.MethodGet, "/-/ready", nil)
339require.NoError(t, err)
340
341handler.router.ServeHTTP(w, req)
342return w.Code
343}
344
345code := getReady()
346require.Equal(t, http.StatusServiceUnavailable, code)
347ready := handler.metrics.readyStatus
348require.Equal(t, 0, int(prom_testutil.ToFloat64(ready)))
349counter := handler.metrics.requestCounter
350require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable)))))
351
352handler.SetReady(true)
353for range [2]int{} {
354code = getReady()
355require.Equal(t, http.StatusOK, code)
356}
357require.Equal(t, 1, int(prom_testutil.ToFloat64(ready)))
358require.Equal(t, 2, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusOK)))))
359require.Equal(t, 1, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable)))))
360
361handler.SetReady(false)
362for range [2]int{} {
363code = getReady()
364require.Equal(t, http.StatusServiceUnavailable, code)
365}
366require.Equal(t, 0, int(prom_testutil.ToFloat64(ready)))
367require.Equal(t, 2, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusOK)))))
368require.Equal(t, 3, int(prom_testutil.ToFloat64(counter.WithLabelValues("/-/ready", strconv.Itoa(http.StatusServiceUnavailable)))))
369}
370
371func TestShutdownWithStaleConnection(t *testing.T) {
372dbDir := t.TempDir()
373
374db, err := tsdb.Open(dbDir, nil, nil, nil, nil)
375require.NoError(t, err)
376t.Cleanup(func() {
377require.NoError(t, db.Close())
378})
379timeout := 10 * time.Second
380
381port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
382
383opts := &Options{
384ListenAddress: port,
385ReadTimeout: timeout,
386MaxConnections: 512,
387Context: nil,
388Storage: nil,
389LocalStorage: &dbAdapter{db},
390TSDBDir: dbDir,
391QueryEngine: nil,
392ScrapeManager: &scrape.Manager{},
393RuleManager: &rules.Manager{},
394Notifier: nil,
395RoutePrefix: "/",
396ExternalURL: &url.URL{
397Scheme: "http",
398Host: "localhost" + port,
399Path: "/",
400},
401Version: &PrometheusVersion{},
402Gatherer: prometheus.DefaultGatherer,
403}
404
405opts.Flags = map[string]string{}
406
407webHandler := New(nil, opts)
408
409webHandler.config = &config.Config{}
410webHandler.notifier = ¬ifier.Manager{}
411l, err := webHandler.Listener()
412if err != nil {
413panic(fmt.Sprintf("Unable to start web listener: %s", err))
414}
415
416closed := make(chan struct{})
417
418ctx, cancel := context.WithCancel(context.Background())
419go func() {
420err := webHandler.Run(ctx, l, "")
421if err != nil {
422panic(fmt.Sprintf("Can't start web handler:%s", err))
423}
424close(closed)
425}()
426
427// Give some time for the web goroutine to run since we need the server
428// to be up before starting tests.
429time.Sleep(5 * time.Second)
430
431// Open a socket, and don't use it. This connection should then be closed
432// after the ReadTimeout.
433c, err := net.Dial("tcp", opts.ExternalURL.Host)
434require.NoError(t, err)
435t.Cleanup(func() { require.NoError(t, c.Close()) })
436
437// Stop the web handler.
438cancel()
439
440select {
441case <-closed:
442case <-time.After(timeout + 5*time.Second):
443require.FailNow(t, "Server still running after read timeout.")
444}
445}
446
447func TestHandleMultipleQuitRequests(t *testing.T) {
448port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
449
450opts := &Options{
451ListenAddress: port,
452MaxConnections: 512,
453EnableLifecycle: true,
454RoutePrefix: "/",
455ExternalURL: &url.URL{
456Scheme: "http",
457Host: "localhost" + port,
458Path: "/",
459},
460}
461webHandler := New(nil, opts)
462webHandler.config = &config.Config{}
463webHandler.notifier = ¬ifier.Manager{}
464l, err := webHandler.Listener()
465if err != nil {
466panic(fmt.Sprintf("Unable to start web listener: %s", err))
467}
468ctx, cancel := context.WithCancel(context.Background())
469closed := make(chan struct{})
470go func() {
471err := webHandler.Run(ctx, l, "")
472if err != nil {
473panic(fmt.Sprintf("Can't start web handler:%s", err))
474}
475close(closed)
476}()
477
478// Give some time for the web goroutine to run since we need the server
479// to be up before starting tests.
480time.Sleep(5 * time.Second)
481
482baseURL := opts.ExternalURL.Scheme + "://" + opts.ExternalURL.Host
483
484start := make(chan struct{})
485var wg sync.WaitGroup
486for i := 0; i < 3; i++ {
487wg.Add(1)
488go func() {
489defer wg.Done()
490<-start
491resp, err := http.Post(baseURL+"/-/quit", "", strings.NewReader(""))
492require.NoError(t, err)
493require.Equal(t, http.StatusOK, resp.StatusCode)
494}()
495}
496close(start)
497wg.Wait()
498
499// Stop the web handler.
500cancel()
501
502select {
503case <-closed:
504case <-time.After(5 * time.Second):
505require.FailNow(t, "Server still running after 5 seconds.")
506}
507}
508
509// Test for availability of API endpoints in Prometheus Agent mode.
510func TestAgentAPIEndPoints(t *testing.T) {
511t.Parallel()
512
513port := fmt.Sprintf(":%d", testutil.RandomUnprivilegedPort(t))
514
515opts := &Options{
516ListenAddress: port,
517ReadTimeout: 30 * time.Second,
518MaxConnections: 512,
519Context: nil,
520Storage: nil,
521QueryEngine: nil,
522ScrapeManager: &scrape.Manager{},
523RuleManager: &rules.Manager{},
524Notifier: nil,
525RoutePrefix: "/",
526EnableAdminAPI: true,
527ExternalURL: &url.URL{
528Scheme: "http",
529Host: "localhost" + port,
530Path: "/",
531},
532Version: &PrometheusVersion{},
533Gatherer: prometheus.DefaultGatherer,
534IsAgent: true,
535}
536
537opts.Flags = map[string]string{}
538
539webHandler := New(nil, opts)
540webHandler.SetReady(true)
541webHandler.config = &config.Config{}
542webHandler.notifier = ¬ifier.Manager{}
543l, err := webHandler.Listener()
544if err != nil {
545panic(fmt.Sprintf("Unable to start web listener: %s", err))
546}
547
548ctx, cancel := context.WithCancel(context.Background())
549defer cancel()
550go func() {
551err := webHandler.Run(ctx, l, "")
552if err != nil {
553panic(fmt.Sprintf("Can't start web handler:%s", err))
554}
555}()
556
557// Give some time for the web goroutine to run since we need the server
558// to be up before starting tests.
559time.Sleep(5 * time.Second)
560baseURL := "http://localhost" + port + "/api/v1"
561
562// Test for non-available endpoints in the Agent mode.
563for path, methods := range map[string][]string{
564"/labels": {http.MethodGet, http.MethodPost},
565"/label/:name/values": {http.MethodGet},
566"/series": {http.MethodGet, http.MethodPost, http.MethodDelete},
567"/alertmanagers": {http.MethodGet},
568"/query": {http.MethodGet, http.MethodPost},
569"/query_range": {http.MethodGet, http.MethodPost},
570"/query_exemplars": {http.MethodGet, http.MethodPost},
571"/status/tsdb": {http.MethodGet},
572"/alerts": {http.MethodGet},
573"/rules": {http.MethodGet},
574"/admin/tsdb/delete_series": {http.MethodPost, http.MethodPut},
575"/admin/tsdb/clean_tombstones": {http.MethodPost, http.MethodPut},
576"/admin/tsdb/snapshot": {http.MethodPost, http.MethodPut},
577} {
578for _, m := range methods {
579req, err := http.NewRequest(m, baseURL+path, nil)
580require.NoError(t, err)
581resp, err := http.DefaultClient.Do(req)
582require.NoError(t, err)
583require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode)
584t.Cleanup(func() {
585require.NoError(t, resp.Body.Close())
586})
587}
588}
589
590// Test for some of available endpoints in the Agent mode.
591for path, methods := range map[string][]string{
592"/targets": {http.MethodGet},
593"/targets/metadata": {http.MethodGet},
594"/metadata": {http.MethodGet},
595"/status/config": {http.MethodGet},
596"/status/runtimeinfo": {http.MethodGet},
597"/status/flags": {http.MethodGet},
598} {
599for _, m := range methods {
600req, err := http.NewRequest(m, baseURL+path, nil)
601require.NoError(t, err)
602resp, err := http.DefaultClient.Do(req)
603require.NoError(t, err)
604require.Equal(t, http.StatusOK, resp.StatusCode)
605t.Cleanup(func() {
606require.NoError(t, resp.Body.Close())
607})
608}
609}
610}
611
612func cleanupTestResponse(t *testing.T, resp *http.Response) {
613_, err := io.Copy(io.Discard, resp.Body)
614require.NoError(t, err)
615require.NoError(t, resp.Body.Close())
616}
617
618func cleanupSnapshot(t *testing.T, dbDir string, resp *http.Response) {
619snapshot := &struct {
620Data struct {
621Name string `json:"name"`
622} `json:"data"`
623}{}
624b, err := io.ReadAll(resp.Body)
625require.NoError(t, err)
626require.NoError(t, json.Unmarshal(b, snapshot))
627require.NotZero(t, snapshot.Data.Name, "snapshot directory not returned")
628require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots", snapshot.Data.Name)))
629require.NoError(t, os.Remove(filepath.Join(dbDir, "snapshots")))
630}
631