@@ -36,6 +36,7 @@ package push
3636
3737import (
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
5460type 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.
8386func New (url , job string ) * Pusher {
8487var (
8588reg = prometheus .NewRegistry ()
@@ -91,9 +94,6 @@ func New(url, job string) *Pusher {
9194if strings .HasSuffix (url , "/" ) {
9295url = url [:len (url )- 1 ]
9396}
94- if strings .Contains (job , "/" ) {
95- err = fmt .Errorf ("job contains '/': %s" , job )
96- }
9797
9898return & Pusher {
9999error : 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.
161158func (p * Pusher ) Grouping (name , value string ) * Pusher {
162159if p .error == nil {
163160if ! model .LabelName (name ).IsValid () {
164161p .error = fmt .Errorf ("grouping label has invalid name: %s" , name )
165162return 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- }
171164p .grouping [name ] = value
172165}
173166return p
@@ -215,13 +208,7 @@ func (p *Pusher) Delete() error {
215208if p .error != nil {
216209return 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 )
225212if err != nil {
226213return err
227214}
@@ -235,7 +222,7 @@ func (p *Pusher) Delete() error {
235222defer resp .Body .Close ()
236223if resp .StatusCode != 202 {
237224body , _ := 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}
240227return nil
241228}
@@ -244,12 +231,6 @@ func (p *Pusher) push(method string) error {
244231if p .error != nil {
245232return 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-
253234mfs , err := p .gatherers .Gather ()
254235if err != nil {
255236return err
@@ -273,7 +254,7 @@ func (p *Pusher) push(method string) error {
273254}
274255enc .Encode (mf )
275256}
276- req , err := http .NewRequest (method , pushURL , buf )
257+ req , err := http .NewRequest (method , p . fullURL () , buf )
277258if err != nil {
278259return err
279260}
@@ -288,7 +269,40 @@ func (p *Pusher) push(method string) error {
288269defer resp .Body .Close ()
289270if resp .StatusCode != 202 {
290271body , _ := 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}
293274return 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+ }
0 commit comments