1// Copyright 2017 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 main
15
16import (
17	"context"
18	"fmt"
19	"io/ioutil"
20	"math"
21	"os"
22	"os/exec"
23	"path/filepath"
24	"syscall"
25	"testing"
26	"time"
27
28	"github.com/go-kit/log"
29	"github.com/prometheus/client_golang/prometheus"
30	"github.com/prometheus/common/model"
31	"github.com/stretchr/testify/require"
32
33	"github.com/prometheus/prometheus/notifier"
34	"github.com/prometheus/prometheus/pkg/labels"
35	"github.com/prometheus/prometheus/rules"
36)
37
38var promPath = os.Args[0]
39var promConfig = filepath.Join("..", "..", "documentation", "examples", "prometheus.yml")
40var promData = filepath.Join(os.TempDir(), "data")
41
42func TestMain(m *testing.M) {
43	for i, arg := range os.Args {
44		if arg == "-test.main" {
45			os.Args = append(os.Args[:i], os.Args[i+1:]...)
46			main()
47			return
48		}
49	}
50
51	// On linux with a global proxy the tests will fail as the go client(http,grpc) tries to connect through the proxy.
52	os.Setenv("no_proxy", "localhost,127.0.0.1,0.0.0.0,:")
53
54	exitCode := m.Run()
55	os.RemoveAll(promData)
56	os.Exit(exitCode)
57}
58
59func TestComputeExternalURL(t *testing.T) {
60	tests := []struct {
61		input string
62		valid bool
63	}{
64		{
65			input: "",
66			valid: true,
67		},
68		{
69			input: "http://proxy.com/prometheus",
70			valid: true,
71		},
72		{
73			input: "'https://url/prometheus'",
74			valid: false,
75		},
76		{
77			input: "'relative/path/with/quotes'",
78			valid: false,
79		},
80		{
81			input: "http://alertmanager.company.com",
82			valid: true,
83		},
84		{
85			input: "https://double--dash.de",
86			valid: true,
87		},
88		{
89			input: "'http://starts/with/quote",
90			valid: false,
91		},
92		{
93			input: "ends/with/quote\"",
94			valid: false,
95		},
96	}
97
98	for _, test := range tests {
99		_, err := computeExternalURL(test.input, "0.0.0.0:9090")
100		if test.valid {
101			require.NoError(t, err)
102		} else {
103			require.Error(t, err, "input=%q", test.input)
104		}
105	}
106}
107
108// Let's provide an invalid configuration file and verify the exit status indicates the error.
109func TestFailedStartupExitCode(t *testing.T) {
110	if testing.Short() {
111		t.Skip("skipping test in short mode.")
112	}
113
114	fakeInputFile := "fake-input-file"
115	expectedExitStatus := 2
116
117	prom := exec.Command(promPath, "-test.main", "--config.file="+fakeInputFile)
118	err := prom.Run()
119	require.Error(t, err)
120
121	if exitError, ok := err.(*exec.ExitError); ok {
122		status := exitError.Sys().(syscall.WaitStatus)
123		require.Equal(t, expectedExitStatus, status.ExitStatus())
124	} else {
125		t.Errorf("unable to retrieve the exit status for prometheus: %v", err)
126	}
127}
128
129type senderFunc func(alerts ...*notifier.Alert)
130
131func (s senderFunc) Send(alerts ...*notifier.Alert) {
132	s(alerts...)
133}
134
135func TestSendAlerts(t *testing.T) {
136	testCases := []struct {
137		in  []*rules.Alert
138		exp []*notifier.Alert
139	}{
140		{
141			in: []*rules.Alert{
142				{
143					Labels:      []labels.Label{{Name: "l1", Value: "v1"}},
144					Annotations: []labels.Label{{Name: "a2", Value: "v2"}},
145					ActiveAt:    time.Unix(1, 0),
146					FiredAt:     time.Unix(2, 0),
147					ValidUntil:  time.Unix(3, 0),
148				},
149			},
150			exp: []*notifier.Alert{
151				{
152					Labels:       []labels.Label{{Name: "l1", Value: "v1"}},
153					Annotations:  []labels.Label{{Name: "a2", Value: "v2"}},
154					StartsAt:     time.Unix(2, 0),
155					EndsAt:       time.Unix(3, 0),
156					GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1",
157				},
158			},
159		},
160		{
161			in: []*rules.Alert{
162				{
163					Labels:      []labels.Label{{Name: "l1", Value: "v1"}},
164					Annotations: []labels.Label{{Name: "a2", Value: "v2"}},
165					ActiveAt:    time.Unix(1, 0),
166					FiredAt:     time.Unix(2, 0),
167					ResolvedAt:  time.Unix(4, 0),
168				},
169			},
170			exp: []*notifier.Alert{
171				{
172					Labels:       []labels.Label{{Name: "l1", Value: "v1"}},
173					Annotations:  []labels.Label{{Name: "a2", Value: "v2"}},
174					StartsAt:     time.Unix(2, 0),
175					EndsAt:       time.Unix(4, 0),
176					GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1",
177				},
178			},
179		},
180		{
181			in: []*rules.Alert{},
182		},
183	}
184
185	for i, tc := range testCases {
186		tc := tc
187		t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
188			senderFunc := senderFunc(func(alerts ...*notifier.Alert) {
189				if len(tc.in) == 0 {
190					t.Fatalf("sender called with 0 alert")
191				}
192				require.Equal(t, tc.exp, alerts)
193			})
194			sendAlerts(senderFunc, "http://localhost:9090")(context.TODO(), "up", tc.in...)
195		})
196	}
197}
198
199func TestWALSegmentSizeBounds(t *testing.T) {
200	if testing.Short() {
201		t.Skip("skipping test in short mode.")
202	}
203
204	for size, expectedExitStatus := range map[string]int{"9MB": 1, "257MB": 1, "10": 2, "1GB": 1, "12MB": 0} {
205		prom := exec.Command(promPath, "-test.main", "--storage.tsdb.wal-segment-size="+size, "--config.file="+promConfig)
206
207		// Log stderr in case of failure.
208		stderr, err := prom.StderrPipe()
209		require.NoError(t, err)
210		go func() {
211			slurp, _ := ioutil.ReadAll(stderr)
212			t.Log(string(slurp))
213		}()
214
215		err = prom.Start()
216		require.NoError(t, err)
217
218		if expectedExitStatus == 0 {
219			done := make(chan error, 1)
220			go func() { done <- prom.Wait() }()
221			select {
222			case err := <-done:
223				t.Errorf("prometheus should be still running: %v", err)
224			case <-time.After(5 * time.Second):
225				prom.Process.Kill()
226			}
227			continue
228		}
229
230		err = prom.Wait()
231		require.Error(t, err)
232		if exitError, ok := err.(*exec.ExitError); ok {
233			status := exitError.Sys().(syscall.WaitStatus)
234			require.Equal(t, expectedExitStatus, status.ExitStatus())
235		} else {
236			t.Errorf("unable to retrieve the exit status for prometheus: %v", err)
237		}
238	}
239}
240
241func TestMaxBlockChunkSegmentSizeBounds(t *testing.T) {
242	if testing.Short() {
243		t.Skip("skipping test in short mode.")
244	}
245
246	for size, expectedExitStatus := range map[string]int{"512KB": 1, "1MB": 0} {
247		prom := exec.Command(promPath, "-test.main", "--storage.tsdb.max-block-chunk-segment-size="+size, "--config.file="+promConfig)
248
249		// Log stderr in case of failure.
250		stderr, err := prom.StderrPipe()
251		require.NoError(t, err)
252		go func() {
253			slurp, _ := ioutil.ReadAll(stderr)
254			t.Log(string(slurp))
255		}()
256
257		err = prom.Start()
258		require.NoError(t, err)
259
260		if expectedExitStatus == 0 {
261			done := make(chan error, 1)
262			go func() { done <- prom.Wait() }()
263			select {
264			case err := <-done:
265				t.Errorf("prometheus should be still running: %v", err)
266			case <-time.After(5 * time.Second):
267				prom.Process.Kill()
268			}
269			continue
270		}
271
272		err = prom.Wait()
273		require.Error(t, err)
274		if exitError, ok := err.(*exec.ExitError); ok {
275			status := exitError.Sys().(syscall.WaitStatus)
276			require.Equal(t, expectedExitStatus, status.ExitStatus())
277		} else {
278			t.Errorf("unable to retrieve the exit status for prometheus: %v", err)
279		}
280	}
281}
282
283func TestTimeMetrics(t *testing.T) {
284	tmpDir, err := ioutil.TempDir("", "time_metrics_e2e")
285	require.NoError(t, err)
286
287	defer func() {
288		require.NoError(t, os.RemoveAll(tmpDir))
289	}()
290
291	reg := prometheus.NewRegistry()
292	db, err := openDBWithMetrics(tmpDir, log.NewNopLogger(), reg, nil, nil)
293	require.NoError(t, err)
294	defer func() {
295		require.NoError(t, db.Close())
296	}()
297
298	// Check initial values.
299	require.Equal(t, map[string]float64{
300		"prometheus_tsdb_lowest_timestamp_seconds": float64(math.MaxInt64) / 1000,
301		"prometheus_tsdb_head_min_time_seconds":    float64(math.MaxInt64) / 1000,
302		"prometheus_tsdb_head_max_time_seconds":    float64(math.MinInt64) / 1000,
303	}, getCurrentGaugeValuesFor(t, reg,
304		"prometheus_tsdb_lowest_timestamp_seconds",
305		"prometheus_tsdb_head_min_time_seconds",
306		"prometheus_tsdb_head_max_time_seconds",
307	))
308
309	app := db.Appender(context.Background())
310	_, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "a"), 1000, 1)
311	require.NoError(t, err)
312	_, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "a"), 2000, 1)
313	require.NoError(t, err)
314	_, err = app.Append(0, labels.FromStrings(model.MetricNameLabel, "a"), 3000, 1)
315	require.NoError(t, err)
316	require.NoError(t, app.Commit())
317
318	require.Equal(t, map[string]float64{
319		"prometheus_tsdb_lowest_timestamp_seconds": 1.0,
320		"prometheus_tsdb_head_min_time_seconds":    1.0,
321		"prometheus_tsdb_head_max_time_seconds":    3.0,
322	}, getCurrentGaugeValuesFor(t, reg,
323		"prometheus_tsdb_lowest_timestamp_seconds",
324		"prometheus_tsdb_head_min_time_seconds",
325		"prometheus_tsdb_head_max_time_seconds",
326	))
327}
328
329func getCurrentGaugeValuesFor(t *testing.T, reg prometheus.Gatherer, metricNames ...string) map[string]float64 {
330	f, err := reg.Gather()
331	require.NoError(t, err)
332
333	res := make(map[string]float64, len(metricNames))
334	for _, g := range f {
335		for _, m := range metricNames {
336			if g.GetName() != m {
337				continue
338			}
339
340			require.Equal(t, 1, len(g.GetMetric()))
341			if _, ok := res[m]; ok {
342				t.Error("expected only one metric family for", m)
343				t.FailNow()
344			}
345			res[m] = *g.GetMetric()[0].GetGauge().Value
346		}
347	}
348	return res
349}
350