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 push
15
16import (
17	"bytes"
18	"io/ioutil"
19	"net/http"
20	"net/http/httptest"
21	"testing"
22
23	"github.com/prometheus/common/expfmt"
24
25	"github.com/prometheus/client_golang/prometheus"
26)
27
28func TestPush(t *testing.T) {
29
30	var (
31		lastMethod string
32		lastBody   []byte
33		lastPath   string
34	)
35
36	// Fake a Pushgateway that responds with 202 to DELETE and with 200 in
37	// all other cases.
38	pgwOK := httptest.NewServer(
39		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40			lastMethod = r.Method
41			var err error
42			lastBody, err = ioutil.ReadAll(r.Body)
43			if err != nil {
44				t.Fatal(err)
45			}
46			lastPath = r.URL.EscapedPath()
47			w.Header().Set("Content-Type", `text/plain; charset=utf-8`)
48			if r.Method == http.MethodDelete {
49				w.WriteHeader(http.StatusAccepted)
50				return
51			}
52			w.WriteHeader(http.StatusOK)
53		}),
54	)
55	defer pgwOK.Close()
56
57	// Fake a Pushgateway that always responds with 500.
58	pgwErr := httptest.NewServer(
59		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60			http.Error(w, "fake error", http.StatusInternalServerError)
61		}),
62	)
63	defer pgwErr.Close()
64
65	metric1 := prometheus.NewCounter(prometheus.CounterOpts{
66		Name: "testname1",
67		Help: "testhelp1",
68	})
69	metric2 := prometheus.NewGauge(prometheus.GaugeOpts{
70		Name:        "testname2",
71		Help:        "testhelp2",
72		ConstLabels: prometheus.Labels{"foo": "bar", "dings": "bums"},
73	})
74
75	reg := prometheus.NewRegistry()
76	reg.MustRegister(metric1)
77	reg.MustRegister(metric2)
78
79	mfs, err := reg.Gather()
80	if err != nil {
81		t.Fatal(err)
82	}
83
84	buf := &bytes.Buffer{}
85	enc := expfmt.NewEncoder(buf, expfmt.FmtProtoDelim)
86
87	for _, mf := range mfs {
88		if err := enc.Encode(mf); err != nil {
89			t.Fatal(err)
90		}
91	}
92	wantBody := buf.Bytes()
93
94	// Push some Collectors, all good.
95	if err := New(pgwOK.URL, "testjob").
96		Collector(metric1).
97		Collector(metric2).
98		Push(); err != nil {
99		t.Fatal(err)
100	}
101	if lastMethod != http.MethodPut {
102		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
103	}
104	if !bytes.Equal(lastBody, wantBody) {
105		t.Errorf("got body %v, want %v", lastBody, wantBody)
106	}
107	if lastPath != "/metrics/job/testjob" {
108		t.Error("unexpected path:", lastPath)
109	}
110
111	// Add some Collectors, with nil grouping, all good.
112	if err := New(pgwOK.URL, "testjob").
113		Collector(metric1).
114		Collector(metric2).
115		Add(); err != nil {
116		t.Fatal(err)
117	}
118	if lastMethod != http.MethodPost {
119		t.Errorf("got method %q for Add, want %q", lastMethod, http.MethodPost)
120	}
121	if !bytes.Equal(lastBody, wantBody) {
122		t.Errorf("got body %v, want %v", lastBody, wantBody)
123	}
124	if lastPath != "/metrics/job/testjob" {
125		t.Error("unexpected path:", lastPath)
126	}
127
128	// Pushes that require base64 encoding.
129	if err := New(pgwOK.URL, "test/job").
130		Collector(metric1).
131		Collector(metric2).
132		Push(); err != nil {
133		t.Fatal(err)
134	}
135	if lastMethod != http.MethodPut {
136		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
137	}
138	if !bytes.Equal(lastBody, wantBody) {
139		t.Errorf("got body %v, want %v", lastBody, wantBody)
140	}
141	if lastPath != "/metrics/job@base64/dGVzdC9qb2I" {
142		t.Error("unexpected path:", lastPath)
143	}
144	if err := New(pgwOK.URL, "testjob").
145		Grouping("foobar", "bu/ms").
146		Collector(metric1).
147		Collector(metric2).
148		Push(); err != nil {
149		t.Fatal(err)
150	}
151	if lastMethod != http.MethodPut {
152		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
153	}
154	if !bytes.Equal(lastBody, wantBody) {
155		t.Errorf("got body %v, want %v", lastBody, wantBody)
156	}
157	if lastPath != "/metrics/job/testjob/foobar@base64/YnUvbXM" {
158		t.Error("unexpected path:", lastPath)
159	}
160
161	// Push that requires URL encoding.
162	if err := New(pgwOK.URL, "testjob").
163		Grouping("titan", "Προμηθεύς").
164		Collector(metric1).
165		Collector(metric2).
166		Push(); err != nil {
167		t.Fatal(err)
168	}
169	if lastMethod != http.MethodPut {
170		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
171	}
172	if !bytes.Equal(lastBody, wantBody) {
173		t.Errorf("got body %v, want %v", lastBody, wantBody)
174	}
175	if lastPath != "/metrics/job/testjob/titan/%CE%A0%CF%81%CE%BF%CE%BC%CE%B7%CE%B8%CE%B5%CF%8D%CF%82" {
176		t.Error("unexpected path:", lastPath)
177	}
178
179	// Empty label value triggers special base64 encoding.
180	if err := New(pgwOK.URL, "testjob").
181		Grouping("empty", "").
182		Collector(metric1).
183		Collector(metric2).
184		Push(); err != nil {
185		t.Fatal(err)
186	}
187	if lastMethod != http.MethodPut {
188		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
189	}
190	if !bytes.Equal(lastBody, wantBody) {
191		t.Errorf("got body %v, want %v", lastBody, wantBody)
192	}
193	if lastPath != "/metrics/job/testjob/empty@base64/=" {
194		t.Error("unexpected path:", lastPath)
195	}
196
197	// Empty job name results in error.
198	if err := New(pgwErr.URL, "").
199		Collector(metric1).
200		Collector(metric2).
201		Push(); err == nil {
202		t.Error("push with empty job succeded")
203	} else {
204		if got, want := err, errJobEmpty; got != want {
205			t.Errorf("got error %q, want %q", got, want)
206		}
207	}
208
209	// Push some Collectors with a broken PGW.
210	if err := New(pgwErr.URL, "testjob").
211		Collector(metric1).
212		Collector(metric2).
213		Push(); err == nil {
214		t.Error("push to broken Pushgateway succeeded")
215	} else {
216		if got, want := err.Error(), "unexpected status code 500 while pushing to "+pgwErr.URL+"/metrics/job/testjob: fake error\n"; got != want {
217			t.Errorf("got error %q, want %q", got, want)
218		}
219	}
220
221	// Push some Collectors with invalid grouping or job.
222	if err := New(pgwOK.URL, "testjob").
223		Grouping("foo", "bums").
224		Collector(metric1).
225		Collector(metric2).
226		Push(); err == nil {
227		t.Error("push with grouping contained in metrics succeeded")
228	}
229	if err := New(pgwOK.URL, "testjob").
230		Grouping("foo-bar", "bums").
231		Collector(metric1).
232		Collector(metric2).
233		Push(); err == nil {
234		t.Error("push with invalid grouping succeeded")
235	}
236
237	// Push registry, all good.
238	if err := New(pgwOK.URL, "testjob").
239		Gatherer(reg).
240		Push(); err != nil {
241		t.Fatal(err)
242	}
243	if lastMethod != http.MethodPut {
244		t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
245	}
246	if !bytes.Equal(lastBody, wantBody) {
247		t.Errorf("got body %v, want %v", lastBody, wantBody)
248	}
249
250	// Add registry, all good.
251	if err := New(pgwOK.URL, "testjob").
252		Grouping("a", "x").
253		Grouping("b", "y").
254		Gatherer(reg).
255		Add(); err != nil {
256		t.Fatal(err)
257	}
258	if lastMethod != http.MethodPost {
259		t.Errorf("got method %q for Add, want %q", lastMethod, http.MethodPost)
260	}
261	if !bytes.Equal(lastBody, wantBody) {
262		t.Errorf("got body %v, want %v", lastBody, wantBody)
263	}
264	if lastPath != "/metrics/job/testjob/a/x/b/y" && lastPath != "/metrics/job/testjob/b/y/a/x" {
265		t.Error("unexpected path:", lastPath)
266	}
267
268	// Delete, all good.
269	if err := New(pgwOK.URL, "testjob").
270		Grouping("a", "x").
271		Grouping("b", "y").
272		Delete(); err != nil {
273		t.Fatal(err)
274	}
275	if lastMethod != http.MethodDelete {
276		t.Errorf("got method %q for Delete, want %q", lastMethod, http.MethodDelete)
277	}
278	if len(lastBody) != 0 {
279		t.Errorf("got body of length %d, want empty body", len(lastBody))
280	}
281	if lastPath != "/metrics/job/testjob/a/x/b/y" && lastPath != "/metrics/job/testjob/b/y/a/x" {
282		t.Error("unexpected path:", lastPath)
283	}
284}
285