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	"bytes"
18	"net/http/httptest"
19	"sort"
20	"strings"
21	"testing"
22
23	"github.com/prometheus/common/model"
24	"github.com/prometheus/prometheus/config"
25	"github.com/prometheus/prometheus/pkg/labels"
26	"github.com/prometheus/prometheus/promql"
27)
28
29var scenarios = map[string]struct {
30	params         string
31	externalLabels labels.Labels
32	code           int
33	body           string
34}{
35	"empty": {
36		params: "",
37		code:   200,
38		body:   ``,
39	},
40	"match nothing": {
41		params: "match[]=does_not_match_anything",
42		code:   200,
43		body:   ``,
44	},
45	"invalid params from the beginning": {
46		params: "match[]=-not-a-valid-metric-name",
47		code:   400,
48		body: `1:1: parse error: unexpected <op:->
49`,
50	},
51	"invalid params somewhere in the middle": {
52		params: "match[]=not-a-valid-metric-name",
53		code:   400,
54		body: `1:4: parse error: unexpected <op:->
55`,
56	},
57	"test_metric1": {
58		params: "match[]=test_metric1",
59		code:   200,
60		body: `# TYPE test_metric1 untyped
61test_metric1{foo="bar",instance="i"} 10000 6000000
62test_metric1{foo="boo",instance="i"} 1 6000000
63`,
64	},
65	"test_metric2": {
66		params: "match[]=test_metric2",
67		code:   200,
68		body: `# TYPE test_metric2 untyped
69test_metric2{foo="boo",instance="i"} 1 6000000
70`,
71	},
72	"test_metric_without_labels": {
73		params: "match[]=test_metric_without_labels",
74		code:   200,
75		body: `# TYPE test_metric_without_labels untyped
76test_metric_without_labels{instance=""} 1001 6000000
77`,
78	},
79	"test_stale_metric": {
80		params: "match[]=test_metric_stale",
81		code:   200,
82		body:   ``,
83	},
84	"test_old_metric": {
85		params: "match[]=test_metric_old",
86		code:   200,
87		body: `# TYPE test_metric_old untyped
88test_metric_old{instance=""} 981 5880000
89`,
90	},
91	"{foo='boo'}": {
92		params: "match[]={foo='boo'}",
93		code:   200,
94		body: `# TYPE test_metric1 untyped
95test_metric1{foo="boo",instance="i"} 1 6000000
96# TYPE test_metric2 untyped
97test_metric2{foo="boo",instance="i"} 1 6000000
98`,
99	},
100	"two matchers": {
101		params: "match[]=test_metric1&match[]=test_metric2",
102		code:   200,
103		body: `# TYPE test_metric1 untyped
104test_metric1{foo="bar",instance="i"} 10000 6000000
105test_metric1{foo="boo",instance="i"} 1 6000000
106# TYPE test_metric2 untyped
107test_metric2{foo="boo",instance="i"} 1 6000000
108`,
109	},
110	"everything": {
111		params: "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
112		code:   200,
113		body: `# TYPE test_metric1 untyped
114test_metric1{foo="bar",instance="i"} 10000 6000000
115test_metric1{foo="boo",instance="i"} 1 6000000
116# TYPE test_metric2 untyped
117test_metric2{foo="boo",instance="i"} 1 6000000
118# TYPE test_metric_old untyped
119test_metric_old{instance=""} 981 5880000
120# TYPE test_metric_without_labels untyped
121test_metric_without_labels{instance=""} 1001 6000000
122`,
123	},
124	"empty label value matches everything that doesn't have that label": {
125		params: "match[]={foo='',__name__=~'.%2b'}",
126		code:   200,
127		body: `# TYPE test_metric_old untyped
128test_metric_old{instance=""} 981 5880000
129# TYPE test_metric_without_labels untyped
130test_metric_without_labels{instance=""} 1001 6000000
131`,
132	},
133	"empty label value for a label that doesn't exist at all, matches everything": {
134		params: "match[]={bar='',__name__=~'.%2b'}",
135		code:   200,
136		body: `# TYPE test_metric1 untyped
137test_metric1{foo="bar",instance="i"} 10000 6000000
138test_metric1{foo="boo",instance="i"} 1 6000000
139# TYPE test_metric2 untyped
140test_metric2{foo="boo",instance="i"} 1 6000000
141# TYPE test_metric_old untyped
142test_metric_old{instance=""} 981 5880000
143# TYPE test_metric_without_labels untyped
144test_metric_without_labels{instance=""} 1001 6000000
145`,
146	},
147	"external labels are added if not already present": {
148		params:         "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
149		externalLabels: labels.Labels{{Name: "zone", Value: "ie"}, {Name: "foo", Value: "baz"}},
150		code:           200,
151		body: `# TYPE test_metric1 untyped
152test_metric1{foo="bar",instance="i",zone="ie"} 10000 6000000
153test_metric1{foo="boo",instance="i",zone="ie"} 1 6000000
154# TYPE test_metric2 untyped
155test_metric2{foo="boo",instance="i",zone="ie"} 1 6000000
156# TYPE test_metric_old untyped
157test_metric_old{foo="baz",instance="",zone="ie"} 981 5880000
158# TYPE test_metric_without_labels untyped
159test_metric_without_labels{foo="baz",instance="",zone="ie"} 1001 6000000
160`,
161	},
162	"instance is an external label": {
163		// This makes no sense as a configuration, but we should
164		// know what it does anyway.
165		params:         "match[]={__name__=~'.%2b'}", // '%2b' is an URL-encoded '+'.
166		externalLabels: labels.Labels{{Name: "instance", Value: "baz"}},
167		code:           200,
168		body: `# TYPE test_metric1 untyped
169test_metric1{foo="bar",instance="i"} 10000 6000000
170test_metric1{foo="boo",instance="i"} 1 6000000
171# TYPE test_metric2 untyped
172test_metric2{foo="boo",instance="i"} 1 6000000
173# TYPE test_metric_old untyped
174test_metric_old{instance="baz"} 981 5880000
175# TYPE test_metric_without_labels untyped
176test_metric_without_labels{instance="baz"} 1001 6000000
177`,
178	},
179}
180
181func TestFederation(t *testing.T) {
182	suite, err := promql.NewTest(t, `
183		load 1m
184			test_metric1{foo="bar",instance="i"}    0+100x100
185			test_metric1{foo="boo",instance="i"}    1+0x100
186			test_metric2{foo="boo",instance="i"}    1+0x100
187			test_metric_without_labels 1+10x100
188			test_metric_stale                       1+10x99 stale
189			test_metric_old                         1+10x98
190	`)
191	if err != nil {
192		t.Fatal(err)
193	}
194	defer suite.Close()
195
196	if err := suite.Run(); err != nil {
197		t.Fatal(err)
198	}
199
200	h := &Handler{
201		storage:     suite.Storage(),
202		queryEngine: suite.QueryEngine(),
203		now:         func() model.Time { return 101 * 60 * 1000 }, // 101min after epoch.
204		config: &config.Config{
205			GlobalConfig: config.GlobalConfig{},
206		},
207	}
208
209	for name, scenario := range scenarios {
210		h.config.GlobalConfig.ExternalLabels = scenario.externalLabels
211		req := httptest.NewRequest("GET", "http://example.org/federate?"+scenario.params, nil)
212		res := httptest.NewRecorder()
213		h.federation(res, req)
214		if got, want := res.Code, scenario.code; got != want {
215			t.Errorf("Scenario %q: got code %d, want %d", name, got, want)
216		}
217		if got, want := normalizeBody(res.Body), scenario.body; got != want {
218			t.Errorf("Scenario %q: got body\n%s\n, want\n%s\n", name, got, want)
219		}
220	}
221}
222
223// normalizeBody sorts the lines within a metric to make it easy to verify the body.
224// (Federation is not taking care of sorting within a metric family.)
225func normalizeBody(body *bytes.Buffer) string {
226	var (
227		lines    []string
228		lastHash int
229	)
230	for line, err := body.ReadString('\n'); err == nil; line, err = body.ReadString('\n') {
231		if line[0] == '#' && len(lines) > 0 {
232			sort.Strings(lines[lastHash+1:])
233			lastHash = len(lines)
234		}
235		lines = append(lines, line)
236	}
237	if len(lines) > 0 {
238		sort.Strings(lines[lastHash+1:])
239	}
240	return strings.Join(lines, "")
241}
242