@@ -36,6 +36,7 @@ package push
36
36
37
37
import (
38
38
"bytes"
39
+ "encoding/base64"
39
40
"fmt"
40
41
"io/ioutil"
41
42
"net/http"
@@ -48,7 +49,12 @@ import (
48
49
"github.com/prometheus/client_golang/prometheus"
49
50
)
50
51
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
+ )
52
58
53
59
// HTTPDoer is an interface for the one method of http.Client that is used by Pusher
54
60
type HTTPDoer interface {
@@ -77,9 +83,6 @@ type Pusher struct {
77
83
// name. You can use just host:port or ip:port as url, in which case “http://”
78
84
// is added automatically. Alternatively, include the schema in the
79
85
// 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.
83
86
func New (url , job string ) * Pusher {
84
87
var (
85
88
reg = prometheus .NewRegistry ()
@@ -91,9 +94,6 @@ func New(url, job string) *Pusher {
91
94
if strings .HasSuffix (url , "/" ) {
92
95
url = url [:len (url )- 1 ]
93
96
}
94
- if strings .Contains (job , "/" ) {
95
- err = fmt .Errorf ("job contains '/': %s" , job )
96
- }
97
97
98
98
return & Pusher {
99
99
error : err ,
@@ -155,19 +155,12 @@ func (p *Pusher) Collector(c prometheus.Collector) *Pusher {
155
155
// will lead to an error.
156
156
//
157
157
// 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.
161
158
func (p * Pusher ) Grouping (name , value string ) * Pusher {
162
159
if p .error == nil {
163
160
if ! model .LabelName (name ).IsValid () {
164
161
p .error = fmt .Errorf ("grouping label has invalid name: %s" , name )
165
162
return p
166
163
}
167
- if strings .Contains (value , "/" ) {
168
- p .error = fmt .Errorf ("value of grouping label %s contains '/': %s" , name , value )
169
- return p
170
- }
171
164
p .grouping [name ] = value
172
165
}
173
166
return p
@@ -215,13 +208,7 @@ func (p *Pusher) Delete() error {
215
208
if p .error != nil {
216
209
return p .error
217
210
}
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 )
225
212
if err != nil {
226
213
return err
227
214
}
@@ -235,7 +222,7 @@ func (p *Pusher) Delete() error {
235
222
defer resp .Body .Close ()
236
223
if resp .StatusCode != 202 {
237
224
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 )
239
226
}
240
227
return nil
241
228
}
@@ -244,12 +231,6 @@ func (p *Pusher) push(method string) error {
244
231
if p .error != nil {
245
232
return p .error
246
233
}
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
-
253
234
mfs , err := p .gatherers .Gather ()
254
235
if err != nil {
255
236
return err
@@ -273,7 +254,7 @@ func (p *Pusher) push(method string) error {
273
254
}
274
255
enc .Encode (mf )
275
256
}
276
- req , err := http .NewRequest (method , pushURL , buf )
257
+ req , err := http .NewRequest (method , p . fullURL () , buf )
277
258
if err != nil {
278
259
return err
279
260
}
@@ -288,7 +269,40 @@ func (p *Pusher) push(method string) error {
288
269
defer resp .Body .Close ()
289
270
if resp .StatusCode != 202 {
290
271
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 )
292
273
}
293
274
return nil
294
275
}
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
+ }
0 commit comments