diff --git a/CHANGELOG.md b/CHANGELOG.md index 2481d3dbc..5374b9973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.7.0 / 2020-06-17 + +* [CHANGE] API client: Add start/end parameters to `LabelNames` and `LabelValues`. #767 +* [FEATURE] testutil: Add `GatherAndCount` and enable filtering in `CollectAndCount` #753 +* [FEATURE] API client: Add support for `status` and `runtimeinfo` endpoints. #755 +* [ENHANCEMENT] Wrapping `nil` with a `WrapRegistererWith...` function creates a no-op `Registerer`. #764 +* [ENHANCEMENT] promlint: Allow Kelvin as a base unit for cases like color temperature. #761 +* [BUGFIX] push: Properly handle empty job and label values. #752 + ## 1.6.0 / 2020-04-28 * [FEATURE] testutil: Add lint checks for metrics, including a sub-package `promlint` to expose the linter engine for external usage. #739 #743 diff --git a/Makefile b/Makefile index b25fb8358..f35cf5868 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,6 @@ include Makefile.common -# http.CloseNotifier is deprecated but we don't want to remove support -# from client_golang to not break anybody still using it. -STATICCHECK_IGNORE = \ - github.com/prometheus/client_golang/prometheus/promhttp/delegator*.go:SA1019 \ - github.com/prometheus/client_golang/prometheus/promhttp/instrument_server_test.go:SA1019 \ - github.com/prometheus/client_golang/prometheus/http.go:SA1019 - .PHONY: test test: deps common-test diff --git a/Makefile.common b/Makefile.common index b978dfc50..9320176ca 100644 --- a/Makefile.common +++ b/Makefile.common @@ -150,6 +150,17 @@ else $(GO) get $(GOOPTS) -t ./... endif +.PHONY: update-go-deps +update-go-deps: + @echo ">> updating Go dependencies" + @for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \ + $(GO) get $$m; \ + done + GO111MODULE=$(GO111MODULE) $(GO) mod tidy +ifneq (,$(wildcard vendor)) + GO111MODULE=$(GO111MODULE) $(GO) mod vendor +endif + .PHONY: common-test-short common-test-short: $(GOTEST_DIR) @echo ">> running short tests" diff --git a/VERSION b/VERSION index dc1e644a1..bd8bf882d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.0 +1.7.0 diff --git a/api/prometheus/v1/api.go b/api/prometheus/v1/api.go index fa35dddee..b77294778 100644 --- a/api/prometheus/v1/api.go +++ b/api/prometheus/v1/api.go @@ -137,6 +137,7 @@ const ( epCleanTombstones = apiPrefix + "/admin/tsdb/clean_tombstones" epConfig = apiPrefix + "/status/config" epFlags = apiPrefix + "/status/flags" + epRuntimeinfo = apiPrefix + "/status/runtimeinfo" ) // AlertState models the state of an alert. @@ -231,13 +232,15 @@ type API interface { // Flags returns the flag values that Prometheus was launched with. Flags(ctx context.Context) (FlagsResult, error) // LabelNames returns all the unique label names present in the block in sorted order. - LabelNames(ctx context.Context) ([]string, Warnings, error) + LabelNames(ctx context.Context, startTime time.Time, endTime time.Time) ([]string, Warnings, error) // LabelValues performs a query for the values of the given label. - LabelValues(ctx context.Context, label string) (model.LabelValues, Warnings, error) + LabelValues(ctx context.Context, label string, startTime time.Time, endTime time.Time) (model.LabelValues, Warnings, error) // Query performs a query for the given time. Query(ctx context.Context, query string, ts time.Time) (model.Value, Warnings, error) // QueryRange performs a query for the given range. QueryRange(ctx context.Context, query string, r Range) (model.Value, Warnings, error) + // Runtimeinfo returns the various runtime information properties about the Prometheus server. + Runtimeinfo(ctx context.Context) (RuntimeinfoResult, error) // Series finds series by label matchers. Series(ctx context.Context, matches []string, startTime time.Time, endTime time.Time) ([]model.LabelSet, Warnings, error) // Snapshot creates a snapshot of all current data into snapshots/- @@ -277,6 +280,22 @@ type ConfigResult struct { // FlagsResult contains the result from querying the flag endpoint. type FlagsResult map[string]string +// RuntimeinfoResult contains the result from querying the runtimeinfo endpoint. +type RuntimeinfoResult struct { + StartTime string `json:"startTime"` + CWD string `json:"CWD"` + ReloadConfigSuccess bool `json:"reloadConfigSuccess"` + LastConfigTime string `json:"lastConfigTime"` + ChunkCount int `json:"chunkCount"` + TimeSeriesCount int `json:"timeSeriesCount"` + CorruptionCount int `json:"corruptionCount"` + GoroutineCount int `json:"goroutineCount"` + GOMAXPROCS int `json:"GOMAXPROCS"` + GOGC string `json:"GOGC"` + GODEBUG string `json:"GODEBUG"` + StorageRetention string `json:"storageRetention"` +} + // SnapshotResult contains the result from querying the snapshot endpoint. type SnapshotResult struct { Name string `json:"name"` @@ -640,8 +659,29 @@ func (h *httpAPI) Flags(ctx context.Context) (FlagsResult, error) { return res, json.Unmarshal(body, &res) } -func (h *httpAPI) LabelNames(ctx context.Context) ([]string, Warnings, error) { +func (h *httpAPI) Runtimeinfo(ctx context.Context) (RuntimeinfoResult, error) { + u := h.client.URL(epRuntimeinfo, nil) + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return RuntimeinfoResult{}, err + } + + _, body, _, err := h.client.Do(ctx, req) + if err != nil { + return RuntimeinfoResult{}, err + } + + var res RuntimeinfoResult + return res, json.Unmarshal(body, &res) +} + +func (h *httpAPI) LabelNames(ctx context.Context, startTime time.Time, endTime time.Time) ([]string, Warnings, error) { u := h.client.URL(epLabels, nil) + q := u.Query() + q.Set("start", formatTime(startTime)) + q.Set("end", formatTime(endTime)) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, nil, err @@ -654,8 +694,12 @@ func (h *httpAPI) LabelNames(ctx context.Context) ([]string, Warnings, error) { return labelNames, w, json.Unmarshal(body, &labelNames) } -func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, Warnings, error) { +func (h *httpAPI) LabelValues(ctx context.Context, label string, startTime time.Time, endTime time.Time) (model.LabelValues, Warnings, error) { u := h.client.URL(epLabelValues, map[string]string{"name": label}) + q := u.Query() + q.Set("start", formatTime(startTime)) + q.Set("end", formatTime(endTime)) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) if err != nil { return nil, nil, err diff --git a/api/prometheus/v1/api_test.go b/api/prometheus/v1/api_test.go index b36c380c7..4bc4d53f5 100644 --- a/api/prometheus/v1/api_test.go +++ b/api/prometheus/v1/api_test.go @@ -144,15 +144,22 @@ func TestAPIs(t *testing.T) { } } + doRuntimeinfo := func() func() (interface{}, Warnings, error) { + return func() (interface{}, Warnings, error) { + v, err := promAPI.Runtimeinfo(context.Background()) + return v, nil, err + } + } + doLabelNames := func(label string) func() (interface{}, Warnings, error) { return func() (interface{}, Warnings, error) { - return promAPI.LabelNames(context.Background()) + return promAPI.LabelNames(context.Background(), time.Now().Add(-100*time.Hour), time.Now()) } } doLabelValues := func(label string) func() (interface{}, Warnings, error) { return func() (interface{}, Warnings, error) { - return promAPI.LabelValues(context.Background(), label) + return promAPI.LabelValues(context.Background(), label, time.Now().Add(-100*time.Hour), time.Now()) } } @@ -605,6 +612,48 @@ func TestAPIs(t *testing.T) { err: fmt.Errorf("some error"), }, + { + do: doRuntimeinfo(), + reqMethod: "GET", + reqPath: "/api/v1/status/runtimeinfo", + inErr: fmt.Errorf("some error"), + err: fmt.Errorf("some error"), + }, + + { + do: doRuntimeinfo(), + reqMethod: "GET", + reqPath: "/api/v1/status/runtimeinfo", + inRes: map[string]interface{}{ + "startTime": "2020-05-18T15:52:53.4503113Z", + "CWD": "/prometheus", + "reloadConfigSuccess": true, + "lastConfigTime": "2020-05-18T15:52:56Z", + "chunkCount": 72692, + "timeSeriesCount": 18476, + "corruptionCount": 0, + "goroutineCount": 217, + "GOMAXPROCS": 2, + "GOGC": "100", + "GODEBUG": "allocfreetrace", + "storageRetention": "1d", + }, + res: RuntimeinfoResult{ + StartTime: "2020-05-18T15:52:53.4503113Z", + CWD: "/prometheus", + ReloadConfigSuccess: true, + LastConfigTime: "2020-05-18T15:52:56Z", + ChunkCount: 72692, + TimeSeriesCount: 18476, + CorruptionCount: 0, + GoroutineCount: 217, + GOMAXPROCS: 2, + GOGC: "100", + GODEBUG: "allocfreetrace", + StorageRetention: "1d", + }, + }, + { do: doAlertManagers(), reqMethod: "GET", diff --git a/go.mod b/go.mod index 61b13edc7..c91a43318 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,14 @@ module github.com/prometheus/client_golang require ( github.com/beorn7/perks v1.0.1 github.com/cespare/xxhash/v2 v2.1.1 - github.com/golang/protobuf v1.4.0 - github.com/json-iterator/go v1.1.9 + github.com/golang/protobuf v1.4.2 + github.com/json-iterator/go v1.1.10 github.com/kr/pretty v0.1.0 // indirect github.com/prometheus/client_model v0.2.0 - github.com/prometheus/common v0.9.1 - github.com/prometheus/procfs v0.0.11 + github.com/prometheus/common v0.10.0 + github.com/prometheus/procfs v0.1.3 github.com/stretchr/testify v1.4.0 // indirect - golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f + golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.5 // indirect ) diff --git a/go.sum b/go.sum index 8932e64d7..1b92f6ebf 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -46,8 +48,8 @@ github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= @@ -82,13 +84,13 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= -github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -116,8 +118,8 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -127,6 +129,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/prometheus/counter_test.go b/prometheus/counter_test.go index e2e31fcf7..56652a0a0 100644 --- a/prometheus/counter_test.go +++ b/prometheus/counter_test.go @@ -19,6 +19,7 @@ import ( "testing" "time" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" diff --git a/prometheus/desc.go b/prometheus/desc.go index e3232d79f..2f19f5e1e 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -20,6 +20,7 @@ import ( "strings" "github.com/cespare/xxhash/v2" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/model" diff --git a/prometheus/examples_test.go b/prometheus/examples_test.go index 903d4d978..8bc051af7 100644 --- a/prometheus/examples_test.go +++ b/prometheus/examples_test.go @@ -22,6 +22,7 @@ import ( "strings" "time" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/expfmt" diff --git a/prometheus/histogram.go b/prometheus/histogram.go index 4271f438a..3a5aac700 100644 --- a/prometheus/histogram.go +++ b/prometheus/histogram.go @@ -22,6 +22,7 @@ import ( "sync/atomic" "time" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" diff --git a/prometheus/histogram_test.go b/prometheus/histogram_test.go index 2e8f4b8c1..0b4826c56 100644 --- a/prometheus/histogram_test.go +++ b/prometheus/histogram_test.go @@ -24,6 +24,7 @@ import ( "testing/quick" "time" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" diff --git a/prometheus/metric.go b/prometheus/metric.go index 0df1eff88..35bd8bde3 100644 --- a/prometheus/metric.go +++ b/prometheus/metric.go @@ -17,6 +17,7 @@ import ( "strings" "time" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/model" diff --git a/prometheus/promauto/auto.go b/prometheus/promauto/auto.go index 3c10c8524..f8d50d1f9 100644 --- a/prometheus/promauto/auto.go +++ b/prometheus/promauto/auto.go @@ -98,7 +98,7 @@ // requestCount = promauto.With(reg).NewCounterVec( // prometheus.CounterOpts{ // Name: "http_requests_total", -// Help: "Total number of HTTP requests by status code end method.", +// Help: "Total number of HTTP requests by status code and method.", // }, // []string{"code", "method"}, // ) @@ -117,7 +117,7 @@ // requestCount = factory.NewCounterVec( // prometheus.CounterOpts{ // Name: "http_requests_total", -// Help: "Total number of HTTP requests by status code end method.", +// Help: "Total number of HTTP requests by status code and method.", // }, // []string{"code", "method"}, // ) @@ -127,29 +127,30 @@ // separate package? // // The main problem is that registration may fail, e.g. if a metric inconsistent -// with the newly to be registered one is already registered. Therefore, the -// Register method in the prometheus.Registerer interface returns an error, and -// the same is the case for the top-level prometheus.Register function that -// registers with the global registry. The prometheus package also provides -// MustRegister versions for both. They panic if the registration fails, and -// they clearly call this out by using the Must… idiom. Panicking is a bit -// problematic here because it doesn't just happen on input provided by the -// caller that is invalid on its own. Things are a bit more subtle here: Metric -// creation and registration tend to be spread widely over the codebase. It can -// easily happen that an incompatible metric is added to an unrelated part of -// the code, and suddenly code that used to work perfectly fine starts to panic -// (provided that the registration of the newly added metric happens before the -// registration of the previously existing metric). This may come as an even -// bigger surprise with the global registry, where simply importing another -// package can trigger a panic (if the newly imported package registers metrics -// in its init function). At least, in the prometheus package, creation of -// metrics and other collectors is separate from registration. You first create -// the metric, and then you decide explicitly if you want to register it with a -// local or the global registry, and if you want to handle the error or risk a -// panic. With the constructors in the promauto package, registration is -// automatic, and if it fails, it will always panic. Furthermore, the -// constructors will often be called in the var section of a file, which means -// that panicking will happen as a side effect of merely importing a package. +// with or equal to the newly to be registered one is already registered. +// Therefore, the Register method in the prometheus.Registerer interface returns +// an error, and the same is the case for the top-level prometheus.Register +// function that registers with the global registry. The prometheus package also +// provides MustRegister versions for both. They panic if the registration +// fails, and they clearly call this out by using the Must… idiom. Panicking is +// problematic in this case because it doesn't just happen on input provided by +// the caller that is invalid on its own. Things are a bit more subtle here: +// Metric creation and registration tend to be spread widely over the +// codebase. It can easily happen that an incompatible metric is added to an +// unrelated part of the code, and suddenly code that used to work perfectly +// fine starts to panic (provided that the registration of the newly added +// metric happens before the registration of the previously existing +// metric). This may come as an even bigger surprise with the global registry, +// where simply importing another package can trigger a panic (if the newly +// imported package registers metrics in its init function). At least, in the +// prometheus package, creation of metrics and other collectors is separate from +// registration. You first create the metric, and then you decide explicitly if +// you want to register it with a local or the global registry, and if you want +// to handle the error or risk a panic. With the constructors in the promauto +// package, registration is automatic, and if it fails, it will always +// panic. Furthermore, the constructors will often be called in the var section +// of a file, which means that panicking will happen as a side effect of merely +// importing a package. // // A separate package allows conservative users to entirely ignore it. And // whoever wants to use it, will do so explicitly, with an opportunity to read @@ -252,7 +253,8 @@ type Factory struct { } // With creates a Factory using the provided Registerer for registration of the -// created Collectors. +// created Collectors. If the provided Registerer is nil, the returned Factory +// creates Collectors that are not registered with any Registerer. func With(r prometheus.Registerer) Factory { return Factory{r} } // NewCounter works like the function of the same name in the prometheus package diff --git a/prometheus/promauto/auto_test.go b/prometheus/promauto/auto_test.go new file mode 100644 index 000000000..44805cb73 --- /dev/null +++ b/prometheus/promauto/auto_test.go @@ -0,0 +1,25 @@ +// Copyright 2020 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promauto + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" +) + +func TestNil(t *testing.T) { + // A nil registerer should be treated as a no-op by promauto. + With(nil).NewCounter(prometheus.CounterOpts{Name: "test"}).Inc() +} diff --git a/prometheus/push/push.go b/prometheus/push/push.go index 77ce9c837..c1a6cb99f 100644 --- a/prometheus/push/push.go +++ b/prometheus/push/push.go @@ -37,6 +37,7 @@ package push import ( "bytes" "encoding/base64" + "errors" "fmt" "io/ioutil" "net/http" @@ -56,6 +57,8 @@ const ( 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 type HTTPDoer interface { 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 -// name. You can use just host:port or ip:port as url, in which case “http://” -// is added automatically. Alternatively, include the schema in the -// URL. However, do not include the “/metrics/jobs/…” part. +// name (which must not be empty). You can use just host:port or ip:port as url, +// in which case “http://” is added automatically. Alternatively, include the +// schema in the URL. However, do not include the “/metrics/jobs/…” part. func New(url, job string) *Pusher { var ( reg = prometheus.NewRegistry() err error ) + if job == "" { + err = errJobEmpty + } if !strings.Contains(url, "://") { url = "http://" + url } @@ -267,7 +273,7 @@ func (p *Pusher) push(method string) error { return err } 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 { 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) @@ -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 // string. The job name and any grouping label values containing a '/' will // trigger a base64 encoding of the affected component and proper suffixing of -// the preceding component. If the component does not contain a '/' but other -// special character, the usual url.QueryEscape is used for compatibility with -// older versions of the Pushgateway and for better readability. +// the preceding component. Similarly, an empty grouping label value will be +// encoded as base64 just with a single `=` padding character (to avoid an empty +// 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 { urlComponents := []string{} 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 -// case it contains '/'. If not, it uses url.QueryEscape instead. It returns -// true in the former case. +// case it contains '/' and as "=" in case it is empty. If neither is the case, +// it uses url.QueryEscape instead. It returns true in the former two cases. func encodeComponent(s string) (string, bool) { + if s == "" { + return "=", true + } if strings.Contains(s, "/") { return base64.RawURLEncoding.EncodeToString([]byte(s)), true } diff --git a/prometheus/push/push_test.go b/prometheus/push/push_test.go index 1fabe39c6..99155b0ed 100644 --- a/prometheus/push/push_test.go +++ b/prometheus/push/push_test.go @@ -176,6 +176,36 @@ func TestPush(t *testing.T) { 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. if err := New(pgwErr.URL, "testjob"). 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" { t.Error("unexpected path:", lastPath) } - } diff --git a/prometheus/registry.go b/prometheus/registry.go index c05d6ee1b..ba94405af 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -26,6 +26,7 @@ import ( "unicode/utf8" "github.com/cespare/xxhash/v2" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/expfmt" diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 67e4fcbc9..48596cd72 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -33,6 +33,7 @@ import ( dto "github.com/prometheus/client_model/go" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/prometheus/common/expfmt" diff --git a/prometheus/summary.go b/prometheus/summary.go index ae42e761a..f3c1440d1 100644 --- a/prometheus/summary.go +++ b/prometheus/summary.go @@ -23,6 +23,7 @@ import ( "time" "github.com/beorn7/perks/quantile" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" diff --git a/prometheus/testutil/promlint/promlint.go b/prometheus/testutil/promlint/promlint.go index e48e4d453..ec8061706 100644 --- a/prometheus/testutil/promlint/promlint.go +++ b/prometheus/testutil/promlint/promlint.go @@ -313,9 +313,10 @@ var ( // Base units. "amperes": "amperes", "bytes": "bytes", - "celsius": "celsius", // Celsius is more common in practice than Kelvin. + "celsius": "celsius", // Also allow Celsius because it is common in typical Prometheus use cases. "grams": "grams", "joules": "joules", + "kelvin": "kelvin", // SI base unit, used in special cases (e.g. color temperature, scientific measurements). "meters": "meters", // Both American and international spelling permitted. "metres": "metres", "seconds": "seconds", @@ -328,8 +329,7 @@ var ( "days": "seconds", "weeks": "seconds", // Temperature. - "kelvin": "celsius", - "kelvins": "celsius", + "kelvins": "kelvin", "fahrenheit": "celsius", "rankine": "celsius", // Length. diff --git a/prometheus/testutil/promlint/promlint_test.go b/prometheus/testutil/promlint/promlint_test.go index c2d9980c4..f545322c6 100644 --- a/prometheus/testutil/promlint/promlint_test.go +++ b/prometheus/testutil/promlint/promlint_test.go @@ -164,6 +164,14 @@ x_seconds 10 # HELP x_joules Test metric. # TYPE x_joules untyped x_joules 10 +`, + }, + { + name: "kelvin", + in: ` +# HELP x_kelvin Test metric. +# TYPE x_kelvin untyped +x_kelvin 10 `, }, // bad cases. @@ -287,18 +295,6 @@ x_days 10 Text: `use base unit "seconds" instead of "days"`, }}, }, - { - name: "kelvin", - in: ` -# HELP x_kelvin Test metric. -# TYPE x_kelvin untyped -x_kelvin 10 -`, - problems: []promlint.Problem{{ - Metric: "x_kelvin", - Text: `use base unit "celsius" instead of "kelvin"`, - }}, - }, { name: "kelvins", in: ` @@ -308,7 +304,7 @@ x_kelvins 10 `, problems: []promlint.Problem{{ Metric: "x_kelvins", - Text: `use base unit "celsius" instead of "kelvins"`, + Text: `use base unit "kelvin" instead of "kelvins"`, }}, }, { diff --git a/prometheus/testutil/testutil.go b/prometheus/testutil/testutil.go index c47373c0b..9af60ce1d 100644 --- a/prometheus/testutil/testutil.go +++ b/prometheus/testutil/testutil.go @@ -112,31 +112,43 @@ func ToFloat64(c prometheus.Collector) float64 { panic(fmt.Errorf("collected a non-gauge/counter/untyped metric: %s", pb)) } -// CollectAndCount collects all Metrics from the provided Collector and returns their number. -// -// This can be used to assert the number of metrics collected by a given collector after certain operations. -// -// This function is only for testing purposes, and even for testing, other approaches -// are often more appropriate (see this package's documentation). -func CollectAndCount(c prometheus.Collector) int { - var ( - mCount int - mChan = make(chan prometheus.Metric) - done = make(chan struct{}) - ) - - go func() { - for range mChan { - mCount++ - } - close(done) - }() +// CollectAndCount registers the provided Collector with a newly created +// pedantic Registry. It then calls GatherAndCount with that Registry and with +// the provided metricNames. In the unlikely case that the registration or the +// gathering fails, this function panics. (This is inconsistent with the other +// CollectAnd… functions in this package and has historical reasons. Changing +// the function signature would be a breaking change and will therefore only +// happen with the next major version bump.) +func CollectAndCount(c prometheus.Collector, metricNames ...string) int { + reg := prometheus.NewPedanticRegistry() + if err := reg.Register(c); err != nil { + panic(fmt.Errorf("registering collector failed: %s", err)) + } + result, err := GatherAndCount(reg, metricNames...) + if err != nil { + panic(err) + } + return result +} - c.Collect(mChan) - close(mChan) - <-done +// GatherAndCount gathers all metrics from the provided Gatherer and counts +// them. It returns the number of metric children in all gathered metric +// families together. If any metricNames are provided, only metrics with those +// names are counted. +func GatherAndCount(g prometheus.Gatherer, metricNames ...string) (int, error) { + got, err := g.Gather() + if err != nil { + return 0, fmt.Errorf("gathering metrics failed: %s", err) + } + if metricNames != nil { + got = filterMetrics(got, metricNames) + } - return mCount + result := 0 + for _, mf := range got { + result += len(mf.GetMetric()) + } + return result, nil } // CollectAndCompare registers the provided Collector with a newly created diff --git a/prometheus/testutil/testutil_test.go b/prometheus/testutil/testutil_test.go index aaf670786..56d993382 100644 --- a/prometheus/testutil/testutil_test.go +++ b/prometheus/testutil/testutil_test.go @@ -306,3 +306,30 @@ some_total{label1="value1"} 1 t.Errorf("Expected\n%#+v\nGot:\n%#+v", expectedError, err.Error()) } } + +func TestCollectAndCount(t *testing.T) { + c := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "some_total", + Help: "A value that represents a counter.", + }, + []string{"foo"}, + ) + if got, want := CollectAndCount(c), 0; got != want { + t.Errorf("unexpected metric count, got %d, want %d", got, want) + } + c.WithLabelValues("bar") + if got, want := CollectAndCount(c), 1; got != want { + t.Errorf("unexpected metric count, got %d, want %d", got, want) + } + c.WithLabelValues("baz") + if got, want := CollectAndCount(c), 2; got != want { + t.Errorf("unexpected metric count, got %d, want %d", got, want) + } + if got, want := CollectAndCount(c, "some_total"), 2; got != want { + t.Errorf("unexpected metric count, got %d, want %d", got, want) + } + if got, want := CollectAndCount(c, "some_other_total"), 0; got != want { + t.Errorf("unexpected metric count, got %d, want %d", got, want) + } +} diff --git a/prometheus/value.go b/prometheus/value.go index 2be470ce1..6206928cc 100644 --- a/prometheus/value.go +++ b/prometheus/value.go @@ -19,6 +19,7 @@ import ( "time" "unicode/utf8" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" diff --git a/prometheus/wrap.go b/prometheus/wrap.go index e303eef6d..438aa5e92 100644 --- a/prometheus/wrap.go +++ b/prometheus/wrap.go @@ -17,6 +17,7 @@ import ( "fmt" "sort" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" @@ -27,7 +28,8 @@ import ( // registered with the wrapped Registerer in a modified way. The modified // Collector adds the provided Labels to all Metrics it collects (as // ConstLabels). The Metrics collected by the unmodified Collector must not -// duplicate any of those labels. +// duplicate any of those labels. Wrapping a nil value is valid, resulting +// in a no-op Registerer. // // WrapRegistererWith provides a way to add fixed labels to a subset of // Collectors. It should not be used to add fixed labels to all metrics exposed. @@ -50,6 +52,7 @@ func WrapRegistererWith(labels Labels, reg Registerer) Registerer { // Registerer. Collectors registered with the returned Registerer will be // registered with the wrapped Registerer in a modified way. The modified // Collector adds the provided prefix to the name of all Metrics it collects. +// Wrapping a nil value is valid, resulting in a no-op Registerer. // // WrapRegistererWithPrefix is useful to have one place to prefix all metrics of // a sub-system. To make this work, register metrics of the sub-system with the @@ -80,6 +83,9 @@ type wrappingRegisterer struct { } func (r *wrappingRegisterer) Register(c Collector) error { + if r.wrappedRegisterer == nil { + return nil + } return r.wrappedRegisterer.Register(&wrappingCollector{ wrappedCollector: c, prefix: r.prefix, @@ -88,6 +94,9 @@ func (r *wrappingRegisterer) Register(c Collector) error { } func (r *wrappingRegisterer) MustRegister(cs ...Collector) { + if r.wrappedRegisterer == nil { + return + } for _, c := range cs { if err := r.Register(c); err != nil { panic(err) @@ -96,6 +105,9 @@ func (r *wrappingRegisterer) MustRegister(cs ...Collector) { } func (r *wrappingRegisterer) Unregister(c Collector) bool { + if r.wrappedRegisterer == nil { + return false + } return r.wrappedRegisterer.Unregister(&wrappingCollector{ wrappedCollector: c, prefix: r.prefix, diff --git a/prometheus/wrap_test.go b/prometheus/wrap_test.go index 0fbb78c04..003544e31 100644 --- a/prometheus/wrap_test.go +++ b/prometheus/wrap_test.go @@ -19,6 +19,7 @@ import ( "strings" "testing" + //lint:ignore SA1019 Need to keep deprecated package for compatibility. "github.com/golang/protobuf/proto" dto "github.com/prometheus/client_model/go" @@ -320,3 +321,12 @@ func TestWrap(t *testing.T) { } } + +func TestNil(t *testing.T) { + // A wrapped nil registerer should be treated as a no-op, and not panic. + c := NewCounter(CounterOpts{Name: "test"}) + err := WrapRegistererWith(Labels{"foo": "bar"}, nil).Register(c) + if err != nil { + t.Fatal("registering failed:", err) + } +}