Skip to content

Commit 389d3c3

Browse files
committed
Properly handle empty job and label values
An empty job name was always an error, but it was previously only detected when pushing to the PGW and receiving an error. With this commit, the error is detected before pushing. An empty label value should have been OK but was encoded in a way that couldn't be pushed to the PGW. Cf. prometheus/pushgateway#344 . This commit changes the creation of the path in the URL so that it works with empty label values. Signed-off-by: beorn7 <[email protected]>
1 parent ce2dae2 commit 389d3c3

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)