Skip to content

Commit bb9b00a

Browse files
authored
Merge pull request #624 from prometheus/beorn/push
Support new base64 encoding for pushing to the Pushgateway
2 parents c0d684b + 4b95c4a commit bb9b00a

File tree

2 files changed

+95
-43
lines changed

2 files changed

+95
-43
lines changed

prometheus/push/push.go

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ package push
3636

3737
import (
3838
"bytes"
39+
"encoding/base64"
3940
"fmt"
4041
"io/ioutil"
4142
"net/http"
@@ -48,7 +49,12 @@ import (
4849
"github.com/prometheus/client_golang/prometheus"
4950
)
5051

51-
const contentTypeHeader = "Content-Type"
52+
const (
53+
contentTypeHeader = "Content-Type"
54+
// base64Suffix is appended to a label name in the request URL path to
55+
// mark the following label value as base64 encoded.
56+
base64Suffix = "@base64"
57+
)
5258

5359
// HTTPDoer is an interface for the one method of http.Client that is used by Pusher
5460
type HTTPDoer interface {
@@ -77,9 +83,6 @@ type Pusher struct {
7783
// name. You can use just host:port or ip:port as url, in which case “http://”
7884
// is added automatically. Alternatively, include the schema in the
7985
// URL. However, do not include the “/metrics/jobs/…” part.
80-
//
81-
// Note that until https://github.com/prometheus/pushgateway/issues/97 is
82-
// resolved, a “/” character in the job name is prohibited.
8386
func New(url, job string) *Pusher {
8487
var (
8588
reg = prometheus.NewRegistry()
@@ -91,9 +94,6 @@ func New(url, job string) *Pusher {
9194
if strings.HasSuffix(url, "/") {
9295
url = url[:len(url)-1]
9396
}
94-
if strings.Contains(job, "/") {
95-
err = fmt.Errorf("job contains '/': %s", job)
96-
}
9797

9898
return &Pusher{
9999
error: err,
@@ -155,19 +155,12 @@ func (p *Pusher) Collector(c prometheus.Collector) *Pusher {
155155
// will lead to an error.
156156
//
157157
// For convenience, this method returns a pointer to the Pusher itself.
158-
//
159-
// Note that until https://github.com/prometheus/pushgateway/issues/97 is
160-
// resolved, this method does not allow a “/” character in the label value.
161158
func (p *Pusher) Grouping(name, value string) *Pusher {
162159
if p.error == nil {
163160
if !model.LabelName(name).IsValid() {
164161
p.error = fmt.Errorf("grouping label has invalid name: %s", name)
165162
return p
166163
}
167-
if strings.Contains(value, "/") {
168-
p.error = fmt.Errorf("value of grouping label %s contains '/': %s", name, value)
169-
return p
170-
}
171164
p.grouping[name] = value
172165
}
173166
return p
@@ -215,13 +208,7 @@ func (p *Pusher) Delete() error {
215208
if p.error != nil {
216209
return p.error
217210
}
218-
urlComponents := []string{url.QueryEscape(p.job)}
219-
for ln, lv := range p.grouping {
220-
urlComponents = append(urlComponents, ln, lv)
221-
}
222-
deleteURL := fmt.Sprintf("%s/metrics/job/%s", p.url, strings.Join(urlComponents, "/"))
223-
224-
req, err := http.NewRequest(http.MethodDelete, deleteURL, nil)
211+
req, err := http.NewRequest(http.MethodDelete, p.fullURL(), nil)
225212
if err != nil {
226213
return err
227214
}
@@ -235,7 +222,7 @@ func (p *Pusher) Delete() error {
235222
defer resp.Body.Close()
236223
if resp.StatusCode != 202 {
237224
body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
238-
return fmt.Errorf("unexpected status code %d while deleting %s: %s", resp.StatusCode, deleteURL, body)
225+
return fmt.Errorf("unexpected status code %d while deleting %s: %s", resp.StatusCode, p.fullURL(), body)
239226
}
240227
return nil
241228
}
@@ -244,12 +231,6 @@ func (p *Pusher) push(method string) error {
244231
if p.error != nil {
245232
return p.error
246233
}
247-
urlComponents := []string{url.QueryEscape(p.job)}
248-
for ln, lv := range p.grouping {
249-
urlComponents = append(urlComponents, ln, lv)
250-
}
251-
pushURL := fmt.Sprintf("%s/metrics/job/%s", p.url, strings.Join(urlComponents, "/"))
252-
253234
mfs, err := p.gatherers.Gather()
254235
if err != nil {
255236
return err
@@ -273,7 +254,7 @@ func (p *Pusher) push(method string) error {
273254
}
274255
enc.Encode(mf)
275256
}
276-
req, err := http.NewRequest(method, pushURL, buf)
257+
req, err := http.NewRequest(method, p.fullURL(), buf)
277258
if err != nil {
278259
return err
279260
}
@@ -288,7 +269,40 @@ func (p *Pusher) push(method string) error {
288269
defer resp.Body.Close()
289270
if resp.StatusCode != 202 {
290271
body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
291-
return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, pushURL, body)
272+
return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, p.fullURL(), body)
292273
}
293274
return nil
294275
}
276+
277+
// fullURL assembles the URL used to push/delete metrics and returns it as a
278+
// string. The job name and any grouping label values containing a '/' will
279+
// trigger a base64 encoding of the affected component and proper suffixing of
280+
// the preceding component. If the component does not contain a '/' but other
281+
// special character, the usual url.QueryEscape is used for compatibility with
282+
// older versions of the Pushgateway and for better readability.
283+
func (p *Pusher) fullURL() string {
284+
urlComponents := []string{}
285+
if encodedJob, base64 := encodeComponent(p.job); base64 {
286+
urlComponents = append(urlComponents, "job"+base64Suffix, encodedJob)
287+
} else {
288+
urlComponents = append(urlComponents, "job", encodedJob)
289+
}
290+
for ln, lv := range p.grouping {
291+
if encodedLV, base64 := encodeComponent(lv); base64 {
292+
urlComponents = append(urlComponents, ln+base64Suffix, encodedLV)
293+
} else {
294+
urlComponents = append(urlComponents, ln, encodedLV)
295+
}
296+
}
297+
return fmt.Sprintf("%s/metrics/%s", p.url, strings.Join(urlComponents, "/"))
298+
}
299+
300+
// encodeComponent encodes the provided string with base64.RawURLEncoding in
301+
// case it contains '/'. If not, it uses url.QueryEscape instead. It returns
302+
// true in the former case.
303+
func encodeComponent(s string) (string, bool) {
304+
if strings.Contains(s, "/") {
305+
return base64.RawURLEncoding.EncodeToString([]byte(s)), true
306+
}
307+
return url.QueryEscape(s), false
308+
}

prometheus/push/push_test.go

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,57 @@ func TestPush(t *testing.T) {
120120
t.Error("unexpected path:", lastPath)
121121
}
122122

123+
// Pushes that require base64 encoding.
124+
if err := New(pgwOK.URL, "test/job").
125+
Collector(metric1).
126+
Collector(metric2).
127+
Push(); err != nil {
128+
t.Fatal(err)
129+
}
130+
if lastMethod != http.MethodPut {
131+
t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
132+
}
133+
if !bytes.Equal(lastBody, wantBody) {
134+
t.Errorf("got body %v, want %v", lastBody, wantBody)
135+
}
136+
if lastPath != "/metrics/job@base64/dGVzdC9qb2I" {
137+
t.Error("unexpected path:", lastPath)
138+
}
139+
if err := New(pgwOK.URL, "testjob").
140+
Grouping("foobar", "bu/ms").
141+
Collector(metric1).
142+
Collector(metric2).
143+
Push(); err != nil {
144+
t.Fatal(err)
145+
}
146+
if lastMethod != http.MethodPut {
147+
t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
148+
}
149+
if !bytes.Equal(lastBody, wantBody) {
150+
t.Errorf("got body %v, want %v", lastBody, wantBody)
151+
}
152+
if lastPath != "/metrics/job/testjob/foobar@base64/YnUvbXM" {
153+
t.Error("unexpected path:", lastPath)
154+
}
155+
156+
// Push that requires URL encoding.
157+
if err := New(pgwOK.URL, "testjob").
158+
Grouping("titan", "Προμηθεύς").
159+
Collector(metric1).
160+
Collector(metric2).
161+
Push(); err != nil {
162+
t.Fatal(err)
163+
}
164+
if lastMethod != http.MethodPut {
165+
t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
166+
}
167+
if !bytes.Equal(lastBody, wantBody) {
168+
t.Errorf("got body %v, want %v", lastBody, wantBody)
169+
}
170+
if lastPath != "/metrics/job/testjob/titan/%CE%A0%CF%81%CE%BF%CE%BC%CE%B7%CE%B8%CE%B5%CF%8D%CF%82" {
171+
t.Error("unexpected path:", lastPath)
172+
}
173+
123174
// Push some Collectors with a broken PGW.
124175
if err := New(pgwErr.URL, "testjob").
125176
Collector(metric1).
@@ -140,19 +191,6 @@ func TestPush(t *testing.T) {
140191
Push(); err == nil {
141192
t.Error("push with grouping contained in metrics succeeded")
142193
}
143-
if err := New(pgwOK.URL, "test/job").
144-
Collector(metric1).
145-
Collector(metric2).
146-
Push(); err == nil {
147-
t.Error("push with invalid job value succeeded")
148-
}
149-
if err := New(pgwOK.URL, "testjob").
150-
Grouping("foobar", "bu/ms").
151-
Collector(metric1).
152-
Collector(metric2).
153-
Push(); err == nil {
154-
t.Error("push with invalid grouping succeeded")
155-
}
156194
if err := New(pgwOK.URL, "testjob").
157195
Grouping("foo-bar", "bums").
158196
Collector(metric1).

0 commit comments

Comments
 (0)