Skip to content

Commit ef4f037

Browse files
authored
Merge pull request #752 from prometheus/beorn7/push
Properly handle empty job and label values
2 parents ce2dae2 + 389d3c3 commit ef4f037

File tree

2 files changed

+50
-10
lines changed

2 files changed

+50
-10
lines changed

prometheus/push/push.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ package push
3737
import (
3838
"bytes"
3939
"encoding/base64"
40+
"errors"
4041
"fmt"
4142
"io/ioutil"
4243
"net/http"
@@ -56,6 +57,8 @@ const (
5657
base64Suffix = "@base64"
5758
)
5859

60+
var errJobEmpty = errors.New("job name is empty")
61+
5962
// HTTPDoer is an interface for the one method of http.Client that is used by Pusher
6063
type HTTPDoer interface {
6164
Do(*http.Request) (*http.Response, error)
@@ -80,14 +83,17 @@ type Pusher struct {
8083
}
8184

8285
// New creates a new Pusher to push to the provided URL with the provided job
83-
// name. You can use just host:port or ip:port as url, in which case “http://”
84-
// is added automatically. Alternatively, include the schema in the
85-
// URL. However, do not include the “/metrics/jobs/…” part.
86+
// name (which must not be empty). You can use just host:port or ip:port as url,
87+
// in which case “http://” is added automatically. Alternatively, include the
88+
// schema in the URL. However, do not include the “/metrics/jobs/…” part.
8689
func New(url, job string) *Pusher {
8790
var (
8891
reg = prometheus.NewRegistry()
8992
err error
9093
)
94+
if job == "" {
95+
err = errJobEmpty
96+
}
9197
if !strings.Contains(url, "://") {
9298
url = "http://" + url
9399
}
@@ -267,7 +273,7 @@ func (p *Pusher) push(method string) error {
267273
return err
268274
}
269275
defer resp.Body.Close()
270-
// Pushgateway 0.10+ responds with StatusOK, earlier versions with StatusAccepted.
276+
// Depending on version and configuration of the PGW, StatusOK or StatusAccepted may be returned.
271277
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
272278
body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
273279
return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, p.fullURL(), body)
@@ -278,9 +284,11 @@ func (p *Pusher) push(method string) error {
278284
// fullURL assembles the URL used to push/delete metrics and returns it as a
279285
// string. The job name and any grouping label values containing a '/' will
280286
// trigger a base64 encoding of the affected component and proper suffixing of
281-
// the preceding component. If the component does not contain a '/' but other
282-
// special character, the usual url.QueryEscape is used for compatibility with
283-
// older versions of the Pushgateway and for better readability.
287+
// the preceding component. Similarly, an empty grouping label value will be
288+
// encoded as base64 just with a single `=` padding character (to avoid an empty
289+
// path component). If the component does not contain a '/' but other special
290+
// characters, the usual url.QueryEscape is used for compatibility with older
291+
// versions of the Pushgateway and for better readability.
284292
func (p *Pusher) fullURL() string {
285293
urlComponents := []string{}
286294
if encodedJob, base64 := encodeComponent(p.job); base64 {
@@ -299,9 +307,12 @@ func (p *Pusher) fullURL() string {
299307
}
300308

301309
// encodeComponent encodes the provided string with base64.RawURLEncoding in
302-
// case it contains '/'. If not, it uses url.QueryEscape instead. It returns
303-
// true in the former case.
310+
// case it contains '/' and as "=" in case it is empty. If neither is the case,
311+
// it uses url.QueryEscape instead. It returns true in the former two cases.
304312
func encodeComponent(s string) (string, bool) {
313+
if s == "" {
314+
return "=", true
315+
}
305316
if strings.Contains(s, "/") {
306317
return base64.RawURLEncoding.EncodeToString([]byte(s)), true
307318
}

prometheus/push/push_test.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,36 @@ func TestPush(t *testing.T) {
176176
t.Error("unexpected path:", lastPath)
177177
}
178178

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+
179209
// Push some Collectors with a broken PGW.
180210
if err := New(pgwErr.URL, "testjob").
181211
Collector(metric1).
@@ -251,5 +281,4 @@ func TestPush(t *testing.T) {
251281
if lastPath != "/metrics/job/testjob/a/x/b/y" && lastPath != "/metrics/job/testjob/b/y/a/x" {
252282
t.Error("unexpected path:", lastPath)
253283
}
254-
255284
}

0 commit comments

Comments
 (0)