1// Copyright 2013 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 scrape
15
16import (
17	"net/http"
18	"strconv"
19	"testing"
20	"time"
21
22	"github.com/pkg/errors"
23	"github.com/prometheus/common/model"
24	yaml "gopkg.in/yaml.v2"
25
26	"github.com/prometheus/prometheus/config"
27	"github.com/prometheus/prometheus/discovery/targetgroup"
28	"github.com/prometheus/prometheus/pkg/labels"
29	"github.com/prometheus/prometheus/pkg/relabel"
30	"github.com/prometheus/prometheus/util/testutil"
31)
32
33func TestPopulateLabels(t *testing.T) {
34	cases := []struct {
35		in      labels.Labels
36		cfg     *config.ScrapeConfig
37		res     labels.Labels
38		resOrig labels.Labels
39		err     error
40	}{
41		// Regular population of scrape config options.
42		{
43			in: labels.FromMap(map[string]string{
44				model.AddressLabel: "1.2.3.4:1000",
45				"custom":           "value",
46			}),
47			cfg: &config.ScrapeConfig{
48				Scheme:      "https",
49				MetricsPath: "/metrics",
50				JobName:     "job",
51			},
52			res: labels.FromMap(map[string]string{
53				model.AddressLabel:     "1.2.3.4:1000",
54				model.InstanceLabel:    "1.2.3.4:1000",
55				model.SchemeLabel:      "https",
56				model.MetricsPathLabel: "/metrics",
57				model.JobLabel:         "job",
58				"custom":               "value",
59			}),
60			resOrig: labels.FromMap(map[string]string{
61				model.AddressLabel:     "1.2.3.4:1000",
62				model.SchemeLabel:      "https",
63				model.MetricsPathLabel: "/metrics",
64				model.JobLabel:         "job",
65				"custom":               "value",
66			}),
67		},
68		// Pre-define/overwrite scrape config labels.
69		// Leave out port and expect it to be defaulted to scheme.
70		{
71			in: labels.FromMap(map[string]string{
72				model.AddressLabel:     "1.2.3.4",
73				model.SchemeLabel:      "http",
74				model.MetricsPathLabel: "/custom",
75				model.JobLabel:         "custom-job",
76			}),
77			cfg: &config.ScrapeConfig{
78				Scheme:      "https",
79				MetricsPath: "/metrics",
80				JobName:     "job",
81			},
82			res: labels.FromMap(map[string]string{
83				model.AddressLabel:     "1.2.3.4:80",
84				model.InstanceLabel:    "1.2.3.4:80",
85				model.SchemeLabel:      "http",
86				model.MetricsPathLabel: "/custom",
87				model.JobLabel:         "custom-job",
88			}),
89			resOrig: labels.FromMap(map[string]string{
90				model.AddressLabel:     "1.2.3.4",
91				model.SchemeLabel:      "http",
92				model.MetricsPathLabel: "/custom",
93				model.JobLabel:         "custom-job",
94			}),
95		},
96		// Provide instance label. HTTPS port default for IPv6.
97		{
98			in: labels.FromMap(map[string]string{
99				model.AddressLabel:  "[::1]",
100				model.InstanceLabel: "custom-instance",
101			}),
102			cfg: &config.ScrapeConfig{
103				Scheme:      "https",
104				MetricsPath: "/metrics",
105				JobName:     "job",
106			},
107			res: labels.FromMap(map[string]string{
108				model.AddressLabel:     "[::1]:443",
109				model.InstanceLabel:    "custom-instance",
110				model.SchemeLabel:      "https",
111				model.MetricsPathLabel: "/metrics",
112				model.JobLabel:         "job",
113			}),
114			resOrig: labels.FromMap(map[string]string{
115				model.AddressLabel:     "[::1]",
116				model.InstanceLabel:    "custom-instance",
117				model.SchemeLabel:      "https",
118				model.MetricsPathLabel: "/metrics",
119				model.JobLabel:         "job",
120			}),
121		},
122		// Address label missing.
123		{
124			in: labels.FromStrings("custom", "value"),
125			cfg: &config.ScrapeConfig{
126				Scheme:      "https",
127				MetricsPath: "/metrics",
128				JobName:     "job",
129			},
130			res:     nil,
131			resOrig: nil,
132			err:     errors.New("no address"),
133		},
134		// Address label missing, but added in relabelling.
135		{
136			in: labels.FromStrings("custom", "host:1234"),
137			cfg: &config.ScrapeConfig{
138				Scheme:      "https",
139				MetricsPath: "/metrics",
140				JobName:     "job",
141				RelabelConfigs: []*relabel.Config{
142					{
143						Action:       relabel.Replace,
144						Regex:        relabel.MustNewRegexp("(.*)"),
145						SourceLabels: model.LabelNames{"custom"},
146						Replacement:  "${1}",
147						TargetLabel:  string(model.AddressLabel),
148					},
149				},
150			},
151			res: labels.FromMap(map[string]string{
152				model.AddressLabel:     "host:1234",
153				model.InstanceLabel:    "host:1234",
154				model.SchemeLabel:      "https",
155				model.MetricsPathLabel: "/metrics",
156				model.JobLabel:         "job",
157				"custom":               "host:1234",
158			}),
159			resOrig: labels.FromMap(map[string]string{
160				model.SchemeLabel:      "https",
161				model.MetricsPathLabel: "/metrics",
162				model.JobLabel:         "job",
163				"custom":               "host:1234",
164			}),
165		},
166		// Address label missing, but added in relabelling.
167		{
168			in: labels.FromStrings("custom", "host:1234"),
169			cfg: &config.ScrapeConfig{
170				Scheme:      "https",
171				MetricsPath: "/metrics",
172				JobName:     "job",
173				RelabelConfigs: []*relabel.Config{
174					{
175						Action:       relabel.Replace,
176						Regex:        relabel.MustNewRegexp("(.*)"),
177						SourceLabels: model.LabelNames{"custom"},
178						Replacement:  "${1}",
179						TargetLabel:  string(model.AddressLabel),
180					},
181				},
182			},
183			res: labels.FromMap(map[string]string{
184				model.AddressLabel:     "host:1234",
185				model.InstanceLabel:    "host:1234",
186				model.SchemeLabel:      "https",
187				model.MetricsPathLabel: "/metrics",
188				model.JobLabel:         "job",
189				"custom":               "host:1234",
190			}),
191			resOrig: labels.FromMap(map[string]string{
192				model.SchemeLabel:      "https",
193				model.MetricsPathLabel: "/metrics",
194				model.JobLabel:         "job",
195				"custom":               "host:1234",
196			}),
197		},
198		// Invalid UTF-8 in label.
199		{
200			in: labels.FromMap(map[string]string{
201				model.AddressLabel: "1.2.3.4:1000",
202				"custom":           "\xbd",
203			}),
204			cfg: &config.ScrapeConfig{
205				Scheme:      "https",
206				MetricsPath: "/metrics",
207				JobName:     "job",
208			},
209			res:     nil,
210			resOrig: nil,
211			err:     errors.New("invalid label value for \"custom\": \"\\xbd\""),
212		},
213	}
214	for _, c := range cases {
215		in := c.in.Copy()
216
217		res, orig, err := populateLabels(c.in, c.cfg)
218		testutil.ErrorEqual(err, c.err)
219		testutil.Equals(t, c.in, in)
220		testutil.Equals(t, c.res, res)
221		testutil.Equals(t, c.resOrig, orig)
222	}
223}
224
225func loadConfiguration(t *testing.T, c string) *config.Config {
226	t.Helper()
227
228	cfg := &config.Config{}
229	if err := yaml.UnmarshalStrict([]byte(c), cfg); err != nil {
230		t.Fatalf("Unable to load YAML config: %s", err)
231	}
232	return cfg
233}
234
235func noopLoop() loop {
236	return &testLoop{
237		startFunc: func(interval, timeout time.Duration, errc chan<- error) {},
238		stopFunc:  func() {},
239	}
240}
241
242func TestManagerApplyConfig(t *testing.T) {
243	// Valid initial configuration.
244	cfgText1 := `
245scrape_configs:
246 - job_name: job1
247   static_configs:
248   - targets: ["foo:9090"]
249`
250	// Invalid configuration.
251	cfgText2 := `
252scrape_configs:
253 - job_name: job1
254   scheme: https
255   static_configs:
256   - targets: ["foo:9090"]
257   tls_config:
258     ca_file: /not/existing/ca/file
259`
260	// Valid configuration.
261	cfgText3 := `
262scrape_configs:
263 - job_name: job1
264   scheme: https
265   static_configs:
266   - targets: ["foo:9090"]
267`
268	var (
269		cfg1 = loadConfiguration(t, cfgText1)
270		cfg2 = loadConfiguration(t, cfgText2)
271		cfg3 = loadConfiguration(t, cfgText3)
272
273		ch = make(chan struct{}, 1)
274	)
275
276	scrapeManager := NewManager(nil, nil)
277	newLoop := func(scrapeLoopOptions) loop {
278		ch <- struct{}{}
279		return noopLoop()
280	}
281	sp := &scrapePool{
282		appendable:    &nopAppendable{},
283		activeTargets: map[uint64]*Target{},
284		loops: map[uint64]loop{
285			1: noopLoop(),
286		},
287		newLoop: newLoop,
288		logger:  nil,
289		config:  cfg1.ScrapeConfigs[0],
290		client:  http.DefaultClient,
291	}
292	scrapeManager.scrapePools = map[string]*scrapePool{
293		"job1": sp,
294	}
295
296	// Apply the initial configuration.
297	if err := scrapeManager.ApplyConfig(cfg1); err != nil {
298		t.Fatalf("unable to apply configuration: %s", err)
299	}
300	select {
301	case <-ch:
302		t.Fatal("reload happened")
303	default:
304	}
305
306	// Apply a configuration for which the reload fails.
307	if err := scrapeManager.ApplyConfig(cfg2); err == nil {
308		t.Fatalf("expecting error but got none")
309	}
310	select {
311	case <-ch:
312		t.Fatal("reload happened")
313	default:
314	}
315
316	// Apply a configuration for which the reload succeeds.
317	if err := scrapeManager.ApplyConfig(cfg3); err != nil {
318		t.Fatalf("unable to apply configuration: %s", err)
319	}
320	select {
321	case <-ch:
322	default:
323		t.Fatal("reload didn't happen")
324	}
325
326	// Re-applying the same configuration shouldn't trigger a reload.
327	if err := scrapeManager.ApplyConfig(cfg3); err != nil {
328		t.Fatalf("unable to apply configuration: %s", err)
329	}
330	select {
331	case <-ch:
332		t.Fatal("reload happened")
333	default:
334	}
335}
336
337func TestManagerTargetsUpdates(t *testing.T) {
338	m := NewManager(nil, nil)
339
340	ts := make(chan map[string][]*targetgroup.Group)
341	go m.Run(ts)
342
343	tgSent := make(map[string][]*targetgroup.Group)
344	for x := 0; x < 10; x++ {
345
346		tgSent[strconv.Itoa(x)] = []*targetgroup.Group{
347			{
348				Source: strconv.Itoa(x),
349			},
350		}
351
352		select {
353		case ts <- tgSent:
354		case <-time.After(10 * time.Millisecond):
355			t.Error("Scrape manager's channel remained blocked after the set threshold.")
356		}
357	}
358
359	m.mtxScrape.Lock()
360	tsetActual := m.targetSets
361	m.mtxScrape.Unlock()
362
363	// Make sure all updates have been received.
364	testutil.Equals(t, tgSent, tsetActual)
365
366	select {
367	case <-m.triggerReload:
368	default:
369		t.Error("No scrape loops reload was triggered after targets update.")
370	}
371}
372
373func TestSetJitter(t *testing.T) {
374	getConfig := func(prometheus string) *config.Config {
375		cfgText := `
376global:
377 external_labels:
378   prometheus: '` + prometheus + `'
379`
380
381		cfg := &config.Config{}
382		if err := yaml.UnmarshalStrict([]byte(cfgText), cfg); err != nil {
383			t.Fatalf("Unable to load YAML config cfgYaml: %s", err)
384		}
385
386		return cfg
387	}
388
389	scrapeManager := NewManager(nil, nil)
390
391	// Load the first config.
392	cfg1 := getConfig("ha1")
393	if err := scrapeManager.setJitterSeed(cfg1.GlobalConfig.ExternalLabels); err != nil {
394		t.Error(err)
395	}
396	jitter1 := scrapeManager.jitterSeed
397
398	if jitter1 == 0 {
399		t.Error("Jitter has to be a hash of uint64")
400	}
401
402	// Load the first config.
403	cfg2 := getConfig("ha2")
404	if err := scrapeManager.setJitterSeed(cfg2.GlobalConfig.ExternalLabels); err != nil {
405		t.Error(err)
406	}
407	jitter2 := scrapeManager.jitterSeed
408
409	if jitter1 == jitter2 {
410		t.Error("Jitter should not be the same on different set of external labels")
411	}
412}
413