Merge pull request #752 from prometheus/beorn7/push

Properly handle empty job and label values
This commit is contained in:
Björn Rabenstein 2020-05-14 18:57:43 +02:00 committed by GitHub
commit ef4f0376f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 50 additions and 10 deletions

View File

@ -37,6 +37,7 @@ package push
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -56,6 +57,8 @@ const (
base64Suffix = "@base64" base64Suffix = "@base64"
) )
var errJobEmpty = errors.New("job name is empty")
// HTTPDoer is an interface for the one method of http.Client that is used by Pusher // HTTPDoer is an interface for the one method of http.Client that is used by Pusher
type HTTPDoer interface { type HTTPDoer interface {
Do(*http.Request) (*http.Response, error) Do(*http.Request) (*http.Response, error)
@ -80,14 +83,17 @@ type Pusher struct {
} }
// New creates a new Pusher to push to the provided URL with the provided job // New creates a new Pusher to push to the provided URL with the provided job
// name. You can use just host:port or ip:port as url, in which case “http://” // name (which must not be empty). You can use just host:port or ip:port as url,
// is added automatically. Alternatively, include the schema in the // in which case “http://” is added automatically. Alternatively, include the
// URL. However, do not include the “/metrics/jobs/…” part. // schema in the URL. However, do not include the “/metrics/jobs/…” part.
func New(url, job string) *Pusher { func New(url, job string) *Pusher {
var ( var (
reg = prometheus.NewRegistry() reg = prometheus.NewRegistry()
err error err error
) )
if job == "" {
err = errJobEmpty
}
if !strings.Contains(url, "://") { if !strings.Contains(url, "://") {
url = "http://" + url url = "http://" + url
} }
@ -267,7 +273,7 @@ func (p *Pusher) push(method string) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
// Pushgateway 0.10+ responds with StatusOK, earlier versions with StatusAccepted. // Depending on version and configuration of the PGW, StatusOK or StatusAccepted may be returned.
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only. body, _ := ioutil.ReadAll(resp.Body) // Ignore any further error as this is for an error message only.
return fmt.Errorf("unexpected status code %d while pushing to %s: %s", resp.StatusCode, p.fullURL(), body) 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 {
// fullURL assembles the URL used to push/delete metrics and returns it as a // fullURL assembles the URL used to push/delete metrics and returns it as a
// string. The job name and any grouping label values containing a '/' will // string. The job name and any grouping label values containing a '/' will
// trigger a base64 encoding of the affected component and proper suffixing of // trigger a base64 encoding of the affected component and proper suffixing of
// the preceding component. If the component does not contain a '/' but other // the preceding component. Similarly, an empty grouping label value will be
// special character, the usual url.QueryEscape is used for compatibility with // encoded as base64 just with a single `=` padding character (to avoid an empty
// older versions of the Pushgateway and for better readability. // path component). If the component does not contain a '/' but other special
// characters, the usual url.QueryEscape is used for compatibility with older
// versions of the Pushgateway and for better readability.
func (p *Pusher) fullURL() string { func (p *Pusher) fullURL() string {
urlComponents := []string{} urlComponents := []string{}
if encodedJob, base64 := encodeComponent(p.job); base64 { if encodedJob, base64 := encodeComponent(p.job); base64 {
@ -299,9 +307,12 @@ func (p *Pusher) fullURL() string {
} }
// encodeComponent encodes the provided string with base64.RawURLEncoding in // encodeComponent encodes the provided string with base64.RawURLEncoding in
// case it contains '/'. If not, it uses url.QueryEscape instead. It returns // case it contains '/' and as "=" in case it is empty. If neither is the case,
// true in the former case. // it uses url.QueryEscape instead. It returns true in the former two cases.
func encodeComponent(s string) (string, bool) { func encodeComponent(s string) (string, bool) {
if s == "" {
return "=", true
}
if strings.Contains(s, "/") { if strings.Contains(s, "/") {
return base64.RawURLEncoding.EncodeToString([]byte(s)), true return base64.RawURLEncoding.EncodeToString([]byte(s)), true
} }

View File

@ -176,6 +176,36 @@ func TestPush(t *testing.T) {
t.Error("unexpected path:", lastPath) t.Error("unexpected path:", lastPath)
} }
// Empty label value triggers special base64 encoding.
if err := New(pgwOK.URL, "testjob").
Grouping("empty", "").
Collector(metric1).
Collector(metric2).
Push(); err != nil {
t.Fatal(err)
}
if lastMethod != http.MethodPut {
t.Errorf("got method %q for Push, want %q", lastMethod, http.MethodPut)
}
if !bytes.Equal(lastBody, wantBody) {
t.Errorf("got body %v, want %v", lastBody, wantBody)
}
if lastPath != "/metrics/job/testjob/empty@base64/=" {
t.Error("unexpected path:", lastPath)
}
// Empty job name results in error.
if err := New(pgwErr.URL, "").
Collector(metric1).
Collector(metric2).
Push(); err == nil {
t.Error("push with empty job succeded")
} else {
if got, want := err, errJobEmpty; got != want {
t.Errorf("got error %q, want %q", got, want)
}
}
// Push some Collectors with a broken PGW. // Push some Collectors with a broken PGW.
if err := New(pgwErr.URL, "testjob"). if err := New(pgwErr.URL, "testjob").
Collector(metric1). Collector(metric1).
@ -251,5 +281,4 @@ func TestPush(t *testing.T) {
if lastPath != "/metrics/job/testjob/a/x/b/y" && lastPath != "/metrics/job/testjob/b/y/a/x" { if lastPath != "/metrics/job/testjob/a/x/b/y" && lastPath != "/metrics/job/testjob/b/y/a/x" {
t.Error("unexpected path:", lastPath) t.Error("unexpected path:", lastPath)
} }
} }